Sfoglia il codice sorgente

Added Server Cpu commands

Tim Jones 2 mesi fa
parent
commit
1485ec0975
35 ha cambiato i file con 592 aggiunte e 435 eliminazioni
  1. 0 39
      RackPeek.Domain/Resources/Hardware/Crud/AddServerUseCase.cs
  2. 0 12
      RackPeek.Domain/Resources/Hardware/Crud/GetServerUseCase.cs
  3. 0 12
      RackPeek.Domain/Resources/Hardware/Crud/GetServersUseCase.cs
  4. 0 54
      RackPeek.Domain/Resources/Hardware/Crud/UpdateServerUseCase.cs
  5. 1 1
      RackPeek.Domain/Resources/Hardware/Reports/ServerHardwareReport.cs
  6. 19 0
      RackPeek.Domain/Resources/Hardware/Server/AddServerUseCase.cs
  7. 29 0
      RackPeek.Domain/Resources/Hardware/Server/Cpu/AddCpuUseCase.cs
  8. 24 0
      RackPeek.Domain/Resources/Hardware/Server/Cpu/RemoveCpuUseCase.cs
  9. 31 0
      RackPeek.Domain/Resources/Hardware/Server/Cpu/UpdateCpuUseCase.cs
  10. 1 1
      RackPeek.Domain/Resources/Hardware/Server/DeleteServerUseCase.cs
  11. 2 4
      RackPeek.Domain/Resources/Hardware/Server/DescribeServerUseCase.cs
  12. 10 0
      RackPeek.Domain/Resources/Hardware/Server/GetServerUseCase.cs
  13. 10 0
      RackPeek.Domain/Resources/Hardware/Server/GetServersUseCase.cs
  14. 32 0
      RackPeek.Domain/Resources/Hardware/Server/UpdateServerUseCase.cs
  15. 40 0
      RackPeek/Commands/Server/Cpus/ServerCpuAddCommand.cs
  16. 29 0
      RackPeek/Commands/Server/Cpus/ServerCpuRemoveCommand.cs
  17. 41 0
      RackPeek/Commands/Server/Cpus/ServerCpuSetCommand.cs
  18. 32 0
      RackPeek/Commands/Server/ServerAddCommand.cs
  19. 9 0
      RackPeek/Commands/Server/ServerCommands.cs
  20. 25 0
      RackPeek/Commands/Server/ServerDeleteCommand.cs
  21. 49 0
      RackPeek/Commands/Server/ServerDescribeCommand.cs
  22. 33 0
      RackPeek/Commands/Server/ServerGetByNameCommand.cs
  23. 53 0
      RackPeek/Commands/Server/ServerGetCommand.cs
  24. 1 1
      RackPeek/Commands/Server/ServerReportCommand.cs
  25. 36 0
      RackPeek/Commands/Server/ServerSetCommand.cs
  26. 0 242
      RackPeek/Commands/ServerCommands.cs
  27. 54 25
      RackPeek/Program.cs
  28. 18 0
      RackPeek/servers.yaml
  29. 4 21
      Tests/Hardware/AddServerUseCaseTests.cs
  30. 1 1
      Tests/Hardware/DeleteServerUseCaseTests.cs
  31. 1 1
      Tests/Hardware/DescribeServerUseCaseTests.cs
  32. 1 1
      Tests/Hardware/GetServerUseCaseTests.cs
  33. 1 1
      Tests/Hardware/GetServersUseCaseTests.cs
  34. 0 1
      Tests/Hardware/ServerHardwareReportTests.cs
  35. 5 18
      Tests/Hardware/UpdateServerUseCaseTests.cs

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

@@ -1,39 +0,0 @@
-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);
-    }
-}

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

@@ -1,12 +0,0 @@
-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;
-    }
-}

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

@@ -1,12 +0,0 @@
-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();
-    }
-}

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

@@ -1,54 +0,0 @@
-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);
-    }
-}

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Reports/ServerHardwareReport.cs

@@ -29,7 +29,7 @@ public class ServerHardwareReportUseCase(IHardwareRepository repository)
     public async Task<ServerHardwareReport> ExecuteAsync()
     public async Task<ServerHardwareReport> ExecuteAsync()
     {
     {
         var hardware = await repository.GetAllAsync();
         var hardware = await repository.GetAllAsync();
-        var servers = hardware.OfType<Server>();
+        var servers = hardware.OfType<Models.Server>();
 
 
         var rows = servers.Select(server =>
         var rows = servers.Select(server =>
         {
         {

+ 19 - 0
RackPeek.Domain/Resources/Hardware/Server/AddServerUseCase.cs

@@ -0,0 +1,19 @@
+namespace RackPeek.Domain.Resources.Hardware.Server;
+
+public class AddServerUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(string name)
+    {
+        // basic guard rails
+        var existing = await repository.GetByNameAsync(name);
+        if (existing != null)
+            throw new InvalidOperationException($"Server '{name}' already exists.");
+
+        var server = new Models.Server
+        {
+            Name = name,
+        };
+
+        await repository.AddAsync(server);
+    }
+}

+ 29 - 0
RackPeek.Domain/Resources/Hardware/Server/Cpu/AddCpuUseCase.cs

@@ -0,0 +1,29 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Cpu;
+
+public class AddCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        string model,
+        int cores,
+        int threads)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server)
+        {
+            return;
+        }
+
+        server.Cpus ??= [];
+        
+        server.Cpus.Add(new Models.Cpu
+        {
+            Model = model,
+            Cores = cores,
+            Threads = threads
+        });
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 24 - 0
RackPeek.Domain/Resources/Hardware/Server/Cpu/RemoveCpuUseCase.cs

@@ -0,0 +1,24 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Cpu;
+
+public class RemoveCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        int index)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+        if (hardware is not Models.Server server)
+        {
+            return;
+        }
+        
+        server.Cpus ??= [];
+
+        if (index < 0 || index >= server.Cpus.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "CPU index out of range.");
+
+        server.Cpus.RemoveAt(index);
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Server/Cpu/UpdateCpuUseCase.cs

@@ -0,0 +1,31 @@
+namespace RackPeek.Domain.Resources.Hardware.Server.Cpu;
+
+public class UpdateCpuUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string serverName,
+        int index,
+        string model,
+        int cores,
+        int threads)
+    {
+        var hardware = await repository.GetByNameAsync(serverName);
+
+        if (hardware is not Models.Server server)
+        {
+            return;
+        }
+        
+        server.Cpus ??= [];
+        
+        if (index < 0 || index >= server.Cpus.Count)
+            throw new ArgumentOutOfRangeException(nameof(index), "CPU index out of range.");
+
+        var cpu = server.Cpus[index];
+        cpu.Model = model;
+        cpu.Cores = cores;
+        cpu.Threads = threads;
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 1 - 1
RackPeek.Domain/Resources/Hardware/Crud/DeleteServerUseCase.cs → RackPeek.Domain/Resources/Hardware/Server/DeleteServerUseCase.cs

@@ -1,4 +1,4 @@
-namespace RackPeek.Domain.Resources.Hardware.Crud;
+namespace RackPeek.Domain.Resources.Hardware.Server;
 
 
 public class DeleteServerUseCase(IHardwareRepository repository)
 public class DeleteServerUseCase(IHardwareRepository repository)
 {
 {

+ 2 - 4
RackPeek.Domain/Resources/Hardware/Crud/DescribeServerUseCase.cs → RackPeek.Domain/Resources/Hardware/Server/DescribeServerUseCase.cs

@@ -1,6 +1,4 @@
-using RackPeek.Domain.Resources.Hardware.Models;
-
-namespace RackPeek.Domain.Resources.Hardware.Crud;
+namespace RackPeek.Domain.Resources.Hardware.Server;
 
 
 public record ServerDescription(
 public record ServerDescription(
     string Name,
     string Name,
@@ -17,7 +15,7 @@ public class DescribeServerUseCase(IHardwareRepository repository)
 {
 {
     public async Task<ServerDescription?> ExecuteAsync(string name)
     public async Task<ServerDescription?> ExecuteAsync(string name)
     {
     {
-        var server = await repository.GetByNameAsync(name) as Server;
+        var server = await repository.GetByNameAsync(name) as Models.Server;
         if (server == null)
         if (server == null)
             return null;
             return null;
 
 

+ 10 - 0
RackPeek.Domain/Resources/Hardware/Server/GetServerUseCase.cs

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

+ 10 - 0
RackPeek.Domain/Resources/Hardware/Server/GetServersUseCase.cs

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

+ 32 - 0
RackPeek.Domain/Resources/Hardware/Server/UpdateServerUseCase.cs

@@ -0,0 +1,32 @@
+using RackPeek.Domain.Resources.Hardware.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Server;
+
+public class UpdateServerUseCase(IHardwareRepository repository)
+{
+    public async Task ExecuteAsync(
+        string name,
+        int? ramGb = null,
+        bool? ipmi = null
+    )
+    {
+        var server = await repository.GetByNameAsync(name) as Models.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;
+        }
+
+        await repository.UpdateAsync(server);
+    }
+}

+ 40 - 0
RackPeek/Commands/Server/Cpus/ServerCpuAddCommand.cs

@@ -0,0 +1,40 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Cpu;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Cpus;
+public class ServerCpuAddSettings : ServerNameSettings
+{
+    [CommandOption("--model <MODEL>")]
+    public string Model { get; set; }
+
+    [CommandOption("--cores <CORES>")]
+    public int Cores { get; set; }
+    
+    [CommandOption("--threads <THREADS>")]
+    public int Threads { get; set; }
+}
+
+public class ServerCpuAddCommand(
+    IServiceProvider serviceProvider
+) : AsyncCommand<ServerCpuAddSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerCpuAddSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<AddCpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Model,
+            settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU added to '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 29 - 0
RackPeek/Commands/Server/Cpus/ServerCpuRemoveCommand.cs

@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Cpu;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Cpus;
+public class ServerCpuRemoveSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")]
+    public int Index { get; set; }
+}
+public class ServerCpuRemoveCommand(IServiceProvider serviceProvider) : AsyncCommand<ServerCpuRemoveSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerCpuRemoveSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<RemoveCpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index);
+
+        AnsiConsole.MarkupLine($"[green]CPU {settings.Index} removed from '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 41 - 0
RackPeek/Commands/Server/Cpus/ServerCpuSetCommand.cs

@@ -0,0 +1,41 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server.Cpu;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server.Cpus;
+public class ServerCpuSetSettings : ServerNameSettings
+{
+    [CommandOption("--index <INDEX>")]
+    public int Index { get; set; }
+
+    [CommandOption("--model <MODEL>")]
+    public string Model { get; set; }
+
+    [CommandOption("--cores <CORES>")]
+    public int Cores { get; set; }
+    
+    [CommandOption("--threads <THREADS>")]
+    public int Threads { get; set; }
+}
+public class ServerCpuSetCommand(IServiceProvider serviceProvider) : AsyncCommand<ServerCpuSetSettings>
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        ServerCpuSetSettings settings,
+        CancellationToken cancellationToken)
+    {
+        using var scope = serviceProvider.CreateScope();
+        var useCase = scope.ServiceProvider.GetRequiredService<UpdateCpuUseCase>();
+
+        await useCase.ExecuteAsync(
+            settings.Name,
+            settings.Index,
+            settings.Model,
+            settings.Cores,
+            settings.Threads);
+
+        AnsiConsole.MarkupLine($"[green]CPU {settings.Index} updated on '{settings.Name}'.[/]");
+        return 0;
+    }
+}

+ 32 - 0
RackPeek/Commands/Server/ServerAddCommand.cs

@@ -0,0 +1,32 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+public class ServerAddSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    public string Name { get; set; } = default!;
+}
+
+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
+        );
+
+        AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' added.[/]");
+        return 0;
+    }
+}

+ 9 - 0
RackPeek/Commands/Server/ServerCommands.cs

@@ -0,0 +1,9 @@
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+
+public class ServerNameSettings : CommandSettings
+{
+    [CommandArgument(0, "<name>")]
+    public string Name { get; set; } = default!;
+}

+ 25 - 0
RackPeek/Commands/Server/ServerDeleteCommand.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+
+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;
+    }
+}

+ 49 - 0
RackPeek/Commands/Server/ServerDescribeCommand.cs

@@ -0,0 +1,49 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+
+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;
+    }
+}

+ 33 - 0
RackPeek/Commands/Server/ServerGetByNameCommand.cs

@@ -0,0 +1,33 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+
+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;
+    }
+}

+ 53 - 0
RackPeek/Commands/Server/ServerGetCommand.cs

@@ -0,0 +1,53 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Reports;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+
+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;
+    }
+}

+ 1 - 1
RackPeek/Commands/ServerReportCommand.cs → RackPeek/Commands/Server/ServerReportCommand.cs

@@ -4,7 +4,7 @@ using RackPeek.Domain.Resources.Hardware.Reports;
 using Spectre.Console;
 using Spectre.Console;
 using Spectre.Console.Cli;
 using Spectre.Console.Cli;
 
 
-namespace RackPeek.Commands;
+namespace RackPeek.Commands.Server;
 public class ServerReportCommand(ILogger<ServerReportCommand> logger, IServiceProvider serviceProvider)
 public class ServerReportCommand(ILogger<ServerReportCommand> logger, IServiceProvider serviceProvider)
     : AsyncCommand
     : AsyncCommand
 {
 {

+ 36 - 0
RackPeek/Commands/Server/ServerSetCommand.cs

@@ -0,0 +1,36 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware.Server;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands.Server;
+public class ServerSetSettings : ServerNameSettings
+{
+    [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);
+
+        AnsiConsole.MarkupLine($"[green]Server '{settings.Name}' updated.[/]");
+        return 0;
+    }
+}

+ 0 - 242
RackPeek/Commands/ServerCommands.cs

@@ -1,242 +0,0 @@
-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.Commands;
-
-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;
-    }
-}
-
-
-

+ 54 - 25
RackPeek/Program.cs

@@ -5,8 +5,11 @@ using Spectre.Console;
 using Spectre.Console.Cli;
 using Spectre.Console.Cli;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using RackPeek.Commands;
 using RackPeek.Commands;
-using RackPeek.Domain.Resources.Hardware.Crud;
+using RackPeek.Commands.Server;
+using RackPeek.Commands.Server.Cpus;
 using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Reports;
+using RackPeek.Domain.Resources.Hardware.Server;
+using RackPeek.Domain.Resources.Hardware.Server.Cpu;
 using RackPeek.Yaml;
 using RackPeek.Yaml;
 
 
 namespace RackPeek;
 namespace RackPeek;
@@ -26,6 +29,31 @@ public static class Program
 
 
         services.AddSingleton<IConfiguration>(configuration);
         services.AddSingleton<IConfiguration>(configuration);
 
 
+        // Infrastructure
+        services.AddScoped<IHardwareRepository>(_ =>
+        {
+            var path = configuration["HardwareFile"] ?? "hardware.yaml";
+            
+            var collection = new YamlResourceCollection();
+            collection.LoadFiles([
+                "servers.yaml",
+                "aps.yaml",
+                "desktops.yaml",
+                "switches.yaml",
+                "ups.yaml",
+                "firewalls.yaml",
+                "laptops.yaml",
+                "routers.yaml"]);
+
+            return new YamlHardwareRepository(collection);
+        });
+        
+        services.AddLogging(configure =>
+            configure
+                .AddSimpleConsole(opts => { opts.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; }));
+
+
+        
         // Application
         // Application
         services.AddScoped<ServerHardwareReportUseCase>();
         services.AddScoped<ServerHardwareReportUseCase>();
         services.AddScoped<ServerReportCommand>();
         services.AddScoped<ServerReportCommand>();
@@ -52,30 +80,17 @@ public static class Program
         
         
         services.AddScoped<UpdateServerUseCase>();
         services.AddScoped<UpdateServerUseCase>();
         services.AddScoped<ServerSetCommand>();
         services.AddScoped<ServerSetCommand>();
-        // Infrastructure
-        services.AddScoped<IHardwareRepository>(_ =>
-        {
-            var path = configuration["HardwareFile"] ?? "hardware.yaml";
-            
-            var collection = new YamlResourceCollection();
-            collection.LoadFiles([
-                "servers.yaml",
-                "aps.yaml",
-                "desktops.yaml",
-                "switches.yaml",
-                "ups.yaml",
-                "firewalls.yaml",
-                "laptops.yaml",
-                "routers.yaml"]);
-
-            return new YamlHardwareRepository(collection);
-        });
         
         
-        services.AddLogging(configure =>
-            configure
-                .AddSimpleConsole(opts => { opts.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; }));
-
-
+        // CPU use cases
+        services.AddScoped<AddCpuUseCase>();
+        services.AddScoped<UpdateCpuUseCase>();
+        services.AddScoped<RemoveCpuUseCase>();
+
+        // CPU commands
+        services.AddScoped<ServerCpuAddCommand>();
+        services.AddScoped<ServerCpuSetCommand>();
+        services.AddScoped<ServerCpuRemoveCommand>();
+        
         // Spectre bootstrap
         // Spectre bootstrap
         var registrar = new TypeRegistrar(services);
         var registrar = new TypeRegistrar(services);
         var app = new CommandApp(registrar);
         var app = new CommandApp(registrar);
@@ -106,8 +121,22 @@ public static class Program
                 server.AddCommand<ServerSetCommand>("set")
                 server.AddCommand<ServerSetCommand>("set")
                     .WithDescription("Update server properties");
                     .WithDescription("Update server properties");
 
 
-                server.AddCommand<ServerDeleteCommand>("delete")
+                server.AddCommand<ServerDeleteCommand>("del")
                     .WithDescription("Delete a server");
                     .WithDescription("Delete a server");
+                
+                server.AddBranch("cpu", cpu =>
+                {
+                    cpu.SetDescription("Manage server CPUs");
+
+                    cpu.AddCommand<ServerCpuAddCommand>("add")
+                        .WithDescription("Add a CPU to a server");
+
+                    cpu.AddCommand<ServerCpuSetCommand>("set")
+                        .WithDescription("Update a CPU on a server");
+
+                    cpu.AddCommand<ServerCpuRemoveCommand>("del")
+                        .WithDescription("Remove a CPU from a server");
+                });
             });
             });
 
 
             // ----------------------------
             // ----------------------------

+ 18 - 0
RackPeek/servers.yaml

@@ -395,3 +395,21 @@ resources:
   ipmi: true
   ipmi: true
   name: dell-r730-archive01
   name: dell-r730-archive01
   tags: 
   tags: 
+- kind: Server
+  cpus: 
+  ram: 
+  drives: 
+  nics: 
+  gpus: 
+  ipmi: 
+  name: noe01
+  tags: 
+- kind: Server
+  cpus: []
+  ram: 
+  drives: 
+  nics: 
+  gpus: 
+  ipmi: 
+  name: node01
+  tags: 

+ 4 - 21
Tests/Hardware/AddServerUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server;
 
 
 namespace Tests.Hardware;
 namespace Tests.Hardware;
 
 
@@ -18,24 +18,12 @@ public class AddServerUseCaseTests
 
 
         // Act
         // Act
         await sut.ExecuteAsync(
         await sut.ExecuteAsync(
-            name: "node01",
-            cpuModel: "Xeon E3",
-            cores: 4,
-            threads: 8,
-            ramGb: 32,
-            ipmi: true
+            name: "node01"
         );
         );
 
 
         // Assert
         // Assert
         await repo.Received(1).AddAsync(Arg.Is<Server>(s =>
         await repo.Received(1).AddAsync(Arg.Is<Server>(s =>
-            s.Name == "node01" &&
-            s.Cpus != null &&
-            s.Cpus.Count == 1 &&
-            s.Cpus[0].Model == "Xeon E3" &&
-            s.Cpus[0].Cores == 4 &&
-            s.Cpus[0].Threads == 8 &&
-            s.Ram.Size == 32 &&
-            s.Ipmi == true
+            s.Name == "node01"
         ));
         ));
     }
     }
 
 
@@ -51,12 +39,7 @@ public class AddServerUseCaseTests
         // Act
         // Act
         var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
         var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
             await sut.ExecuteAsync(
             await sut.ExecuteAsync(
-                name: "node01",
-                cpuModel: "Xeon E3",
-                cores: 4,
-                threads: 8,
-                ramGb: 32,
-                ipmi: true
+                name: "node01"
             )
             )
         );
         );
 
 

+ 1 - 1
Tests/Hardware/DeleteServerUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server;
 
 
 namespace Tests.Hardware;
 namespace Tests.Hardware;
 
 

+ 1 - 1
Tests/Hardware/DescribeServerUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server;
 
 
 namespace Tests.Hardware;
 namespace Tests.Hardware;
 
 

+ 1 - 1
Tests/Hardware/GetServerUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server;
 
 
 namespace Tests.Hardware;
 namespace Tests.Hardware;
 
 

+ 1 - 1
Tests/Hardware/GetServersUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server;
 
 
 namespace Tests.Hardware;
 namespace Tests.Hardware;
 
 

+ 0 - 1
Tests/Hardware/ServerHardwareReportTests.cs

@@ -1,6 +1,5 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Reports;
 using RackPeek.Domain.Resources.Hardware.Reports;
 
 

+ 5 - 18
Tests/Hardware/UpdateServerUseCaseTests.cs

@@ -1,7 +1,7 @@
 using NSubstitute;
 using NSubstitute;
 using RackPeek.Domain.Resources.Hardware;
 using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Hardware.Crud;
 using RackPeek.Domain.Resources.Hardware.Models;
 using RackPeek.Domain.Resources.Hardware.Models;
+using RackPeek.Domain.Resources.Hardware.Server;
 
 
 namespace Tests.Hardware;
 namespace Tests.Hardware;
 
 
@@ -29,21 +29,14 @@ public class UpdateServerUseCaseTests
         await sut.ExecuteAsync(
         await sut.ExecuteAsync(
             name: "node01",
             name: "node01",
             ramGb: 64,
             ramGb: 64,
-            ipmi: true,
-            cpuModel: "Xeon E3",
-            cores: 4,
-            threads: 8
+            ipmi: true
         );
         );
 
 
         // Assert
         // Assert
         await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
         await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
             s.Name == "node01" &&
             s.Name == "node01" &&
             s.Ram.Size == 64 &&
             s.Ram.Size == 64 &&
-            s.Ipmi == true &&
-            s.Cpus != null &&
-            s.Cpus.First().Model == "Xeon E3" &&
-            s.Cpus.First().Cores == 4 &&
-            s.Cpus.First().Threads == 8
+            s.Ipmi == true
         ));
         ));
     }
     }
 
 
@@ -88,19 +81,13 @@ public class UpdateServerUseCaseTests
         await sut.ExecuteAsync(
         await sut.ExecuteAsync(
             name: "node01",
             name: "node01",
             ramGb: null,
             ramGb: null,
-            ipmi: null,
-            cpuModel: null,
-            cores: null,
-            threads: null
+            ipmi: null
         );
         );
 
 
         // Assert
         // Assert
         await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
         await repo.Received(1).UpdateAsync(Arg.Is<Server>(s =>
             s.Ram.Size == 32 &&
             s.Ram.Size == 32 &&
-            s.Ipmi == false &&
-            s.Cpus.First().Model == "Old" &&
-            s.Cpus.First().Cores == 2 &&
-            s.Cpus.First().Threads == 4
+            s.Ipmi == false
         ));
         ));
     }
     }
 }
 }