Просмотр исходного кода

Merge branch 'staging' of https://github.com/Timmoth/RackPeek into staging

Tim Jones 2 дней назад
Родитель
Сommit
6e8281db2b

+ 3 - 0
RackPeek.Domain/Api/InventoryResponse.cs

@@ -10,4 +10,7 @@ public class ImportYamlResponse {
 
 
     public Dictionary<string, string> NewYaml { get; set; }
     public Dictionary<string, string> NewYaml { get; set; }
         = new(StringComparer.OrdinalIgnoreCase);
         = new(StringComparer.OrdinalIgnoreCase);
+
+    public List<string> ConnectionsAdded { get; set; } = new();
+    public List<string> ConnectionsRemoved { get; set; } = new();
 }
 }

+ 18 - 0
RackPeek.Domain/Api/UpsertInventoryUseCase.cs

@@ -118,6 +118,24 @@ public class UpsertInventoryUseCase(
             else if (oldYaml != newYaml) response.Updated.Add(incoming.Name);
             else if (oldYaml != newYaml) response.Updated.Add(incoming.Name);
         }
         }
 
 
+        IReadOnlyList<Connection> currentConnections = await repo.GetConnectionsAsync();
+        List<Connection>? mergedConnections = ConnectionMerger.Merge(
+            currentConnections,
+            incomingRoot.Connections,
+            request.Mode);
+
+        if (mergedConnections != null) {
+            response.ConnectionsAdded = mergedConnections
+                .Select(ConnectionMerger.Describe)
+                .Except(currentConnections.Select(ConnectionMerger.Describe))
+                .ToList();
+
+            response.ConnectionsRemoved = currentConnections
+                .Select(ConnectionMerger.Describe)
+                .Except(mergedConnections.Select(ConnectionMerger.Describe))
+                .ToList();
+        }
+
         if (!request.DryRun) await repo.Merge(yamlInput, request.Mode);
         if (!request.DryRun) await repo.Merge(yamlInput, request.Mode);
 
 
         return response;
         return response;

+ 50 - 0
RackPeek.Domain/Persistence/ConnectionMerger.cs

@@ -0,0 +1,50 @@
+using RackPeek.Domain.Resources.Connections;
+
+namespace RackPeek.Domain.Persistence;
+
+public static class ConnectionMerger {
+    /// <summary>
+    ///     Merges an imported connections section into the existing set.
+    ///     Returns null when the import carries no connections section — the
+    ///     existing connections are kept untouched in that case (#308).
+    ///     Replace mode swaps the whole set; Merge mode applies the same
+    ///     overwrite rule as the UI (a port holds at most one connection, so
+    ///     each incoming connection evicts anything touching its endpoints).
+    /// </summary>
+    public static List<Connection>? Merge(
+        IReadOnlyList<Connection> existing,
+        List<Connection>? incoming,
+        MergeMode mode) {
+        if (incoming == null)
+            return null;
+
+        if (mode == MergeMode.Replace)
+            return incoming.ToList();
+
+        var merged = existing.ToList();
+
+        foreach (Connection connection in incoming) {
+            merged.RemoveAll(c =>
+                Touches(c, connection.A) || Touches(c, connection.B));
+
+            merged.Add(connection);
+        }
+
+        return merged;
+    }
+
+    public static string Describe(Connection c) =>
+        $"{Describe(c.A)} <-> {Describe(c.B)}";
+
+    private static string Describe(PortReference p) =>
+        $"{p.Resource}[{p.PortGroup}.{p.PortIndex}]";
+
+    private static bool Touches(Connection c, PortReference port) =>
+        PortsMatch(c.A, port) || PortsMatch(c.B, port);
+
+    private static bool PortsMatch(PortReference a, PortReference b) {
+        return a.Resource.Equals(b.Resource, StringComparison.OrdinalIgnoreCase)
+               && a.PortGroup == b.PortGroup
+               && a.PortIndex == b.PortIndex;
+    }
+}

+ 10 - 0
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -136,6 +136,16 @@ public sealed class YamlResourceCollection(
             resourceCollection.Resources.Clear();
             resourceCollection.Resources.Clear();
             resourceCollection.Resources.AddRange(merged);
             resourceCollection.Resources.AddRange(merged);
 
 
+            List<Connection>? mergedConnections = ConnectionMerger.Merge(
+                resourceCollection.Connections,
+                incomingRoot.Connections,
+                mode);
+
+            if (mergedConnections != null) {
+                resourceCollection.Connections.Clear();
+                resourceCollection.Connections.AddRange(mergedConnections);
+            }
+
             var rootToSave = new YamlRoot {
             var rootToSave = new YamlRoot {
                 Version = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count,
                 Version = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count,
                 Resources = resourceCollection.Resources,
                 Resources = resourceCollection.Resources,

+ 3 - 2
RackPeek.Domain/UseCases/DeleteResourceUseCase.cs

@@ -21,13 +21,14 @@ public class DeleteResourceUseCase<T>(IResourceCollection repo) : IDeleteResourc
 
 
         IReadOnlyList<Resource> dependants = await repo.GetDependantsAsync(name);
         IReadOnlyList<Resource> dependants = await repo.GetDependantsAsync(name);
         foreach (Resource resource in dependants) {
         foreach (Resource resource in dependants) {
-            resource.RunsOn.Remove(name);
+            resource.RunsOn.RemoveAll(p => p.Equals(name, StringComparison.OrdinalIgnoreCase));
             await repo.UpdateAsync(resource);
             await repo.UpdateAsync(resource);
         }
         }
 
 
         IReadOnlyList<Connection> connections = await repo.GetConnectionsAsync();
         IReadOnlyList<Connection> connections = await repo.GetConnectionsAsync();
         foreach (Connection connection in connections) {
         foreach (Connection connection in connections) {
-            if (connection.A.Resource == name || connection.B.Resource == name) {
+            if (connection.A.Resource.Equals(name, StringComparison.OrdinalIgnoreCase)
+                || connection.B.Resource.Equals(name, StringComparison.OrdinalIgnoreCase)) {
                 await repo.RemoveConnectionAsync(connection);
                 await repo.RemoveConnectionAsync(connection);
             }
             }
         }
         }

+ 4 - 4
RackPeek.Domain/UseCases/RenameResourceUseCase.cs

@@ -32,9 +32,9 @@ public class RenameResourceUseCase<T>(IResourceCollection repo) : IRenameResourc
         IReadOnlyList<Resource> allResources = await repo.GetAllOfTypeAsync<Resource>();
         IReadOnlyList<Resource> allResources = await repo.GetAllOfTypeAsync<Resource>();
 
 
         foreach (Resource resource in allResources) {
         foreach (Resource resource in allResources) {
-            if (resource.RunsOn.Contains(originalName)) {
+            if (resource.RunsOn.Contains(originalName, StringComparer.OrdinalIgnoreCase)) {
                 resource.RunsOn = resource.RunsOn
                 resource.RunsOn = resource.RunsOn
-                    .ConvertAll(p => p == originalName ? newName : p);
+                    .ConvertAll(p => p.Equals(originalName, StringComparison.OrdinalIgnoreCase) ? newName : p);
 
 
                 await repo.UpdateAsync(resource);
                 await repo.UpdateAsync(resource);
             }
             }
@@ -44,12 +44,12 @@ public class RenameResourceUseCase<T>(IResourceCollection repo) : IRenameResourc
         foreach (Connection connection in connections) {
         foreach (Connection connection in connections) {
             var updated = false;
             var updated = false;
 
 
-            if (connection.A.Resource == originalName) {
+            if (connection.A.Resource.Equals(originalName, StringComparison.OrdinalIgnoreCase)) {
                 connection.A.Resource = newName;
                 connection.A.Resource = newName;
                 updated = true;
                 updated = true;
             }
             }
 
 
-            if (connection.B.Resource == originalName) {
+            if (connection.B.Resource.Equals(originalName, StringComparison.OrdinalIgnoreCase)) {
                 connection.B.Resource = newName;
                 connection.B.Resource = newName;
                 updated = true;
                 updated = true;
             }
             }

+ 2 - 0
RackPeek.Web.Viewer/Program.cs

@@ -6,6 +6,7 @@ using RackPeek.Domain.Git;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence.Yaml;
 using RackPeek.Domain.Persistence.Yaml;
 using Shared.Rcl;
 using Shared.Rcl;
+using Shared.Rcl.Docs;
 
 
 namespace RackPeek.Web.Viewer;
 namespace RackPeek.Web.Viewer;
 
 
@@ -51,6 +52,7 @@ public class Program {
         builder.Services.AddUseCases();
         builder.Services.AddUseCases();
 
 
         builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
         builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+        builder.Services.AddScoped<IDocsContentProvider, HttpDocsContentProvider>();
 
 
         await builder.Build().RunAsync();
         await builder.Build().RunAsync();
     }
     }

+ 7 - 4
RackPeek.Web/Dockerfile

@@ -48,10 +48,12 @@ RUN apt-get update \
     && apt-get install -y --no-install-recommends curl \
     && apt-get install -y --no-install-recommends curl \
     && rm -rf /var/lib/apt/lists/*
     && rm -rf /var/lib/apt/lists/*
 
 
-# Create shared config directory safely
-RUN mkdir -p /app/config \
-    && chown -R ${APP_UID}:0 /app/config \
-    && chmod -R g=u /app/config
+# Create shared config directory safely, plus an app-owned temp directory:
+# key persistence writes staging files via the process temp dir, and /tmp
+# can be unavailable in hardened/rootless Docker setups (#312).
+RUN mkdir -p /app/config /app/tmp \
+    && chown -R ${APP_UID}:0 /app/config /app/tmp \
+    && chmod -R g=u /app/config /app/tmp
 
 
 VOLUME ["/app/config"]
 VOLUME ["/app/config"]
 
 
@@ -70,6 +72,7 @@ RUN if [ -f /usr/local/bin/rpk-dir/RackPeek ]; then \
 # Make sure ASP.NET binds correctly in containers
 # Make sure ASP.NET binds correctly in containers
 ENV ASPNETCORE_URLS=http://+:8080
 ENV ASPNETCORE_URLS=http://+:8080
 ENV RPK_YAML_DIR=/app/config
 ENV RPK_YAML_DIR=/app/config
+ENV TMPDIR=/app/tmp
 
 
 # Drop privileges
 # Drop privileges
 USER ${APP_UID}
 USER ${APP_UID}

+ 16 - 7
RackPeek.Web/Program.cs

@@ -1,5 +1,6 @@
 using System.Text.Json.Serialization;
 using System.Text.Json.Serialization;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.DataProtection;
 using Microsoft.AspNetCore.Hosting.StaticWebAssets;
 using Microsoft.AspNetCore.Hosting.StaticWebAssets;
 using RackPeek.Domain;
 using RackPeek.Domain;
 using RackPeek.Domain.Git;
 using RackPeek.Domain.Git;
@@ -8,6 +9,8 @@ using RackPeek.Domain.Persistence.Yaml;
 using RackPeek.Web.Api;
 using RackPeek.Web.Api;
 using RackPeek.Web.Components;
 using RackPeek.Web.Components;
 using Shared.Rcl;
 using Shared.Rcl;
+using Shared.Rcl.Docs;
+using Shared.Rcl.Servers;
 
 
 namespace RackPeek.Web;
 namespace RackPeek.Web;
 
 
@@ -47,18 +50,23 @@ public class Program {
             }
             }
         }
         }
 
 
+        // Persist DataProtection keys next to the config so they live on the
+        // mounted volume: they survive container recreation, and key writes
+        // no longer depend on a writable user profile or /tmp — both of
+        // which are unavailable in hardened Docker setups (#312).
+        var keysPath = Path.Combine(yamlPath, ".dataprotection");
+        Directory.CreateDirectory(keysPath);
+        builder.Services.AddDataProtection()
+            .PersistKeysToFileSystem(new DirectoryInfo(keysPath))
+            .SetApplicationName("RackPeek");
+
         builder.Services.ConfigureHttpJsonOptions(options => {
         builder.Services.ConfigureHttpJsonOptions(options => {
             options.SerializerOptions.Converters.Add(
             options.SerializerOptions.Converters.Add(
                 new JsonStringEnumConverter());
                 new JsonStringEnumConverter());
         });
         });
         builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();
         builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();
 
 
-        builder.Services.AddScoped(sp => {
-            NavigationManager nav = sp.GetRequiredService<NavigationManager>();
-            return new HttpClient {
-                BaseAddress = new Uri(nav.BaseUri)
-            };
-        });
+        builder.Services.AddScoped<IDocsContentProvider, StaticWebAssetDocsContentProvider>();
 
 
         builder.Services.AddGitServices(builder.Configuration, yamlPath);
         builder.Services.AddGitServices(builder.Configuration, yamlPath);
 
 
@@ -102,7 +110,8 @@ public class Program {
         app.MapStaticAssets();
         app.MapStaticAssets();
 
 
         app.MapRazorComponents<App>()
         app.MapRazorComponents<App>()
-            .AddInteractiveServerRenderMode();
+            .AddInteractiveServerRenderMode()
+            .AddAdditionalAssemblies(typeof(ServersListPage).Assembly);
 
 
         return app;
         return app;
     }
     }

+ 25 - 0
RackPeek.Web/StaticWebAssetDocsContentProvider.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.FileProviders;
+using Shared.Rcl.Docs;
+
+namespace RackPeek.Web;
+
+/// <summary>
+///     Reads the docs directly from the composed static web assets
+///     (Shared.Rcl's wwwroot in development, wwwroot/_content/Shared.Rcl in a
+///     published app), so the server never has to call back to its own public
+///     URL — which is unreachable from inside the container behind a reverse
+///     proxy (issue #304).
+/// </summary>
+public class StaticWebAssetDocsContentProvider(IWebHostEnvironment environment) : IDocsContentProvider {
+    public async Task<string> GetAsync(string fileName) {
+        IFileInfo file = environment.WebRootFileProvider
+            .GetFileInfo($"_content/Shared.Rcl/raw_docs/{fileName}");
+
+        if (!file.Exists)
+            throw new FileNotFoundException($"Docs file '{fileName}' not found.");
+
+        await using Stream stream = file.CreateReadStream();
+        using var reader = new StreamReader(stream);
+        return await reader.ReadToEndAsync();
+    }
+}

+ 5 - 4
Shared.Rcl/Docs/DocsHomePage.razor

@@ -1,10 +1,11 @@
 @page "/docs"
 @page "/docs"
 @page "/docs/{*Page}"
 @page "/docs/{*Page}"
 
 
-@inject HttpClient Http
+@inject IDocsContentProvider Docs
 @inject NavigationManager Nav
 @inject NavigationManager Nav
 @inject IJSRuntime JS
 @inject IJSRuntime JS
 @using Markdig
 @using Markdig
+@using System.Text.Json
 @implements IDisposable
 @implements IDisposable
 
 
 <PageTitle>Docs@(!string.IsNullOrWhiteSpace(_activeTitle) ? $": {_activeTitle}" : "")</PageTitle>
 <PageTitle>Docs@(!string.IsNullOrWhiteSpace(_activeTitle) ? $": {_activeTitle}" : "")</PageTitle>
@@ -164,7 +165,7 @@
             var url = $"_content/Shared.Rcl/raw_docs/{decoded}";
             var url = $"_content/Shared.Rcl/raw_docs/{decoded}";
             _lastFetchUrl = url;
             _lastFetchUrl = url;
 
 
-            var markdown = await Http.GetStringAsync(url);
+            var markdown = await Docs.GetAsync(decoded);
             _htmlContent = Markdown.ToHtml(markdown, Pipeline);
             _htmlContent = Markdown.ToHtml(markdown, Pipeline);
 
 
             _activeTitle = DisplayName(decoded);
             _activeTitle = DisplayName(decoded);
@@ -215,8 +216,8 @@
 
 
         try
         try
         {
         {
-            var url = "_content/Shared.Rcl/raw_docs/docs-index.json";
-            var items = await Http.GetFromJsonAsync<List<string>>(url);
+            var json = await Docs.GetAsync("docs-index.json");
+            var items = JsonSerializer.Deserialize<List<string>>(json);
 
 
             _docsIndex = (items ?? new List<string>())
             _docsIndex = (items ?? new List<string>())
                 .Where(x => !string.IsNullOrWhiteSpace(x))
                 .Where(x => !string.IsNullOrWhiteSpace(x))

+ 6 - 0
Shared.Rcl/Docs/HttpDocsContentProvider.cs

@@ -0,0 +1,6 @@
+namespace Shared.Rcl.Docs;
+
+public class HttpDocsContentProvider(HttpClient http) : IDocsContentProvider {
+    public Task<string> GetAsync(string fileName) =>
+        http.GetStringAsync($"_content/Shared.Rcl/raw_docs/{fileName}");
+}

+ 17 - 0
Shared.Rcl/Docs/IDocsContentProvider.cs

@@ -0,0 +1,17 @@
+namespace Shared.Rcl.Docs;
+
+/// <summary>
+///     Supplies the raw documentation assets shipped under
+///     _content/Shared.Rcl/raw_docs. The Blazor Server host reads them from
+///     the static web assets on disk; fetching them over HTTP would require
+///     the container to reach its own public URL, which fails behind a
+///     reverse proxy (issue #304). The WebAssembly viewer fetches them over
+///     HTTP relative to the app base, which is always reachable there.
+/// </summary>
+public interface IDocsContentProvider {
+    /// <summary>
+    ///     Returns the content of a file under raw_docs (e.g. "overview.md",
+    ///     "docs-index.json"). Throws if the file does not exist.
+    /// </summary>
+    Task<string> GetAsync(string fileName);
+}

+ 1 - 1
Shared.Rcl/Systems/SystemCardComponent.razor

@@ -94,7 +94,7 @@
             }
             }
             else if (!string.IsNullOrWhiteSpace(System.Type))
             else if (!string.IsNullOrWhiteSpace(System.Type))
             {
             {
-                <div class="text-zinc-300">@System.Type</div>
+                <div class="text-zinc-300" data-testid="system-type-value">@System.Type</div>
             }
             }
         </div>
         </div>
 
 

+ 4 - 1
Shared.Rcl/Systems/SystemEditModel.cs

@@ -23,7 +23,10 @@ public sealed class SystemEditModel {
     public static SystemEditModel From(SystemResource system) {
     public static SystemEditModel From(SystemResource system) {
         return new SystemEditModel {
         return new SystemEditModel {
             Name = system.Name,
             Name = system.Name,
-            Type = system.Type,
+            // The type dropdown has no empty option, so a system without a
+            // type still displays the first choice; default to it so what the
+            // user sees is what gets saved (#306).
+            Type = system.Type ?? SystemResource.ValidSystemTypes[0],
             Os = system.Os,
             Os = system.Os,
             Cores = system.Cores,
             Cores = system.Cores,
             Ram = system.Ram,
             Ram = system.Ram,

+ 31 - 1
Shared.Rcl/YamlImportPage.razor

@@ -110,7 +110,8 @@
                 Import Summary
                 Import Summary
             </div>
             </div>
 
 
-            @if (!_added.Any() && !_updated.Any() && !_replaced.Any())
+            @if (!_added.Any() && !_updated.Any() && !_replaced.Any()
+                 && !_connectionsAdded.Any() && !_connectionsRemoved.Any())
             {
             {
                 <div class="text-zinc-500 italic mt-2">
                 <div class="text-zinc-500 italic mt-2">
                     No changes detected.
                     No changes detected.
@@ -194,6 +195,27 @@
                 }
                 }
             }
             }
 
 
+            @if (_connectionsAdded.Any() || _connectionsRemoved.Any())
+            {
+                <div class="text-zinc-400 mt-4 mb-2" data-testid="import-connections-summary">
+                    Connections
+                </div>
+
+                @foreach (var connection in _connectionsAdded)
+                {
+                    <div class="ml-4 text-emerald-400" data-testid="import-connection-added">
+                        + @connection
+                    </div>
+                }
+
+                @foreach (var connection in _connectionsRemoved)
+                {
+                    <div class="ml-4 text-red-400" data-testid="import-connection-removed">
+                        - @connection
+                    </div>
+                }
+            }
+
         </div>
         </div>
     }
     }
 
 
@@ -204,6 +226,8 @@
     private List<string> _added = new();
     private List<string> _added = new();
     private List<string> _updated = new();
     private List<string> _updated = new();
     private List<string> _replaced = new();
     private List<string> _replaced = new();
+    private List<string> _connectionsAdded = new();
+    private List<string> _connectionsRemoved = new();
 
 
     private Dictionary<string, string> _oldYaml = new(StringComparer.OrdinalIgnoreCase);
     private Dictionary<string, string> _oldYaml = new(StringComparer.OrdinalIgnoreCase);
     private Dictionary<string, string> _newYaml = new(StringComparer.OrdinalIgnoreCase);
     private Dictionary<string, string> _newYaml = new(StringComparer.OrdinalIgnoreCase);
@@ -228,6 +252,8 @@
         _added.Clear();
         _added.Clear();
         _updated.Clear();
         _updated.Clear();
         _replaced.Clear();
         _replaced.Clear();
+        _connectionsAdded.Clear();
+        _connectionsRemoved.Clear();
         _oldYaml.Clear();
         _oldYaml.Clear();
         _newYaml.Clear();
         _newYaml.Clear();
 
 
@@ -246,6 +272,8 @@
             _added = result.Added;
             _added = result.Added;
             _updated = result.Updated;
             _updated = result.Updated;
             _replaced = result.Replaced;
             _replaced = result.Replaced;
+            _connectionsAdded = result.ConnectionsAdded;
+            _connectionsRemoved = result.ConnectionsRemoved;
 
 
             _oldYaml = result.OldYaml;
             _oldYaml = result.OldYaml;
             _newYaml = result.NewYaml;
             _newYaml = result.NewYaml;
@@ -278,6 +306,8 @@
             _added.Clear();
             _added.Clear();
             _updated.Clear();
             _updated.Clear();
             _replaced.Clear();
             _replaced.Clear();
+            _connectionsAdded.Clear();
+            _connectionsRemoved.Clear();
         }
         }
         catch (Exception ex)
         catch (Exception ex)
         {
         {

+ 8 - 0
Shared.Rcl/wwwroot/raw_docs/install-guide.md

@@ -58,6 +58,14 @@ http://localhost:8080
 
 
 This uses a **named volume**, which avoids permission issues and is recommended for most users.
 This uses a **named volume**, which avoids permission issues and is recommended for most users.
 
 
+RackPeek stores its ASP.NET DataProtection keys under `config/.dataprotection` on the same volume, so browser sessions survive container upgrades. If you run the container with a **read-only root filesystem**, also mount a tmpfs for the app's temp directory:
+
+```yaml
+    read_only: true
+    tmpfs:
+      - /app/tmp:uid=1654,gid=0
+```
+
 ---
 ---
 
 
 ## Portainer
 ## Portainer

+ 3 - 0
Tests.E2e/PageObjectModels/SystemCardPom.cs

@@ -81,6 +81,9 @@ public class SystemCardPom(IPage page) {
     public ILocator TypeSelect(string name)
     public ILocator TypeSelect(string name)
         => Card(name).GetByTestId("system-type-select");
         => Card(name).GetByTestId("system-type-select");
 
 
+    public ILocator TypeValue(string name)
+        => Card(name).GetByTestId("system-type-value");
+
     public ILocator OsInput(string name)
     public ILocator OsInput(string name)
         => Card(name).GetByTestId("system-os-input");
         => Card(name).GetByTestId("system-os-input");
 
 

+ 34 - 0
Tests.E2e/SystemCardTests.cs

@@ -119,6 +119,40 @@ public class SystemCardTests(
         }
         }
     }
     }
 
 
+    [Fact]
+    public async Task Saving_Without_Touching_Type_Persists_The_Displayed_Default() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        var name = $"e2e-sys-type-{Guid.NewGuid():N}"[..16];
+
+        try {
+            await page.GotoAsync($"{_fixture.BaseUrl}/systems/list");
+
+            var list = new SystemsListPom(page);
+            await list.AddSystemAsync(name);
+
+            if (!page.Url.Contains($"/resources/systems/{name}",
+                    StringComparison.OrdinalIgnoreCase))
+                await list.OpenSystemAsync(name);
+
+            var card = new SystemCardPom(page);
+            await card.AssertVisibleAsync(name);
+
+            // The dropdown shows 'baremetal' for a fresh system; save without
+            // touching it — the displayed default must actually persist (#306).
+            await card.BeginEditAsync(name);
+            await card.SaveAsync(name);
+
+            await Assertions.Expect(card.TypeValue(name)).ToHaveTextAsync("baremetal");
+
+            await page.ReloadAsync();
+            await card.AssertVisibleAsync(name);
+            await Assertions.Expect(card.TypeValue(name)).ToHaveTextAsync("baremetal");
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+
     // ============================================================
     // ============================================================
     // Cancel Edit
     // Cancel Edit
     // ============================================================
     // ============================================================

+ 126 - 0
Tests/Api/InventoryEndpointTests.cs

@@ -849,6 +849,132 @@ public class InventoryEndpointTests(ITestOutputHelper output) : ApiTestBase(outp
         Assert.DoesNotContain("env: production", newYaml);
         Assert.DoesNotContain("env: production", newYaml);
     }
     }
 
 
+    [Fact]
+    public async Task Import_Persists_And_Updates_Connections() {
+        HttpClient client = CreateClient(true);
+
+        var initial = """
+                      version: 3
+                      resources:
+                      - kind: Switch
+                        ports:
+                        - type: rj45
+                          speed: 1
+                          count: 8
+                        name: switch1
+                      - kind: Server
+                        ports:
+                        - type: rj45
+                          speed: 1
+                          count: 1
+                        name: server1
+                      connections:
+                      - a:
+                          resource: server1
+                          portGroup: 0
+                          portIndex: 0
+                        b:
+                          resource: switch1
+                          portGroup: 0
+                          portIndex: 0
+                      """;
+
+        HttpResponseMessage response = await client.PostAsJsonAsync("/api/inventory",
+            new { yaml = initial, mode = "Merge" });
+
+        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+        ImportYamlResponse? result = await response.Content.ReadFromJsonAsync<ImportYamlResponse>();
+        Assert.Single(result!.ConnectionsAdded);
+        Assert.Empty(result.ConnectionsRemoved);
+
+        // Re-importing the same config is idempotent
+        HttpResponseMessage response2 = await client.PostAsJsonAsync("/api/inventory",
+            new { yaml = initial, dryRun = true, mode = "Merge" });
+        ImportYamlResponse? result2 = await response2.Content.ReadFromJsonAsync<ImportYamlResponse>();
+        Assert.Empty(result2!.ConnectionsAdded);
+        Assert.Empty(result2.ConnectionsRemoved);
+
+        // Adjusting the switch port replaces the old connection (#308)
+        var adjusted = initial.Replace(
+            """
+                resource: switch1
+                portGroup: 0
+                portIndex: 0
+            """,
+            """
+                resource: switch1
+                portGroup: 0
+                portIndex: 3
+            """);
+
+        HttpResponseMessage response3 = await client.PostAsJsonAsync("/api/inventory",
+            new { yaml = adjusted, mode = "Merge" });
+        ImportYamlResponse? result3 = await response3.Content.ReadFromJsonAsync<ImportYamlResponse>();
+
+        Assert.Single(result3!.ConnectionsAdded);
+        Assert.Contains("switch1[0.3]", result3.ConnectionsAdded[0]);
+        Assert.Single(result3.ConnectionsRemoved);
+        Assert.Contains("switch1[0.0]", result3.ConnectionsRemoved[0]);
+
+        // And the adjusted state is now stable
+        HttpResponseMessage response4 = await client.PostAsJsonAsync("/api/inventory",
+            new { yaml = adjusted, dryRun = true, mode = "Merge" });
+        ImportYamlResponse? result4 = await response4.Content.ReadFromJsonAsync<ImportYamlResponse>();
+        Assert.Empty(result4!.ConnectionsAdded);
+        Assert.Empty(result4.ConnectionsRemoved);
+    }
+
+    [Fact]
+    public async Task Import_Without_Connections_Section_Keeps_Existing_Connections() {
+        HttpClient client = CreateClient(true);
+
+        var initial = """
+                      version: 3
+                      resources:
+                      - kind: Switch
+                        ports:
+                        - type: rj45
+                          speed: 1
+                          count: 8
+                        name: sw-keep
+                      - kind: Server
+                        ports:
+                        - type: rj45
+                          speed: 1
+                          count: 1
+                        name: srv-keep
+                      connections:
+                      - a:
+                          resource: srv-keep
+                          portGroup: 0
+                          portIndex: 0
+                        b:
+                          resource: sw-keep
+                          portGroup: 0
+                          portIndex: 0
+                      """;
+
+        await client.PostAsJsonAsync("/api/inventory", new { yaml = initial, mode = "Merge" });
+
+        // An import that says nothing about connections must not drop them
+        var resourcesOnly = """
+                            resources:
+                            - kind: Server
+                              name: srv-keep
+                              notes: updated
+                            """;
+
+        await client.PostAsJsonAsync("/api/inventory", new { yaml = resourcesOnly, mode = "Merge" });
+
+        // The original connection still exists: re-importing the initial
+        // config reports no connection changes.
+        HttpResponseMessage check = await client.PostAsJsonAsync("/api/inventory",
+            new { yaml = initial, dryRun = true, mode = "Merge" });
+        ImportYamlResponse? result = await check.Content.ReadFromJsonAsync<ImportYamlResponse>();
+        Assert.Empty(result!.ConnectionsAdded);
+        Assert.Empty(result.ConnectionsRemoved);
+    }
+
     [Fact]
     [Fact]
     public async Task Merge_Other_Hardware_Persists() {
     public async Task Merge_Other_Hardware_Persists() {
         HttpClient client = CreateClient(true);
         HttpClient client = CreateClient(true);

+ 41 - 0
Tests/EndToEnd/ConnectionTests/RenameResourceTests.cs

@@ -142,6 +142,47 @@ public class RenameResourceTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.Contains("switch-uplink", yaml);
         Assert.Contains("switch-uplink", yaml);
     }
     }
 
 
+    [Fact]
+    public async Task rename_typed_in_different_case_preserves_connection() {
+        await ExecuteAsync("servers", "add", "Case01");
+        await ExecuteAsync("servers", "add", "Case02");
+
+        await ExecuteAsync("servers", "nic", "add", "Case01",
+            "--type", "RJ45", "--speed", "10", "--ports", "2");
+
+        await ExecuteAsync("servers", "nic", "add", "Case02",
+            "--type", "RJ45", "--speed", "10", "--ports", "2");
+
+        await ExecuteAsync("connections", "add",
+            "Case01", "0", "0",
+            "Case02", "0", "0",
+            "--label", "mixed-case-link");
+
+        await ExecuteAsync("servers", "rename", "case01", "Case01-renamed");
+
+        (_, var yaml) = await ExecuteAsync("servers", "get", "Case01-renamed");
+
+        Assert.Contains("name: Case01-renamed", yaml);
+        Assert.Contains("mixed-case-link", yaml);
+        // The connection endpoint should follow the rename, not keep pointing at the old name
+        Assert.Contains("resource: Case01-renamed", yaml);
+    }
+
+    [Fact]
+    public async Task rename_typed_in_different_case_updates_runs_on() {
+        await ExecuteAsync("servers", "add", "Case11");
+        await ExecuteAsync("systems", "add", "sys-case-11");
+        await ExecuteAsync("systems", "set", "sys-case-11", "--runs-on", "Case11");
+
+        await ExecuteAsync("servers", "rename", "case11", "Case12");
+
+        (_, var yaml) = await ExecuteAsync("servers", "get", "Case12");
+
+        Assert.Contains("name: Case12", yaml);
+        // The dependant system should follow the rename
+        Assert.Contains("- Case12", yaml);
+    }
+
     [Fact]
     [Fact]
     public async Task rename_with_special_naming_preserves_connections() {
     public async Task rename_with_special_naming_preserves_connections() {
         await ExecuteAsync("servers", "add", "srv-prod-web-01");
         await ExecuteAsync("servers", "add", "srv-prod-web-01");

+ 47 - 0
Tests/EndToEnd/DeleteResourceTests.cs

@@ -105,6 +105,53 @@ public class DeleteResourceTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.DoesNotContain("srv01", yaml);
         Assert.DoesNotContain("srv01", yaml);
     }
     }
 
 
+    [Fact]
+    public async Task deleting_resource_typed_in_different_case_removes_dependant_runs_on() {
+        await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), "");
+        await ExecuteAsync("servers", "add", "Srv01");
+        await ExecuteAsync("systems", "add", "sys01");
+        await ExecuteAsync("systems", "set", "sys01", "--runs-on", "Srv01");
+
+        (var output, var yaml) = await ExecuteAsync("servers", "del", "srv01");
+
+        Assert.Contains("Server 'srv01' deleted.", output);
+        Assert.Contains("sys01", yaml);
+        // The runs-on reference should be removed even though the name was typed in a different case
+        Assert.DoesNotContain("Srv01", yaml, StringComparison.OrdinalIgnoreCase);
+    }
+
+    [Fact]
+    public async Task deleting_resource_typed_in_different_case_removes_connections() {
+        await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), "");
+        await ExecuteAsync("servers", "add", "Srv01");
+        await ExecuteAsync("servers", "add", "Srv02");
+
+        await ExecuteAsync(
+            "servers", "nic", "add", "Srv01",
+            "--type", "RJ45",
+            "--speed", "10",
+            "--ports", "2");
+
+        await ExecuteAsync(
+            "servers", "nic", "add", "Srv02",
+            "--type", "RJ45",
+            "--speed", "10",
+            "--ports", "2");
+
+        await ExecuteAsync(
+            "connections", "add",
+            "Srv01", "0", "0",
+            "Srv02", "0", "0",
+            "--label", "mixed-case-connection");
+
+        (var output, var yaml) = await ExecuteAsync("servers", "del", "srv01");
+
+        Assert.Contains("Server 'srv01' deleted.", output);
+        Assert.Contains("Srv02", yaml);
+        Assert.DoesNotContain("mixed-case-connection", yaml);
+        Assert.DoesNotContain("Srv01", yaml, StringComparison.OrdinalIgnoreCase);
+    }
+
     [Fact]
     [Fact]
     public async Task deleting_resource_with_multiple_connections_removes_all() {
     public async Task deleting_resource_with_multiple_connections_removes_all() {
         await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), "");
         await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), "");

+ 3 - 1
Tests/Tests.csproj

@@ -39,7 +39,9 @@
         <Folder Include="EndToEnd\ServiceTests\"/>
         <Folder Include="EndToEnd\ServiceTests\"/>
     </ItemGroup>
     </ItemGroup>
     <ItemGroup>
     <ItemGroup>
-        <None Include="schemas\**\*.json" CopyToOutputDirectory="PreserveNewest"/>
+        <!-- Validate against the published schemas directly so the test and
+             published copies can never drift apart again (#310, #311). -->
+        <None Include="..\schemas\**\*.json" Link="schemas\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest"/>
         <None Include="TestConfigs\**\*.yaml" CopyToOutputDirectory="PreserveNewest"/>
         <None Include="TestConfigs\**\*.yaml" CopyToOutputDirectory="PreserveNewest"/>
         <None Update="TestConfigs\v3\01-server.yaml">
         <None Update="TestConfigs\v3\01-server.yaml">
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

+ 0 - 606
Tests/schemas/schema.v1.json

@@ -1,606 +0,0 @@
-{
-  "$schema": "https://json-schema.org/draft/2020-12/schema",
-  "$id": "https://timmoth.github.io/RackPeek/schemas/v1/schema.v1.json",
-  "title": "RackPeek Infrastructure Specification",
-  "type": "object",
-  "additionalProperties": false,
-  "required": [
-    "version",
-    "resources"
-  ],
-  "properties": {
-    "version": {
-      "type": "integer",
-      "const": 1
-    },
-    "resources": {
-      "type": "array",
-      "items": {
-        "$ref": "#/$defs/resource"
-      }
-    }
-  },
-  "$defs": {
-    "labels": {
-      "type": "object",
-      "additionalProperties": {
-        "type": "string"
-      }
-    },
-    "resourceBase": {
-      "type": "object",
-      "required": [
-        "kind",
-        "name"
-      ],
-      "properties": {
-        "kind": {
-          "type": "string"
-        },
-        "name": {
-          "type": "string",
-          "minLength": 1
-        },
-        "tags": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          },
-          "default": []
-        },
-        "labels": {
-          "$ref": "#/$defs/labels",
-          "default": {}
-        },
-        "notes": {
-          "type": [
-            "string",
-            "null"
-          ]
-        },
-        "runsOn": {
-          "type": [
-            "string",
-            "null"
-          ]
-        }
-      }
-    },
-    "resource": {
-      "oneOf": [
-        {
-          "$ref": "#/$defs/server"
-        },
-        {
-          "$ref": "#/$defs/firewall"
-        },
-        {
-          "$ref": "#/$defs/router"
-        },
-        {
-          "$ref": "#/$defs/switch"
-        },
-        {
-          "$ref": "#/$defs/accessPoint"
-        },
-        {
-          "$ref": "#/$defs/ups"
-        },
-        {
-          "$ref": "#/$defs/desktop"
-        },
-        {
-          "$ref": "#/$defs/laptop"
-        },
-        {
-          "$ref": "#/$defs/service"
-        },
-        {
-          "$ref": "#/$defs/system"
-        }
-      ]
-    },
-    "ram": {
-      "type": "object",
-      "required": [
-        "size"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "size": {
-          "type": "number",
-          "minimum": 0
-        },
-        "mts": {
-          "type": "integer",
-          "minimum": 0
-        }
-      }
-    },
-    "cpu": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "model": {
-          "type": "string"
-        },
-        "cores": {
-          "type": "integer",
-          "minimum": 1
-        },
-        "threads": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "drive": {
-      "type": "object",
-      "required": [
-        "size"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string",
-          "enum": [
-            "nvme",
-            "ssd",
-            "hdd",
-            "sas",
-            "sata",
-            "usb",
-            "sdcard",
-            "micro-sd"
-          ]
-        },
-        "size": {
-          "type": "number",
-          "minimum": 1
-        }
-      }
-    },
-    "gpu": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "model": {
-          "type": "string"
-        },
-        "vram": {
-          "type": "number",
-          "minimum": 0
-        }
-      }
-    },
-    "nic": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string",
-          "enum": [
-            "rj45",
-            "sfp",
-            "sfp+",
-            "sfp28",
-            "sfp56",
-            "qsfp+",
-            "qsfp28",
-            "qsfp56",
-            "qsfp-dd",
-            "osfp",
-            "xfp",
-            "cx4",
-            "mgmt"
-          ]
-        },
-        "speed": {
-          "type": "number",
-          "minimum": 0
-        },
-        "ports": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "port": {
-      "type": "object",
-      "required": [
-        "type",
-        "speed",
-        "count"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string"
-        },
-        "speed": {
-          "type": "number",
-          "minimum": 0
-        },
-        "count": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "network": {
-      "type": "object",
-      "required": [
-        "ip",
-        "port",
-        "protocol"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "ip": {
-          "type": "string",
-          "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
-        },
-        "port": {
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 65535
-        },
-        "protocol": {
-          "type": "string",
-          "enum": [
-            "TCP",
-            "UDP"
-          ]
-        },
-        "url": {
-          "type": "string",
-          "format": "uri"
-        }
-      }
-    },
-    "server": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Server"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "ipmi": {
-              "type": "boolean"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            },
-            "gpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/gpu"
-              }
-            },
-            "nics": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/nic"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "desktop": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Desktop"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            },
-            "gpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/gpu"
-              }
-            },
-            "nics": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/nic"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "laptop": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Laptop"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "firewall": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Firewall"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "router": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Router"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "switch": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Switch"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "accessPoint": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "AccessPoint"
-            },
-            "model": {
-              "type": "string"
-            },
-            "speed": {
-              "type": "number",
-              "minimum": 0
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "ups": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Ups"
-            },
-            "model": {
-              "type": "string"
-            },
-            "va": {
-              "type": "integer",
-              "minimum": 1
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "service": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "network"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Service"
-            },
-            "network": {
-              "$ref": "#/$defs/network"
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "system": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "type",
-            "os",
-            "cores",
-            "ram"
-          ],
-          "properties": {
-            "kind": {
-              "const": "System"
-            },
-            "type": {
-              "type": "string",
-              "enum": [
-                "baremetal",
-                "Baremetal",
-                "hypervisor",
-                "Hypervisor",
-                "vm",
-                "VM",
-                "container",
-                "embedded",
-                "cloud",
-                "other"
-              ]
-            },
-            "os": {
-              "type": "string"
-            },
-            "cores": {
-              "type": "integer",
-              "minimum": 1
-            },
-            "ram": {
-              "type": "number",
-              "minimum": 0
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    }
-  }
-}

+ 0 - 624
Tests/schemas/schema.v2.json

@@ -1,624 +0,0 @@
-{
-  "$schema": "https://json-schema.org/draft/2020-12/schema",
-  "$id": "https://timmoth.github.io/RackPeek/schemas/v2/schema.v2.json",
-  "title": "RackPeek Infrastructure Specification",
-  "type": "object",
-  "additionalProperties": false,
-  "required": [
-    "version",
-    "resources"
-  ],
-  "properties": {
-    "version": {
-      "type": "integer",
-      "const": 2
-    },
-    "resources": {
-      "type": "array",
-      "items": {
-        "$ref": "#/$defs/resource"
-      }
-    }
-  },
-  "$defs": {
-    "labels": {
-      "type": "object",
-      "additionalProperties": {
-        "type": "string"
-      }
-    },
-    "runsOn": {
-      "type": [
-        "array",
-        "null"
-      ],
-      "items": {
-        "type": "string",
-        "minLength": 1
-      }
-    },
-    "resourceBase": {
-      "type": "object",
-      "required": [
-        "kind",
-        "name"
-      ],
-      "properties": {
-        "kind": {
-          "type": "string"
-        },
-        "name": {
-          "type": "string",
-          "minLength": 1
-        },
-        "tags": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          },
-          "default": []
-        },
-        "labels": {
-          "$ref": "#/$defs/labels",
-          "default": {}
-        },
-        "notes": {
-          "type": [
-            "string",
-            "null"
-          ]
-        },
-        "runsOn": {
-          "type": [
-            "array",
-            "null"
-          ],
-          "items": {
-            "type": "string"
-          }
-        }
-      }
-    },
-    "resource": {
-      "oneOf": [
-        {
-          "$ref": "#/$defs/server"
-        },
-        {
-          "$ref": "#/$defs/firewall"
-        },
-        {
-          "$ref": "#/$defs/router"
-        },
-        {
-          "$ref": "#/$defs/switch"
-        },
-        {
-          "$ref": "#/$defs/accessPoint"
-        },
-        {
-          "$ref": "#/$defs/ups"
-        },
-        {
-          "$ref": "#/$defs/desktop"
-        },
-        {
-          "$ref": "#/$defs/laptop"
-        },
-        {
-          "$ref": "#/$defs/service"
-        },
-        {
-          "$ref": "#/$defs/system"
-        }
-      ]
-    },
-    "ram": {
-      "type": "object",
-      "required": [
-        "size"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "size": {
-          "type": "number",
-          "minimum": 0
-        },
-        "mts": {
-          "type": "integer",
-          "minimum": 0
-        }
-      }
-    },
-    "cpu": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "model": {
-          "type": "string"
-        },
-        "cores": {
-          "type": "integer",
-          "minimum": 1
-        },
-        "threads": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "drive": {
-      "type": "object",
-      "required": [
-        "size"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string",
-          "enum": [
-            "nvme",
-            "ssd",
-            "hdd",
-            "sas",
-            "sata",
-            "usb",
-            "sdcard",
-            "micro-sd"
-          ]
-        },
-        "size": {
-          "type": "number",
-          "minimum": 1
-        }
-      }
-    },
-    "gpu": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "model": {
-          "type": "string"
-        },
-        "vram": {
-          "type": "number",
-          "minimum": 0
-        }
-      }
-    },
-    "nic": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string",
-          "enum": [
-            "rj45",
-            "sfp",
-            "sfp+",
-            "sfp28",
-            "sfp56",
-            "qsfp+",
-            "qsfp28",
-            "qsfp56",
-            "qsfp-dd",
-            "osfp",
-            "xfp",
-            "cx4",
-            "mgmt"
-          ]
-        },
-        "speed": {
-          "type": "number",
-          "minimum": 0
-        },
-        "ports": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "port": {
-      "type": "object",
-      "required": [
-        "type",
-        "speed",
-        "count"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string"
-        },
-        "speed": {
-          "type": "number",
-          "minimum": 0
-        },
-        "count": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "network": {
-      "type": "object",
-      "required": [
-        "ip",
-        "port",
-        "protocol"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "ip": {
-          "type": "string",
-          "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
-        },
-        "port": {
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 65535
-        },
-        "protocol": {
-          "type": "string",
-          "enum": [
-            "TCP",
-            "UDP"
-          ]
-        },
-        "url": {
-          "type": "string",
-          "format": "uri"
-        }
-      }
-    },
-    "server": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Server"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "ipmi": {
-              "type": "boolean"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            },
-            "gpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/gpu"
-              }
-            },
-            "nics": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/nic"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "desktop": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Desktop"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            },
-            "gpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/gpu"
-              }
-            },
-            "nics": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/nic"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "laptop": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Laptop"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "firewall": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Firewall"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "router": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Router"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "switch": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Switch"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "accessPoint": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "AccessPoint"
-            },
-            "model": {
-              "type": "string"
-            },
-            "speed": {
-              "type": "number",
-              "minimum": 0
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "ups": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Ups"
-            },
-            "model": {
-              "type": "string"
-            },
-            "va": {
-              "type": "integer",
-              "minimum": 1
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "service": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "network"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Service"
-            },
-            "network": {
-              "$ref": "#/$defs/network"
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "system": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "type",
-            "os",
-            "cores",
-            "ram"
-          ],
-          "properties": {
-            "kind": {
-              "const": "System"
-            },
-            "type": {
-              "type": "string",
-              "enum": [
-                "baremetal",
-                "Baremetal",
-                "cluster",
-                "Cluster",
-                "hypervisor",
-                "Hypervisor",
-                "vm",
-                "VM",
-                "container",
-                "embedded",
-                "cloud",
-                "other"
-              ]
-            },
-            "ip": {
-              "type": "string"
-            },
-            "os": {
-              "type": "string"
-            },
-            "cores": {
-              "type": "integer",
-              "minimum": 1
-            },
-            "ram": {
-              "type": "number",
-              "minimum": 0
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    }
-  }
-}

+ 0 - 692
Tests/schemas/schema.v3.json

@@ -1,692 +0,0 @@
-{
-  "$schema": "https://json-schema.org/draft/2020-12/schema",
-  "$id": "https://timmoth.github.io/RackPeek/schemas/v3/schema.v3.json",
-  "title": "RackPeek Infrastructure Specification",
-  "type": "object",
-  "additionalProperties": false,
-  "required": [
-    "version",
-    "resources"
-  ],
-  "properties": {
-    "version": {
-      "type": "integer",
-      "const": 3
-    },
-    "resources": {
-      "type": "array",
-      "items": {
-        "$ref": "#/$defs/resource"
-      }
-    },
-    "connections": {
-      "type": [
-        "array",
-        "null"
-      ],
-      "items": {
-        "$ref": "#/$defs/connection"
-      }
-    }
-  },
-  "$defs": {
-    "labels": {
-      "type": "object",
-      "additionalProperties": {
-        "type": "string"
-      }
-    },
-    "runsOn": {
-      "type": [
-        "array",
-        "null"
-      ],
-      "items": {
-        "type": "string",
-        "minLength": 1
-      }
-    },
-    "resourceBase": {
-      "type": "object",
-      "required": [
-        "kind",
-        "name"
-      ],
-      "properties": {
-        "kind": {
-          "type": "string"
-        },
-        "name": {
-          "type": "string",
-          "minLength": 1
-        },
-        "tags": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          },
-          "default": []
-        },
-        "labels": {
-          "$ref": "#/$defs/labels",
-          "default": {}
-        },
-        "notes": {
-          "type": [
-            "string",
-            "null"
-          ]
-        },
-        "runsOn": {
-          "$ref": "#/$defs/runsOn"
-        }
-      }
-    },
-    "resource": {
-      "oneOf": [
-        {
-          "$ref": "#/$defs/server"
-        },
-        {
-          "$ref": "#/$defs/firewall"
-        },
-        {
-          "$ref": "#/$defs/router"
-        },
-        {
-          "$ref": "#/$defs/switch"
-        },
-        {
-          "$ref": "#/$defs/accessPoint"
-        },
-        {
-          "$ref": "#/$defs/ups"
-        },
-        {
-          "$ref": "#/$defs/other"
-        },
-        {
-          "$ref": "#/$defs/desktop"
-        },
-        {
-          "$ref": "#/$defs/laptop"
-        },
-        {
-          "$ref": "#/$defs/service"
-        },
-        {
-          "$ref": "#/$defs/system"
-        }
-      ]
-    },
-    "portReference": {
-      "type": "object",
-      "required": [
-        "resource",
-        "portGroup",
-        "portIndex"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "resource": {
-          "type": "string",
-          "minLength": 1
-        },
-        "portGroup": {
-          "type": "integer",
-          "minimum": 0
-        },
-        "portIndex": {
-          "type": "integer",
-          "minimum": 0
-        }
-      }
-    },
-    "connection": {
-      "type": "object",
-      "required": [
-        "a",
-        "b"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "a": {
-          "$ref": "#/$defs/portReference"
-        },
-        "b": {
-          "$ref": "#/$defs/portReference"
-        },
-        "label": {
-          "type": [
-            "string",
-            "null"
-          ]
-        },
-        "notes": {
-          "type": [
-            "string",
-            "null"
-          ]
-        }
-      }
-    },
-    "ram": {
-      "type": "object",
-      "required": [
-        "size"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "size": {
-          "type": "number",
-          "minimum": 0
-        },
-        "mts": {
-          "type": "integer",
-          "minimum": 0
-        }
-      }
-    },
-    "cpu": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "model": {
-          "type": "string"
-        },
-        "cores": {
-          "type": "integer",
-          "minimum": 1
-        },
-        "threads": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "drive": {
-      "type": "object",
-      "required": [
-        "size"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string",
-          "enum": [
-            "nvme",
-            "ssd",
-            "hdd",
-            "sas",
-            "sata",
-            "usb",
-            "sdcard",
-            "micro-sd"
-          ]
-        },
-        "size": {
-          "type": "number",
-          "minimum": 1
-        }
-      }
-    },
-    "gpu": {
-      "type": "object",
-      "additionalProperties": false,
-      "properties": {
-        "model": {
-          "type": "string"
-        },
-        "vram": {
-          "type": "number",
-          "minimum": 0
-        }
-      }
-    },
-    "port": {
-      "type": "object",
-      "required": [
-        "type",
-        "speed",
-        "count"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "type": {
-          "type": "string",
-          "enum": [
-            "rj45",
-            "sfp",
-            "sfp+",
-            "sfp28",
-            "sfp56",
-            "qsfp+",
-            "qsfp28",
-            "qsfp56",
-            "qsfp-dd",
-            "osfp",
-            "xfp",
-            "cx4",
-            "mgmt"
-          ]
-        },
-        "speed": {
-          "type": "number",
-          "minimum": 0
-        },
-        "count": {
-          "type": "integer",
-          "minimum": 1
-        }
-      }
-    },
-    "network": {
-      "type": "object",
-      "required": [
-        "ip",
-        "port",
-        "protocol"
-      ],
-      "additionalProperties": false,
-      "properties": {
-        "ip": {
-          "type": "string",
-          "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
-        },
-        "port": {
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 65535
-        },
-        "protocol": {
-          "type": "string",
-          "enum": [
-            "TCP",
-            "UDP"
-          ]
-        },
-        "url": {
-          "type": "string",
-          "format": "uri"
-        }
-      }
-    },
-    "server": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Server"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "ipmi": {
-              "type": "boolean"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            },
-            "gpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/gpu"
-              }
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "desktop": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Desktop"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            },
-            "gpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/gpu"
-              }
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "laptop": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Laptop"
-            },
-            "ram": {
-              "$ref": "#/$defs/ram"
-            },
-            "cpus": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/cpu"
-              }
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "firewall": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Firewall"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "router": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Router"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "switch": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "ports"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Switch"
-            },
-            "model": {
-              "type": "string"
-            },
-            "managed": {
-              "type": "boolean"
-            },
-            "poe": {
-              "type": "boolean"
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "accessPoint": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "AccessPoint"
-            },
-            "model": {
-              "type": "string"
-            },
-            "speed": {
-              "type": "number",
-              "minimum": 0
-            },
-            "ports": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/port"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "ups": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Ups"
-            },
-            "model": {
-              "type": "string"
-            },
-            "va": {
-              "type": "integer",
-              "minimum": 1
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "other": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "properties": {
-            "kind": {
-              "const": "Other"
-            },
-            "model": {
-              "type": "string"
-            },
-            "description": {
-              "type": "string"
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "service": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "network"
-          ],
-          "properties": {
-            "kind": {
-              "const": "Service"
-            },
-            "network": {
-              "$ref": "#/$defs/network"
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    },
-    "system": {
-      "allOf": [
-        {
-          "$ref": "#/$defs/resourceBase"
-        },
-        {
-          "type": "object",
-          "required": [
-            "type",
-            "os",
-            "cores",
-            "ram"
-          ],
-          "properties": {
-            "kind": {
-              "const": "System"
-            },
-            "type": {
-              "type": "string",
-              "enum": [
-                "baremetal",
-                "Baremetal",
-                "cluster",
-                "Cluster",
-                "hypervisor",
-                "Hypervisor",
-                "vm",
-                "VM",
-                "container",
-                "embedded",
-                "cloud",
-                "other"
-              ]
-            },
-            "ip": {
-              "type": "string"
-            },
-            "os": {
-              "type": "string"
-            },
-            "cores": {
-              "type": "integer",
-              "minimum": 1
-            },
-            "ram": {
-              "type": "number",
-              "minimum": 0
-            },
-            "drives": {
-              "type": "array",
-              "items": {
-                "$ref": "#/$defs/drive"
-              }
-            }
-          }
-        }
-      ],
-      "unevaluatedProperties": false
-    }
-  }
-}

+ 431 - 143
schemas/v2/schema.v2.json

@@ -4,104 +4,188 @@
   "title": "RackPeek Infrastructure Specification",
   "title": "RackPeek Infrastructure Specification",
   "type": "object",
   "type": "object",
   "additionalProperties": false,
   "additionalProperties": false,
-  "required": ["version", "resources"],
+  "required": [
+    "version",
+    "resources"
+  ],
   "properties": {
   "properties": {
-    "version": { "type": "integer", "const": 2 },
+    "version": {
+      "type": "integer",
+      "const": 2
+    },
     "resources": {
     "resources": {
       "type": "array",
       "type": "array",
-      "items": { "$ref": "#/$defs/resource" }
+      "items": {
+        "$ref": "#/$defs/resource"
+      }
     }
     }
   },
   },
-
   "$defs": {
   "$defs": {
     "labels": {
     "labels": {
       "type": "object",
       "type": "object",
-      "additionalProperties": { "type": "string" }
+      "additionalProperties": {
+        "type": "string"
+      }
     },
     },
-
     "runsOn": {
     "runsOn": {
-      "type": ["array", "null"],
+      "type": [
+        "array",
+        "null"
+      ],
       "items": {
       "items": {
         "type": "string",
         "type": "string",
         "minLength": 1
         "minLength": 1
       }
       }
     },
     },
-
     "resourceBase": {
     "resourceBase": {
       "type": "object",
       "type": "object",
-      "required": ["kind", "name"],
+      "required": [
+        "kind",
+        "name"
+      ],
       "properties": {
       "properties": {
-        "kind": { "type": "string" },
-        "name": { "type": "string", "minLength": 1 },
-
-        "tags": { "type": "array", "items": { "type": "string" }, "default": [] },
-        "labels": { "$ref": "#/$defs/labels", "default": {} },
-        "notes": { "type": ["string", "null"] },
-
+        "kind": {
+          "type": "string"
+        },
+        "name": {
+          "type": "string",
+          "minLength": 1
+        },
+        "tags": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "default": []
+        },
+        "labels": {
+          "$ref": "#/$defs/labels",
+          "default": {}
+        },
+        "notes": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "runsOn": {
         "runsOn": {
-          "type": ["array", "null"],
-          "items": { "type": "string" }
+          "type": [
+            "array",
+            "null"
+          ],
+          "items": {
+            "type": "string"
+          }
         }
         }
       }
       }
     },
     },
-
     "resource": {
     "resource": {
       "oneOf": [
       "oneOf": [
-        { "$ref": "#/$defs/server" },
-        { "$ref": "#/$defs/firewall" },
-        { "$ref": "#/$defs/router" },
-        { "$ref": "#/$defs/switch" },
-        { "$ref": "#/$defs/accessPoint" },
-        { "$ref": "#/$defs/ups" },
-        { "$ref": "#/$defs/desktop" },
-        { "$ref": "#/$defs/laptop" },
-        { "$ref": "#/$defs/service" },
-        { "$ref": "#/$defs/system" }
+        {
+          "$ref": "#/$defs/server"
+        },
+        {
+          "$ref": "#/$defs/firewall"
+        },
+        {
+          "$ref": "#/$defs/router"
+        },
+        {
+          "$ref": "#/$defs/switch"
+        },
+        {
+          "$ref": "#/$defs/accessPoint"
+        },
+        {
+          "$ref": "#/$defs/ups"
+        },
+        {
+          "$ref": "#/$defs/desktop"
+        },
+        {
+          "$ref": "#/$defs/laptop"
+        },
+        {
+          "$ref": "#/$defs/service"
+        },
+        {
+          "$ref": "#/$defs/system"
+        }
       ]
       ]
     },
     },
-
     "ram": {
     "ram": {
       "type": "object",
       "type": "object",
-      "required": ["size"],
+      "required": [
+        "size"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "size": { "type": "number", "minimum": 0 },
-        "mts": { "type": "integer", "minimum": 0 }
+        "size": {
+          "type": "number",
+          "minimum": 0
+        },
+        "mts": {
+          "type": "integer",
+          "minimum": 0
+        }
       }
       }
     },
     },
-
     "cpu": {
     "cpu": {
       "type": "object",
       "type": "object",
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "model": { "type": "string" },
-        "cores": { "type": "integer", "minimum": 1 },
-        "threads": { "type": "integer", "minimum": 1 }
+        "model": {
+          "type": "string"
+        },
+        "cores": {
+          "type": "integer",
+          "minimum": 1
+        },
+        "threads": {
+          "type": "integer",
+          "minimum": 1
+        }
       }
       }
     },
     },
-
     "drive": {
     "drive": {
       "type": "object",
       "type": "object",
-      "required": ["size"],
+      "required": [
+        "size"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
         "type": {
         "type": {
           "type": "string",
           "type": "string",
-          "enum": ["nvme", "ssd", "hdd", "sas", "sata", "usb", "sdcard", "micro-sd"]
+          "enum": [
+            "nvme",
+            "ssd",
+            "hdd",
+            "sas",
+            "sata",
+            "usb",
+            "sdcard",
+            "micro-sd"
+          ]
         },
         },
-        "size": { "type": "number", "minimum": 1 }
+        "size": {
+          "type": "number",
+          "minimum": 1
+        }
       }
       }
     },
     },
-
     "gpu": {
     "gpu": {
       "type": "object",
       "type": "object",
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "model": { "type": "string" },
-        "vram": { "type": "number", "minimum": 0 }
+        "model": {
+          "type": "string"
+        },
+        "vram": {
+          "type": "number",
+          "minimum": 0
+        }
       }
       }
     },
     },
-
     "nic": {
     "nic": {
       "type": "object",
       "type": "object",
       "additionalProperties": false,
       "additionalProperties": false,
@@ -109,224 +193,428 @@
         "type": {
         "type": {
           "type": "string",
           "type": "string",
           "enum": [
           "enum": [
-            "rj45", "sfp", "sfp+", "sfp28", "sfp56",
-            "qsfp+", "qsfp28", "qsfp56", "qsfp-dd",
-            "osfp", "xfp", "cx4", "mgmt"
+            "rj45",
+            "sfp",
+            "sfp+",
+            "sfp28",
+            "sfp56",
+            "qsfp+",
+            "qsfp28",
+            "qsfp56",
+            "qsfp-dd",
+            "osfp",
+            "xfp",
+            "cx4",
+            "mgmt"
           ]
           ]
         },
         },
-        "speed": { "type": "number", "minimum": 0 },
-        "ports": { "type": "integer", "minimum": 1 }
+        "speed": {
+          "type": "number",
+          "minimum": 0
+        },
+        "ports": {
+          "type": "integer",
+          "minimum": 1
+        }
       }
       }
     },
     },
-
     "port": {
     "port": {
       "type": "object",
       "type": "object",
-      "required": ["type", "speed", "count"],
+      "required": [
+        "type",
+        "speed",
+        "count"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "type": { "type": "string" },
-        "speed": { "type": "number", "minimum": 0 },
-        "count": { "type": "integer", "minimum": 1 }
+        "type": {
+          "type": "string"
+        },
+        "speed": {
+          "type": "number",
+          "minimum": 0
+        },
+        "count": {
+          "type": "integer",
+          "minimum": 1
+        }
       }
       }
     },
     },
-
     "network": {
     "network": {
       "type": "object",
       "type": "object",
-      "required": ["ip", "port", "protocol"],
+      "required": [
+        "ip",
+        "port",
+        "protocol"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
         "ip": {
         "ip": {
           "type": "string",
           "type": "string",
           "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
           "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
         },
         },
-        "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
-        "protocol": { "type": "string", "enum": ["TCP", "UDP"] },
-        "url": { "type": "string", "format": "uri" }
+        "port": {
+          "type": "integer",
+          "minimum": 1,
+          "maximum": 65535
+        },
+        "protocol": {
+          "type": "string",
+          "enum": [
+            "TCP",
+            "UDP"
+          ]
+        },
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
       }
       }
     },
     },
-
     "server": {
     "server": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Server" },
-
-            "ram": { "$ref": "#/$defs/ram" },
-            "ipmi": { "type": "boolean" },
-            "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } },
-            "gpus": { "type": "array", "items": { "$ref": "#/$defs/gpu" } },
-            "nics": { "type": "array", "items": { "$ref": "#/$defs/nic" } }
+            "kind": {
+              "const": "Server"
+            },
+            "ram": {
+              "$ref": "#/$defs/ram"
+            },
+            "ipmi": {
+              "type": "boolean"
+            },
+            "cpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/cpu"
+              }
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            },
+            "gpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/gpu"
+              }
+            },
+            "nics": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/nic"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "desktop": {
     "desktop": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Desktop" },
-
-            "ram": { "$ref": "#/$defs/ram" },
-            "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } },
-            "gpus": { "type": "array", "items": { "$ref": "#/$defs/gpu" } },
-            "nics": { "type": "array", "items": { "$ref": "#/$defs/nic" } }
+            "kind": {
+              "const": "Desktop"
+            },
+            "ram": {
+              "$ref": "#/$defs/ram"
+            },
+            "cpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/cpu"
+              }
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            },
+            "gpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/gpu"
+              }
+            },
+            "nics": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/nic"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "laptop": {
     "laptop": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Laptop" },
-
-            "ram": { "$ref": "#/$defs/ram" },
-            "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+            "kind": {
+              "const": "Laptop"
+            },
+            "ram": {
+              "$ref": "#/$defs/ram"
+            },
+            "cpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/cpu"
+              }
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "firewall": {
     "firewall": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["ports"],
+          "required": [
+            "ports"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Firewall" },
-
-            "model": { "type": "string" },
-            "managed": { "type": "boolean" },
-            "poe": { "type": "boolean" },
-            "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+            "kind": {
+              "const": "Firewall"
+            },
+            "model": {
+              "type": "string"
+            },
+            "managed": {
+              "type": "boolean"
+            },
+            "poe": {
+              "type": "boolean"
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "router": {
     "router": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["ports"],
+          "required": [
+            "ports"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Router" },
-
-            "model": { "type": "string" },
-            "managed": { "type": "boolean" },
-            "poe": { "type": "boolean" },
-            "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+            "kind": {
+              "const": "Router"
+            },
+            "model": {
+              "type": "string"
+            },
+            "managed": {
+              "type": "boolean"
+            },
+            "poe": {
+              "type": "boolean"
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "switch": {
     "switch": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["ports"],
+          "required": [
+            "ports"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Switch" },
-
-            "model": { "type": "string" },
-            "managed": { "type": "boolean" },
-            "poe": { "type": "boolean" },
-            "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+            "kind": {
+              "const": "Switch"
+            },
+            "model": {
+              "type": "string"
+            },
+            "managed": {
+              "type": "boolean"
+            },
+            "poe": {
+              "type": "boolean"
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "accessPoint": {
     "accessPoint": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "AccessPoint" },
-
-            "model": { "type": "string" },
-            "speed": { "type": "number", "minimum": 0 }
+            "kind": {
+              "const": "AccessPoint"
+            },
+            "model": {
+              "type": "string"
+            },
+            "speed": {
+              "type": "number",
+              "minimum": 0
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "ups": {
     "ups": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Ups" },
-
-            "model": { "type": "string" },
-            "va": { "type": "integer", "minimum": 1 }
+            "kind": {
+              "const": "Ups"
+            },
+            "model": {
+              "type": "string"
+            },
+            "va": {
+              "type": "integer",
+              "minimum": 1
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "service": {
     "service": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["network"],
+          "required": [
+            "network"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Service" },
-            "network": { "$ref": "#/$defs/network" }
+            "kind": {
+              "const": "Service"
+            },
+            "network": {
+              "$ref": "#/$defs/network"
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "system": {
     "system": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["type", "os", "cores", "ram"],
+          "required": [
+            "type",
+            "os",
+            "cores",
+            "ram"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "System" },
-
+            "kind": {
+              "const": "System"
+            },
             "type": {
             "type": {
               "type": "string",
               "type": "string",
               "enum": [
               "enum": [
-                "baremetal", "Baremetal",
-                "hypervisor", "Hypervisor",
-                "vm", "VM",
-                "container", "embedded", "cloud", "other"
+                "baremetal",
+                "Baremetal",
+                "cluster",
+                "Cluster",
+                "hypervisor",
+                "Hypervisor",
+                "vm",
+                "VM",
+                "container",
+                "embedded",
+                "cloud",
+                "other"
               ]
               ]
             },
             },
-            "os": { "type": "string" },
-            "cores": { "type": "integer", "minimum": 1 },
-            "ram": { "type": "number", "minimum": 0 },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+            "ip": {
+              "type": "string"
+            },
+            "os": {
+              "type": "string"
+            },
+            "cores": {
+              "type": "integer",
+              "minimum": 1
+            },
+            "ram": {
+              "type": "number",
+              "minimum": 0
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            }
           }
           }
         }
         }
       ],
       ],

+ 499 - 159
schemas/v3/schema.v3.json

@@ -1,353 +1,693 @@
 {
 {
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "$schema": "https://json-schema.org/draft/2020-12/schema",
-  "$id": "https://timmoth.github.io/RackPeek/schemas/v2/schema.v2.json",
+  "$id": "https://timmoth.github.io/RackPeek/schemas/v3/schema.v3.json",
   "title": "RackPeek Infrastructure Specification",
   "title": "RackPeek Infrastructure Specification",
   "type": "object",
   "type": "object",
   "additionalProperties": false,
   "additionalProperties": false,
-  "required": ["version", "resources"],
+  "required": [
+    "version",
+    "resources"
+  ],
   "properties": {
   "properties": {
-    "version": { "type": "integer", "const": 2 },
+    "version": {
+      "type": "integer",
+      "const": 3
+    },
     "resources": {
     "resources": {
       "type": "array",
       "type": "array",
-      "items": { "$ref": "#/$defs/resource" }
+      "items": {
+        "$ref": "#/$defs/resource"
+      }
+    },
+    "connections": {
+      "type": [
+        "array",
+        "null"
+      ],
+      "items": {
+        "$ref": "#/$defs/connection"
+      }
     }
     }
   },
   },
-
   "$defs": {
   "$defs": {
     "labels": {
     "labels": {
       "type": "object",
       "type": "object",
-      "additionalProperties": { "type": "string" }
+      "additionalProperties": {
+        "type": "string"
+      }
     },
     },
-
     "runsOn": {
     "runsOn": {
-      "type": ["array", "null"],
+      "type": [
+        "array",
+        "null"
+      ],
       "items": {
       "items": {
         "type": "string",
         "type": "string",
         "minLength": 1
         "minLength": 1
       }
       }
     },
     },
-
     "resourceBase": {
     "resourceBase": {
       "type": "object",
       "type": "object",
-      "required": ["kind", "name"],
+      "required": [
+        "kind",
+        "name"
+      ],
       "properties": {
       "properties": {
-        "kind": { "type": "string" },
-        "name": { "type": "string", "minLength": 1 },
-
-        "tags": { "type": "array", "items": { "type": "string" }, "default": [] },
-        "labels": { "$ref": "#/$defs/labels", "default": {} },
-        "notes": { "type": ["string", "null"] },
-
+        "kind": {
+          "type": "string"
+        },
+        "name": {
+          "type": "string",
+          "minLength": 1
+        },
+        "tags": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "default": []
+        },
+        "labels": {
+          "$ref": "#/$defs/labels",
+          "default": {}
+        },
+        "notes": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "runsOn": {
         "runsOn": {
-          "type": ["array", "null"],
-          "items": { "type": "string" }
+          "$ref": "#/$defs/runsOn"
         }
         }
       }
       }
     },
     },
-
     "resource": {
     "resource": {
       "oneOf": [
       "oneOf": [
-        { "$ref": "#/$defs/server" },
-        { "$ref": "#/$defs/firewall" },
-        { "$ref": "#/$defs/router" },
-        { "$ref": "#/$defs/switch" },
-        { "$ref": "#/$defs/accessPoint" },
-        { "$ref": "#/$defs/ups" },
-        { "$ref": "#/$defs/other" },
-        { "$ref": "#/$defs/desktop" },
-        { "$ref": "#/$defs/laptop" },
-        { "$ref": "#/$defs/service" },
-        { "$ref": "#/$defs/system" }
+        {
+          "$ref": "#/$defs/server"
+        },
+        {
+          "$ref": "#/$defs/firewall"
+        },
+        {
+          "$ref": "#/$defs/router"
+        },
+        {
+          "$ref": "#/$defs/switch"
+        },
+        {
+          "$ref": "#/$defs/accessPoint"
+        },
+        {
+          "$ref": "#/$defs/ups"
+        },
+        {
+          "$ref": "#/$defs/other"
+        },
+        {
+          "$ref": "#/$defs/desktop"
+        },
+        {
+          "$ref": "#/$defs/laptop"
+        },
+        {
+          "$ref": "#/$defs/service"
+        },
+        {
+          "$ref": "#/$defs/system"
+        }
       ]
       ]
     },
     },
-
-    "ram": {
+    "portReference": {
       "type": "object",
       "type": "object",
-      "required": ["size"],
+      "required": [
+        "resource",
+        "portGroup",
+        "portIndex"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "size": { "type": "number", "minimum": 0 },
-        "mts": { "type": "integer", "minimum": 0 }
+        "resource": {
+          "type": "string",
+          "minLength": 1
+        },
+        "portGroup": {
+          "type": "integer",
+          "minimum": 0
+        },
+        "portIndex": {
+          "type": "integer",
+          "minimum": 0
+        }
       }
       }
     },
     },
-
-    "cpu": {
+    "connection": {
       "type": "object",
       "type": "object",
+      "required": [
+        "a",
+        "b"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "model": { "type": "string" },
-        "cores": { "type": "integer", "minimum": 1 },
-        "threads": { "type": "integer", "minimum": 1 }
+        "a": {
+          "$ref": "#/$defs/portReference"
+        },
+        "b": {
+          "$ref": "#/$defs/portReference"
+        },
+        "label": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "notes": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
       }
       }
     },
     },
-
-    "drive": {
+    "ram": {
       "type": "object",
       "type": "object",
-      "required": ["size"],
+      "required": [
+        "size"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "type": {
-          "type": "string",
-          "enum": ["nvme", "ssd", "hdd", "sas", "sata", "usb", "sdcard", "micro-sd"]
+        "size": {
+          "type": "number",
+          "minimum": 0
         },
         },
-        "size": { "type": "number", "minimum": 1 }
+        "mts": {
+          "type": "integer",
+          "minimum": 0
+        }
       }
       }
     },
     },
-
-    "gpu": {
+    "cpu": {
       "type": "object",
       "type": "object",
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "model": { "type": "string" },
-        "vram": { "type": "number", "minimum": 0 }
+        "model": {
+          "type": "string"
+        },
+        "cores": {
+          "type": "integer",
+          "minimum": 1
+        },
+        "threads": {
+          "type": "integer",
+          "minimum": 1
+        }
       }
       }
     },
     },
-
-    "nic": {
+    "drive": {
       "type": "object",
       "type": "object",
+      "required": [
+        "size"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
         "type": {
         "type": {
           "type": "string",
           "type": "string",
           "enum": [
           "enum": [
-            "rj45", "sfp", "sfp+", "sfp28", "sfp56",
-            "qsfp+", "qsfp28", "qsfp56", "qsfp-dd",
-            "osfp", "xfp", "cx4", "mgmt"
+            "nvme",
+            "ssd",
+            "hdd",
+            "sas",
+            "sata",
+            "usb",
+            "sdcard",
+            "micro-sd"
           ]
           ]
         },
         },
-        "speed": { "type": "number", "minimum": 0 },
-        "ports": { "type": "integer", "minimum": 1 }
+        "size": {
+          "type": "number",
+          "minimum": 1
+        }
+      }
+    },
+    "gpu": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "model": {
+          "type": "string"
+        },
+        "vram": {
+          "type": "number",
+          "minimum": 0
+        }
       }
       }
     },
     },
-
     "port": {
     "port": {
       "type": "object",
       "type": "object",
-      "required": ["type", "speed", "count"],
+      "required": [
+        "type",
+        "speed",
+        "count"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
-        "type": { "type": "string" },
-        "speed": { "type": "number", "minimum": 0 },
-        "count": { "type": "integer", "minimum": 1 }
+        "type": {
+          "type": "string",
+          "enum": [
+            "rj45",
+            "sfp",
+            "sfp+",
+            "sfp28",
+            "sfp56",
+            "qsfp+",
+            "qsfp28",
+            "qsfp56",
+            "qsfp-dd",
+            "osfp",
+            "xfp",
+            "cx4",
+            "mgmt"
+          ]
+        },
+        "speed": {
+          "type": "number",
+          "minimum": 0
+        },
+        "count": {
+          "type": "integer",
+          "minimum": 1
+        }
       }
       }
     },
     },
-
     "network": {
     "network": {
       "type": "object",
       "type": "object",
-      "required": ["ip", "port", "protocol"],
+      "required": [
+        "ip",
+        "port",
+        "protocol"
+      ],
       "additionalProperties": false,
       "additionalProperties": false,
       "properties": {
       "properties": {
         "ip": {
         "ip": {
           "type": "string",
           "type": "string",
           "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
           "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
         },
         },
-        "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
-        "protocol": { "type": "string", "enum": ["TCP", "UDP"] },
-        "url": { "type": "string", "format": "uri" }
+        "port": {
+          "type": "integer",
+          "minimum": 1,
+          "maximum": 65535
+        },
+        "protocol": {
+          "type": "string",
+          "enum": [
+            "TCP",
+            "UDP"
+          ]
+        },
+        "url": {
+          "type": "string",
+          "format": "uri"
+        }
       }
       }
     },
     },
-
     "server": {
     "server": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Server" },
-
-            "ram": { "$ref": "#/$defs/ram" },
-            "ipmi": { "type": "boolean" },
-            "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } },
-            "gpus": { "type": "array", "items": { "$ref": "#/$defs/gpu" } },
-            "nics": { "type": "array", "items": { "$ref": "#/$defs/nic" } }
+            "kind": {
+              "const": "Server"
+            },
+            "ram": {
+              "$ref": "#/$defs/ram"
+            },
+            "ipmi": {
+              "type": "boolean"
+            },
+            "cpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/cpu"
+              }
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            },
+            "gpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/gpu"
+              }
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "desktop": {
     "desktop": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Desktop" },
-
-            "ram": { "$ref": "#/$defs/ram" },
-            "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } },
-            "gpus": { "type": "array", "items": { "$ref": "#/$defs/gpu" } },
-            "nics": { "type": "array", "items": { "$ref": "#/$defs/nic" } }
+            "kind": {
+              "const": "Desktop"
+            },
+            "ram": {
+              "$ref": "#/$defs/ram"
+            },
+            "cpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/cpu"
+              }
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            },
+            "gpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/gpu"
+              }
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "laptop": {
     "laptop": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Laptop" },
-
-            "ram": { "$ref": "#/$defs/ram" },
-            "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+            "kind": {
+              "const": "Laptop"
+            },
+            "ram": {
+              "$ref": "#/$defs/ram"
+            },
+            "cpus": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/cpu"
+              }
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "firewall": {
     "firewall": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["ports"],
+          "required": [
+            "ports"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Firewall" },
-
-            "model": { "type": "string" },
-            "managed": { "type": "boolean" },
-            "poe": { "type": "boolean" },
-            "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+            "kind": {
+              "const": "Firewall"
+            },
+            "model": {
+              "type": "string"
+            },
+            "managed": {
+              "type": "boolean"
+            },
+            "poe": {
+              "type": "boolean"
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "router": {
     "router": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["ports"],
+          "required": [
+            "ports"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Router" },
-
-            "model": { "type": "string" },
-            "managed": { "type": "boolean" },
-            "poe": { "type": "boolean" },
-            "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+            "kind": {
+              "const": "Router"
+            },
+            "model": {
+              "type": "string"
+            },
+            "managed": {
+              "type": "boolean"
+            },
+            "poe": {
+              "type": "boolean"
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "switch": {
     "switch": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["ports"],
+          "required": [
+            "ports"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Switch" },
-
-            "model": { "type": "string" },
-            "managed": { "type": "boolean" },
-            "poe": { "type": "boolean" },
-            "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+            "kind": {
+              "const": "Switch"
+            },
+            "model": {
+              "type": "string"
+            },
+            "managed": {
+              "type": "boolean"
+            },
+            "poe": {
+              "type": "boolean"
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "accessPoint": {
     "accessPoint": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "AccessPoint" },
-
-            "model": { "type": "string" },
-            "speed": { "type": "number", "minimum": 0 }
+            "kind": {
+              "const": "AccessPoint"
+            },
+            "model": {
+              "type": "string"
+            },
+            "speed": {
+              "type": "number",
+              "minimum": 0
+            },
+            "ports": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/port"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "ups": {
     "ups": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Ups" },
-
-            "model": { "type": "string" },
-            "va": { "type": "integer", "minimum": 1 }
+            "kind": {
+              "const": "Ups"
+            },
+            "model": {
+              "type": "string"
+            },
+            "va": {
+              "type": "integer",
+              "minimum": 1
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "other": {
     "other": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
           "properties": {
           "properties": {
-            "kind": { "const": "Other" },
-
-            "model": { "type": "string" },
-            "description": { "type": "string" }
+            "kind": {
+              "const": "Other"
+            },
+            "model": {
+              "type": "string"
+            },
+            "description": {
+              "type": "string"
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "service": {
     "service": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["network"],
+          "required": [
+            "network"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "Service" },
-            "network": { "$ref": "#/$defs/network" }
+            "kind": {
+              "const": "Service"
+            },
+            "network": {
+              "$ref": "#/$defs/network"
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     },
     },
-
     "system": {
     "system": {
       "allOf": [
       "allOf": [
-        { "$ref": "#/$defs/resourceBase" },
+        {
+          "$ref": "#/$defs/resourceBase"
+        },
         {
         {
           "type": "object",
           "type": "object",
-          "required": ["type", "os", "cores", "ram"],
+          "required": [
+            "type",
+            "os",
+            "cores",
+            "ram"
+          ],
           "properties": {
           "properties": {
-            "kind": { "const": "System" },
-
+            "kind": {
+              "const": "System"
+            },
             "type": {
             "type": {
               "type": "string",
               "type": "string",
               "enum": [
               "enum": [
-                "baremetal", "Baremetal",
-                "hypervisor", "Hypervisor",
-                "vm", "VM",
-                "container", "embedded", "cloud", "other"
+                "baremetal",
+                "Baremetal",
+                "cluster",
+                "Cluster",
+                "hypervisor",
+                "Hypervisor",
+                "vm",
+                "VM",
+                "container",
+                "embedded",
+                "cloud",
+                "other"
               ]
               ]
             },
             },
-            "os": { "type": "string" },
-            "cores": { "type": "integer", "minimum": 1 },
-            "ram": { "type": "number", "minimum": 0 },
-            "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+            "ip": {
+              "type": "string",
+              "pattern": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$"
+            },
+            "os": {
+              "type": "string"
+            },
+            "cores": {
+              "type": "integer",
+              "minimum": 1
+            },
+            "ram": {
+              "type": "number",
+              "minimum": 0
+            },
+            "drives": {
+              "type": "array",
+              "items": {
+                "$ref": "#/$defs/drive"
+              }
+            }
           }
           }
         }
         }
       ],
       ],
       "unevaluatedProperties": false
       "unevaluatedProperties": false
     }
     }
   }
   }
-}
+}