Browse Source

Merge branch 'main' into Testing-Improvements

Chester-alt 1 month ago
parent
commit
6a628e0881
38 changed files with 2036 additions and 448 deletions
  1. 47 0
      .github/workflows/publish-docker-nightly.yaml
  2. 61 0
      .github/workflows/publish-docker.yaml
  3. 1 1
      .github/workflows/publish-webui.yml
  4. 0 2
      README.md
  5. 8 0
      RackPeek.Domain/Resources/Servers/ICpuResource.cs
  6. 8 0
      RackPeek.Domain/Resources/Servers/IDriveResource.cs
  7. 8 0
      RackPeek.Domain/Resources/Servers/IGpuResource.cs
  8. 8 0
      RackPeek.Domain/Resources/Servers/INicResource.cs
  9. 8 0
      RackPeek.Domain/Resources/Servers/IPortResource.cs
  10. 0 25
      RackPeek.Domain/Resources/Servers/Server.cs
  11. 49 10
      RackPeek.Web/Dockerfile
  12. 3 1
      RackPeek.Web/Program.cs
  13. 43 6
      Shared.Rcl/Layout/MainLayout.razor
  14. 265 0
      Shared.Rcl/wwwroot/schemas/v1/schema.v1.json
  15. 1 1
      Tests/EndToEnd/FirewallTests/FirewallCommandTests.cs
  16. 1 1
      Tests/EndToEnd/FirewallTests/FirewallErrorTests.cs
  17. 1 1
      Tests/EndToEnd/FirewallTests/FirewallWorkflowTests.cs
  18. 1 1
      Tests/EndToEnd/SystemTests/SystemCommandTests.cs
  19. 1 1
      Tests/EndToEnd/SystemTests/SystemErrorTests.cs
  20. 1 1
      Tests/EndToEnd/SystemTests/SystemWorkflowTests.cs
  21. 32 0
      Tests/TestConfigs/v1/01-server.yaml
  22. 14 0
      Tests/TestConfigs/v1/02-firewall.yaml
  23. 14 0
      Tests/TestConfigs/v1/03-router.yaml
  24. 14 0
      Tests/TestConfigs/v1/04-switch.yaml
  25. 8 0
      Tests/TestConfigs/v1/05-accesspoint.yaml
  26. 8 0
      Tests/TestConfigs/v1/06-ups.yaml
  27. 22 0
      Tests/TestConfigs/v1/07-desktop.yaml
  28. 15 0
      Tests/TestConfigs/v1/08-laptop.yaml
  29. 10 0
      Tests/TestConfigs/v1/09-service.yaml
  30. 13 0
      Tests/TestConfigs/v1/10-system.yaml
  31. 428 0
      Tests/TestConfigs/v1/11-demo-config.yaml
  32. 62 0
      Tests/Tests.csproj
  33. 136 0
      Tests/Yaml/SchemaTests.cs
  34. 265 0
      Tests/schemas/schema.v1.json
  35. 102 0
      docs/development/dev-setup.md
  36. 113 0
      justfile
  37. 265 0
      schemas/v1/schema.v1.json
  38. 0 397
      servers.yaml

+ 47 - 0
.github/workflows/publish-docker-nightly.yaml

@@ -0,0 +1,47 @@
+name: Docker Nightly Publish (amd64)
+
+on:
+  push:
+    branches: [ main ]
+  workflow_dispatch:
+
+permissions:
+  contents: read
+
+jobs:
+  docker:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Extract metadata
+        run: |
+          echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
+          echo "DATE_TAG=$(date +%Y%m%d)" >> $GITHUB_ENV
+
+      - name: Build and push nightly image (amd64 only)
+        uses: docker/build-push-action@v5
+        env:
+          BUILDKIT_PROGRESS: plain
+        with:
+          context: .
+          file: ./RackPeek.Web/Dockerfile
+          platforms: linux/amd64
+          push: true
+          target: final
+          tags: |
+            aptacode/rackpeek:nightly
+            aptacode/rackpeek:nightly-${{ env.SHORT_SHA }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max

+ 61 - 0
.github/workflows/publish-docker.yaml

@@ -0,0 +1,61 @@
+name: Docker Release Publish (multi-arch)
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "RackPeek version (e.g. v1.0.0)"
+        required: true
+        default: "v0.0.12"
+
+permissions:
+  contents: read
+
+jobs:
+  docker:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      # Multi-arch support
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+      - name: Validate version input
+        run: |
+          VERSION="${{ github.event.inputs.version }}"
+          echo "VERSION=$VERSION" >> $GITHUB_ENV
+
+          # Require v-prefixed semver like v1.2.3
+          if ! echo "$VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
+            echo "Invalid version: $VERSION"
+            echo "Expected format: vMAJOR.MINOR.PATCH (e.g. v1.0.0)"
+            exit 1
+          fi
+
+      - name: Build and push release image (multi-arch)
+        uses: docker/build-push-action@v5
+        env:
+          BUILDKIT_PROGRESS: plain
+        with:
+          context: .
+          file: ./RackPeek.Web/Dockerfile
+          platforms: linux/amd64,linux/arm64
+          push: true
+          target: final
+          tags: |
+            aptacode/rackpeek:${{ env.VERSION }}
+            aptacode/rackpeek:latest
+          cache-from: type=gha
+          cache-to: type=gha,mode=max

+ 1 - 1
.github/workflows/publish-webui.yml

@@ -34,7 +34,7 @@ jobs:
       # patch base href FIRST
       - name: Fix base href for Pages
         run: |
-          sed -i 's|<base href="/" />|<base href="/RackPeek/" />|g' publish/wwwroot/index.html
+          sed -i 's|<base href="/"|<base href="/RackPeek/"|g' publish/wwwroot/index.html
 
       # THEN copy to 404.html
       - name: SPA fallback

+ 0 - 2
README.md

@@ -141,8 +141,6 @@ RackPeek is built to solve real problems we actively have. If a feature isn’t
 **Opinionated**  
 The project is optimized for home labs and self-hosted environments, not enterprise CMDBs or corporate documentation workflows.
 
-Here’s a clean section you can drop into your **README.md**.
-
 
 ## Development Docs
 

+ 8 - 0
RackPeek.Domain/Resources/Servers/ICpuResource.cs

@@ -0,0 +1,8 @@
+using RackPeek.Domain.Resources.SubResources;
+
+namespace RackPeek.Domain.Resources.Servers;
+
+public interface ICpuResource
+{
+    public List<Cpu>? Cpus { get; set; }
+}

+ 8 - 0
RackPeek.Domain/Resources/Servers/IDriveResource.cs

@@ -0,0 +1,8 @@
+using RackPeek.Domain.Resources.SubResources;
+
+namespace RackPeek.Domain.Resources.Servers;
+
+public interface IDriveResource
+{
+    public List<Drive>? Drives { get; set; }
+}

+ 8 - 0
RackPeek.Domain/Resources/Servers/IGpuResource.cs

@@ -0,0 +1,8 @@
+using RackPeek.Domain.Resources.SubResources;
+
+namespace RackPeek.Domain.Resources.Servers;
+
+public interface IGpuResource
+{
+    public List<Gpu>? Gpus { get; set; }
+}

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

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

+ 8 - 0
RackPeek.Domain/Resources/Servers/IPortResource.cs

@@ -0,0 +1,8 @@
+using RackPeek.Domain.Resources.SubResources;
+
+namespace RackPeek.Domain.Resources.Servers;
+
+public interface IPortResource
+{
+    public List<Port>? Ports { get; set; }
+}

+ 0 - 25
RackPeek.Domain/Resources/Servers/Server.cs

@@ -11,29 +11,4 @@ public class Server : Hardware.Hardware, ICpuResource, IDriveResource, IGpuResou
     public List<Drive>? Drives { get; set; }
     public List<Gpu>? Gpus { get; set; }
     public List<Nic>? Nics { get; set; }
-}
-
-public interface ICpuResource
-{
-    public List<Cpu>? Cpus { get; set; }
-}
-
-public interface IDriveResource
-{
-    public List<Drive>? Drives { get; set; }
-}
-
-public interface IPortResource
-{
-    public List<Port>? Ports { get; set; }
-}
-
-public interface IGpuResource
-{
-    public List<Gpu>? Gpus { get; set; }
-}
-
-public interface INicResource
-{
-    public List<Nic>? Nics { get; set; }
 }

+ 49 - 10
RackPeek.Web/Dockerfile

@@ -1,32 +1,71 @@
-FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
-USER $APP_UID
+# -----------------------------
+# Runtime base
+# -----------------------------
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+
+ENV APP_UID=$APP_UID
+
 WORKDIR /app
 EXPOSE 8080
-EXPOSE 8081
 
+# -----------------------------
+# Build stage
+# -----------------------------
 FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
 ARG BUILD_CONFIGURATION=Release
 WORKDIR /src
+
 COPY ["RackPeek.Web/RackPeek.Web.csproj", "RackPeek.Web/"]
 COPY ["RackPeek.Domain/RackPeek.Domain.csproj", "RackPeek.Domain/"]
 COPY ["Shared.Rcl/Shared.Rcl.csproj", "Shared.Rcl/"]
 COPY ["RackPeek/RackPeek.csproj", "RackPeek/"]
+
 RUN dotnet restore "RackPeek.Web/RackPeek.Web.csproj"
+
 COPY . .
+
+# Publish Web
 WORKDIR "/src/RackPeek.Web"
-RUN dotnet build "./RackPeek.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build
+RUN dotnet publish "./RackPeek.Web.csproj" -c $BUILD_CONFIGURATION -o /app/web-publish /p:UseAppHost=false
+
+# Publish CLI
+WORKDIR "/src/RackPeek"
+RUN dotnet publish "./RackPeek.csproj" -c $BUILD_CONFIGURATION -o /app/cli-publish /p:UseAppHost=false
 
-FROM build AS publish
-ARG BUILD_CONFIGURATION=Release
-RUN dotnet publish "./RackPeek.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
 
+# -----------------------------
+# Final runtime image
+# -----------------------------
 FROM base AS final
+
 WORKDIR /app
 
-RUN mkdir -p /app/config && chown -R $APP_UID /app/config
+USER root
+
+# Create shared config directory safely
+RUN mkdir -p /app/config \
+    && chown -R ${APP_UID}:0 /app/config \
+    && chmod -R g=u /app/config
 
 VOLUME ["/app/config"]
 
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "RackPeek.Web.dll"]
+# Copy published outputs
+COPY --from=build /app/web-publish .
+COPY --from=build /app/cli-publish /usr/local/bin/rpk-dir
+
+# Create CLI wrapper
+RUN if [ -f /usr/local/bin/rpk-dir/RackPeek ]; then \
+        mv /usr/local/bin/rpk-dir/RackPeek /usr/local/bin/rpk; \
+    else \
+        echo '#!/bin/sh\nexec dotnet /usr/local/bin/rpk-dir/RackPeek.dll "$@"' > /usr/local/bin/rpk && \
+        chmod +x /usr/local/bin/rpk; \
+    fi
 
+# Make sure ASP.NET binds correctly in containers
+ENV ASPNETCORE_URLS=http://+:8080
+ENV RPK_YAML_DIR=/app/config
+
+# Drop privileges
+USER ${APP_UID}
+
+ENTRYPOINT ["dotnet", "RackPeek.Web.dll"]

+ 3 - 1
RackPeek.Web/Program.cs

@@ -17,6 +17,8 @@ public class Program
             builder.Configuration
         );
 
+        builder.Configuration.AddJsonFile($"appsettings.json", optional: true, reloadOnChange: false);
+        
         var yamlDir = builder.Configuration.GetValue<string>("RPK_YAML_DIR") ?? "./config";
         var yamlFileName = "config.yaml";
 
@@ -102,7 +104,7 @@ public class Program
 
     public static async Task Main(string[] args)
     {
-        var builder = WebApplication.CreateBuilder(args);
+        var builder = WebApplication.CreateBuilder(args);        
         var app = await BuildApp(builder);
         await app.RunAsync();
     }

+ 43 - 6
Shared.Rcl/Layout/MainLayout.razor

@@ -25,13 +25,50 @@
                 Home
             </NavLink>
 
-            <NavLink href="cli" data-testid="nav-cli">CLI</NavLink>
-            <NavLink href="yaml" data-testid="nav-yaml">Yaml</NavLink>
-            <NavLink href="hardware/tree" data-testid="nav-hardware">Hardware</NavLink>
-            <NavLink href="systems/list" data-testid="nav-systems">Systems</NavLink>
-            <NavLink href="services/list" data-testid="nav-services">Services</NavLink>
-            <NavLink href="docs/CommandIndex" data-testid="nav-docs">Docs</NavLink>
+            <NavLink href="cli"
+                     class="hover:text-emerald-400"
+                     activeClass="text-emerald-400 font-semibold"
+                     data-testid="nav-cli">
+                CLI
+            </NavLink>
+
+            <NavLink href="yaml"
+                     class="hover:text-emerald-400"
+                     activeClass="text-emerald-400 font-semibold"
+                     data-testid="nav-yaml">
+                Yaml
+            </NavLink>
+
+            <NavLink href="hardware/tree"
+                     class="hover:text-emerald-400"
+                     activeClass="text-emerald-400 font-semibold"
+                     data-testid="nav-hardware">
+                Hardware
+            </NavLink>
+
+            <NavLink href="systems/list"
+                     class="hover:text-emerald-400"
+                     activeClass="text-emerald-400 font-semibold"
+                     data-testid="nav-systems">
+                Systems
+            </NavLink>
+
+            <NavLink href="services/list"
+                     class="hover:text-emerald-400"
+                     activeClass="text-emerald-400 font-semibold"
+                     data-testid="nav-services">
+                Services
+            </NavLink>
+
+            <NavLink href="docs/CommandIndex"
+                     class="hover:text-emerald-400"
+                     activeClass="text-emerald-400 font-semibold"
+                     data-testid="nav-docs">
+                Docs
+            </NavLink>
+
 
+            
         </nav>
     </header>
 

+ 265 - 0
Shared.Rcl/wwwroot/schemas/v1/schema.v1.json

@@ -0,0 +1,265 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://timmoth.github.io/RackPeek/schemas/v1/schema.v1.json",
+  "title": "RackPeek Infrastructure Specification",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["version", "resources"],
+  "properties": {
+    "version": {
+      "type": "integer",
+      "const": 1
+    },
+    "resources": {
+      "type": "array",
+      "items": {
+        "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" }
+        ]
+      }
+    }
+  },
+  "$defs": {
+    "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][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}$"
+        },
+        "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
+        "protocol": { "type": "string", "enum": ["TCP","UDP"] },
+        "url": { "type": "string" }
+      }
+    },
+
+    "server": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Server" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "notes": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "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" } }
+      }
+    },
+
+    "desktop": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Desktop" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "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" } }
+      }
+    },
+
+    "laptop": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Laptop" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "ram": { "$ref": "#/$defs/ram" },
+        "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
+        "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+      }
+    },
+
+    "firewall": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Firewall" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "router": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Router" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "switch": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Switch" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "accessPoint": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "AccessPoint" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "model": { "type": "string" },
+        "speed": { "type": "number" }
+      }
+    },
+
+    "ups": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Ups" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "model": { "type": "string" },
+        "va": { "type": "integer", "minimum": 1 }
+      }
+    },
+
+    "service": {
+      "type": "object",
+      "required": ["kind","name","network","runsOn"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Service" },
+        "name": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "network": { "$ref": "#/$defs/network" }
+      }
+    },
+
+    "system": {
+      "type": "object",
+      "required": ["kind","name","type","os","cores","ram"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "System" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "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" } }
+      }
+    }
+  }
+}

+ 1 - 1
Tests/EndToEnd/FirewallTests/FirewallCommandTests.cs

@@ -1,7 +1,7 @@
 using Tests.EndToEnd.Infra;
 using Xunit.Abstractions;
 
-namespace Tests.EndToEnd;
+namespace Tests.EndToEnd.FirewallTests;
 
 [Collection("Yaml CLI tests")]
 public class FirewallCommandTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)

+ 1 - 1
Tests/EndToEnd/FirewallTests/FirewallErrorTests.cs

@@ -1,7 +1,7 @@
 using Tests.EndToEnd.Infra;
 using Xunit.Abstractions;
 
-namespace Tests.EndToEnd;
+namespace Tests.EndToEnd.FirewallTests;
 
 [Collection("Yaml CLI tests")]
 public class FirewallErrorTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)

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

@@ -1,7 +1,7 @@
 using Tests.EndToEnd.Infra;
 using Xunit.Abstractions;
 
-namespace Tests.EndToEnd;
+namespace Tests.EndToEnd.FirewallTests;
 
 [Collection("Yaml CLI tests")]
 public class FirewallWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)

+ 1 - 1
Tests/EndToEnd/SystemTests/SystemCommandTests.cs

@@ -1,7 +1,7 @@
 using Tests.EndToEnd.Infra;
 using Xunit.Abstractions;
 
-namespace Tests.EndToEnd;
+namespace Tests.EndToEnd.SystemTests;
 
 [Collection("Yaml CLI tests")]
 public class SystemCommandTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)

+ 1 - 1
Tests/EndToEnd/SystemTests/SystemErrorTests.cs

@@ -1,7 +1,7 @@
 using Tests.EndToEnd.Infra;
 using Xunit.Abstractions;
 
-namespace Tests.EndToEnd;
+namespace Tests.EndToEnd.SystemTests;
 
 [Collection("Yaml CLI tests")]
 public class SystemErrorTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)

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

@@ -1,7 +1,7 @@
 using Tests.EndToEnd.Infra;
 using Xunit.Abstractions;
 
-namespace Tests.EndToEnd;
+namespace Tests.EndToEnd.SystemTests;
 
 [Collection("Yaml CLI tests")]
 public class SystemWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outputHelper)

+ 32 - 0
Tests/TestConfigs/v1/01-server.yaml

@@ -0,0 +1,32 @@
+version: 1
+resources:
+  - kind: Server
+    name: example-server
+    tags:
+      - production
+      - compute
+    notes: Primary hypervisor host
+    runsOn: rack-a1
+    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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 2

+ 14 - 0
Tests/TestConfigs/v1/02-firewall.yaml

@@ -0,0 +1,14 @@
+version: 1
+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

+ 14 - 0
Tests/TestConfigs/v1/03-router.yaml

@@ -0,0 +1,14 @@
+version: 1
+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

+ 14 - 0
Tests/TestConfigs/v1/04-switch.yaml

@@ -0,0 +1,14 @@
+version: 1
+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

+ 8 - 0
Tests/TestConfigs/v1/05-accesspoint.yaml

@@ -0,0 +1,8 @@
+version: 1
+resources:
+  - kind: AccessPoint
+    name: example-accesspoint
+    tags:
+      - wireless
+    model: UniFi-U6-Pro
+    speed: 2.5

+ 8 - 0
Tests/TestConfigs/v1/06-ups.yaml

@@ -0,0 +1,8 @@
+version: 1
+resources:
+  - kind: Ups
+    name: example-ups
+    tags:
+      - power
+    model: APC-SmartUPS-2200
+    va: 2200

+ 22 - 0
Tests/TestConfigs/v1/07-desktop.yaml

@@ -0,0 +1,22 @@
+version: 1
+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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 1

+ 15 - 0
Tests/TestConfigs/v1/08-laptop.yaml

@@ -0,0 +1,15 @@
+version: 1
+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

+ 10 - 0
Tests/TestConfigs/v1/09-service.yaml

@@ -0,0 +1,10 @@
+version: 1
+resources:
+  - kind: Service
+    name: example-service
+    runsOn: example-system
+    network:
+      ip: 192.168.1.10
+      port: 8080
+      protocol: TCP
+      url: http://example.local:8080

+ 13 - 0
Tests/TestConfigs/v1/10-system.yaml

@@ -0,0 +1,13 @@
+version: 1
+resources:
+  - kind: System
+    name: example-system
+    notes: Virtual machine instance
+    runsOn: example-server
+    type: VM
+    os: ubuntu-22.04
+    cores: 4
+    ram: 8
+    drives:
+      - size: 128
+      - size: 256

+ 428 - 0
Tests/TestConfigs/v1/11-demo-config.yaml

@@ -0,0 +1,428 @@
+version: 1
+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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 2
+      - type: sfp+
+        speed: 10
+        ports: 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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 1
+      - type: sfp+
+        speed: 10
+        ports: 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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 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
+    nics:
+      - type: rj45
+        speed: 1
+        ports: 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
+  - 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
+    runsOn: vm-monitoring
+  - kind: Service
+    network:
+      ip: 192.168.0.42
+      port: 9093
+      protocol: TCP
+      url: http://alertmanager.lan:9093
+    name: alertmanager
+    runsOn: vm-monitoring
+  - 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
+  - kind: Service
+    network:
+      ip: 192.168.0.53
+      port: 80
+      protocol: TCP
+      url: http://pihole.lan
+    name: pihole
+    runsOn: vm-monitoring
+  - 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
+    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

+ 62 - 0
Tests/Tests.csproj

@@ -9,6 +9,7 @@
 
     <ItemGroup>
         <PackageReference Include="coverlet.collector" Version="6.0.4"/>
+        <PackageReference Include="JsonSchema.Net" Version="9.1.1" />
         <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
         <PackageReference Include="NSubstitute" Version="5.3.0"/>
         <PackageReference Include="xunit" Version="2.9.3"/>
@@ -26,4 +27,65 @@
         <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj"/>
     </ItemGroup>
 
+    <ItemGroup>
+      <Folder Include="EndToEnd\LaptopTests\" />
+      <Folder Include="EndToEnd\RouterTests\" />
+      <Folder Include="EndToEnd\ServerTests\" />
+      <Folder Include="EndToEnd\ServiceTests\" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Include="TestConfigs\**\*.yaml">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Include="schemas\schema.v1.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\valid-config-1.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\01-server.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\02-firewall.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\03-router.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\04-switch.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\05-accesspoint.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\06-ups.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\07-desktop.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\08-laptop.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\09-service.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+
+        <None Update="TestConfigs\v1\10-system.yaml">
+          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </None>
+    </ItemGroup>
+    
 </Project>

+ 136 - 0
Tests/Yaml/SchemaTests.cs

@@ -0,0 +1,136 @@
+using System.Text.Json;
+using Json.Schema;
+using YamlDotNet.RepresentationModel;
+
+namespace Tests.Yaml;
+
+public class SchemaConformanceTests
+{
+    private static JsonSchema LoadSchema()
+    {
+        var schemaText = File.ReadAllText("schemas/schema.v1.json");
+        return JsonSchema.FromText(schemaText);
+    }
+    private static JsonElement ConvertYamlToJsonElement(string yaml)
+    {
+        // Load YAML into YAML DOM
+        var yamlStream = new YamlStream();
+        yamlStream.Load(new StringReader(yaml));
+
+        var root = yamlStream.Documents[0].RootNode;
+
+        // Convert YAML node → JSON string
+        var json = ConvertYamlNodeToJson(root);
+
+        using var document = JsonDocument.Parse(json);
+        return document.RootElement.Clone();
+    }
+
+    private static string ConvertYamlNodeToJson(YamlNode node)
+    {
+        if (node is YamlScalarNode scalar)
+        {
+            // Try numeric
+            if (int.TryParse(scalar.Value, out var i))
+                return i.ToString();
+
+            if (double.TryParse(scalar.Value, out var d))
+                return d.ToString(System.Globalization.CultureInfo.InvariantCulture);
+
+            if (bool.TryParse(scalar.Value, out var b))
+                return b.ToString().ToLowerInvariant();
+
+            // Otherwise string
+            return JsonSerializer.Serialize(scalar.Value);
+        }
+
+        if (node is YamlSequenceNode sequence)
+        {
+            var items = sequence.Children
+                .Select(ConvertYamlNodeToJson);
+
+            return "[" + string.Join(",", items) + "]";
+        }
+
+        if (node is YamlMappingNode mapping)
+        {
+            var props = mapping.Children
+                .Select(kvp =>
+                    JsonSerializer.Serialize(((YamlScalarNode)kvp.Key).Value)
+                    + ":"
+                    + ConvertYamlNodeToJson(kvp.Value));
+
+            return "{" + string.Join(",", props) + "}";
+        }
+
+        return "null";
+    }
+    [Fact]
+    public void All_v1_yaml_files_conform_to_schema()
+    {
+        // Arrange
+        var schema = LoadSchema();
+        
+        var yamlFolder = Path.Combine(
+            AppContext.BaseDirectory,
+            "TestConfigs",
+            "v1");
+
+        var yamlFiles = Directory
+            .EnumerateFiles(yamlFolder, "*.yaml", SearchOption.AllDirectories)
+            .ToList();
+
+        Assert.NotEmpty(yamlFiles);
+
+        var failures = new List<string>();
+
+        // Act
+        foreach (var file in yamlFiles)
+        {
+            var yaml = File.ReadAllText(file);
+            var jsonNode = ConvertYamlToJsonElement(yaml);
+
+            var options = new EvaluationOptions
+            {
+                OutputFormat = OutputFormat.Hierarchical            };
+
+            var result = schema.Evaluate(jsonNode, options);
+            
+            if (!result.IsValid)
+            {
+                var errors = new List<string>();
+
+                void CollectErrors(EvaluationResults node)
+                {
+                    if (node.Errors != null)
+                    {
+                        foreach (var error in node.Errors)
+                            errors.Add($"{error.Key}: {error.Value}");
+                    }
+
+                    if (node.Details != null)
+                    {
+                        foreach (var child in node.Details)
+                            CollectErrors(child);
+                    }
+                }
+
+                CollectErrors(result);
+
+                failures.Add(
+                    $"File: {file}{Environment.NewLine}" +
+                    string.Join(Environment.NewLine, errors));
+            }
+        }
+
+        // Assert
+        if (failures.Any())
+        {
+            var message = string.Join(
+                $"{Environment.NewLine}--------------------{Environment.NewLine}",
+                failures);
+
+            Assert.Fail(message);
+        }
+    }
+}

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

@@ -0,0 +1,265 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://timmoth.github.io/RackPeek/schemas/v1/schema.v1.json",
+  "title": "RackPeek Infrastructure Specification",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["version", "resources"],
+  "properties": {
+    "version": {
+      "type": "integer",
+      "const": 1
+    },
+    "resources": {
+      "type": "array",
+      "items": {
+        "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" }
+        ]
+      }
+    }
+  },
+  "$defs": {
+    "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][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}$"
+        },
+        "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
+        "protocol": { "type": "string", "enum": ["TCP","UDP"] },
+        "url": { "type": "string" }
+      }
+    },
+
+    "server": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Server" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "notes": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "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" } }
+      }
+    },
+
+    "desktop": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Desktop" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "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" } }
+      }
+    },
+
+    "laptop": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Laptop" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "ram": { "$ref": "#/$defs/ram" },
+        "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
+        "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+      }
+    },
+
+    "firewall": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Firewall" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "router": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Router" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "switch": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Switch" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "accessPoint": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "AccessPoint" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "model": { "type": "string" },
+        "speed": { "type": "number" }
+      }
+    },
+
+    "ups": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Ups" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "model": { "type": "string" },
+        "va": { "type": "integer", "minimum": 1 }
+      }
+    },
+
+    "service": {
+      "type": "object",
+      "required": ["kind","name","network","runsOn"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Service" },
+        "name": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "network": { "$ref": "#/$defs/network" }
+      }
+    },
+
+    "system": {
+      "type": "object",
+      "required": ["kind","name","type","os","cores","ram"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "System" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "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" } }
+      }
+    }
+  }
+}

+ 102 - 0
docs/development/dev-setup.md

@@ -0,0 +1,102 @@
+# Developer Setup
+
+---
+
+This guide is targetted at developers willing to get involved in the development of RackPeek.
+
+Please review all commands in this guide carefully before execution and ensure you understand the implications of each step.
+
+This guide is by no means exhaustive, so please feel free to contribute back to it if you have any additional information or tips.
+
+## Brew Installation
+
+This project leverages the [brew](https://brew.sh) package manager for installation of dependencies on MacOS/Linux
+
+Please follow the installation instructions for Brew as found [here](https://brew.sh/index#installation)
+
+## Just Installation
+
+This project makes use of the [Just](https://github.com/casey/just) tool for streamlining development and developer productivity.
+
+If using Homebrew, installation is as simple as:
+
+```shell
+brew install just
+```
+
+Please follow your preferred installation method for Just as found [here](https://github.com/casey/just?tab=readme-ov-file#installation) if not using [brew](https://brew.sh).
+
+## VHS Installation
+
+This project makes use of the [VHS](https://github.com/charmbracelet/vhs) tool for recording and creating GIFs of the CLI for documentation purposes.
+
+If using Homebrew, installation is as simple as:
+
+```shell
+brew install vhs
+```
+
+Please follow your preferred installation method for VHS as found [here](https://github.com/charmbracelet/vhs?tab=readme-ov-file#installation) if not using [brew](https://brew.sh).
+
+## DotNet Installation
+
+For those looking for a quick start for getting setup with `dotnet` for development on RackPeek, please follow the instructions below.
+
+### Ubuntu Linux
+
+Setup the development environment on Ubuntu Linux.
+
+#### Install Prerequisites
+
+```shell
+sudo apt update
+sudo apt install -y wget apt-transport-https software-properties-common
+```
+
+#### Download and Register Microsoft Package Repository
+
+Find linux distribution version:
+
+```shell
+export RACKPEEK_KERNEL_VERSION=$(dpkg -l | grep linux-image | grep ii | head -1 | awk '{print $3}' | sed 's/.*~\([0-9]*\.[0-9]*\)\..*/\1/')
+```
+
+Ensure the correct kernel version was found (example: `22.04`, `24.04`, etc.):
+
+```shell
+echo "KERNEL_VERSION: ${RACKPEEK_KERNEL_VERSION}"
+```
+
+Download and register the Microsoft package repository:
+
+```shell
+wget https://packages.microsoft.com/config/ubuntu/${RACKPEEK_KERNEL_VERSION}/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
+sudo dpkg -i packages-microsoft-prod.deb
+rm packages-microsoft-prod.deb
+```
+
+Update package lists:
+
+```shell
+sudo apt update
+```
+
+#### Install .NET 10 SDK
+
+```shell
+sudo apt install -y dotnet-sdk-10.0
+```
+
+#### Verify Installation
+
+```shell
+dotnet --version
+```
+
+### MacOS
+
+🏗️ Help wanted for guide on MacOS development environment setup.
+
+### Windows
+
+🏗️ Help wanted for guide on Windows development environment setup.

+ 113 - 0
justfile

@@ -0,0 +1,113 @@
+# RackPeek Development Commands
+# Run `just` or `just --list` to see available recipes.
+
+# Environment Variables
+# ---------------------
+
+# Add .dotnet/tools to PATH for Playwright CLI
+export PATH := env_var('HOME') + "/.dotnet/tools:" + env_var_or_default('PATH', '')
+
+
+# Variables
+# ---------
+_dotnet := "dotnet"
+_dockerfile := "RackPeek.Web/Dockerfile"
+_image := "rackpeek:ci"
+_setup_guide := "docs/development/dev-setup.md"
+
+# ─── Helpers/Private ────────────────────────────────────────────────────────
+
+[doc("Check if dotnet is installed, show setup guide redirect if not found.")]
+[private]
+_check-dotnet:
+    @command -v {{ _dotnet }} >/dev/null 2>&1 || (echo "dotnet not found. See {{ _setup_guide }} for setup instructions." && exit 1)
+
+# ─── Default ────────────────────────────────────────────────────────────────
+
+[default]
+[doc("List all recipes with documentation")]
+[private]
+default:
+    @just --list --justfile {{ justfile() }}
+
+# ─── Build ──────────────────────────────────────────────────────────────────
+
+[doc("Build the full solution (Debug)")]
+[group("build")]
+build: _check-dotnet
+    {{ _dotnet }} build RackPeek.sln
+
+[doc("Build the full solution in Release mode")]
+[group("build")]
+build-release: _check-dotnet
+    {{ _dotnet }} build RackPeek.sln -c Release
+
+[doc("Publish CLI as self-contained single-file binary")]
+[group("build")]
+build-cli runtime="linux-x64": _check-dotnet
+    {{ _dotnet }} publish RackPeek/RackPeek.csproj -c Release -r {{ runtime }} \
+        --self-contained true \
+        -p:PublishSingleFile=true
+
+[doc("Build Web Docker image (required before E2E tests)")]
+[group("build")]
+build-web:
+    docker build -t {{ _image }} -f {{ _dockerfile }} .
+
+# ─── Test ───────────────────────────────────────────────────────────────────
+
+[doc("Run CLI tests (fast; no Docker required)")]
+[group("test")]
+test-cli: _check-dotnet
+    {{ _dotnet }} test Tests/Tests.csproj
+
+[doc("Install Playwright + browsers for E2E (first-time only)")]
+[group("test")]
+e2e-setup: _check-dotnet
+    cd Tests.E2e && {{ _dotnet }} tool install --global Microsoft.Playwright.CLI
+    cd Tests.E2e && {{ _dotnet }} build
+    cd Tests.E2e && playwright install
+
+[doc("Run E2E tests (depends on build-web; run e2e-setup once)")]
+[group("test")]
+test-e2e: _check-dotnet build-web
+    cd Tests.E2e && {{ _dotnet }} test
+
+[doc("Run CLI + E2E tests (rebuilds Web image)")]
+[group("test")]
+test-all: _check-dotnet build-web e2e-setup test-cli test-e2e
+
+[doc("Run full test suite (alias for test-all; matches CI / pre-PR checklist)")]
+[group("test")]
+ci: test-all
+
+# ─── Demo ───────────────────────────────────────────────────────────────────
+
+[doc("Generate CLI demo with VHS (needs: vhs, imagemagick, chrome)")]
+[group("demo")]
+build-cli-demo:
+    cd vhs && vhs ./rpk.tape
+
+[doc("Capture Web UI demo as GIF (needs: Chrome, ImageMagick)")]
+[group("demo")]
+build-web-demo:
+    cd vhs && chmod +x webui_capture.sh && ./webui_capture.sh
+
+# ─── Release ────────────────────────────────────────────────────────────────
+
+[doc("Build and push multi-arch Docker image to registry")]
+[group("release")]
+docker-push version:
+    docker buildx build \
+        --platform linux/amd64,linux/arm64 \
+        -f {{ _dockerfile }} \
+        -t aptacode/rackpeek:{{ version }} \
+        -t aptacode/rackpeek:latest \
+        --push .
+
+# ─── Utility ────────────────────────────────────────────────────────────────
+
+[doc("Clean build artifacts (bin, obj)")]
+[group("utility")]
+clean: _check-dotnet
+    {{ _dotnet }} clean RackPeek.sln

+ 265 - 0
schemas/v1/schema.v1.json

@@ -0,0 +1,265 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://timmoth.github.io/RackPeek/schemas/v1/schema.v1.json",
+  "title": "RackPeek Infrastructure Specification",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["version", "resources"],
+  "properties": {
+    "version": {
+      "type": "integer",
+      "const": 1
+    },
+    "resources": {
+      "type": "array",
+      "items": {
+        "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" }
+        ]
+      }
+    }
+  },
+  "$defs": {
+    "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][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}$"
+        },
+        "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
+        "protocol": { "type": "string", "enum": ["TCP","UDP"] },
+        "url": { "type": "string" }
+      }
+    },
+
+    "server": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Server" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "notes": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "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" } }
+      }
+    },
+
+    "desktop": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Desktop" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "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" } }
+      }
+    },
+
+    "laptop": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Laptop" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "ram": { "$ref": "#/$defs/ram" },
+        "cpus": { "type": "array", "items": { "$ref": "#/$defs/cpu" } },
+        "drives": { "type": "array", "items": { "$ref": "#/$defs/drive" } }
+      }
+    },
+
+    "firewall": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Firewall" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "router": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Router" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "switch": {
+      "type": "object",
+      "required": ["kind","name","ports"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Switch" },
+        "name": { "type": "string" },
+        "model": { "type": "string" },
+        "managed": { "type": "boolean" },
+        "poe": { "type": "boolean" },
+        "ports": { "type": "array", "items": { "$ref": "#/$defs/port" } }
+      }
+    },
+
+    "accessPoint": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "AccessPoint" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "model": { "type": "string" },
+        "speed": { "type": "number" }
+      }
+    },
+
+    "ups": {
+      "type": "object",
+      "required": ["kind","name"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Ups" },
+        "name": { "type": "string" },
+        "tags": { "type": "array", "items": { "type": "string" } },
+        "model": { "type": "string" },
+        "va": { "type": "integer", "minimum": 1 }
+      }
+    },
+
+    "service": {
+      "type": "object",
+      "required": ["kind","name","network","runsOn"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "Service" },
+        "name": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "network": { "$ref": "#/$defs/network" }
+      }
+    },
+
+    "system": {
+      "type": "object",
+      "required": ["kind","name","type","os","cores","ram"],
+      "additionalProperties": false,
+      "properties": {
+        "kind": { "const": "System" },
+        "name": { "type": "string" },
+        "notes": { "type": "string" },
+        "runsOn": { "type": "string" },
+        "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" } }
+      }
+    }
+  }
+}

+ 0 - 397
servers.yaml

@@ -1,397 +0,0 @@
-resources:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) Silver 4110
-        cores: 8
-        threads: 16
-    ram:
-      size: 64
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 480
-      - type: ssd
-        size: 480
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-      - type: sfp+
-        speed: 10
-        ports: 2
-    gpus:
-    ipmi: true
-    name: dell-c6400-node01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) Silver 4110
-        cores: 8
-        threads: 16
-    ram:
-      size: 128
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 960
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-      - type: sfp+
-        speed: 10
-        ports: 2
-    gpus:
-    ipmi: true
-    name: dell-c6400-node02
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) Silver 4110
-        cores: 8
-        threads: 16
-    ram:
-      size: 64
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 480
-      - type: ssd
-        size: 480
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-      - type: sfp+
-        speed: 10
-        ports: 2
-    gpus:
-    ipmi: true
-    name: dell-c6400-node03
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) Silver 4110
-        cores: 8
-        threads: 16
-    ram:
-      size: 128
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 960
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-      - type: sfp+
-        speed: 10
-        ports: 2
-    gpus:
-    ipmi: true
-    name: dell-c6400-node04
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E5-2620 v4
-        cores: 8
-        threads: 16
-    ram:
-      size: 64
-      mts: 2133
-    drives:
-      - type: hdd
-        size: 8192
-      - type: hdd
-        size: 8192
-      - type: hdd
-        size: 8192
-      - type: hdd
-        size: 8192
-      - type: ssd
-        size: 120
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 1
-      - type: sfp+
-        speed: 10
-        ports: 1
-    gpus:
-    ipmi: true
-    name: truenas-storage01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Core(TM) i5-8500
-        cores: 6
-        threads: 6
-    ram:
-      size: 32
-      mts: 2666
-    drives:
-      - type: ssd
-        size: 512
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 4
-    gpus:
-    ipmi: false
-    name: proxmox-edge01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Celeron(R) J4125
-        cores: 4
-        threads: 4
-    ram:
-      size: 8
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 64
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 4
-    gpus:
-    ipmi: false
-    name: opnsense-fw01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E3-1270 v6
-        cores: 4
-        threads: 8
-    ram:
-      size: 16
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 256
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 1
-    gpus:
-    ipmi: true
-    name: mgmt-bastion01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E5-2630 v4
-        cores: 10
-        threads: 20
-    ram:
-      size: 64
-      mts: 2133
-    drives:
-      - type: hdd
-        size: 6144
-      - type: hdd
-        size: 6144
-      - type: hdd
-        size: 6144
-      - type: hdd
-        size: 6144
-      - type: ssd
-        size: 240
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-      - type: sfp+
-        speed: 10
-        ports: 1
-    gpus:
-    ipmi: true
-    name: truenas-backup01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) Silver 4214
-        cores: 12
-        threads: 24
-    ram:
-      size: 128
-      mts: 2666
-    drives:
-      - type: ssd
-        size: 1024
-    nics:
-      - type: sfp+
-        speed: 10
-        ports: 2
-    gpus:
-      - model: NVIDIA Tesla P40
-        vram: 24
-      - model: NVIDIA Tesla P40
-        vram: 24
-      - model: NVIDIA Tesla P4
-        vram: 8
-    ipmi: true
-    name: compute-gpu01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E3-1240 v5
-        cores: 4
-        threads: 8
-    ram:
-      size: 32
-      mts: 2133
-    drives:
-      - type: ssd
-        size: 512
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-    gpus:
-    ipmi: true
-    name: proxmox-lab01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E-2224
-        cores: 4
-        threads: 4
-    ram:
-      size: 16
-      mts: 2666
-    drives:
-      - type: ssd
-        size: 256
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-    gpus:
-    ipmi: true
-    name: k8s-control01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E-2224
-        cores: 4
-        threads: 4
-    ram:
-      size: 16
-      mts: 2666
-    drives:
-      - type: ssd
-        size: 256
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-    gpus:
-    ipmi: true
-    name: k8s-control02
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) Silver 4108
-        cores: 8
-        threads: 16
-    ram:
-      size: 64
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 1024
-      - type: ssd
-        size: 1024
-    nics:
-      - type: sfp+
-        speed: 10
-        ports: 1
-    gpus:
-    ipmi: true
-    name: elk-logging01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Core(TM) i3-8100
-        cores: 4
-        threads: 4
-    ram:
-      size: 16
-      mts: 2400
-    drives:
-      - type: ssd
-        size: 256
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 2
-    gpus:
-    ipmi: false
-    name: edge-node01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E5-1650 v3
-        cores: 6
-        threads: 12
-    ram:
-      size: 64
-      mts: 2133
-    drives:
-      - type: ssd
-        size: 480
-      - type: hdd
-        size: 4096
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 4
-    gpus:
-    ipmi: true
-    name: backup-proxmox01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Core(TM) i7-8700
-        cores: 6
-        threads: 12
-    ram:
-      size: 32
-      mts: 2666
-    drives:
-      - type: ssd
-        size: 512
-    nics:
-      - type: rj45
-        speed: 1
-        ports: 1
-    gpus:
-    ipmi: false
-    name: lab-general01
-    tags:
-  - kind: Server
-    cpus:
-      - model: Intel(R) Xeon(R) E5-2650 v3
-        cores: 10
-        threads: 20
-    ram:
-      size: 128
-      mts: 2133
-    drives:
-      - type: hdd
-        size: 4096
-      - type: hdd
-        size: 4096
-      - type: hdd
-        size: 4096
-      - type: hdd
-        size: 4096
-    nics:
-      - type: sfp+
-        speed: 10
-        ports: 2
-    gpus:
-    ipmi: true
-    name: dell-r730-archive01
-    tags: