Browse Source

Migrate nics -> ports in v3 schema

Tim Jones 1 month ago
parent
commit
bca57ce48f
47 changed files with 2977 additions and 217 deletions
  1. 58 1
      RackPeek.Domain/Persistence/Yaml/RackPeekConfigMigrationDeserializer.cs
  2. 1 1
      RackPeek.Domain/Resources/Desktops/DescribeDesktopUseCase.cs
  3. 2 2
      RackPeek.Domain/Resources/Desktops/Desktop.cs
  4. 3 3
      RackPeek.Domain/Resources/Desktops/DesktopHardwareReport.cs
  5. 1 1
      RackPeek.Domain/Resources/Servers/DescribeServerUseCase.cs
  6. 0 7
      RackPeek.Domain/Resources/Servers/INicResource.cs
  7. 2 2
      RackPeek.Domain/Resources/Servers/Server.cs
  8. 6 6
      RackPeek.Domain/Resources/Servers/ServerHardwareReport.cs
  9. 0 5
      RackPeek.Domain/ServiceCollectionExtensions.cs
  10. 0 46
      RackPeek.Domain/UseCases/Nics/AddNicUseCase.cs
  11. 0 29
      RackPeek.Domain/UseCases/Nics/RemoveNicUseCase.cs
  12. 0 50
      RackPeek.Domain/UseCases/Nics/UpdateNicUseCase.cs
  13. 624 0
      RackPeek.Web.Viewer/wwwroot/schemas/v3/schema.v3.json
  14. 607 0
      RackPeek.Web/wwwroot/schemas/v3/schema.v3.json
  15. 1 1
      Shared.Rcl/Commands/Desktops/DesktopGetCommand.cs
  16. 2 2
      Shared.Rcl/Commands/Desktops/Nics/DesktopNicAddCommand.cs
  17. 2 2
      Shared.Rcl/Commands/Desktops/Nics/DesktopNicRemoveCommand.cs
  18. 2 2
      Shared.Rcl/Commands/Desktops/Nics/DesktopNicSetCommand.cs
  19. 2 2
      Shared.Rcl/Commands/Servers/Nics/ServerNicAddCommand.cs
  20. 2 2
      Shared.Rcl/Commands/Servers/Nics/ServerNicRemoveCommand.cs
  21. 2 2
      Shared.Rcl/Commands/Servers/Nics/ServerNicUpdateCommand.cs
  22. 17 17
      Shared.Rcl/Desktops/DesktopCardComponent.razor
  23. 17 17
      Shared.Rcl/Servers/ServerCardComponent.razor
  24. 3 3
      Tests/Api/InventoryEndpointTests.cs
  25. 2 2
      Tests/EndToEnd/AccessPointTests/AccessPointWorkflowTests.cs
  26. 2 2
      Tests/EndToEnd/FirewallTests/FirewallWorkflowTests.cs
  27. 2 2
      Tests/EndToEnd/RouterTests/RouterWorkflowTests.cs
  28. 1 1
      Tests/EndToEnd/ServerTests/ServerWorkflowTests.cs
  29. 1 1
      Tests/EndToEnd/ServiceTests/ServiceWorkflowTests.cs
  30. 2 2
      Tests/EndToEnd/SwitchTests/SwitchWorkflowTests.cs
  31. 2 2
      Tests/EndToEnd/SystemTests/SystemWorkflowTests.cs
  32. 2 2
      Tests/EndToEnd/UpsTests/UpsWorkflowtests.cs
  33. 34 0
      Tests/TestConfigs/v3/01-server.yaml
  34. 17 0
      Tests/TestConfigs/v3/02-firewall.yaml
  35. 17 0
      Tests/TestConfigs/v3/03-router.yaml
  36. 17 0
      Tests/TestConfigs/v3/04-switch.yaml
  37. 11 0
      Tests/TestConfigs/v3/05-accesspoint.yaml
  38. 11 0
      Tests/TestConfigs/v3/06-ups.yaml
  39. 25 0
      Tests/TestConfigs/v3/07-desktop.yaml
  40. 18 0
      Tests/TestConfigs/v3/08-laptop.yaml
  41. 12 0
      Tests/TestConfigs/v3/09-service.yaml
  42. 16 0
      Tests/TestConfigs/v3/10-system.yaml
  43. 454 0
      Tests/TestConfigs/v3/11-demo-config.yaml
  44. 33 0
      Tests/Tests.csproj
  45. 1 0
      Tests/Yaml/SchemaTests.cs
  46. 607 0
      Tests/schemas/schema.v3.json
  47. 336 0
      schemas/v3/schema.v3.json

+ 58 - 1
RackPeek.Domain/Persistence/Yaml/RackPeekConfigMigrationDeserializer.cs

@@ -22,7 +22,8 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
         ListOfMigrations = new List<Func<IServiceProvider, Dictionary<object, object>, ValueTask>>
         {
             EnsureSchemaVersionExists,
-            ConvertScalarRunsOnToList
+            ConvertScalarRunsOnToList,
+            ConvertNicsToPortsV3
         };
 
     public RackPeekConfigMigrationDeserializer(IServiceProvider serviceProvider,
@@ -110,5 +111,61 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
         return ValueTask.CompletedTask;
     }
 
+    public static ValueTask ConvertNicsToPortsV3(
+        IServiceProvider serviceProvider,
+        Dictionary<object, object> obj) {
+        if (!obj.TryGetValue("resources", out var resourcesObj))
+            return ValueTask.CompletedTask;
+
+        if (resourcesObj is not List<object> resources)
+            return ValueTask.CompletedTask;
+
+        foreach (var resourceObj in resources) {
+            if (resourceObj is not Dictionary<object, object> resourceDict)
+                continue;
+
+            if (!resourceDict.TryGetValue("nics", out var nicsObj))
+                continue;
+
+            if (nicsObj is not List<object> nics)
+                continue;
+
+            var ports = new List<Dictionary<object, object>>();
+
+            foreach (var nicObj in nics) {
+                if (nicObj is not Dictionary<object, object> nicDict)
+                    continue;
+
+                var port = new Dictionary<object, object>();
+
+                if (nicDict.TryGetValue("type", out var type))
+                    port["type"] = type;
+
+                if (nicDict.TryGetValue("speed", out var speed))
+                    port["speed"] = speed;
+
+                if (nicDict.TryGetValue("ports", out var portCount))
+                    port["count"] = portCount;
+
+                ports.Add(port);
+            }
+
+            resourceDict.Remove("nics");
+
+            if (resourceDict.TryGetValue("ports", out var existingPortsObj)
+                && existingPortsObj is List<object> existingPorts) {
+                foreach (Dictionary<object, object> p in ports)
+                    existingPorts.Add(p);
+            }
+            else {
+                resourceDict["ports"] = ports.Cast<object>().ToList();
+            }
+        }
+
+        obj["version"] = 3;
+
+        return ValueTask.CompletedTask;
+    }
+
     #endregion
 }

+ 1 - 1
RackPeek.Domain/Resources/Desktops/DescribeDesktopUseCase.cs

@@ -33,7 +33,7 @@ public class DescribeDesktopUseCase(IResourceCollection repository) : IUseCase {
             desktop.Cpus?.Count ?? 0,
             ramSummary,
             desktop.Drives?.Count ?? 0,
-            desktop.Nics?.Count ?? 0,
+            desktop.Ports?.Count ?? 0,
             desktop.Gpus?.Count ?? 0,
             desktop.Labels
         );

+ 2 - 2
RackPeek.Domain/Resources/Desktops/Desktop.cs

@@ -3,12 +3,12 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Desktops;
 
-public class Desktop : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, INicResource {
+public class Desktop : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, IPortResource {
     public const string KindLabel = "Desktop";
     public Ram? Ram { get; set; }
     public string? Model { get; set; }
     public List<Cpu>? Cpus { get; set; }
     public List<Drive>? Drives { get; set; }
     public List<Gpu>? Gpus { get; set; }
-    public List<Nic>? Nics { get; set; }
+    public List<Port>? Ports { get; set; }
 }

+ 3 - 3
RackPeek.Domain/Resources/Desktops/DesktopHardwareReport.cs

@@ -44,14 +44,14 @@ public class DesktopHardwareReportUseCase(IResourceCollection repository) : IUse
                 .Where(d => d.Type == "hdd")
                 .Sum(d => d.Size) ?? 0;
 
-            var nicSummary = desktop.Nics == null
+            var nicSummary = desktop.Ports == null
                 ? "Unknown"
                 : string.Join(", ",
-                    desktop.Nics
+                    desktop.Ports
                         .GroupBy(n => n.Speed ?? 0)
                         .OrderBy(g => g.Key)
                         .Select(g => {
-                            var count = g.Sum(n => n.Ports ?? 0);
+                            var count = g.Sum(n => n.Count ?? 0);
                             return $"{count}×{g.Key}G";
                         }));
 

+ 1 - 1
RackPeek.Domain/Resources/Servers/DescribeServerUseCase.cs

@@ -37,7 +37,7 @@ public class DescribeServerUseCase(IResourceCollection repository) : IUseCase {
             server.Cpus?.Sum(c => c.Threads) ?? 0,
             server.Ram?.Size ?? 0,
             server.Drives?.Sum(d => d.Size) ?? 0,
-            server.Nics?.Sum(n => n.Ports) ?? 0,
+            server.Ports?.Sum(n => n.Count) ?? 0,
             server.Ipmi ?? false
         );
     }

+ 0 - 7
RackPeek.Domain/Resources/Servers/INicResource.cs

@@ -1,7 +0,0 @@
-using RackPeek.Domain.Resources.SubResources;
-
-namespace RackPeek.Domain.Resources.Servers;
-
-public interface INicResource {
-    public List<Nic>? Nics { get; set; }
-}

+ 2 - 2
RackPeek.Domain/Resources/Servers/Server.cs

@@ -2,12 +2,12 @@ using RackPeek.Domain.Resources.SubResources;
 
 namespace RackPeek.Domain.Resources.Servers;
 
-public class Server : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, INicResource {
+public class Server : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResource, IPortResource {
     public const string KindLabel = "Server";
     public Ram? Ram { get; set; }
     public bool? Ipmi { get; set; }
     public List<Cpu>? Cpus { get; set; }
     public List<Drive>? Drives { get; set; }
     public List<Gpu>? Gpus { get; set; }
-    public List<Nic>? Nics { get; set; }
+    public List<Port>? Ports { get; set; }
 }

+ 6 - 6
RackPeek.Domain/Resources/Servers/ServerHardwareReport.cs

@@ -22,13 +22,13 @@ public record ServerHardwareRow(
     int TotalGpuVramGb,
     string GpuSummary,
     bool Ipmi,
-    IReadOnlyList<Nic> Nics
+    IReadOnlyList<Port> Ports
 ) {
     public string NicSummary =>
         string.Join(", ",
-            (Nics ?? [])
+            (Ports ?? [])
             .SelectMany(n => {
-                var ports = n.Ports ?? 1;
+                var ports = n.Count ?? 1;
                 var speed = n.Speed ?? 0;
                 return Enumerable.Repeat(speed, ports);
             })
@@ -64,8 +64,8 @@ public class ServerHardwareReportUseCase(IResourceCollection repository) : IUseC
                 .Where(d => d.Type == "hdd")
                 .Sum(d => d.Size) ?? 0;
 
-            var totalNicPorts = server.Nics?.Sum(n => n.Ports) ?? 0;
-            var maxNicSpeed = server.Nics?.Max(n => n.Speed) ?? 0;
+            var totalNicPorts = server.Ports?.Sum(n => n.Count) ?? 0;
+            var maxNicSpeed = server.Ports?.Max(n => n.Speed) ?? 0;
 
             var gpuCount = server.Gpus?.Count ?? 0;
 
@@ -95,7 +95,7 @@ public class ServerHardwareReportUseCase(IResourceCollection repository) : IUseC
                 totalGpuVram,
                 gpuSummary,
                 server.Ipmi ?? false,
-                server.Nics ?? new List<Nic>()
+                server.Ports ?? new List<Port>()
             );
         }).ToList();
 

+ 0 - 5
RackPeek.Domain/ServiceCollectionExtensions.cs

@@ -10,7 +10,6 @@ using RackPeek.Domain.UseCases.Cpus;
 using RackPeek.Domain.UseCases.Drives;
 using RackPeek.Domain.UseCases.Gpus;
 using RackPeek.Domain.UseCases.Labels;
-using RackPeek.Domain.UseCases.Nics;
 using RackPeek.Domain.UseCases.Ports;
 using RackPeek.Domain.UseCases.Tags;
 
@@ -71,10 +70,6 @@ public static class ServiceCollectionExtensions {
         services.AddScoped(typeof(IRemovePortUseCase<>), typeof(RemovePortUseCase<>));
         services.AddScoped(typeof(IUpdatePortUseCase<>), typeof(UpdatePortUseCase<>));
 
-        services.AddScoped(typeof(IAddNicUseCase<>), typeof(AddNicUseCase<>));
-        services.AddScoped(typeof(IRemoveNicUseCase<>), typeof(RemoveNicUseCase<>));
-        services.AddScoped(typeof(IUpdateNicUseCase<>), typeof(UpdateNicUseCase<>));
-
         IEnumerable<Type>? usecases = Assembly.GetAssembly(typeof(IUseCase))
             ?.GetTypes()
             .Where(t =>

+ 0 - 46
RackPeek.Domain/UseCases/Nics/AddNicUseCase.cs

@@ -1,46 +0,0 @@
-using RackPeek.Domain.Helpers;
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Resources;
-using RackPeek.Domain.Resources.Servers;
-using RackPeek.Domain.Resources.SubResources;
-
-namespace RackPeek.Domain.UseCases.Nics;
-
-public interface IAddNicUseCase<T> : IResourceUseCase<T>
-    where T : Resource {
-    public Task ExecuteAsync(
-        string name,
-        string? type,
-        double? speed,
-        int? ports);
-}
-
-public class AddNicUseCase<T>(IResourceCollection repository) : IAddNicUseCase<T> where T : Resource {
-    public async Task ExecuteAsync(
-        string name,
-        string? type,
-        double? speed,
-        int? ports) {
-        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
-        // ToDo validate / normalize all inputs
-
-        name = Normalize.HardwareName(name);
-        ThrowIfInvalid.ResourceName(name);
-
-        var nicType = Normalize.NicType(type ?? string.Empty);
-        ThrowIfInvalid.NicType(nicType);
-
-        T resource = await repository.GetByNameAsync<T>(name) ??
-                     throw new NotFoundException($"Resource '{name}' not found.");
-
-        if (resource is not INicResource nr) throw new NotFoundException($"Resource '{name}' not found.");
-
-        nr.Nics ??= new List<Nic>();
-        nr.Nics.Add(new Nic {
-            Type = nicType,
-            Speed = speed,
-            Ports = ports
-        });
-        await repository.UpdateAsync(resource);
-    }
-}

+ 0 - 29
RackPeek.Domain/UseCases/Nics/RemoveNicUseCase.cs

@@ -1,29 +0,0 @@
-using RackPeek.Domain.Helpers;
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Resources;
-using RackPeek.Domain.Resources.Servers;
-
-namespace RackPeek.Domain.UseCases.Nics;
-
-public interface IRemoveNicUseCase<T> : IResourceUseCase<T>
-    where T : Resource {
-    public Task ExecuteAsync(string name, int index);
-}
-
-public class RemoveNicUseCase<T>(IResourceCollection repository) : IRemoveNicUseCase<T> where T : Resource {
-    public async Task ExecuteAsync(string name, int index) {
-        name = Normalize.HardwareName(name);
-        ThrowIfInvalid.ResourceName(name);
-        T resource = await repository.GetByNameAsync<T>(name) ??
-                     throw new NotFoundException($"Resource '{name}' not found.");
-
-        if (resource is not INicResource nr) throw new NotFoundException($"Resource '{name}' not found.");
-
-        if (nr.Nics == null || index < 0 || index >= nr.Nics.Count)
-            throw new NotFoundException($"NIC index {index} not found on desktop '{name}'.");
-
-        nr.Nics.RemoveAt(index);
-
-        await repository.UpdateAsync(resource);
-    }
-}

+ 0 - 50
RackPeek.Domain/UseCases/Nics/UpdateNicUseCase.cs

@@ -1,50 +0,0 @@
-using RackPeek.Domain.Helpers;
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Resources;
-using RackPeek.Domain.Resources.Servers;
-using RackPeek.Domain.Resources.SubResources;
-
-namespace RackPeek.Domain.UseCases.Nics;
-
-public interface IUpdateNicUseCase<T> : IResourceUseCase<T>
-    where T : Resource {
-    public Task ExecuteAsync(
-        string name,
-        int index,
-        string? type,
-        double? speed,
-        int? ports);
-}
-
-public class UpdateNicUseCase<T>(IResourceCollection repository) : IUpdateNicUseCase<T> where T : Resource {
-    public async Task ExecuteAsync(
-        string name,
-        int index,
-        string? type,
-        double? speed,
-        int? ports) {
-        // ToDo pass in properties as inputs, construct the entity in the usecase, ensure optional inputs are nullable
-        // ToDo validate / normalize all inputs
-
-        name = Normalize.HardwareName(name);
-        ThrowIfInvalid.ResourceName(name);
-
-        var nicType = Normalize.NicType(type ?? string.Empty);
-        ThrowIfInvalid.NicType(nicType);
-
-        T resource = await repository.GetByNameAsync<T>(name) ??
-                     throw new NotFoundException($"Resource '{name}' not found.");
-
-        if (resource is not INicResource nr) throw new NotFoundException($"Resource '{name}' not found.");
-
-        if (nr.Nics == null || index < 0 || index >= nr.Nics.Count)
-            throw new NotFoundException($"NIC index {index} not found on desktop '{name}'.");
-
-        Nic nic = nr.Nics[index];
-        nic.Type = nicType;
-        nic.Speed = speed;
-        nic.Ports = ports;
-
-        await repository.UpdateAsync(resource);
-    }
-}

+ 624 - 0
RackPeek.Web.Viewer/wwwroot/schemas/v3/schema.v3.json

@@ -0,0 +1,624 @@
+{
+  "$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
+    }
+  }
+}

+ 607 - 0
RackPeek.Web/wwwroot/schemas/v3/schema.v3.json

@@ -0,0 +1,607 @@
+{
+  "$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"
+      }
+    }
+  },
+  "$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
+        }
+      }
+    },
+    "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
+            }
+          }
+        }
+      ],
+      "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
+    }
+  }
+}

+ 1 - 1
Shared.Rcl/Commands/Desktops/DesktopGetCommand.cs

@@ -39,7 +39,7 @@ public class DesktopGetCommand(IServiceProvider provider)
                 (d.Cpus?.Count ?? 0).ToString(),
                 d.Ram == null ? "None" : $"{d.Ram.Size}GB",
                 (d.Drives?.Count ?? 0).ToString(),
-                (d.Nics?.Count ?? 0).ToString(),
+                (d.Ports?.Count ?? 0).ToString(),
                 (d.Gpus?.Count ?? 0).ToString()
             );
 

+ 2 - 2
Shared.Rcl/Commands/Desktops/Nics/DesktopNicAddCommand.cs

@@ -1,7 +1,7 @@
 using System.ComponentModel;
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.Desktops;
-using RackPeek.Domain.UseCases.Nics;
+using RackPeek.Domain.UseCases.Ports;
 using Spectre.Console;
 using Spectre.Console.Cli;
 
@@ -32,7 +32,7 @@ public class DesktopNicAddCommand(IServiceProvider provider)
         DesktopNicAddSettings settings,
         CancellationToken cancellationToken) {
         using IServiceScope scope = provider.CreateScope();
-        IAddNicUseCase<Desktop> useCase = scope.ServiceProvider.GetRequiredService<IAddNicUseCase<Desktop>>();
+        IAddPortUseCase<Desktop> useCase = scope.ServiceProvider.GetRequiredService<IAddPortUseCase<Desktop>>();
 
         await useCase.ExecuteAsync(settings.DesktopName, settings.Type, settings.Speed, settings.Ports);
 

+ 2 - 2
Shared.Rcl/Commands/Desktops/Nics/DesktopNicRemoveCommand.cs

@@ -1,7 +1,7 @@
 using System.ComponentModel;
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.Desktops;
-using RackPeek.Domain.UseCases.Nics;
+using RackPeek.Domain.UseCases.Ports;
 using Spectre.Console;
 using Spectre.Console.Cli;
 
@@ -24,7 +24,7 @@ public class DesktopNicRemoveCommand(IServiceProvider provider)
         DesktopNicRemoveSettings settings,
         CancellationToken cancellationToken) {
         using IServiceScope scope = provider.CreateScope();
-        IRemoveNicUseCase<Desktop> useCase = scope.ServiceProvider.GetRequiredService<IRemoveNicUseCase<Desktop>>();
+        IRemovePortUseCase<Desktop> useCase = scope.ServiceProvider.GetRequiredService<IRemovePortUseCase<Desktop>>();
 
         await useCase.ExecuteAsync(settings.DesktopName, settings.Index);
 

+ 2 - 2
Shared.Rcl/Commands/Desktops/Nics/DesktopNicSetCommand.cs

@@ -1,7 +1,7 @@
 using System.ComponentModel;
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.Desktops;
-using RackPeek.Domain.UseCases.Nics;
+using RackPeek.Domain.UseCases.Ports;
 using Spectre.Console;
 using Spectre.Console.Cli;
 
@@ -36,7 +36,7 @@ public class DesktopNicSetCommand(IServiceProvider provider)
         DesktopNicSetSettings settings,
         CancellationToken cancellationToken) {
         using IServiceScope scope = provider.CreateScope();
-        IUpdateNicUseCase<Desktop> useCase = scope.ServiceProvider.GetRequiredService<IUpdateNicUseCase<Desktop>>();
+        IUpdatePortUseCase<Desktop> useCase = scope.ServiceProvider.GetRequiredService<IUpdatePortUseCase<Desktop>>();
 
         await useCase.ExecuteAsync(settings.DesktopName, settings.Index, settings.Type, settings.Speed, settings.Ports);
 

+ 2 - 2
Shared.Rcl/Commands/Servers/Nics/ServerNicAddCommand.cs

@@ -1,6 +1,6 @@
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.Servers;
-using RackPeek.Domain.UseCases.Nics;
+using RackPeek.Domain.UseCases.Ports;
 using Spectre.Console;
 using Spectre.Console.Cli;
 
@@ -21,7 +21,7 @@ public class ServerNicAddCommand(IServiceProvider serviceProvider)
         ServerNicAddSettings settings,
         CancellationToken cancellationToken) {
         using IServiceScope scope = serviceProvider.CreateScope();
-        IAddNicUseCase<Server> useCase = scope.ServiceProvider.GetRequiredService<IAddNicUseCase<Server>>();
+        IAddPortUseCase<Server> useCase = scope.ServiceProvider.GetRequiredService<IAddPortUseCase<Server>>();
 
         await useCase.ExecuteAsync(
             settings.Name,

+ 2 - 2
Shared.Rcl/Commands/Servers/Nics/ServerNicRemoveCommand.cs

@@ -1,6 +1,6 @@
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.Servers;
-using RackPeek.Domain.UseCases.Nics;
+using RackPeek.Domain.UseCases.Ports;
 using Spectre.Console;
 using Spectre.Console.Cli;
 
@@ -17,7 +17,7 @@ public class ServerNicRemoveCommand(IServiceProvider serviceProvider)
         ServerNicRemoveSettings settings,
         CancellationToken cancellationToken) {
         using IServiceScope scope = serviceProvider.CreateScope();
-        IRemoveNicUseCase<Server> useCase = scope.ServiceProvider.GetRequiredService<IRemoveNicUseCase<Server>>();
+        IRemovePortUseCase<Server> useCase = scope.ServiceProvider.GetRequiredService<IRemovePortUseCase<Server>>();
 
         await useCase.ExecuteAsync(
             settings.Name,

+ 2 - 2
Shared.Rcl/Commands/Servers/Nics/ServerNicUpdateCommand.cs

@@ -1,6 +1,6 @@
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.Servers;
-using RackPeek.Domain.UseCases.Nics;
+using RackPeek.Domain.UseCases.Ports;
 using Spectre.Console;
 using Spectre.Console.Cli;
 
@@ -23,7 +23,7 @@ public class ServerNicUpdateCommand(IServiceProvider serviceProvider)
         ServerNicUpdateSettings settings,
         CancellationToken cancellationToken) {
         using IServiceScope scope = serviceProvider.CreateScope();
-        IUpdateNicUseCase<Server> useCase = scope.ServiceProvider.GetRequiredService<IUpdateNicUseCase<Server>>();
+        IUpdatePortUseCase<Server> useCase = scope.ServiceProvider.GetRequiredService<IUpdatePortUseCase<Server>>();
 
         await useCase.ExecuteAsync(
             settings.Name,

+ 17 - 17
Shared.Rcl/Desktops/DesktopCardComponent.razor

@@ -3,7 +3,7 @@
 @using RackPeek.Domain.UseCases.Cpus
 @using RackPeek.Domain.UseCases.Drives
 @using RackPeek.Domain.UseCases.Gpus
-@using RackPeek.Domain.UseCases.Nics
+@using RackPeek.Domain.UseCases.Ports
 @inject IGetResourceByNameUseCase<Desktop> GetByNameUseCase
 @inject UpdateDesktopUseCase UpdateUseCase
 @inject IDeleteResourceUseCase<Desktop> DeleteUseCase
@@ -13,9 +13,9 @@
 @inject IAddDriveUseCase<Desktop> AddDriveUseCase
 @inject IUpdateDriveUseCase<Desktop> UpdateDriveUseCase
 @inject IRemoveDriveUseCase<Desktop> RemoveDriveUseCase
-@inject IAddNicUseCase<Desktop> AddNicUseCase
-@inject IUpdateNicUseCase<Desktop> UpdateNicUseCase
-@inject IRemoveNicUseCase<Desktop> RemoveNicUseCase
+@inject IAddPortUseCase<Desktop> AddNicUseCase
+@inject IUpdatePortUseCase<Desktop> UpdateNicUseCase
+@inject IRemovePortUseCase<Desktop> RemoveNicUseCase
 @inject IAddGpuUseCase<Desktop> AddGpuUseCase
 @inject IUpdateGpuUseCase<Desktop> UpdateGpuUseCase
 @inject IRemoveGpuUseCase<Desktop> RemoveGpuUseCase
@@ -164,16 +164,16 @@
                 </div>
             </div>
 
-            @if (Desktop.Nics?.Any() == true)
+            @if (Desktop.Ports?.Any() == true)
             {
-                @foreach (var nic in Desktop.Nics)
+                @foreach (var nic in Desktop.Ports)
                 {
                     <div
                         class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
                         <button data-testid=@($"edit-nic-{nic.Type}-{nic.Speed}")
                                 class="hover:text-emerald-400"
                                 @onclick="() => OpenEditNic(nic)">
-                            @nic.Type — @nic.Speed Gbps (@nic.Ports ports)
+                            @nic.Type — @nic.Speed Gbps (@nic.Count ports)
                         </button>
                     </div>
                 }
@@ -298,7 +298,7 @@
           TestIdPrefix="desktop"/>
 
 
-<NicModal
+<PortModal
     IsOpen="@_nicModalOpen"
     IsOpenChanged="v => _nicModalOpen = v"
     Value="@_editingNic"
@@ -425,7 +425,7 @@
 
     bool _nicModalOpen;
     int _editingNicIndex;
-    Nic? _editingNic;
+    Port? _editingNic;
 
     void OpenAddNic()
     {
@@ -434,17 +434,17 @@
         _nicModalOpen = true;
     }
 
-    void OpenEditNic(Nic nic)
+    void OpenEditNic(Port nic)
     {
-        Desktop.Nics ??= new List<Nic>();
-        _editingNicIndex = Desktop.Nics.IndexOf(nic);
+        Desktop.Ports ??= new List<Port>();
+        _editingNicIndex = Desktop.Ports.IndexOf(nic);
         _editingNic = nic;
         _nicModalOpen = true;
     }
 
-    async Task HandleNicSubmit(Nic nic)
+    async Task HandleNicSubmit(Port nic)
     {
-        Desktop.Nics ??= new List<Nic>();
+        Desktop.Ports ??= new List<Port>();
 
         if (_editingNicIndex < 0)
         {
@@ -452,7 +452,7 @@
                 Desktop.Name,
                 nic.Type,
                 nic.Speed,
-                nic.Ports);
+                nic.Count);
         }
         else
         {
@@ -461,13 +461,13 @@
                 _editingNicIndex,
                 nic.Type,
                 nic.Speed,
-                nic.Ports);
+                nic.Count);
         }
 
         Desktop = await GetByNameUseCase.ExecuteAsync(Desktop.Name);
     }
 
-    async Task HandleNicDelete(Nic nic)
+    async Task HandleNicDelete(Port nic)
     {
         await RemoveNicUseCase.ExecuteAsync(Desktop.Name, _editingNicIndex);
         Desktop = await GetByNameUseCase.ExecuteAsync(Desktop.Name);

+ 17 - 17
Shared.Rcl/Servers/ServerCardComponent.razor

@@ -3,7 +3,7 @@
 @using RackPeek.Domain.UseCases.Cpus
 @using RackPeek.Domain.UseCases.Drives
 @using RackPeek.Domain.UseCases.Gpus
-@using RackPeek.Domain.UseCases.Nics
+@using RackPeek.Domain.UseCases.Ports
 @inject IAddCpuUseCase<Server> AddCpuUseCase
 @inject IRemoveCpuUseCase<Server> RemoveCpuUseCase
 @inject IUpdateCpuUseCase<Server> UpdateCpuUseCase
@@ -12,9 +12,9 @@
 @inject IUpdateDriveUseCase<Server> UpdateDriveUseCase
 @inject IRemoveDriveUseCase<Server> RemoveDriveUseCase
 
-@inject IAddNicUseCase<Server> AddNicUseCase
-@inject IUpdateNicUseCase<Server> UpdateNicUseCase
-@inject IRemoveNicUseCase<Server> RemoveNicUseCase
+@inject IAddPortUseCase<Server> AddNicUseCase
+@inject IUpdatePortUseCase<Server> UpdateNicUseCase
+@inject IRemovePortUseCase<Server> RemoveNicUseCase
 
 @inject IAddGpuUseCase<Server> AddGpuUseCase
 @inject IUpdateGpuUseCase<Server> UpdateGpuUseCase
@@ -181,9 +181,9 @@
                 </div>
             </div>
 
-            @if (Server.Nics?.Any() == true)
+            @if (Server.Ports?.Any() == true)
             {
-                @foreach (var nic in Server.Nics)
+                @foreach (var nic in Server.Ports)
                 {
                     <div
                         class="flex items-center justify-between text-zinc-300 group hover:bg-zinc-800/40 rounded px-1 py-0.5">
@@ -191,7 +191,7 @@
                             class="hover:text-emerald-400"
                             title="Edit NIC"
                             @onclick="() => OpenEditNic(nic)">
-                            @nic.Type — @nic.Speed Gbps (@nic.Ports ports)
+                            @nic.Type — @nic.Speed Gbps (@nic.Count ports)
                         </button>
                     </div>
                 }
@@ -287,7 +287,7 @@
     TestIdPrefix="server-drive"/>
 
 
-<NicModal
+<PortModal
     IsOpen="@_nicModalOpen"
     IsOpenChanged="v => _nicModalOpen = v"
     Value="@_editingNic"
@@ -456,7 +456,7 @@
 
     bool _nicModalOpen;
     int _editingNicIndex;
-    Nic? _editingNic;
+    Port? _editingNic;
 
     void OpenAddNic()
     {
@@ -465,17 +465,17 @@
         _nicModalOpen = true;
     }
 
-    void OpenEditNic(Nic nic)
+    void OpenEditNic(Port nic)
     {
-        Server.Nics ??= new List<Nic>();
-        _editingNicIndex = Server.Nics.IndexOf(nic);
+        Server.Ports ??= new List<Port>();
+        _editingNicIndex = Server.Ports.IndexOf(nic);
         _editingNic = nic;
         _nicModalOpen = true;
     }
 
-    async Task HandleNicSubmit(Nic nic)
+    async Task HandleNicSubmit(Port nic)
     {
-        Server.Nics ??= new List<Nic>();
+        Server.Ports ??= new List<Port>();
 
         if (_editingNicIndex < 0)
         {
@@ -483,7 +483,7 @@
                 Server.Name,
                 nic.Type,
                 nic.Speed,
-                nic.Ports);
+                nic.Count);
         }
         else
         {
@@ -492,13 +492,13 @@
                 _editingNicIndex,
                 nic.Type,
                 nic.Speed,
-                nic.Ports);
+                nic.Count);
         }
 
         Server = await GetByNameUseCase.ExecuteAsync(Server.Name);
     }
 
-    async Task HandleNicDelete(Nic nic)
+    async Task HandleNicDelete(Port nic)
     {
         await RemoveNicUseCase.ExecuteAsync(Server.Name, _editingNicIndex);
         Server = await GetByNameUseCase.ExecuteAsync(Server.Name);

+ 3 - 3
Tests/Api/InventoryEndpointTests.cs

@@ -94,7 +94,7 @@ public class InventoryEndpointTests(ITestOutputHelper output) : ApiTestBase(outp
             new { Yaml = initial });
 
         var update = """
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Server
                        name: server-update
@@ -264,7 +264,7 @@ public class InventoryEndpointTests(ITestOutputHelper output) : ApiTestBase(outp
         await client.PostAsJsonAsync("/api/inventory", new { yaml = initial });
 
         var update = """
-                     version: 2
+                     version: 3
                      resources:
                        - kind: Server
                          name: nested-test
@@ -355,7 +355,7 @@ public class InventoryEndpointTests(ITestOutputHelper output) : ApiTestBase(outp
         await client.PostAsJsonAsync("/api/inventory", new { yaml = initial });
 
         var update = """
-                     version: 2
+                     version: 3
                      resources:
                        - kind: Firewall
                          name: polymorph-test

+ 2 - 2
Tests/EndToEnd/AccessPointTests/AccessPointWorkflowTests.cs

@@ -35,7 +35,7 @@ public class AccessPointWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper o
         Assert.Equal("Access Point 'ap01' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: AccessPoint
                        model: Unifi-U6-Lite
@@ -55,7 +55,7 @@ public class AccessPointWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper o
         Assert.Equal("Access Point 'ap02' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: AccessPoint
                        model: Unifi-U6-Lite

+ 2 - 2
Tests/EndToEnd/FirewallTests/FirewallWorkflowTests.cs

@@ -41,7 +41,7 @@ public class FirewallWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outp
         Assert.Equal("Firewall 'fw01' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Firewall
                        model: Fortinet FG-60F
@@ -64,7 +64,7 @@ public class FirewallWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outp
         Assert.Equal("Firewall 'fw02' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Firewall
                        model: Fortinet FG-60F

+ 2 - 2
Tests/EndToEnd/RouterTests/RouterWorkflowTests.cs

@@ -41,7 +41,7 @@ public class RouterWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.Equal("Router 'rt01' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Router
                        model: Ubiquiti EdgeRouter 4
@@ -64,7 +64,7 @@ public class RouterWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.Equal("Router 'rt02' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Router
                        model: Ubiquiti EdgeRouter 4

+ 1 - 1
Tests/EndToEnd/ServerTests/ServerWorkflowTests.cs

@@ -40,7 +40,7 @@ public class ServerWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.Equal("Server 'srv01' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Server
                        ram:

+ 1 - 1
Tests/EndToEnd/ServiceTests/ServiceWorkflowTests.cs

@@ -46,7 +46,7 @@ public class ServiceWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outpu
         outputHelper.WriteLine(yaml);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: System
                        name: sys01

+ 2 - 2
Tests/EndToEnd/SwitchTests/SwitchWorkflowTests.cs

@@ -42,7 +42,7 @@ public class SwitchWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.Equal("Switch 'sw01' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Switch
                        model: Netgear GS108
@@ -66,7 +66,7 @@ public class SwitchWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
         Assert.Equal("Switch 'sw02' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Switch
                        model: Netgear GS108

+ 2 - 2
Tests/EndToEnd/SystemTests/SystemWorkflowTests.cs

@@ -46,7 +46,7 @@ public class SystemWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
 
         outputHelper.WriteLine(yaml);
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Server
                        name: proxmox-node01
@@ -157,7 +157,7 @@ public class SystemWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
 
         // Assert resulting YAML
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Server
                        name: proxmox-node01

+ 2 - 2
Tests/EndToEnd/UpsTests/UpsWorkflowtests.cs

@@ -37,7 +37,7 @@ public class UpsWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outputHel
         Assert.Equal("UPS 'ups01' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Ups
                        model: APC-SmartUPS-1500
@@ -58,7 +58,7 @@ public class UpsWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outputHel
         Assert.Equal("UPS 'ups02' updated.\n", output);
 
         Assert.Equal("""
-                     version: 2
+                     version: 3
                      resources:
                      - kind: Ups
                        model: APC-SmartUPS-1500

+ 34 - 0
Tests/TestConfigs/v3/01-server.yaml

@@ -0,0 +1,34 @@
+version: 3
+resources:
+  - kind: Server
+    name: example-server
+    tags:
+      - production
+      - compute
+    notes: Primary hypervisor host
+    runsOn:
+      - rack-a1
+      - rack-a2
+    ram:
+      size: 128
+      mts: 3200
+    ipmi: true
+    cpus:
+      - model: AMD EPYC 7302P
+        cores: 16
+        threads: 32
+    drives:
+      - type: nvme
+        size: 1024
+      - type: ssd
+        size: 2048
+    gpus:
+      - model: NVIDIA RTX 4000
+        vram: 16
+    ports:
+      - type: rj45
+        speed: 1
+        count: 2
+      - type: sfp+
+        speed: 10
+        count: 2

+ 17 - 0
Tests/TestConfigs/v3/02-firewall.yaml

@@ -0,0 +1,17 @@
+version: 3
+resources:
+  - kind: Firewall
+    name: example-firewall
+    model: Netgate-6100
+    managed: true
+    poe: false
+    ports:
+      - type: rj45
+        speed: 1
+        count: 4
+      - type: sfp+
+        speed: 10
+        count: 2
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 17 - 0
Tests/TestConfigs/v3/03-router.yaml

@@ -0,0 +1,17 @@
+version: 3
+resources:
+  - kind: Router
+    name: example-router
+    model: Ubiquiti-ER-4
+    managed: true
+    poe: false
+    ports:
+      - type: rj45
+        speed: 1
+        count: 4
+      - type: sfp
+        speed: 10
+        count: 1
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 17 - 0
Tests/TestConfigs/v3/04-switch.yaml

@@ -0,0 +1,17 @@
+version: 3
+resources:
+  - kind: Switch
+    name: example-switch
+    model: UniFi-USW-Enterprise-24
+    managed: true
+    poe: true
+    ports:
+      - type: rj45
+        speed: 1
+        count: 12
+      - type: sfp+
+        speed: 10
+        count: 4
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 11 - 0
Tests/TestConfigs/v3/05-accesspoint.yaml

@@ -0,0 +1,11 @@
+version: 3
+resources:
+  - kind: AccessPoint
+    name: example-accesspoint
+    tags:
+      - wireless
+    model: UniFi-U6-Pro
+    speed: 2.5
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 11 - 0
Tests/TestConfigs/v3/06-ups.yaml

@@ -0,0 +1,11 @@
+version: 3
+resources:
+  - kind: Ups
+    name: example-ups
+    tags:
+      - power
+    model: APC-SmartUPS-2200
+    va: 2200
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 25 - 0
Tests/TestConfigs/v3/07-desktop.yaml

@@ -0,0 +1,25 @@
+version: 3
+resources:
+  - kind: Desktop
+    name: example-desktop
+    notes: Engineering workstation
+    ram:
+      size: 64
+      mts: 3600
+    cpus:
+      - model: Intel Core i9-13900K
+        cores: 24
+        threads: 32
+    drives:
+      - type: ssd
+        size: 2048
+    gpus:
+      - model: NVIDIA RTX 4090
+        vram: 24
+    ports:
+      - type: rj45
+        speed: 1
+        count: 1
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 18 - 0
Tests/TestConfigs/v3/08-laptop.yaml

@@ -0,0 +1,18 @@
+version: 3
+resources:
+  - kind: Laptop
+    name: example-laptop
+    notes: Developer machine
+    ram:
+      size: 32
+      mts: 5200
+    cpus:
+      - model: Intel Core i7-1260P
+        cores: 12
+        threads: 16
+    drives:
+      - type: ssd
+        size: 1024
+    runsOn:
+      - rack-a1
+      - rack-a2

+ 12 - 0
Tests/TestConfigs/v3/09-service.yaml

@@ -0,0 +1,12 @@
+version: 3
+resources:
+  - kind: Service
+    name: example-service
+    runsOn:
+      - rack-a1
+      - rack-a2
+    network:
+      ip: 192.168.1.10
+      port: 8080
+      protocol: TCP
+      url: http://example.local:8080

+ 16 - 0
Tests/TestConfigs/v3/10-system.yaml

@@ -0,0 +1,16 @@
+version: 3
+resources:
+  - kind: System
+    name: example-system
+    notes: Virtual machine instance
+    runsOn:
+      - rack-a1
+      - rack-a2
+    type: VM
+    os: ubuntu-22.04
+    cores: 4
+    ram: 8
+    ip: 10.0.20.10
+    drives:
+      - size: 128
+      - size: 256

+ 454 - 0
Tests/TestConfigs/v3/11-demo-config.yaml

@@ -0,0 +1,454 @@
+version: 3
+resources:
+  - kind: Server
+    ram:
+      size: 128
+      mts: 3200
+    ipmi: true
+    cpus:
+      - model: AMD EPYC 7302P
+        cores: 16
+        threads: 32
+    drives:
+      - type: ssd
+        size: 1024
+      - type: ssd
+        size: 1024
+    ports:
+      - type: rj45
+        speed: 1
+        count: 2
+      - type: sfp+
+        speed: 10
+        count: 2
+    name: proxmox-node01
+  - kind: Server
+    ram:
+      size: 96
+      mts: 2666
+    ipmi: true
+    cpus:
+      - model: Intel Xeon Silver 4210
+        cores: 10
+        threads: 20
+    drives:
+      - type: ssd
+        size: 1024
+      - type: hdd
+        size: 4096
+    ports:
+      - type: rj45
+        speed: 1
+        count: 2
+      - type: sfp+
+        speed: 10
+        count: 1
+    name: proxmox-node02
+  - kind: Server
+    ram:
+      size: 64
+      mts: 2666
+    ipmi: true
+    cpus:
+      - model: Intel Xeon E-2236
+        cores: 6
+        threads: 12
+    drives:
+      - type: hdd
+        size: 8192
+      - type: hdd
+        size: 8192
+      - type: hdd
+        size: 8192
+      - type: hdd
+        size: 8192
+    ports:
+      - type: rj45
+        speed: 1
+        count: 1
+      - type: sfp+
+        speed: 10
+        count: 1
+    name: truenas-storage
+  - kind: Firewall
+    model: Netgate-6100
+    managed: true
+    poe: false
+    ports:
+      - type: rj45
+        speed: 1
+        count: 4
+      - type: sfp+
+        speed: 10
+        count: 2
+    name: pfsense-fw
+  - kind: Router
+    model: Ubiquiti-ER-4
+    managed: true
+    poe: false
+    ports:
+      - type: rj45
+        speed: 1
+        count: 4
+      - type: sfp
+        speed: 10
+        count: 1
+    name: core-router
+  - kind: Switch
+    model: UniFi-USW-Enterprise-24
+    managed: true
+    poe: true
+    ports:
+      - type: rj45
+        speed: 1
+        count: 12
+      - type: rj45
+        speed: 2.5
+        count: 8
+      - type: sfp+
+        speed: 10
+        count: 4
+    name: core-switch
+  - kind: Switch
+    model: UniFi-USW-16-PoE
+    managed: true
+    poe: true
+    ports:
+      - type: rj45
+        speed: 1
+        count: 16
+      - type: sfp
+        speed: 1
+        count: 2
+    name: access-switch
+  - kind: AccessPoint
+    model: UniFi-U6-Pro
+    speed: 2.5
+    name: lounge-ap
+  - kind: Ups
+    model: APC-SmartUPS-2200
+    va: 2200
+    name: rack-ups
+  - kind: Desktop
+    ram:
+      size: 64
+      mts: 3600
+    cpus:
+      - model: AMD Ryzen 9 5900X
+        cores: 12
+        threads: 24
+    drives:
+      - type: ssd
+        size: 1024
+      - type: ssd
+        size: 2048
+    gpus:
+      - model: NVIDIA RTX 3080
+        vram: 10
+    ports:
+      - type: rj45
+        speed: 1
+        count: 1
+    name: workstation-linux
+  - kind: Desktop
+    ram:
+      size: 32
+      mts: 3200
+    cpus:
+      - model: Intel Core i7-12700K
+        cores: 12
+        threads: 20
+    drives:
+      - type: ssd
+        size: 1024
+    gpus:
+      - model: NVIDIA RTX 3070
+        vram: 8
+    ports:
+      - type: rj45
+        speed: 1
+        count: 1
+    name: gaming-pc
+  - kind: Laptop
+    ram:
+      size: 32
+      mts: 5200
+    cpus:
+      - model: Intel Core i7-1260P
+        cores: 12
+        threads: 16
+    drives:
+      - type: ssd
+        size: 1024
+    name: dev-laptop
+  - kind: Service
+    network:
+      ip: 192.168.0.10
+      port: 8123
+      protocol: TCP
+      url: http://homeassistant.lan:8123
+    name: home-assistant
+    runsOn:
+      - vm-home-assistant
+  - kind: Service
+    network:
+      ip: 192.168.0.20
+      port: 32400
+      protocol: TCP
+      url: http://plex.lan:32400
+    name: plex
+    runsOn:
+      - vm-media-server
+      - vm-home-assistant
+  - kind: Service
+    network:
+      ip: 192.168.0.21
+      port: 8096
+      protocol: TCP
+      url: http://jellyfin.lan:8096
+    name: jellyfin
+    runsOn:
+      - vm-media-server
+  - kind: Service
+    network:
+      ip: 192.168.0.22
+      port: 8080
+      protocol: TCP
+      url: http://immich.lan:8080
+    name: immich
+    runsOn:
+      - vm-media-server
+  - kind: Service
+    network:
+      ip: 192.168.0.30
+      port: 443
+      protocol: TCP
+      url: https://truenas.lan
+    name: truenas-webui
+    runsOn:
+      - truenas-core-os
+  - kind: Service
+    network:
+      ip: 192.168.0.31
+      port: 9000
+      protocol: TCP
+      url: http://minio.lan:9000
+    name: minio
+    runsOn:
+      - vm-media-server
+  - kind: Service
+    network:
+      ip: 192.168.0.40
+      port: 9090
+      protocol: TCP
+      url: http://prometheus.lan:9090
+    name: prometheus
+    runsOn:
+      - vm-monitoring
+  - kind: Service
+    network:
+      ip: 192.168.0.41
+      port: 3000
+      protocol: TCP
+      url: http://grafana.lan:3000
+    name: grafana
+  - kind: Service
+    network:
+      ip: 192.168.0.42
+      port: 9093
+      protocol: TCP
+      url: http://alertmanager.lan:9093
+    name: alertmanager
+  - kind: Service
+    network:
+      ip: 192.168.0.50
+      port: 3001
+      protocol: TCP
+      url: http://git.lan:3001
+    name: gitea
+    runsOn:
+      - vm-monitoring
+  - kind: Service
+    network:
+      ip: 192.168.0.51
+      port: 5000
+      protocol: TCP
+      url: http://registry.lan:5000
+    name: docker-registry
+    runsOn:
+      - vm-monitoring
+  - kind: Service
+    network:
+      ip: 192.168.0.52
+      port: 9000
+      protocol: TCP
+      url: http://portainer.lan:9000
+    name: portainer
+    runsOn:
+      - vm-monitoring
+      - vm-logging
+  - kind: Service
+    network:
+      ip: 192.168.0.53
+      port: 80
+      protocol: TCP
+      url: http://pihole.lan
+    name: pihole
+  - kind: Service
+    network:
+      ip: 192.168.0.1
+      port: 443
+      protocol: TCP
+      url: https://firewall.lan
+    name: firewall-webui
+    runsOn:
+      - firewall-os
+  - kind: Service
+    network:
+      ip: 192.168.0.254
+      port: 443
+      protocol: TCP
+      url: https://router.lan
+    name: router-webui
+    runsOn:
+      - router-os
+  - kind: System
+    type: Hypervisor
+    os: proxmox
+    cores: 16
+    ram: 128
+    ip: 10.0.20.10
+    drives:
+      - size: 1024
+      - size: 1024
+    name: proxmox-cluster-node01
+    runsOn:
+      - proxmox-node01
+  - kind: System
+    type: Hypervisor
+    os: proxmox
+    cores: 10
+    ram: 96
+    drives:
+      - size: 1024
+      - size: 4096
+    name: proxmox-cluster-node02
+    runsOn:
+      - proxmox-node02
+  - kind: System
+    type: Baremetal
+    os: truenas
+    cores: 6
+    ram: 64
+    drives:
+      - size: 8192
+      - size: 8192
+      - size: 8192
+      - size: 8192
+    name: truenas-core-os
+    runsOn:
+      - truenas-storage
+  - kind: System
+    type: Baremetal
+    os: idrac
+    cores: 1
+    ram: 1
+    name: ipmi-proxmox-node01
+    runsOn:
+      - proxmox-node01
+  - kind: System
+    type: Baremetal
+    os: ipmi
+    cores: 1
+    ram: 1
+    name: ipmi-proxmox-node02
+    runsOn:
+      - proxmox-node02
+  - kind: System
+    type: Baremetal
+    os: ipmi
+    cores: 1
+    ram: 1
+    name: ipmi-truenas-storage
+    runsOn:
+      - truenas-storage
+  - kind: System
+    type: Baremetal
+    os: pfsense
+    cores: 4
+    ram: 8
+    drives:
+      - size: 32
+    name: firewall-os
+    runsOn:
+      - pfsense-fw
+  - kind: System
+    type: Baremetal
+    os: edgeos
+    cores: 4
+    ram: 4
+    drives:
+      - size: 4
+    name: router-os
+    runsOn:
+      - core-router
+  - kind: System
+    type: Baremetal
+    os: unifi-os
+    cores: 2
+    ram: 2
+    drives:
+      - size: 8
+    name: unifi-core-switch-os
+    runsOn:
+      - core-switch
+  - kind: System
+    type: Baremetal
+    os: unifi-os
+    cores: 2
+    ram: 2
+    drives:
+      - size: 8
+    name: unifi-access-switch-os
+    runsOn:
+      - access-switch
+  - kind: System
+    type: Baremetal
+    os: unifi-firmware
+    cores: 2
+    ram: 1
+    drives:
+      - size: 4
+    name: unifi-lounge-ap-os
+    runsOn:
+      - lounge-ap
+  - kind: System
+    type: VM
+    os: hassos
+    cores: 2
+    ram: 4
+    drives:
+      - size: 64
+    name: vm-home-assistant
+    runsOn:
+      - proxmox-node01
+  - kind: System
+    type: VM
+    os: ubuntu-22.04
+    cores: 4
+    ram: 8
+    drives:
+      - size: 500
+    name: vm-media-server
+    runsOn:
+      - proxmox-node02
+  - kind: System
+    type: VM
+    os: debian-12
+    cores: 2
+    ram: 4
+    drives:
+      - size: 64
+    name: vm-monitoring
+    runsOn:
+      - proxmox-node01

+ 33 - 0
Tests/Tests.csproj

@@ -38,5 +38,38 @@
     <ItemGroup>
         <None Include="schemas\**\*.json" CopyToOutputDirectory="PreserveNewest"/>
         <None Include="TestConfigs\**\*.yaml" CopyToOutputDirectory="PreserveNewest"/>
+        <None Update="TestConfigs\v3\01-server.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\02-firewall.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\03-router.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\04-switch.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\05-accesspoint.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\06-ups.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\07-desktop.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\08-laptop.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\09-service.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\10-system.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+        <None Update="TestConfigs\v3\11-demo-config.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
     </ItemGroup>
 </Project>

+ 1 - 0
Tests/Yaml/SchemaTests.cs

@@ -64,6 +64,7 @@ public class SchemaConformanceTests {
     [Theory]
     [InlineData(1)]
     [InlineData(2)]
+    [InlineData(3)]
     public void All_yaml_files_conform_to_schema(int version) {
         // Arrange
         JsonSchema schema = LoadSchema(version);

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

@@ -0,0 +1,607 @@
+{
+  "$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"
+      }
+    }
+  },
+  "$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
+        }
+      }
+    },
+    "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
+            }
+          }
+        }
+      ],
+      "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
+    }
+  }
+}

+ 336 - 0
schemas/v3/schema.v3.json

@@ -0,0 +1,336 @@
+{
+  "$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",
+                "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
+    }
+  }
+}