소스 검색

Added basic server CRUD commands

Tim Jones 2 달 전
부모
커밋
db0ff54793

+ 39 - 0
RackPeek.Domain/Resources/Hardware/Crud/AddServerUseCase.cs

@@ -0,0 +1,39 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Crud;
+
+public class AddServerUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        string? cpuModel,
+        int? cores,
+        int? threads,
+        int? ramGb,
+        bool? ipmi
+    )
+    {
+        // basic guard rails
+        var existing = await repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"Server '{name}' already exists.");
+
+        var server = new Server
+        {
+            Name = name,
+            Cpus = [new Cpu()
+            {
+                Model = cpuModel,
+                Cores = cores,
+                Threads = threads
+            }],
+            Ram = new Ram
+            {
+                Size = ramGb
+            },
+            Ipmi = ipmi
+        };
+
+        await repository.AddAsync(server);
+    }
+}

+ 13 - 0
RackPeek.Domain/Resources/Hardware/Crud/DeleteServerUseCase.cs

@@ -0,0 +1,13 @@
+namespace RackPeek.Domain.Resources.Hardware.Crud;
+
+public class DeleteServerUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        if (hardware == null)
+            throw new InvalidOperationException($"Server '{name}' not found.");
+
+        await repository.DeleteAsync(name);
+    }
+}

+ 42 - 0
RackPeek.Domain/Resources/Hardware/Crud/DescribeServerUseCase.cs

@@ -0,0 +1,42 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Crud;
+
+public record ServerDescription(
+    string Name,
+    string CpuSummary,
+    int TotalCores,
+    int TotalThreads,
+    int RamGb,
+    int TotalStorageGb,
+    int NicPorts,
+    bool Ipmi
+);
+
+public class DescribeServerUseCase(IHardwareRepository repository)
+{
+    public async Task<ServerDescription?> ExecuteAsync(string name)
+    {
+        var server = await repository.GetByNameAsync(name) as Server;
+        if (server == null)
+            return null;
+
+        var cpuSummary = server.Cpus == null
+            ? "Unknown"
+            : string.Join(", ",
+                server.Cpus
+                    .GroupBy(c => c.Model)
+                    .Select(g => $"{g.Count()}× {g.Key}"));
+
+        return new ServerDescription(
+            Name: server.Name,
+            CpuSummary: cpuSummary,
+            TotalCores: server.Cpus?.Sum(c => c.Cores) ?? 0,
+            TotalThreads: server.Cpus?.Sum(c => c.Threads) ?? 0,
+            RamGb: server.Ram?.Size ?? 0,
+            TotalStorageGb: server.Drives?.Sum(d => d.Size) ?? 0,
+            NicPorts: server.Nics?.Sum(n => n.Ports) ?? 0,
+            Ipmi: server.Ipmi ?? false
+        );
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/Crud/GetServerUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Crud;
+
+public class GetServerUseCase(IHardwareRepository repository)
+{
+    public async Task<Server?> ExecuteAsync(string name)
+    {
+        var hardware = await repository.GetByNameAsync(name);
+        return hardware as Server;
+    }
+}

+ 12 - 0
RackPeek.Domain/Resources/Hardware/Crud/GetServersUseCase.cs

@@ -0,0 +1,12 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Crud;
+
+public class GetServersUseCase(IHardwareRepository repository)
+{
+    public async Task<IReadOnlyList<Server>> ExecuteAsync()
+    {
+        var hardware = await repository.GetAllAsync();
+        return hardware.OfType<Server>().ToList();
+    }
+}

+ 54 - 0
RackPeek.Domain/Resources/Hardware/Crud/UpdateServerUseCase.cs

@@ -0,0 +1,54 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Crud;
+
+using RackPeek.Domain.Resources.Hardware.Models;
+
+public class UpdateServerUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        int? ramGb = null,
+        bool? ipmi = null,
+        string? cpuModel = null,
+        int? cores = null,
+        int? threads = null
+    )
+    {
+        var server = await repository.GetByNameAsync(name) as Server;
+        if (server == null)
+            throw new InvalidOperationException($"Server '{name}' not found.");
+
+        // ---- RAM ----
+        if (ramGb.HasValue)
+        {
+            server.Ram ??= new Ram();
+            server.Ram.Size = ramGb.Value;
+        }
+
+        // ---- IPMI ----
+        if (ipmi.HasValue)
+        {
+            server.Ipmi = ipmi.Value;
+        }
+
+        // ---- CPU (first CPU for now) ----
+        if (cpuModel != null || cores.HasValue || threads.HasValue)
+        {
+            server.Cpus ??= new List<Cpu> { new Cpu() };
+
+            var cpu = server.Cpus.First();
+
+            if (cpuModel != null)
+                cpu.Model = cpuModel;
+
+            if (cores.HasValue)
+                cpu.Cores = cores.Value;
+
+            if (threads.HasValue)
+                cpu.Threads = threads.Value;
+        }
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 4 - 0
RackPeek.Domain/Resources/Hardware/IHardwareRepository.cs

@@ -3,4 +3,8 @@ namespace RackPeek.Domain.Resources.Hardware;
 public interface IHardwareRepository
 {
     Task<IReadOnlyList<Models.Hardware>> GetAllAsync();
+    Task AddAsync(Models.Hardware hardware);
+    Task UpdateAsync(Models.Hardware hardware);
+    Task DeleteAsync(string name);
+    Task<Models.Hardware?> GetByNameAsync(string name);
 }

+ 242 - 0
RackPeek/Commands/ServerCommands.cs

@@ -0,0 +1,242 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Crud;
+using RackPeek.Domain.Resources.Hardware.Reports;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek;
+
+public class ServerNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    public string Name { get; set; } = default!;
+}
+
+public class ServerAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    public string Name { get; set; } = default!;
+
+    [CommandOption("--cpu <MODEL>")]
+    public string? CpuModel { get; set; } = default!;
+
+    [CommandOption("--cores <CORES>")]
+    public int? Cores { get; set; }
+
+    [CommandOption("--threads <THREADS>")]
+    public int? Threads { get; set; }
+
+    [CommandOption("--ram <GB>")]
+    public int? RamGb { get; set; }
+
+    [CommandOption("--ipmi")]
+    public bool? Ipmi { get; set; }
+}
+
+public class ServerAddCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddServerUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.CpuModel,
+            settings.Cores,
+            settings.Threads,
+            settings.RamGb,
+            settings.Ipmi
+        );
+
+        AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' added.[/]");
+        return 0;
+    }
+}
+
+public class ServerGetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<ServerHardwareReportUseCase>();
+
+        var report = await useCase.ExecuteAsync();
+
+        if (report.Servers.Count == 0)
+        {
+            AnsiConsole.MarkupLine("[yellow]No servers found.[/]");
+            return 0;
+        }
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .AddColumn("Name")
+            .AddColumn("CPU")
+            .AddColumn("C/T")
+            .AddColumn("RAM")
+            .AddColumn("Storage")
+            .AddColumn("NICs")
+            .AddColumn("IPMI");
+
+        foreach (var s in report.Servers)
+        {
+            table.AddRow(
+                s.Name,
+                s.CpuSummary,
+                $"{s.TotalCores}/{s.TotalThreads}",
+                $"{s.RamGb} GB",
+                $"{s.TotalStorageGb} GB",
+                $"{s.TotalNicPorts}×{s.MaxNicSpeedGb}G",
+                s.Ipmi ? "[green]yes[/]" : "[red]no[/]"
+            );
+        }
+
+        AnsiConsole.Write(table);
+        return 0;
+    }
+}
+
+public class ServerGetByNameCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<GetServerUseCase>();
+
+        var server = await useCase.ExecuteAsync(settings.Name);
+
+        if (server == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Server '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        AnsiConsole.MarkupLine(
+            $"[green]{server.Name}[/]  RAM: {server.Ram?.Size} GB, IPMI: {(server.Ipmi == true ? "yes" : "no")}");
+
+        return 0;
+    }
+}
+
+public class ServerDescribeCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<GetServerUseCase>();
+
+        var server = await useCase.ExecuteAsync(settings.Name);
+
+        if (server == null)
+        {
+            AnsiConsole.MarkupLine($"[red]Server '{settings.Name}' not found.[/]");
+            return 1;
+        }
+
+        var grid = new Grid()
+            .AddColumn()
+            .AddColumn();
+
+        grid.AddRow("Name", server.Name);
+        grid.AddRow("IPMI", server.Ipmi == true ? "yes" : "no");
+        grid.AddRow("RAM", $"{server.Ram?.Size ?? 0} GB");
+
+        if (server.Cpus != null)
+        {
+            foreach (var cpu in server.Cpus)
+                grid.AddRow("CPU", $"{cpu.Model} ({cpu.Cores}/{cpu.Threads})");
+        }
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("Server")
+                .Border(BoxBorder.Rounded));
+
+        return 0;
+    }
+}
+
+public class ServerSetSettings : ServerNameSettings
+{
+    [CommandOption("--cpu <MODEL>")]
+    public string CpuModel { get; set; } = default!;
+
+    [CommandOption("--cores <CORES>")]
+    public int Cores { get; set; }
+
+    [CommandOption("--threads <THREADS>")]
+    public int Threads { get; set; }
+
+    [CommandOption("--ram <GB>")]
+    public int RamGb { get; set; }
+
+    [CommandOption("--ipmi")]
+    public bool Ipmi { get; set; }
+}
+
+public class ServerSetCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateServerUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.RamGb,
+            settings.Ipmi,
+            settings.CpuModel,
+            settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}
+
+public class ServerDeleteCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerNameSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerNameSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<DeleteServerUseCase>();
+
+        await useCase.ExecuteAsync(settings.Name);
+
+        AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' deleted.[/]");
+        return 0;
+    }
+}
+
+
+

+ 58 - 17
RackPeek/Program.cs

@@ -4,6 +4,7 @@ using RackPeek.Domain.Resources.Hardware;
 using Spectre.Console;
 using Spectre.Console.Cli;
 using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Reports;
 
 namespace RackPeek;
@@ -34,22 +35,36 @@ public static class Program
         services.AddScoped<UpsReportCommand>();
         services.AddScoped<DesktopHardwareReportUseCase>();
         services.AddScoped<DesktopReportCommand>();
+        
+        services.AddScoped<AddServerUseCase>();
+        services.AddScoped<ServerAddCommand>();
+        
+        services.AddScoped<DeleteServerUseCase>();
+        services.AddScoped<ServerDeleteCommand>();
 
+        services.AddScoped<DescribeServerUseCase>();
+        services.AddScoped<ServerDescribeCommand>();
+        
+        services.AddScoped<GetServerUseCase>();
+        services.AddScoped<ServerGetByNameCommand>();
+        
+        services.AddScoped<UpdateServerUseCase>();
+        services.AddScoped<ServerSetCommand>();
         // Infrastructure
         services.AddScoped<IHardwareRepository>(_ =>
         {
             var path = configuration["HardwareFile"] ?? "hardware.yaml";
             
             var collection = new YamlResourceCollection();
-            collection.Load([
-                File.ReadAllText("servers.yaml"),
-                File.ReadAllText("aps.yaml"),
-                File.ReadAllText("desktops.yaml"),
-                File.ReadAllText("switches.yaml"),
-                File.ReadAllText("ups.yaml"),
-                File.ReadAllText("firewalls.yaml"),
-                File.ReadAllText("laptops.yaml"),
-                File.ReadAllText("routers.yaml")]);
+            collection.LoadFiles([
+                "servers.yaml",
+                "aps.yaml",
+                "desktops.yaml",
+                "switches.yaml",
+                "ups.yaml",
+                "firewalls.yaml",
+                "laptops.yaml",
+                "routers.yaml"]);
 
             return new YamlHardwareRepository(collection);
         });
@@ -67,21 +82,47 @@ public static class Program
         {
             config.SetApplicationName("rackpeek");
 
-            config.AddCommand<ServerReportCommand>("servers")
-                .WithDescription("Show server hardware report");
-
+            // ----------------------------
+            // Server commands (CRUD-style)
+            // ----------------------------
+            config.AddBranch("servers", server =>
+            {
+                server.SetDescription("Manage servers");
+                
+                server.AddCommand<ServerReportCommand>("summary")
+                    .WithDescription("Show server hardware report");
+                
+                server.AddCommand<ServerAddCommand>("add")
+                    .WithDescription("Add a new server");
+
+                server.AddCommand<ServerGetByNameCommand>("get")
+                    .WithDescription("List servers or get a server by name");
+
+                server.AddCommand<ServerDescribeCommand>("describe")
+                    .WithDescription("Show detailed information about a server");
+
+                server.AddCommand<ServerSetCommand>("set")
+                    .WithDescription("Update server properties");
+
+                server.AddCommand<ServerDeleteCommand>("delete")
+                    .WithDescription("Delete a server");
+            });
+
+            // ----------------------------
+            // Reports (read-only summaries)
+            // ----------------------------
             config.AddCommand<AccessPointReportCommand>("ap")
                 .WithDescription("Show access point hardware report");
-            
+
             config.AddCommand<DesktopReportCommand>("desktops")
                 .WithDescription("Show desktop hardware report");
-            
+
             config.AddCommand<SwitchReportCommand>("switches")
                 .WithDescription("Show switch hardware report");
-            
+
             config.AddCommand<UpsReportCommand>("ups")
-                .WithDescription("Show ups hardware report");
-            
+                .WithDescription("Show UPS hardware report");
+
             config.ValidateExamples();
         });
 

+ 59 - 3
RackPeek/Yaml/YamlHardwareRepository.cs

@@ -1,12 +1,68 @@
+using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware.Models;
 
 namespace RackPeek;
 
-public class YamlHardwareRepository(YamlResourceCollection resourceCollection) : IHardwareRepository
+public class YamlHardwareRepository : IHardwareRepository
 {
+    private readonly YamlResourceCollection _resources;
+
+    public YamlHardwareRepository(YamlResourceCollection resources)
+    {
+        _resources = resources;
+    }
+
     public Task<IReadOnlyList<Hardware>> GetAllAsync()
+        => Task.FromResult(_resources.HardwareResources);
+
+    public Task<Hardware?> GetByNameAsync(string name)
+        => Task.FromResult(_resources.GetByName(name) as Hardware);
+
+    public Task AddAsync(Hardware hardware)
     {
-        return Task.FromResult(resourceCollection.HardwareResources);
+        if (_resources.HardwareResources.Any(r =>
+            r.Name.Equals(hardware.Name, StringComparison.OrdinalIgnoreCase)))
+        {
+            throw new InvalidOperationException(
+                $"Hardware with name '{hardware.Name}' already exists.");
+        }
+
+        // Use first file as default for new resources
+        var targetFile = _resources.SourceFiles.FirstOrDefault()
+                         ?? throw new InvalidOperationException("No YAML file loaded.");
+
+        _resources.Add(hardware, targetFile);
+        _resources.SaveAll();
+
+        return Task.CompletedTask;
+    }
+
+    public Task UpdateAsync(Hardware hardware)
+    {
+        var existing = _resources.HardwareResources
+            .FirstOrDefault(r => r.Name.Equals(hardware.Name, StringComparison.OrdinalIgnoreCase));
+
+        if (existing == null)
+            throw new InvalidOperationException($"Hardware '{hardware.Name}' not found.");
+
+        _resources.Update(hardware);
+        _resources.SaveAll();
+
+        return Task.CompletedTask;
+    }
+
+    public Task DeleteAsync(string name)
+    {
+        var existing = _resources.HardwareResources
+            .FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+        if (existing == null)
+            throw new InvalidOperationException($"Hardware '{name}' not found.");
+
+        _resources.Delete(name);
+        _resources.SaveAll();
+
+        return Task.CompletedTask;
     }
-}
+}

+ 146 - 18
RackPeek/Yaml/YamlResourceCollection.cs

@@ -1,26 +1,150 @@
+using System.Collections.Specialized;
 using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.SystemResources;
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
+using YamlDotNet.Helpers;
 
 namespace RackPeek;
 
-public class YamlResourceCollection
+public sealed class YamlResourceCollection
 {
-    private readonly List<Resource> _resources = new();
-    
-    public IReadOnlyList<Hardware> HardwareResources => _resources.OfType<Hardware>().ToList();
-    public IReadOnlyList<SystemResource> SystemResources => _resources.OfType<SystemResource>().ToList();
+    private readonly List<ResourceEntry> _entries = [];
+    public IReadOnlyList<string> SourceFiles => _entries.Select(e => e.SourceFile).Distinct().ToList();
 
-    public void Load(List<string> yamlContents)
+    public IReadOnlyList<Hardware> HardwareResources =>
+        _entries.Select(e => e.Resource).OfType<Hardware>().ToList();
+
+    public IReadOnlyList<SystemResource> SystemResources =>
+        _entries.Select(e => e.Resource).OfType<SystemResource>().ToList();
+
+    public void LoadFiles(IEnumerable<string> filePaths)
+    {
+        foreach (var file in filePaths)
+        {
+            var yaml = File.ReadAllText(file);
+            foreach (var resource in Deserialize(yaml))
+            {
+                _entries.Add(new ResourceEntry(resource, file));
+            }
+        }
+    }
+
+    public void Load(string yaml, string file)
+    {
+        foreach (var resource in Deserialize(yaml))
+        {
+            _entries.Add(new ResourceEntry(resource, file));
+        }
+    }
+
+    public void SaveAll()
+    {
+        foreach (var group in _entries.GroupBy(e => e.SourceFile))
+        {
+            SaveToFile(group.Key, group.Select(e => e.Resource));
+        }
+    }
+
+    // ----------------------------
+    // CRUD operations
+    // ----------------------------
+
+    public void Add(Resource resource, string sourceFile)
+    {
+        _entries.Add(new ResourceEntry(resource, sourceFile));
+    }
+
+    public void Update(Resource resource)
+    {
+        var existing = _entries.FirstOrDefault(e =>
+            e.Resource.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
+
+        if (existing == null)
+            throw new InvalidOperationException($"Resource '{resource.Name}' not found.");
+
+        // keep file ownership
+        _entries.Remove(existing);
+        _entries.Add(new ResourceEntry(resource, existing.SourceFile));
+    }
+
+    public void Delete(string name)
+    {
+        var existing = _entries.FirstOrDefault(e =>
+            e.Resource.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+
+        if (existing == null)
+            throw new InvalidOperationException($"Resource '{name}' not found.");
+
+        _entries.Remove(existing);
+    }
+
+    public Resource? GetByName(string name)
+    {
+        return _entries
+            .Select(e => e.Resource)
+            .FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+    }
+
+    // ----------------------------
+    // Serialization helpers
+    // ----------------------------
+
+    private static void SaveToFile(string filePath, IEnumerable<Resource> resources)
+    {
+        var serializer = new SerializerBuilder()
+            .WithNamingConvention(CamelCaseNamingConvention.Instance)
+            .Build();
+
+        var payload = new OrderedDictionary
+        {
+            ["resources"] = resources
+                .Select(SerializeResource)
+                .ToList()
+        };
+
+        File.WriteAllText(filePath, serializer.Serialize(payload));
+    }
+
+    private static OrderedDictionary SerializeResource(Resource resource)
     {
-        foreach (var yamlContent in yamlContents)
+        var map = new OrderedDictionary
+        {
+            ["kind"] = resource switch
+            {
+                Server => "Server",
+                Switch => "Switch",
+                Firewall => "Firewall",
+                Router => "Router",
+                Desktop => "Desktop",
+                Laptop => "Laptop",
+                AccessPoint => "AccessPoint",
+                Ups => "Ups",
+                SystemResource => "System",
+                _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
+            }
+        };
+
+        var serializer = new SerializerBuilder()
+            .WithNamingConvention(CamelCaseNamingConvention.Instance)
+            .Build();
+
+        var yaml = serializer.Serialize(resource);
+
+        var props = new DeserializerBuilder()
+            .Build()
+            .Deserialize<Dictionary<string, object?>>(yaml);
+
+        foreach (var (key, value) in props)
         {
-            _resources.AddRange(Deserialize(yamlContent));
+            if (key == "kind") continue;
+            map[key] = value;
         }
+
+        return map;
     }
-    
+
     private static List<Resource> Deserialize(string yaml)
     {
         var deserializer = new DeserializerBuilder()
@@ -29,19 +153,24 @@ public class YamlResourceCollection
             .WithTypeConverter(new StorageSizeYamlConverter())
             .Build();
 
-        var raw = deserializer.Deserialize<Dictionary<string, List<Dictionary<string, object>>>>(yaml);
+        var raw = deserializer.Deserialize<
+            Dictionary<string, List<Dictionary<string, object>>>>(yaml);
+
+        if (!raw.TryGetValue("resources", out var items))
+            return [];
 
         var resources = new List<Resource>();
 
-        foreach (var item in raw["resources"])
+        foreach (var item in items)
         {
             var kind = item["kind"].ToString();
+            var typedYaml = new SerializerBuilder()
+                .WithNamingConvention(CamelCaseNamingConvention.Instance)
+                .Build()
+                .Serialize(item);
 
-            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),
@@ -50,8 +179,6 @@ public class YamlResourceCollection
                 "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}")
             };
@@ -61,5 +188,6 @@ public class YamlResourceCollection
 
         return resources;
     }
-    
-}
+
+    private sealed record ResourceEntry(Resource Resource, string SourceFile);
+}

+ 30 - 28
RackPeek/aps.yaml

@@ -1,29 +1,31 @@
 resources:
-  - kind: AccessPoint
-    name: lounge-ap
-    model: Unifi-Ap-Pro
-    speed: 1gb
-  - kind: AccessPoint
-    name: office-ap
-    model: Unifi-U6-Lite
-    speed: 1gb
-
-  - kind: AccessPoint
-    name: garage-ap
-    model: TP-Link-EAP245
-    speed: 1gb
-
-  - kind: AccessPoint
-    name: upstairs-ap
-    model: Aruba-AP-515
-    speed: 2.5gb
-
-  - kind: AccessPoint
-    name: guest-ap
-    model: Unifi-U6-Mesh
-    speed: 1gb
-
-  - kind: AccessPoint
-    name: warehouse-ap
-    model: Cisco-Aironet-1832i
-    speed: 1gb
+- kind: AccessPoint
+  model: Unifi-Ap-Pro
+  speed: 1
+  name: lounge-ap
+  tags: 
+- kind: AccessPoint
+  model: Unifi-U6-Lite
+  speed: 1
+  name: office-ap
+  tags: 
+- kind: AccessPoint
+  model: TP-Link-EAP245
+  speed: 1
+  name: garage-ap
+  tags: 
+- kind: AccessPoint
+  model: Aruba-AP-515
+  speed: 2.5
+  name: upstairs-ap
+  tags: 
+- kind: AccessPoint
+  model: Unifi-U6-Mesh
+  speed: 1
+  name: guest-ap
+  tags: 
+- kind: AccessPoint
+  model: Cisco-Aironet-1832i
+  speed: 1
+  name: warehouse-ap
+  tags: 

+ 20 - 19
RackPeek/desktops.yaml

@@ -1,20 +1,21 @@
 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
+- kind: Desktop
+  cpus:
+  - model: Intel(R) Core(TM) i5-9500
+    cores: 6
+    threads: 6
+  ram:
+    size: 16
+    mts: 2666
+  drives:
+  - type: ssd
+    size: 512
+  nics:
+  - type: rj45
+    speed: 1
+    ports: 1
+  gpus:
+  - model: RTX 3080
+    vram: 12
+  name: dell-optiplex
+  tags: 

+ 13 - 12
RackPeek/firewalls.yaml

@@ -1,13 +1,14 @@
 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
+- kind: Firewall
+  model: pfSense-1100
+  managed: true
+  poe: true
+  ports:
+  - type: rj45
+    speed: 1
+    count: 8
+  - type: sfp
+    speed: 10
+    count: 2
+  name: pfsense
+  tags: 

+ 16 - 15
RackPeek/laptops.yaml

@@ -1,16 +1,17 @@
 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
+- kind: Laptop
+  cpus:
+  - model: Intel(R) Core(TM) i7-10510U
+    cores: 4
+    threads: 8
+  ram:
+    size: 16
+    mts: 2666
+  drives:
+  - type: ssd
+    size: 1024
+  gpus:
+  - model: RTX 3080
+    vram: 12
+  name: thinkpad-x1
+  tags: 

+ 13 - 12
RackPeek/routers.yaml

@@ -1,13 +1,14 @@
 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
+- kind: Router
+  model: ER-4
+  managed: true
+  poe: true
+  ports:
+  - type: rj45
+    speed: 1
+    count: 8
+  - type: sfp
+    speed: 10
+    count: 2
+  name: ubiquiti-edge-router
+  tags: 

+ 396 - 379
RackPeek/servers.yaml

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

+ 13 - 12
RackPeek/switches.yaml

@@ -1,13 +1,14 @@
 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
+- kind: Switch
+  model: GS324
+  managed: true
+  poe: true
+  ports:
+  - type: rj45
+    speed: 1
+    count: 8
+  - type: sfp
+    speed: 10
+    count: 2
+  name: netgear-s24
+  tags: 

+ 5 - 4
RackPeek/ups.yaml

@@ -1,5 +1,6 @@
 resources:
-  - kind: Ups
-    name: rack-ups
-    model: Volta
-    va: 2200
+- kind: Ups
+  model: Volta
+  va: 2200
+  name: rack-ups
+  tags: 

+ 1 - 1
Tests/Yaml/HardwareDeserializationTests.cs

@@ -9,7 +9,7 @@ public class HardwareDeserializationTests
     public static IHardwareRepository CreateSut(string yaml)
     {
         var yamlResourceCollection = new YamlResourceCollection();
-        yamlResourceCollection.Load([yaml]);
+        yamlResourceCollection.Load(yaml, "test.yaml");
         return new YamlHardwareRepository(yamlResourceCollection);
     }
     

+ 1 - 1
Tests/Yaml/SystemDeserializationTests.cs

@@ -8,7 +8,7 @@ public class ServiceDeserializationTests
     public static ISystemRepository CreateSut(string yaml)
     {
         var yamlResourceCollection = new YamlResourceCollection();
-        yamlResourceCollection.Load([yaml]);
+        yamlResourceCollection.Load(yaml, "test.yaml");
         return new YamlSystemRepository(yamlResourceCollection);
     }