فهرست منبع

Merge pull request #77 from Timmoth/Clone-Rename

Added Clone / Rename usecases
Tim Jones 1 ماه پیش
والد
کامیت
58dc2def04
23فایلهای تغییر یافته به همراه838 افزوده شده و 43 حذف شده
  1. 12 0
      CommandIndex.md
  2. 217 3
      Commands.md
  3. 13 1
      README.md
  4. 13 0
      RackPeek.Domain/Helpers/DeepClone.cs
  5. 37 0
      RackPeek.Domain/Resources/Hardware/AccessPoints/RenameAccessPointUseCase.cs
  6. 31 0
      RackPeek.Domain/Resources/Hardware/Desktops/CloneDesktopUsecase.cs
  7. 37 0
      RackPeek.Domain/Resources/Hardware/Desktops/RenameDesktopUseCase.cs
  8. 31 0
      RackPeek.Domain/Resources/Hardware/Firewalls/CloneFirewallUseCase.cs
  9. 37 0
      RackPeek.Domain/Resources/Hardware/Firewalls/RenameFirewallUseCase.cs
  10. 31 0
      RackPeek.Domain/Resources/Hardware/Laptops/CloneLaptopUseCase.cs
  11. 37 0
      RackPeek.Domain/Resources/Hardware/Laptops/RenameLaptopUseCase.cs
  12. 31 0
      RackPeek.Domain/Resources/Hardware/Routers/CloneRouterUseCase.cs
  13. 37 0
      RackPeek.Domain/Resources/Hardware/Routers/RenameRouterUseCase.cs
  14. 31 0
      RackPeek.Domain/Resources/Hardware/Servers/CloneServerUseCase.cs
  15. 37 0
      RackPeek.Domain/Resources/Hardware/Servers/RenameSystemUseCase.cs
  16. 31 0
      RackPeek.Domain/Resources/Hardware/Switches/CloneSwitchUseCase.cs
  17. 37 0
      RackPeek.Domain/Resources/Hardware/Switches/RenameSwitchUseCase.cs
  18. 31 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/CloneUpsUseCase.cs
  19. 37 0
      RackPeek.Domain/Resources/Hardware/UpsUnits/RenameUpsUseCase.cs
  20. 2 18
      RackPeek.Domain/Resources/Services/CloneSystemUseCase.cs
  21. 29 0
      RackPeek.Domain/Resources/Services/RenameSystemUseCase.cs
  22. 2 21
      RackPeek.Domain/Resources/SystemResources/UseCases/CloneSystemUseCase.cs
  23. 37 0
      RackPeek.Domain/Resources/SystemResources/UseCases/RenameSystemUseCase.cs

+ 12 - 0
CommandIndex.md

@@ -33,6 +33,10 @@
     - [describe](Commands.md#rpk-switches-describe) - Show detailed information about a switch
     - [set](Commands.md#rpk-switches-set) - Update properties of a switch
     - [del](Commands.md#rpk-switches-del) - Delete a switch from the inventory
+    - [port](Commands.md#rpk-switches-port) - Manage ports on a network switch
+      - [add](Commands.md#rpk-switches-port-add) - Add a port to a switch
+      - [set](Commands.md#rpk-switches-port-set) - Update a switch port
+      - [del](Commands.md#rpk-switches-port-del) - Remove a port from a switch
   - [routers](Commands.md#rpk-routers) - Manage network routers
     - [summary](Commands.md#rpk-routers-summary) - Show a hardware report for all routers
     - [add](Commands.md#rpk-routers-add) - Add a new network router to the inventory
@@ -41,6 +45,10 @@
     - [describe](Commands.md#rpk-routers-describe) - Show detailed information about a router
     - [set](Commands.md#rpk-routers-set) - Update properties of a router
     - [del](Commands.md#rpk-routers-del) - Delete a router from the inventory
+    - [port](Commands.md#rpk-routers-port) - Manage ports on a router
+      - [add](Commands.md#rpk-routers-port-add) - Add a port to a router
+      - [set](Commands.md#rpk-routers-port-set) - Update a router port
+      - [del](Commands.md#rpk-routers-port-del) - Remove a port from a router
   - [firewalls](Commands.md#rpk-firewalls) - Manage firewalls
     - [summary](Commands.md#rpk-firewalls-summary) - Show a hardware report for all firewalls
     - [add](Commands.md#rpk-firewalls-add) - Add a new firewall to the inventory
@@ -49,6 +57,10 @@
     - [describe](Commands.md#rpk-firewalls-describe) - Show detailed information about a firewall
     - [set](Commands.md#rpk-firewalls-set) - Update properties of a firewall
     - [del](Commands.md#rpk-firewalls-del) - Delete a firewall from the inventory
+    - [port](Commands.md#rpk-firewalls-port) - Manage ports on a firewall
+      - [add](Commands.md#rpk-firewalls-port-add) - Add a port to a firewall
+      - [set](Commands.md#rpk-firewalls-port-set) - Update a firewall port
+      - [del](Commands.md#rpk-firewalls-port-del) - Remove a port from a firewall
   - [systems](Commands.md#rpk-systems) - Manage systems and their dependencies
     - [summary](Commands.md#rpk-systems-summary) - Show a summary report for all systems
     - [add](Commands.md#rpk-systems-add) - Add a new system to the inventory

+ 217 - 3
Commands.md

@@ -128,9 +128,10 @@ ARGUMENTS:
     <name>     
 
 OPTIONS:
-    -h, --help        Prints help information
-        --ram <GB>                           
-        --ipmi                               
+    -h, --help             Prints help information
+        --ram <GB>                                
+        --ram_mts <MTS>                           
+        --ipmi                                    
 ```
 
 ## `rpk servers del`
@@ -458,6 +459,7 @@ COMMANDS:
     describe <name>    Show detailed information about a switch     
     set <name>         Update properties of a switch                
     del <name>         Delete a switch from the inventory           
+    port               Manage ports on a network switch             
 ```
 
 ## `rpk switches summary`
@@ -562,6 +564,76 @@ OPTIONS:
     -h, --help    Prints help information
 ```
 
+## `rpk switches port`
+```
+DESCRIPTION:
+Manage ports on a network switch
+
+USAGE:
+    rpk switches port [OPTIONS] <COMMAND>
+
+OPTIONS:
+    -h, --help    Prints help information
+
+COMMANDS:
+    add <name>    Add a port to a switch     
+    set <name>    Update a switch port       
+    del <name>    Remove a port from a switch
+```
+
+## `rpk switches port add`
+```
+DESCRIPTION:
+Add a port to a switch
+
+USAGE:
+    rpk switches port add <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help     Prints help information          
+        --type     The port type (e.g., rj45, sfp+) 
+        --speed    The port speed (e.g., 1, 2.5, 10)
+        --count    Number of ports of this type     
+```
+
+## `rpk switches port set`
+```
+DESCRIPTION:
+Update a switch port
+
+USAGE:
+    rpk switches port set <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help             Prints help information
+        --index <INDEX>                           
+        --type                                    
+        --speed                                   
+        --count                                   
+```
+
+## `rpk switches port del`
+```
+DESCRIPTION:
+Remove a port from a switch
+
+USAGE:
+    rpk switches port del <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help             Prints help information
+        --index <INDEX>                           
+```
+
 ## `rpk routers`
 ```
 DESCRIPTION:
@@ -581,6 +653,7 @@ COMMANDS:
     describe <name>    Show detailed information about a router     
     set <name>         Update properties of a router                
     del <name>         Delete a router from the inventory           
+    port               Manage ports on a router                     
 ```
 
 ## `rpk routers summary`
@@ -685,6 +758,76 @@ OPTIONS:
     -h, --help    Prints help information
 ```
 
+## `rpk routers port`
+```
+DESCRIPTION:
+Manage ports on a router
+
+USAGE:
+    rpk routers port [OPTIONS] <COMMAND>
+
+OPTIONS:
+    -h, --help    Prints help information
+
+COMMANDS:
+    add <name>    Add a port to a router     
+    set <name>    Update a router port       
+    del <name>    Remove a port from a router
+```
+
+## `rpk routers port add`
+```
+DESCRIPTION:
+Add a port to a router
+
+USAGE:
+    rpk routers port add <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help     Prints help information
+        --type                            
+        --speed                           
+        --count                           
+```
+
+## `rpk routers port set`
+```
+DESCRIPTION:
+Update a router port
+
+USAGE:
+    rpk routers port set <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help             Prints help information
+        --index <INDEX>                           
+        --type                                    
+        --speed                                   
+        --count                                   
+```
+
+## `rpk routers port del`
+```
+DESCRIPTION:
+Remove a port from a router
+
+USAGE:
+    rpk routers port del <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help             Prints help information
+        --index <INDEX>                           
+```
+
 ## `rpk firewalls`
 ```
 DESCRIPTION:
@@ -704,6 +847,7 @@ COMMANDS:
     describe <name>    Show detailed information about a firewall     
     set <name>         Update properties of a firewall                
     del <name>         Delete a firewall from the inventory           
+    port               Manage ports on a firewall                     
 ```
 
 ## `rpk firewalls summary`
@@ -808,6 +952,76 @@ OPTIONS:
     -h, --help    Prints help information
 ```
 
+## `rpk firewalls port`
+```
+DESCRIPTION:
+Manage ports on a firewall
+
+USAGE:
+    rpk firewalls port [OPTIONS] <COMMAND>
+
+OPTIONS:
+    -h, --help    Prints help information
+
+COMMANDS:
+    add <name>    Add a port to a firewall     
+    set <name>    Update a firewall port       
+    del <name>    Remove a port from a firewall
+```
+
+## `rpk firewalls port add`
+```
+DESCRIPTION:
+Add a port to a firewall
+
+USAGE:
+    rpk firewalls port add <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help     Prints help information
+        --type                            
+        --speed                           
+        --count                           
+```
+
+## `rpk firewalls port set`
+```
+DESCRIPTION:
+Update a firewall port
+
+USAGE:
+    rpk firewalls port set <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help             Prints help information
+        --index <INDEX>                           
+        --type                                    
+        --speed                                   
+        --count                                   
+```
+
+## `rpk firewalls port del`
+```
+DESCRIPTION:
+Remove a port from a firewall
+
+USAGE:
+    rpk firewalls port del <name> [OPTIONS]
+
+ARGUMENTS:
+    <name>     
+
+OPTIONS:
+    -h, --help             Prints help information
+        --index <INDEX>                           
+```
+
 ## `rpk systems`
 ```
 DESCRIPTION:

+ 13 - 1
README.md

@@ -113,6 +113,10 @@ The project is optimized for home labs and self-hosted environments, not enterpr
     - [describe](Commands.md#rpk-switches-describe) - Show detailed information about a switch
     - [set](Commands.md#rpk-switches-set) - Update properties of a switch
     - [del](Commands.md#rpk-switches-del) - Delete a switch from the inventory
+    - [port](Commands.md#rpk-switches-port) - Manage ports on a network switch
+      - [add](Commands.md#rpk-switches-port-add) - Add a port to a switch
+      - [set](Commands.md#rpk-switches-port-set) - Update a switch port
+      - [del](Commands.md#rpk-switches-port-del) - Remove a port from a switch
   - [routers](Commands.md#rpk-routers) - Manage network routers
     - [summary](Commands.md#rpk-routers-summary) - Show a hardware report for all routers
     - [add](Commands.md#rpk-routers-add) - Add a new network router to the inventory
@@ -121,6 +125,10 @@ The project is optimized for home labs and self-hosted environments, not enterpr
     - [describe](Commands.md#rpk-routers-describe) - Show detailed information about a router
     - [set](Commands.md#rpk-routers-set) - Update properties of a router
     - [del](Commands.md#rpk-routers-del) - Delete a router from the inventory
+    - [port](Commands.md#rpk-routers-port) - Manage ports on a router
+      - [add](Commands.md#rpk-routers-port-add) - Add a port to a router
+      - [set](Commands.md#rpk-routers-port-set) - Update a router port
+      - [del](Commands.md#rpk-routers-port-del) - Remove a port from a router
   - [firewalls](Commands.md#rpk-firewalls) - Manage firewalls
     - [summary](Commands.md#rpk-firewalls-summary) - Show a hardware report for all firewalls
     - [add](Commands.md#rpk-firewalls-add) - Add a new firewall to the inventory
@@ -129,6 +137,10 @@ The project is optimized for home labs and self-hosted environments, not enterpr
     - [describe](Commands.md#rpk-firewalls-describe) - Show detailed information about a firewall
     - [set](Commands.md#rpk-firewalls-set) - Update properties of a firewall
     - [del](Commands.md#rpk-firewalls-del) - Delete a firewall from the inventory
+    - [port](Commands.md#rpk-firewalls-port) - Manage ports on a firewall
+      - [add](Commands.md#rpk-firewalls-port-add) - Add a port to a firewall
+      - [set](Commands.md#rpk-firewalls-port-set) - Update a firewall port
+      - [del](Commands.md#rpk-firewalls-port-del) - Remove a port from a firewall
   - [systems](Commands.md#rpk-systems) - Manage systems and their dependencies
     - [summary](Commands.md#rpk-systems-summary) - Show a summary report for all systems
     - [add](Commands.md#rpk-systems-add) - Add a new system to the inventory
@@ -207,4 +219,4 @@ The project is optimized for home labs and self-hosted environments, not enterpr
     - [describe](Commands.md#rpk-services-describe) - Show detailed information about a service
     - [set](Commands.md#rpk-services-set) - Update properties of a service
     - [del](Commands.md#rpk-services-del) - Delete a service
-    - [subnets](Commands.md#rpk-services-subnets) - List subnets associated with a service, optionally filtered by CIDR
+    - [subnets](Commands.md#rpk-services-subnets) - List subnets associated with a service, optionally filtered by CIDR

+ 13 - 0
RackPeek.Domain/Helpers/DeepClone.cs

@@ -0,0 +1,13 @@
+namespace RackPeek.Domain.Helpers;
+
+using System.Text.Json;
+
+public static class Clone
+{
+    public static T DeepClone<T>(T obj)
+    {
+        var json = JsonSerializer.Serialize(obj);
+        return JsonSerializer.Deserialize<T>(json)!;
+    }
+
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/AccessPoints/RenameAccessPointUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.AccessPoints;
+
+public class RenameAccessPointUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as AccessPoint;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Desktops/CloneDesktopUsecase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktops;
+
+public class CloneDesktopUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Firewall;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Desktops/RenameDesktopUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.Desktops;
+
+public class RenameDesktopUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Desktop;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Firewalls/CloneFirewallUseCase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Firewalls;
+
+public class CloneFirewallUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Firewall;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Firewalls/RenameFirewallUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.Firewalls;
+
+public class RenameFirewallUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Firewall;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Laptops/CloneLaptopUseCase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Laptops;
+
+public class CloneLaptopUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Laptop;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Laptops/RenameLaptopUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.Laptops;
+
+public class RenameLaptopUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Laptop;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Routers/CloneRouterUseCase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Routers;
+
+public class CloneRouterUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Router;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Routers/RenameRouterUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.Routers;
+
+public class RenameRouterUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Router;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Servers/CloneServerUseCase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Servers;
+
+public class CloneServerUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Server;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Servers/RenameSystemUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.Servers;
+
+public class RenameServerUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Server;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/Switches/CloneSwitchUseCase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.Switches;
+
+public class CloneSwitchUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Switch;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/Switches/RenameSwitchUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.Switches;
+
+public class RenameSwitchUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Switch;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 31 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/CloneUpsUseCase.cs

@@ -0,0 +1,31 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class CloneUpsUseCase(IHardwareRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string cloneName)
+    {
+        originalName = Normalize.HardwareName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+
+        cloneName = Normalize.HardwareName(cloneName);
+        ThrowIfInvalid.ResourceName(cloneName);
+        
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(cloneName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{cloneName}' already exists.");
+
+        var original = await repository.GetByNameAsync(originalName) as Ups;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
+        
+        await repository.AddAsync(clone);
+    }
+}

+ 37 - 0
RackPeek.Domain/Resources/Hardware/UpsUnits/RenameUpsUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.SystemResources;
+
+namespace RackPeek.Domain.Resources.Hardware.UpsUnits;
+
+public class RenameUpsUseCase(IHardwareRepository repository, ISystemRepository systemRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName) as Ups;
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await systemRepo.GetByPhysicalHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await systemRepo.UpdateAsync(child);
+        }
+    }
+}

+ 2 - 18
RackPeek.Domain/Resources/Services/CloneSystemUseCase.cs

@@ -22,24 +22,8 @@ public class CloneServiceUseCase(IServiceRepository repository, IResourceReposit
             throw new NotFoundException($"Resource '{originalName}' not found.");
         }
 
-        Network? clonedNetwork = null;
-        if (original.Network != null)
-        {
-            clonedNetwork = new Network()
-            {
-                Ip = original.Network.Ip,
-                Port = original.Network.Port,
-                Protocol = original.Network.Protocol,
-                Url = original.Network.Url,
-            };
-        }
-        
-        var clone = new Service()
-        {
-            Name = cloneName,
-            Network = clonedNetwork,
-            RunsOn = original.RunsOn,
-        };
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
         
         await repository.AddAsync(clone);
     }

+ 29 - 0
RackPeek.Domain/Resources/Services/RenameSystemUseCase.cs

@@ -0,0 +1,29 @@
+using RackPeek.Domain.Helpers;
+
+namespace RackPeek.Domain.Resources.Services;
+
+public class RenameServiceUseCase(IServiceRepository repository, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.ServiceName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.ServiceName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName);
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+        
+        original.Name = newName;
+        
+        await repository.UpdateAsync(original);
+    }
+}

+ 2 - 21
RackPeek.Domain/Resources/SystemResources/UseCases/CloneSystemUseCase.cs

@@ -23,28 +23,9 @@ public class CloneSystemUseCase(ISystemRepository repository, IResourceRepositor
         {
             throw new NotFoundException($"Resource '{originalName}' not found.");
         }
-
-        List<Drive>? clonedDrives = null;
-        if (original.Drives != null)
-        {
-            clonedDrives = original
-                .Drives
-                .Select(drive => new Drive() { Size = drive.Size, Type = drive.Type })
-                .ToList();
-        }
-        
-        var clone = new SystemResource()
-        {
-            Name = cloneName,
-            Cores = original.Cores,
-            Kind = original.Kind,
-            Os = original.Os,
-            Ram = original.Ram,
-            Type = original.Type,
-            Drives = clonedDrives,
-            RunsOn = original.RunsOn,
-        };
         
+        var clone = Clone.DeepClone(original);
+        clone.Name = cloneName;
         await repository.AddAsync(clone);
     }
 }

+ 37 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/RenameSystemUseCase.cs

@@ -0,0 +1,37 @@
+using RackPeek.Domain.Helpers;
+using RackPeek.Domain.Resources.Models;
+using RackPeek.Domain.Resources.Services;
+
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public class RenameSystemUseCase(ISystemRepository repository, IServiceRepository serviceRepo, IResourceRepository resourceRepo) : IUseCase
+{
+    public async Task ExecuteAsync(string originalName, string newName)
+    {
+        originalName = Normalize.SystemName(originalName);
+        ThrowIfInvalid.ResourceName(originalName);
+        
+        newName = Normalize.SystemName(newName);
+        ThrowIfInvalid.ResourceName(newName);
+
+        var existingResourceKind = await resourceRepo.GetResourceKindAsync(newName);
+        if (!string.IsNullOrEmpty(existingResourceKind))
+            throw new ConflictException($"{existingResourceKind} resource '{newName}' already exists.");
+        
+        var original = await repository.GetByNameAsync(originalName);
+        if (original == null)
+        {
+            throw new NotFoundException($"Resource '{originalName}' not found.");
+        }
+
+        original.Name = newName;
+        await repository.UpdateAsync(original);
+        
+        var children = await serviceRepo.GetBySystemHostAsync(originalName);
+        foreach (var child in children)
+        {
+            child.RunsOn = newName;
+            await serviceRepo.UpdateAsync(child);
+        }
+    }
+}