Sfoglia il codice sorgente

Modeled Hardware & System

Tim Jones 2 mesi fa
parent
commit
917527885b

BIN
.DS_Store


+ 13 - 0
.idea/.idea.RackPeek/.idea/.gitignore

@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/projectSettingsUpdater.xml
+/.idea.RackPeek.iml
+/modules.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 1 - 0
.idea/.idea.RackPeek/.idea/.name

@@ -0,0 +1 @@
+RackPeek

+ 8 - 0
.idea/.idea.RackPeek/.idea/indexLayout.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="UserContentModel">
+    <attachedFolders />
+    <explicitIncludes />
+    <explicitExcludes />
+  </component>
+</project>

+ 7 - 0
.idea/.idea.RackPeek/.idea/vcs.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 22 - 0
RackPeek.sln

@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RackPeek", "RackPeek\RackPeek.csproj", "{EFB7357E-A6B7-4359-BA0F-45D733849E4C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{2B19149A-FBD7-415E-9FE3-3BA53F2A0DD8}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{EFB7357E-A6B7-4359-BA0F-45D733849E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EFB7357E-A6B7-4359-BA0F-45D733849E4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EFB7357E-A6B7-4359-BA0F-45D733849E4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EFB7357E-A6B7-4359-BA0F-45D733849E4C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2B19149A-FBD7-415E-9FE3-3BA53F2A0DD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2B19149A-FBD7-415E-9FE3-3BA53F2A0DD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2B19149A-FBD7-415E-9FE3-3BA53F2A0DD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2B19149A-FBD7-415E-9FE3-3BA53F2A0DD8}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+EndGlobal

+ 102 - 0
RackPeek/.idea/.idea.RackPeek/.idea/workspace.xml

@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="AutoGeneratedRunConfigurationManager">
+    <projectFile>RackPeek/RackPeek.csproj</projectFile>
+  </component>
+  <component name="AutoImportSettings">
+    <option name="autoReloadType" value="SELECTIVE" />
+  </component>
+  <component name="ChangeListManager">
+    <list default="true" id="14236957-c690-47fc-a902-829825723976" name="Changes" comment="" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="KubernetesApiPersistence"><![CDATA[{}]]></component>
+  <component name="KubernetesApiProvider"><![CDATA[{
+  "isMigrated": true
+}]]></component>
+  <component name="ProjectColorInfo"><![CDATA[{
+  "associatedIndex": 3
+}]]></component>
+  <component name="ProjectId" id="38hs489yovquGUVXK5TIaIpuUT9" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent"><![CDATA[{
+  "keyToString": {
+    "ModuleVcsDetector.initialDetectionPerformed": "true",
+    "RunOnceActivity.ShowReadmeOnStart": "true",
+    "node.js.detected.package.eslint": "true",
+    "node.js.detected.package.tslint": "true",
+    "node.js.selected.package.eslint": "(autodetect)",
+    "node.js.selected.package.tslint": "(autodetect)",
+    "nodejs_package_manager_path": "npm",
+    "settings.editor.selected.configurable": "preferences.pluginManager",
+    "vue.rearranger.settings.migration": "true"
+  }
+}]]></component>
+  <component name="RunManager" selected=".NET Project.RackPeek">
+    <configuration name="RackPeek" type="DotNetProject" factoryName=".NET Project">
+      <option name="EXE_PATH" value="" />
+      <option name="PROGRAM_PARAMETERS" value="" />
+      <option name="WORKING_DIRECTORY" value="" />
+      <option name="PASS_PARENT_ENVS" value="1" />
+      <option name="USE_EXTERNAL_CONSOLE" value="0" />
+      <option name="ENV_FILE_PATHS" value="" />
+      <option name="REDIRECT_INPUT_PATH" value="" />
+      <option name="PTY_MODE" value="Auto" />
+      <option name="USE_MONO" value="0" />
+      <option name="RUNTIME_ARGUMENTS" value="" />
+      <option name="AUTO_ATTACH_CHILDREN" value="0" />
+      <option name="MIXED_MODE_DEBUG" value="0" />
+      <option name="PROJECT_PATH" value="$PROJECT_DIR$/RackPeek/RackPeek.csproj" />
+      <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
+      <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
+      <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
+      <option name="PROJECT_KIND" value="DotNetCore" />
+      <option name="PROJECT_TFM" value="" />
+      <method v="2">
+        <option name="Build" />
+      </method>
+    </configuration>
+  </component>
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="14236957-c690-47fc-a902-829825723976" name="Changes" comment="" />
+      <created>1769262943500</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1769262943500</updated>
+      <workItem from="1769262945049" duration="4224000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="3" />
+  </component>
+  <component name="UnityProjectConfiguration" hasMinimizedUI="false" />
+  <component name="VcsManagerConfiguration">
+    <option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
+  </component>
+  <component name="XDebuggerManager">
+    <breakpoint-manager>
+      <breakpoints>
+        <breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
+          <properties exception="System.OperationCanceledException" breakIfHandledByOtherCode="false" displayValue="System.OperationCanceledException" />
+          <option name="timeStamp" value="1" />
+        </breakpoint>
+        <breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
+          <properties exception="System.Threading.Tasks.TaskCanceledException" breakIfHandledByOtherCode="false" displayValue="System.Threading.Tasks.TaskCanceledException" />
+          <option name="timeStamp" value="2" />
+        </breakpoint>
+        <breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
+          <properties exception="System.Threading.ThreadAbortException" breakIfHandledByOtherCode="false" displayValue="System.Threading.ThreadAbortException" />
+          <option name="timeStamp" value="3" />
+        </breakpoint>
+      </breakpoints>
+    </breakpoint-manager>
+  </component>
+</project>

+ 64 - 0
RackPeek/Converters.cs

@@ -0,0 +1,64 @@
+using YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+using YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+
+namespace RackPeek;
+
+public static class StorageSizeParser
+{
+    private static readonly Regex SizeRegex =
+        new(@"^\s*(\d+(?:\.\d+)?)\s*(gb|tb)?\s*$",
+            RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+    public static int ParseToGb(string input)
+    {
+        var match = SizeRegex.Match(input);
+
+        if (!match.Success)
+            throw new FormatException($"Invalid storage size: '{input}'");
+
+        var value = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
+        var unit = match.Groups[2].Value.ToLowerInvariant();
+
+        return unit switch
+        {
+            "tb" => (int)Math.Round(value * 1024),
+            "gb" or "" => (int)Math.Round(value),
+            _ => throw new FormatException($"Unknown unit in '{input}'")
+        };
+    }
+}
+
+public class StorageSizeYamlConverter : IYamlTypeConverter
+{
+    public bool Accepts(Type type) =>
+        type == typeof(int) || type == typeof(int?);
+
+    public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
+    {
+        var scalar = parser.Consume<Scalar>();
+        var value = scalar.Value;
+
+        if (string.IsNullOrWhiteSpace(value))
+            return null;
+
+        // If it's already a number, just parse it
+        if (int.TryParse(value, out var numeric))
+            return numeric;
+
+        // Otherwise try size parsing
+        return StorageSizeParser.ParseToGb(value);
+        
+    }
+
+    public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
+    {
+        emitter.Emit(new Scalar(value?.ToString() ?? string.Empty));
+    }
+}

+ 9 - 0
RackPeek/Program.cs

@@ -0,0 +1,9 @@
+namespace RackPeek;
+
+class Program
+{
+    static void Main(string[] args)
+    {
+        Console.WriteLine("RackPeek");
+    }
+}

+ 14 - 0
RackPeek/RackPeek.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <OutputType>Exe</OutputType>
+        <TargetFramework>net10.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="YamlDotNet" Version="16.3.0" />
+    </ItemGroup>
+
+</Project>

+ 7 - 0
RackPeek/Resources/Hardware/AccessPoint.cs

@@ -0,0 +1,7 @@
+namespace RackPeek.Resources.Hardware;
+
+public class AccessPoint : Hardware
+{
+    public string? Model { get; set; }
+    public int? Speed { get; set; }
+}

+ 8 - 0
RackPeek/Resources/Hardware/Cpu.cs

@@ -0,0 +1,8 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Cpu
+{
+    public string? Model { get; set; }
+    public int? Cores { get; set; }
+    public int? Threads { get; set; }
+}

+ 10 - 0
RackPeek/Resources/Hardware/Desktop.cs

@@ -0,0 +1,10 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Desktop : Hardware
+{
+    public List<Cpu>? Cpus { get; set; }
+    public Ram? Ram { get; set; }
+    public List<Drive>? Drives { get; set; }
+    public List<Nic>? Nics { get; set; }
+    public List<Gpu>? Gpus { get; set; } 
+}

+ 7 - 0
RackPeek/Resources/Hardware/Drive.cs

@@ -0,0 +1,7 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Drive
+{
+    public string? Type { get; set; }
+    public int? Size { get; set; }
+}

+ 9 - 0
RackPeek/Resources/Hardware/Firewall.cs

@@ -0,0 +1,9 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Firewall : Hardware
+{
+    public string? Model { get; set; }
+    public bool? Managed { get; set; }
+    public bool? Poe { get; set; }
+    public List<Port>? Ports { get; set; }
+}

+ 7 - 0
RackPeek/Resources/Hardware/Gpu.cs

@@ -0,0 +1,7 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Gpu
+{
+    public string? Model { get; set; }
+    public int? Vram { get; set; }
+}

+ 6 - 0
RackPeek/Resources/Hardware/Hardware.cs

@@ -0,0 +1,6 @@
+namespace RackPeek.Resources.Hardware;
+
+public abstract class Hardware : Resource
+{
+
+}

+ 10 - 0
RackPeek/Resources/Hardware/Laptop.cs

@@ -0,0 +1,10 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Laptop : Hardware
+{
+    public List<Cpu>? Cpus { get; set; }
+    public Ram? Ram { get; set; }
+    public List<Drive>? Drives { get; set; } 
+    public List<Gpu>? Gpus { get; set; } 
+
+}

+ 8 - 0
RackPeek/Resources/Hardware/Nic.cs

@@ -0,0 +1,8 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Nic
+{
+    public string? Type { get; set; }
+    public int? Speed { get; set; }
+    public int? Ports { get; set; }
+}

+ 8 - 0
RackPeek/Resources/Hardware/Port.cs

@@ -0,0 +1,8 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Port
+{
+    public string? Type { get; set; }
+    public int? Speed { get; set; }
+    public int? Count { get; set; }
+}

+ 7 - 0
RackPeek/Resources/Hardware/Ram.cs

@@ -0,0 +1,7 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Ram
+{
+    public int? Size { get; set; }
+    public int? Mts { get; set; }
+}

+ 9 - 0
RackPeek/Resources/Hardware/Router.cs

@@ -0,0 +1,9 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Router : Hardware
+{
+    public string? Model { get; set; }
+    public bool? Managed { get; set; }
+    public bool? Poe { get; set; }
+    public List<Port>? Ports { get; set; }
+}

+ 11 - 0
RackPeek/Resources/Hardware/Server.cs

@@ -0,0 +1,11 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Server : Hardware
+{
+    public List<Cpu>? Cpus { get; set; }
+    public Ram? Ram { get; set; }
+    public List<Drive>? Drives { get; set; }
+    public List<Nic>? Nics { get; set; }
+    public List<Gpu>? Gpus { get; set; } 
+    public bool? Ipmi { get; set; }
+}

+ 9 - 0
RackPeek/Resources/Hardware/Switch.cs

@@ -0,0 +1,9 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Switch : Hardware
+{
+    public string? Model { get; set; }
+    public bool? Managed { get; set; }
+    public bool? Poe { get; set; }
+    public List<Port>? Ports { get; set; }
+}

+ 12 - 0
RackPeek/Resources/Hardware/SystemResource.cs

@@ -0,0 +1,12 @@
+namespace RackPeek.Resources.Hardware;
+
+public class SystemResource : Resource
+{
+    public string? Type { get; set; }
+    public string? Os { get; set; }
+    public int? Cores { get; set; }
+    public int? Ram { get; set; }
+    public List<Drive>? Drives { get; set; }
+
+    public string? RunsOn { get; set; }
+}

+ 7 - 0
RackPeek/Resources/Hardware/Ups.cs

@@ -0,0 +1,7 @@
+namespace RackPeek.Resources.Hardware;
+
+public class Ups : Hardware
+{
+    public string? Model { get; set; }
+    public int? Va { get; set; }
+}

+ 6 - 0
RackPeek/Resources/LabFile.cs

@@ -0,0 +1,6 @@
+namespace RackPeek.Resources;
+
+public class LabFile
+{
+    public List<Dictionary<string, object>> Resources { get; set; } = new();
+}

+ 49 - 0
RackPeek/Resources/LabLoader.cs

@@ -0,0 +1,49 @@
+using RackPeek.Resources.Hardware;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace RackPeek.Resources;
+
+public static class LabLoader
+{
+    public static List<Resource> Load(string yaml)
+    {
+        var deserializer = new DeserializerBuilder()
+            .WithNamingConvention(CamelCaseNamingConvention.Instance)
+            .WithCaseInsensitivePropertyMatching()
+            .WithTypeConverter(new StorageSizeYamlConverter())
+            .Build();
+
+        var raw = deserializer.Deserialize<Dictionary<string, List<Dictionary<string, object>>>>(yaml);
+
+        var resources = new List<Resource>();
+
+        foreach (var item in raw["resources"])
+        {
+            var kind = item["kind"].ToString();
+
+            var typedYaml = new SerializerBuilder().Build().Serialize(item);
+    
+            Resource resource = kind switch
+            {
+                // Hardware
+                "Server" => deserializer.Deserialize<Server>(typedYaml),
+                "Switch" => deserializer.Deserialize<Switch>(typedYaml),
+                "Firewall" => deserializer.Deserialize<Firewall>(typedYaml),
+                "Router" => deserializer.Deserialize<Router>(typedYaml),
+                "Desktop" => deserializer.Deserialize<Desktop>(typedYaml),
+                "Laptop" => deserializer.Deserialize<Laptop>(typedYaml),
+                "AccessPoint" => deserializer.Deserialize<AccessPoint>(typedYaml),
+                "Ups" => deserializer.Deserialize<Ups>(typedYaml),
+                
+                // System
+                "System" => deserializer.Deserialize<SystemResource>(typedYaml),
+                _ => throw new InvalidOperationException($"Unknown kind: {kind}")
+            };
+
+            resources.Add(resource);
+        }
+
+        return resources;
+    }
+}

+ 19 - 0
RackPeek/Resources/Resource.cs

@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+using YamlDotNet.Serialization;
+
+namespace RackPeek.Resources;
+
+public abstract class Resource
+{
+    [JsonPropertyName("kind")]
+    [YamlMember(Alias = "kind")]
+    public string Kind { get; set; } = string.Empty;
+
+    [JsonPropertyName("name")]
+    [YamlMember(Alias = "name")]
+    public required string Name { get; set; }
+
+    [JsonPropertyName("tags")]
+    [YamlMember(Alias = "tags")]
+    public Dictionary<string, string>? Tags { get; set; }
+}

+ 414 - 0
Tests/HardwareDeserializationTests.cs

@@ -0,0 +1,414 @@
+using RackPeek.Resources;
+using RackPeek.Resources.Hardware;
+
+namespace Tests;
+
+public class HardwareDeserializationTests
+{
+    [Theory]
+    [InlineData("Server", typeof(Server))]
+    [InlineData("Switch", typeof(Switch))]
+    [InlineData("Firewall", typeof(Firewall))]
+    [InlineData("Desktop", typeof(Desktop))]
+    [InlineData("Laptop", typeof(Laptop))]
+    [InlineData("Router", typeof(Router))]
+    [InlineData("AccessPoint", typeof(AccessPoint))]
+    [InlineData("Ups", typeof(Ups))]
+    public void deserialize_yaml_kind(string kind, Type type)
+    {
+        // Given
+        var yaml = $@"
+resources:
+  - kind: {kind}
+";
+        
+        // When
+        var resources = LabLoader.Load(yaml);
+        
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType(type, hardware);
+    }
+    
+    [Fact]
+    public void deserialize_yaml_kind_server()
+    {
+        // Given
+        var yaml = $@"
+resources:
+  - kind: Server
+    name: dell-c6400-node01
+    cpus:
+        - model: Intel(R) Xeon(R) CPU E3-1270 v6
+          cores: 4
+          threads: 8
+    ram:
+        size: 32gb
+        Mts: 2400
+    drives:
+        - type: hdd
+          size: 2Tb
+        - type: ssd
+          size: 256gb
+    nics:
+        - type: rj45
+          speed: 1gb
+          ports: 2
+        - type: sfp+
+          speed: 10gb
+          ports: 2
+    ipmi: true
+";
+        // When
+        var resources = LabLoader.Load(yaml);
+        
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Server>(hardware);
+        var server = hardware as Server;
+        Assert.NotNull(server);
+        Assert.Equal("dell-c6400-node01", server.Name);
+        // Cpu
+        Assert.NotNull(server.Cpus);
+        var cpu = server.Cpus[0];
+        Assert.Equal("Intel(R) Xeon(R) CPU E3-1270 v6", cpu.Model);
+        Assert.Equal(4, cpu.Cores);
+        Assert.Equal(8, cpu.Threads);
+        
+        // Ram
+        Assert.NotNull(server.Ram);
+        Assert.Equal(32, server.Ram.Size);
+        Assert.Equal(2400, server.Ram.Mts);
+        
+        // Drives
+        Assert.NotNull(server.Drives);
+        var hdd = server.Drives[0];
+        Assert.Equal("hdd", hdd.Type);
+        Assert.Equal(2048, hdd.Size);
+        var ssd = server.Drives[1];
+        Assert.Equal("ssd", ssd.Type);
+        Assert.Equal(256, ssd.Size);
+
+        // ipmi
+        Assert.True(server.Ipmi);
+        
+        // Nics
+        Assert.NotNull(server.Nics);
+        var nic0 = server.Nics[0];
+        Assert.Equal("rj45", nic0.Type);
+        Assert.Equal(1, nic0.Speed);
+        Assert.Equal(2, nic0.Ports);
+        var nic1 = server.Nics[1];
+        Assert.Equal("sfp+", nic1.Type);
+        Assert.Equal(10, nic1.Speed);
+        Assert.Equal(2, nic1.Ports);
+    }
+    
+    [Fact]
+    public void deserialize_yaml_kind_switch()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: Switch
+    name: netgear-s24
+    model: GS324
+    ports:
+        - type: rj45
+          speed: 1gb
+          count: 8
+        - type: sfp
+          speed: 10gb
+          count: 2
+    managed: true
+    poe: true
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Switch>(hardware);
+
+        var sw = hardware as Switch;
+        Assert.NotNull(sw);
+
+        Assert.Equal("netgear-s24", sw.Name);
+        Assert.Equal("GS324", sw.Model);
+        Assert.Equal(true, sw.Managed);
+        Assert.Equal(true, sw.Poe);
+        
+        // Nics
+        Assert.NotNull(sw.Ports);
+        var nic0 = sw.Ports[0];
+        Assert.Equal("rj45", nic0.Type);
+        Assert.Equal(1, nic0.Speed);
+        Assert.Equal(8, nic0.Count);
+        var nic1 = sw.Ports[1];
+        Assert.Equal("sfp", nic1.Type);
+        Assert.Equal(10, nic1.Speed);
+        Assert.Equal(2, nic1.Count);
+        
+    }
+    [Fact]
+    public void deserialize_yaml_kind_firewall()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: Firewall
+    name: pfsense
+    model: pfSense-1100
+    ports:
+        - type: rj45
+          speed: 1gb
+          count: 8
+        - type: sfp
+          speed: 10gb
+          count: 2
+    managed: true
+    poe: true
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Firewall>(hardware);
+
+        var fw = hardware as Firewall;
+        Assert.NotNull(fw);
+
+        Assert.Equal("pfsense", fw.Name);
+        Assert.Equal("pfSense-1100", fw.Model);
+        Assert.Equal(true, fw.Managed);
+        Assert.Equal(true, fw.Poe);
+        
+        // Nics
+        Assert.NotNull(fw.Ports);
+        var nic0 = fw.Ports[0];
+        Assert.Equal("rj45", nic0.Type);
+        Assert.Equal(1, nic0.Speed);
+        Assert.Equal(8, nic0.Count);
+        var nic1 = fw.Ports[1];
+        Assert.Equal("sfp", nic1.Type);
+        Assert.Equal(10, nic1.Speed);
+        Assert.Equal(2, nic1.Count);
+    }
+    [Fact]
+    public void deserialize_yaml_kind_router()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: Router
+    name: ubiquiti-edge-router
+    model: ER-4
+    ports:
+        - type: rj45
+          speed: 1gb
+          count: 8
+        - type: sfp
+          speed: 10gb
+          count: 2
+    managed: true
+    poe: true
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Router>(hardware);
+
+        var router = hardware as Router;
+        Assert.NotNull(router);
+
+        Assert.Equal("ubiquiti-edge-router", router.Name);
+        Assert.Equal("ER-4", router.Model);
+        Assert.Equal(true, router.Managed);
+        Assert.Equal(true, router.Poe);
+        
+        // Nics
+        Assert.NotNull(router.Ports);
+        var nic0 = router.Ports[0];
+        Assert.Equal("rj45", nic0.Type);
+        Assert.Equal(1, nic0.Speed);
+        Assert.Equal(8, nic0.Count);
+        var nic1 = router.Ports[1];
+        Assert.Equal("sfp", nic1.Type);
+        Assert.Equal(10, nic1.Speed);
+        Assert.Equal(2, nic1.Count);
+    }
+    [Fact]
+    public void deserialize_yaml_kind_desktop()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: Desktop
+    name: dell-optiplex
+    cpus:
+      - model: Intel(R) Core(TM) i5-9500
+        cores: 6
+        threads: 6
+    ram:
+      size: 16gb
+      mts: 2666
+    drives:
+      - type: ssd
+        size: 512gb
+    nics:
+      - type: rj45
+        speed: 1gb
+        ports: 1
+    gpus:
+       - model: RTX 3080
+         vram: 12gb
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Desktop>(hardware);
+
+        var desktop = hardware as Desktop;
+        Assert.NotNull(desktop);
+
+        Assert.Equal("dell-optiplex", desktop.Name);
+
+        // CPU
+        Assert.NotNull(desktop.Cpus);
+        Assert.Equal("Intel(R) Core(TM) i5-9500", desktop.Cpus[0].Model);
+        Assert.Equal(6, desktop.Cpus[0].Cores);
+        Assert.Equal(6, desktop.Cpus[0].Threads);
+
+        // RAM
+        Assert.NotNull(desktop.Ram);
+        Assert.Equal(16, desktop.Ram.Size);
+        Assert.Equal(2666, desktop.Ram.Mts);
+
+        // Drives
+        Assert.NotNull(desktop.Drives);
+        Assert.Equal("ssd", desktop.Drives[0].Type);
+        Assert.Equal(512, desktop.Drives[0].Size);
+
+        // NIC
+        Assert.NotNull(desktop.Nics);
+        Assert.Equal("rj45", desktop.Nics[0].Type);
+        Assert.Equal(1, desktop.Nics[0].Speed);
+        Assert.Equal(1, desktop.Nics[0].Ports);
+    }
+    [Fact]
+    public void deserialize_yaml_kind_laptop()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: Laptop
+    name: thinkpad-x1
+    cpus:
+        - model: Intel(R) Core(TM) i7-10510U
+          cores: 4
+          threads: 8
+    ram:
+        size: 16gb
+        mts: 2666
+    drives:
+        - type: ssd
+          size: 1tb
+    gpus:
+       - model: RTX 3080
+         vram: 12gb
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Laptop>(hardware);
+
+        var laptop = hardware as Laptop;
+        Assert.NotNull(laptop);
+
+        Assert.Equal("thinkpad-x1", laptop.Name);
+
+        // CPU
+        Assert.NotNull(laptop.Cpus);
+        Assert.Equal("Intel(R) Core(TM) i7-10510U", laptop.Cpus[0].Model);
+        Assert.Equal(4, laptop.Cpus[0].Cores);
+        Assert.Equal(8, laptop.Cpus[0].Threads);
+
+        // RAM
+        Assert.NotNull(laptop.Ram);
+        Assert.Equal(16, laptop.Ram.Size);
+        Assert.Equal(2666, laptop.Ram.Mts);
+
+        // Drives
+        Assert.NotNull(laptop.Drives);
+        Assert.Equal("ssd", laptop.Drives[0].Type);
+        Assert.Equal(1024, laptop.Drives[0].Size);
+    }
+    
+    [Fact]
+    public void deserialize_yaml_kind_accesspoint()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: AccessPoint
+    name: lounge-ap
+    model: Unifi-Ap-Pro
+    speed: 1gb
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<AccessPoint>(hardware);
+
+        var accessPoint = hardware as AccessPoint;
+        Assert.NotNull(accessPoint);
+
+        Assert.Equal("lounge-ap", accessPoint.Name);
+        Assert.Equal("Unifi-Ap-Pro", accessPoint.Model);
+        Assert.Equal(1, accessPoint.Speed);
+
+    }
+    
+    [Fact]
+    public void deserialize_yaml_kind_ups()
+    {
+        // Given
+        var yaml = @"
+resources:
+  - kind: Ups
+    name: rack-ups
+    model: Volta
+    va: 2200
+";
+
+        // When
+        var resources = LabLoader.Load(yaml);
+
+        // Then
+        var hardware = Assert.Single(resources);
+        Assert.IsType<Ups>(hardware);
+
+        var ups = hardware as Ups;
+        Assert.NotNull(ups);
+
+        Assert.Equal("rack-ups", ups.Name);
+        Assert.Equal("Volta", ups.Model);
+        Assert.Equal(2200, ups.Va);
+
+    }
+}

+ 49 - 0
Tests/SystemDeserializationTests.cs

@@ -0,0 +1,49 @@
+using RackPeek.Resources;
+using RackPeek.Resources.Hardware;
+
+namespace Tests;
+
+public class ServiceDeserializationTests
+{
+    [Fact]
+    public void deserialize_yaml_kind_System()
+    {
+        // type: Hypervisor | Baremetal | VM | Container 
+        
+        // Given
+        var yaml = $@"
+resources:
+  - kind: System
+    type: Hypervisor
+    name: home-virtualization-host
+    os: proxmox     
+    cores: 2
+    ram: 12gb
+    drives:
+        - size: 2Tb
+        - size: 1tb   
+    runsOn: dell-c6400-node-01
+";
+        // When
+        var resources = LabLoader.Load(yaml);
+        
+        // Then
+        var resource = Assert.Single(resources);
+        Assert.IsType<SystemResource>(resource);
+        var system = resource as SystemResource;
+        Assert.NotNull(system);
+        Assert.Equal("Hypervisor", system.Type);
+        Assert.Equal("home-virtualization-host", system.Name);
+        Assert.Equal("proxmox", system.Os);
+        Assert.Equal(2, system.Cores);
+        Assert.Equal(12, system.Ram);
+
+        // Drives
+        Assert.NotNull(system.Drives);
+        Assert.Equal(2048, system.Drives[0].Size);
+        Assert.Equal(1024, system.Drives[1].Size);
+        
+        Assert.Equal("dell-c6400-node-01", system.RunsOn);
+
+    }
+}

+ 25 - 0
Tests/Tests.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net10.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+        <IsPackable>false</IsPackable>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="coverlet.collector" Version="6.0.4"/>
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
+        <PackageReference Include="xunit" Version="2.9.3"/>
+        <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Using Include="Xunit"/>
+    </ItemGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\RackPeek\RackPeek.csproj" />
+    </ItemGroup>
+
+</Project>