Tim Jones 1 месяц назад
Родитель
Сommit
946ba89e88

+ 29 - 16
RackPeek.Domain/Persistence/HardwareRepository.cs

@@ -19,23 +19,36 @@ public class YamlHardwareRepository(IResourceCollection resources) : IHardwareRe
     public Task<List<HardwareTree>> GetTreeAsync()
     {
         var hardwareTree = new List<HardwareTree>();
+        
+            var systemGroups = resources.SystemResources
+                .Where(s => s.RunsOn.Count != 0)
+                .SelectMany(
+                    s => s.RunsOn,
+                    (system, hardwareName) => new
+                    {
+                        Hardware = hardwareName.Trim(),
+                        System = system
+                    })
+                .GroupBy(x => x.Hardware, StringComparer.OrdinalIgnoreCase)
+                .ToDictionary(
+                    g => g.Key,
+                    g => g.Select(x => x.System).ToList(),
+                    StringComparer.OrdinalIgnoreCase);
 
-        // List<string> systemGroups = resources.SystemResources.ToList<string>();
-            // .Where(s => s.RunsOn != null)
-            // .ToList();
-
-        var systemGroups = resources.SystemResources
-            // .Where(s => !string.IsNullOrWhiteSpace(s.RunsOn))
-            // .Where(s => s.RunsOn != null)
-            // TODO: Get rid of 'First'
-            .GroupBy(s => s.RunsOn.First().Trim(), StringComparer.OrdinalIgnoreCase)
-            .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
-
-        var serviceGroups = resources.ServiceResources
-            // .Where(s => !string.IsNullOrWhiteSpace(s.RunsOn))
-            // TODO: Get rid of 'First'
-            .GroupBy(s => s.RunsOn.First().Trim(), StringComparer.OrdinalIgnoreCase)
-            .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
+            var serviceGroups = resources.ServiceResources
+                .Where(s => s.RunsOn.Count != 0)
+                .SelectMany(
+                    s => s.RunsOn,
+                    (service, systemName) => new
+                    {
+                        System = systemName.Trim(),
+                        Service = service
+                    })
+                .GroupBy(x => x.System, StringComparer.OrdinalIgnoreCase)
+                .ToDictionary(
+                    g => g.Key,
+                    g => g.Select(x => x.Service).ToList(),
+                    StringComparer.OrdinalIgnoreCase);
 
         foreach (var hardware in resources.HardwareResources)
         {

+ 1 - 1
RackPeek.Domain/Persistence/InMemoryResourceCollection.cs

@@ -99,7 +99,7 @@ public sealed class InMemoryResourceCollection(IEnumerable<Resource>? seed = nul
         lock (_lock)
         {
             return Task.FromResult<IReadOnlyList<Resource>>(_resources
-                .Where(r => r.RunsOn.Select(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)).ToList().Count() > 0 ).ToList());
+                .Where(r => r.RunsOn.Select(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)).ToList().Count != 0).ToList());
         }
     }
 

+ 35 - 25
RackPeek.Domain/Persistence/Yaml/RackPeekConfigMigrationDeserializer.cs

@@ -70,39 +70,49 @@ public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<Yam
         
         return ValueTask.CompletedTask;
     }
-
-    public static ValueTask ConvertScalarRunsOnToList(IServiceProvider serviceProvider, Dictionary<object, object> obj)
+    public static ValueTask ConvertScalarRunsOnToList(
+        IServiceProvider serviceProvider,
+        Dictionary<object, object> obj)
     {
         const string key = "runsOn";
-        var resourceList = obj["resources"];
-        if (resourceList is List<object> resources)
+
+        if (!obj.TryGetValue("resources", out var resourceListObj))
+            return ValueTask.CompletedTask;
+
+        if (resourceListObj is not List<object> resources)
+            return ValueTask.CompletedTask;
+
+        foreach (var resourceObj in resources)
         {
-            foreach(var resourceObj in resources)
+            if (resourceObj is not Dictionary<object, object> resourceDict)
+                continue;
+
+            if (!resourceDict.TryGetValue(key, out var runsOn))
+                continue;
+
+            switch (runsOn)
             {
-                if (resourceObj is Dictionary<object,object> resourceDict)
-                {
-                    if (resourceDict.ContainsKey(key))
-                    {
-                        var runsOn = resourceDict[key];
-                        Type t = runsOn.GetType();
-                        switch (runsOn)
-                        {
-                            case string r:
-                                resourceDict[key] = new List<string>{r};
-                                break;
-                            case List<string> r:
-                                // Nothing to do
-                                break;
-                            default:
-                                throw new InvalidCastException($"Cannot convert from {t} to List<string> in {resourceDict}[{key}]");
-                        }
-                    }
-                }
+                case string single:
+                    resourceDict[key] = new List<string> { single };
+                    break;
+
+                case List<object> list:
+                    resourceDict[key] = list
+                        .OfType<string>()
+                        .ToList();
+                    break;
+
+                case List<string>:
+                    // Already correct
+                    break;
+
+                default:
+                    throw new InvalidCastException(
+                        $"Cannot convert {runsOn.GetType()} to List<string> for resource '{resourceDict}'.");
             }
         }
 
         return ValueTask.CompletedTask;
     }
-
     #endregion
 }

+ 6 - 4
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -54,12 +54,14 @@ public sealed class YamlResourceCollection(
     {
         return Task.FromResult<IReadOnlyList<T>>(resourceCollection.Resources.OfType<T>().ToList());
     }
-
+    
     public Task<IReadOnlyList<Resource>> GetDependantsAsync(string name)
     {
-        return Task.FromResult<IReadOnlyList<Resource>>(resourceCollection.Resources
-            .Where(r => r.RunsOn.Select(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)).ToList().Count > 0 )
-            .ToList());
+        var result = resourceCollection.Resources
+            .Where(r => r.RunsOn.Any(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)))
+            .ToList();
+
+        return Task.FromResult<IReadOnlyList<Resource>>(result);
     }
 
     public Task<IReadOnlyList<Resource>> GetByTagAsync(string name)

+ 6 - 6
RackPeek.Domain/Resources/Hardware/GetHardwareSystemTreeUseCase.cs

@@ -12,22 +12,22 @@ public class GetHardwareSystemTreeUseCase(
     {
         ThrowIfInvalid.ResourceName(hardwareName);
 
-        var server = await repo.GetByNameAsync(hardwareName) as Hardware;
-        if (server is null)
+        var hardware = await repo.GetByNameAsync(hardwareName) as Hardware;
+        if (hardware is null)
             throw new NotFoundException($"Hardware '{hardwareName}' not found.");
 
-        return await BuildDependencyTreeAsync(server);
+        return await BuildDependencyTreeAsync(hardware);
     }
 
-    private async Task<HardwareDependencyTree> BuildDependencyTreeAsync(Hardware server)
+    private async Task<HardwareDependencyTree> BuildDependencyTreeAsync(Hardware hardware)
     {
-        var systems = await repo.GetDependantsAsync(server.Name);
+        var systems = await repo.GetDependantsAsync(hardware.Name);
 
         var systemTrees = new List<SystemDependencyTree>();
         foreach (var system in systems.OfType<SystemResource>())
             systemTrees.Add(await BuildSystemDependencyTreeAsync(system));
 
-        return new HardwareDependencyTree(server, systemTrees);
+        return new HardwareDependencyTree(hardware, systemTrees);
     }
 
     private async Task<SystemDependencyTree> BuildSystemDependencyTreeAsync(SystemResource system)

+ 11 - 3
RackPeek.Domain/Resources/Services/UseCases/UpdateServiceUseCase.cs

@@ -50,16 +50,24 @@ public class UpdateServiceUseCase(IResourceCollection repository) : IUseCase
 
         if (runsOn is not null)
         {
-            foreach (var parent in runsOn)
+            var normalizedParents = new List<string>();
+
+            foreach (var parent in runsOn
+                         .Where(p => !string.IsNullOrWhiteSpace(p))
+                         .Select(p => p.Trim())
+                         .Distinct(StringComparer.OrdinalIgnoreCase))
             {
                 ThrowIfInvalid.ResourceName(parent);
 
                 var parentSystem = await repository.GetByNameAsync(parent);
 
-                if (parentSystem == null) throw new NotFoundException($"Parent system '{parent}' not found.");
+                if (parentSystem == null)
+                    throw new NotFoundException($"Parent system '{parent}' not found.");
 
-                if (!service.RunsOn.Contains(parent)) service.RunsOn.Add(parent);
+                normalizedParents.Add(parent);
             }
+
+            service.RunsOn = normalizedParents;
         }
 
         if (notes != null) service.Notes = notes;

+ 3 - 4
Shared.Rcl/Commands/Services/ServiceGetCommand.cs

@@ -35,10 +35,9 @@ public class ServiceGetCommand(
 
         foreach (var s in report.Services)
         {
-            var sys = "Unknown";
-            var phys = "Unkown";
-
-
+            string? sys = null;
+            string? phys = null;
+            
             if (s.RunsOnSystemHost is not null)
             {
                 sys = string.Join(", ", s.RunsOnSystemHost);

+ 2 - 2
Shared.Rcl/Commands/Services/ServiceReportCommand.cs

@@ -35,8 +35,8 @@ public class ServiceReportCommand(
 
         foreach (var s in report.Services)
         {
-            string sys = "Unknown";
-            string phys = "Unknown";
+            string? sys = null;
+            string? phys = null;
 
             if (s.RunsOnSystemHost?.Count > 0 )
             {

+ 4 - 7
Shared.Rcl/Commands/Services/ServiceSetCommand.cs

@@ -25,12 +25,9 @@ public class ServiceSetSettings : ServerNameSettings
     [Description("The service URL.")]
     public string? Url { get; set; }
 
-    // TODO: How do you specify a list?
-    // foo --runs-on a --runs-on b
-    // foo --runs-on a,b
-    [CommandOption("--runs-on")]
-    [Description("The system the service is running on.")]
-    public List<string>? RunsOn { get; set; }
+    [CommandOption("--runs-on <RUNSON>")]
+    [Description("The system(s) the service is running on.")]
+    public string[]? RunsOn { get; set; }
 }
 
 public class ServiceSetCommand(
@@ -51,7 +48,7 @@ public class ServiceSetCommand(
             settings.Port,
             settings.Protocol,
             settings.Url,
-            settings.RunsOn
+            settings.RunsOn?.ToList()
         );
 
         AnsiConsole.MarkupLine($"[green]Service '{settings.Name}' updated.[/]");

+ 1 - 1
Shared.Rcl/Commands/Services/ServicesFormatExtensions.cs

@@ -4,7 +4,7 @@ public static class ServicesFormatExtensions
 {
     public static string FormatRunsOn(string? runsOn, string? runsOnHost)
     {
-        if (string.IsNullOrEmpty(runsOn) && string.IsNullOrEmpty(runsOnHost)) return "Unknown";
+        if (string.IsNullOrEmpty(runsOn) && string.IsNullOrEmpty(runsOnHost)) return "";
 
         if (string.IsNullOrEmpty(runsOn)) return runsOnHost!;
 

+ 6 - 8
Shared.Rcl/Commands/Systems/SystemSetCommand.cs

@@ -1,3 +1,4 @@
+using System.ComponentModel;
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources.SystemResources.UseCases;
 using Shared.Rcl.Commands.Servers;
@@ -16,7 +17,10 @@ public class SystemSetSettings : ServerNameSettings
 
     [CommandOption("--ram")] public int? Ram { get; set; }
 
-    [CommandOption("--runs-on")] public string? RunsOn { get; set; }
+    [CommandOption("--runs-on <RUNSON>")]
+    [Description("The physical machine(s) the service is running on.")]
+    public string[]? RunsOn { get; set; }
+    
 }
 
 public class SystemSetCommand(
@@ -31,19 +35,13 @@ public class SystemSetCommand(
         using var scope = serviceProvider.CreateScope();
         var useCase = scope.ServiceProvider.GetRequiredService<UpdateSystemUseCase>();
 
-        List<string> runsOn = new List<string>();
-        if (settings.RunsOn is not null)
-        {
-            runsOn.Add(settings.RunsOn);
-        }
-
         await useCase.ExecuteAsync(
             settings.Name,
             settings.Type,
             settings.Os,
             settings.Cores,
             settings.Ram,
-            runsOn
+            settings.RunsOn?.ToList()
         );
 
         AnsiConsole.MarkupLine($"[green]System '{settings.Name}' updated.[/]");

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

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

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

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

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

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

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

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

+ 4 - 2
Tests/EndToEnd/ServiceTests/ServiceWorkflowTests.cs

@@ -46,9 +46,10 @@ public class ServiceWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outpu
             "--runs-on", "sys01"
         );
         Assert.Equal("Service 'svc01' updated.\n", output);
+        outputHelper.WriteLine(yaml);
 
         Assert.Equal("""
-                     version: 1
+                     version: 2
                      resources:
                      - kind: System
                        name: sys01
@@ -59,7 +60,8 @@ public class ServiceWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper outpu
                          protocol: http
                          url: http://10.0.0.5:8080
                        name: svc01
-                       runsOn: sys01
+                       runsOn:
+                       - sys01
 
                      """, yaml);
 

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

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

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

@@ -47,8 +47,9 @@ public class SystemWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
         );
         Assert.Equal("System 'sys01' updated.\n", output);
 
+        outputHelper.WriteLine(yaml);
         Assert.Equal("""
-                     version: 1
+                     version: 2
                      resources:
                      - kind: Server
                        name: proxmox-node01
@@ -58,7 +59,8 @@ public class SystemWorkflowTests(TempYamlCliFixture fs, ITestOutputHelper output
                        cores: 2
                        ram: 4
                        name: sys01
-                       runsOn: proxmox-node01
+                       runsOn:
+                       - proxmox-node01
 
                      """, yaml);
 

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

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