refactor: route the network and SDN cmdlets through NetworkService (#126) (#201)

Wires the 12 direct-PveHttpClient Network/SDN cmdlets (Get/New/Set/Remove-PveNetwork,
Invoke-PveNetworkApply, Get/New-PveSdnZone, Get/New-PveSdnVnet, Get/New-PveSdnSubnet,
Remove-PveSdnSubnet) to the existing NetworkService methods, which previously had zero
callers for the New/Set/Get paths. NetworkService.ParseTask now stamps
Status = "running" on the UPID-string branch, matching SnapshotService's ParseTask from
PR #196 (the pattern PR). Offline tests for every wired service method are added to
NetworkServiceTests.cs.

Fold-in from issue #126's own comment: ValidatePattern identifier validation, already
present on Remove-PveSdnZone/Vnet and Remove-PveStorage from PR #161, is added to the
Zone/Vnet identifier parameters on the New-PveSdnZone/Vnet/Subnet and
Remove-PveSdnSubnet cmdlets converted here.

Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
This commit is contained in:
goodolclint-claude[bot]
2026-09-03 00:14:40 +00:00
committed by GitHub
parent 3f7f69b250
commit 87fa7a7a9c
14 changed files with 673 additions and 99 deletions
@@ -597,7 +597,7 @@ namespace PSProxmoxVE.Core.Services
{
var data = JObject.Parse(response)["data"];
if (data?.Type == JTokenType.String)
return new PveTask { Upid = data.ToString(), Node = node };
return new PveTask { Upid = data.ToString(), Node = node, Status = "running" };
var task = data?.ToObject<PveTask>() ?? new PveTask();
task.Node = node;
@@ -1,8 +1,6 @@
using System;
using System.Management.Automation;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -37,20 +35,13 @@ namespace PSProxmoxVE.Cmdlets.Network
protected override void ProcessRecord()
{
var session = GetSession();
using var client = new PveHttpClient(session);
WriteVerbose($"Getting network interfaces on node '{Node}'...");
var resource = $"nodes/{Uri.EscapeDataString(Node)}/network";
if (!string.IsNullOrEmpty(Type))
resource += $"?type={Uri.EscapeDataString(Type)}";
var service = new NetworkService();
var networks = service.GetNetworks(session, Node, Type);
var json = client.GetAsync(resource).GetAwaiter().GetResult();
var root = JObject.Parse(json);
var data = root["data"] as JArray ?? new JArray();
foreach (var item in data)
foreach (var network in networks)
{
var network = item.ToObject<PveNetwork>()!;
network.Node = Node;
if (!string.IsNullOrEmpty(Iface) &&
!string.Equals(network.Iface, Iface, System.StringComparison.OrdinalIgnoreCase))
@@ -1,7 +1,6 @@
using System.Management.Automation;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -28,18 +27,13 @@ namespace PSProxmoxVE.Cmdlets.Network
{
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
WriteVerbose($"Getting SDN subnets for VNet '{Vnet}'...");
var json = client.GetAsync($"cluster/sdn/vnets/{System.Uri.EscapeDataString(Vnet)}/subnets")
.GetAwaiter().GetResult();
var root = JObject.Parse(json);
var data = root["data"] as JArray ?? new JArray();
var service = new NetworkService();
var subnets = service.GetSdnSubnets(session, Vnet);
foreach (var item in data)
foreach (var subnet in subnets)
{
var subnet = item.ToObject<PveSdnSubnet>()!;
if (!string.IsNullOrEmpty(Subnet) &&
!string.Equals(subnet.Subnet, Subnet, System.StringComparison.OrdinalIgnoreCase))
continue;
@@ -1,7 +1,6 @@
using System.Management.Automation;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -28,17 +27,13 @@ namespace PSProxmoxVE.Cmdlets.Network
{
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
WriteVerbose("Getting SDN VNets...");
var json = client.GetAsync("cluster/sdn/vnets").GetAwaiter().GetResult();
var root = JObject.Parse(json);
var data = root["data"] as JArray ?? new JArray();
var service = new NetworkService();
var vnets = service.GetSdnVnets(session);
foreach (var item in data)
foreach (var vnet in vnets)
{
var vnet = item.ToObject<PveSdnVnet>()!;
if (!string.IsNullOrEmpty(Zone) &&
!string.Equals(vnet.Zone, Zone, System.StringComparison.OrdinalIgnoreCase))
continue;
@@ -1,7 +1,6 @@
using System.Management.Automation;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -23,17 +22,13 @@ namespace PSProxmoxVE.Cmdlets.Network
{
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
WriteVerbose("Getting SDN zones...");
var resource = "cluster/sdn/zones";
var json = client.GetAsync(resource).GetAwaiter().GetResult();
var root = JObject.Parse(json);
var data = root["data"] as JArray ?? new JArray();
var service = new NetworkService();
var zones = service.GetSdnZones(session);
foreach (var item in data)
foreach (var zone in zones)
{
var zone = item.ToObject<PveSdnZone>()!;
if (!string.IsNullOrEmpty(Zone) &&
!string.Equals(zone.Zone, Zone, System.StringComparison.OrdinalIgnoreCase))
continue;
@@ -1,7 +1,4 @@
using System;
using System.Management.Automation;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
@@ -33,19 +30,15 @@ namespace PSProxmoxVE.Cmdlets.Network
return;
var session = GetSession();
using var client = new PveHttpClient(session);
WriteVerbose($"Applying network configuration on node '{Node}'...");
var json = client.PutAsync($"nodes/{Uri.EscapeDataString(Node)}/network").GetAwaiter().GetResult();
var root = JObject.Parse(json);
var upid = root["data"]?.ToString() ?? string.Empty;
var service = new NetworkService();
var task = service.ApplyNetworkConfig(session, Node);
var task = new PveTask { Upid = upid, Node = Node, Status = "running" };
if (Wait.IsPresent && !string.IsNullOrEmpty(upid))
if (Wait.IsPresent && !string.IsNullOrEmpty(task.Upid))
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, Node, upid);
task = taskService.WaitForTask(session, Node, task.Upid);
}
WriteObject(task);
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -76,10 +75,9 @@ namespace PSProxmoxVE.Cmdlets.Network
return;
var session = GetSession();
using var client = new PveHttpClient(session);
WriteVerbose($"Creating network interface '{Iface}' on node '{Node}'...");
var data = new Dictionary<string, string>
var data = new Dictionary<string, object>
{
["iface"] = Iface,
["type"] = Type
@@ -96,7 +94,8 @@ namespace PSProxmoxVE.Cmdlets.Network
if (Autostart.IsPresent) data["autostart"] = "1";
if (!string.IsNullOrEmpty(Comments)) data["comments"] = Comments!;
client.PostAsync($"nodes/{Uri.EscapeDataString(Node)}/network", data).GetAwaiter().GetResult();
var service = new NetworkService();
service.CreateNetwork(session, Node, data);
}
}
}
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -17,6 +17,7 @@ namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>The SDN VNet to add the subnet to.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The SDN VNet name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Vnet { get; set; } = string.Empty;
/// <summary>The subnet CIDR notation (e.g. "10.0.0.0/24").</summary>
@@ -54,10 +55,8 @@ namespace PSProxmoxVE.Cmdlets.Network
+ $"Connected server is PVE {session.ServerVersion}. The parameter will be sent but may be ignored.");
}
using var client = new PveHttpClient(session);
WriteVerbose($"Creating SDN subnet '{Subnet}' on VNet '{Vnet}'...");
var data = new Dictionary<string, string>
var data = new Dictionary<string, object>
{
["subnet"] = Subnet,
["type"] = "subnet"
@@ -68,9 +67,8 @@ namespace PSProxmoxVE.Cmdlets.Network
if (!string.IsNullOrEmpty(DnsZonePrefix)) data["dnszoneprefix"] = DnsZonePrefix!;
if (!string.IsNullOrEmpty(DhcpRange)) data["dhcp-range"] = DhcpRange!;
client.PostAsync(
$"cluster/sdn/vnets/{System.Uri.EscapeDataString(Vnet)}/subnets", data)
.GetAwaiter().GetResult();
var service = new NetworkService();
service.CreateSdnSubnet(session, Vnet, data);
}
}
}
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -16,10 +16,12 @@ namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>The VNet identifier (alphanumeric, up to 8 characters).</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The SDN VNet name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Vnet { get; set; } = string.Empty;
/// <summary>The SDN zone this VNet belongs to.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The SDN zone name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Zone { get; set; } = string.Empty;
/// <summary>VLAN tag for VLAN-type zones.</summary>
@@ -41,10 +43,9 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
WriteVerbose($"Creating SDN VNet '{Vnet}'...");
var data = new Dictionary<string, string>
var data = new Dictionary<string, object>
{
["vnet"] = Vnet,
["zone"] = Zone
@@ -54,7 +55,8 @@ namespace PSProxmoxVE.Cmdlets.Network
if (!string.IsNullOrEmpty(Alias)) data["alias"] = Alias!;
if (VlanAware.IsPresent) data["vlanaware"] = "1";
client.PostAsync("cluster/sdn/vnets", data).GetAwaiter().GetResult();
var service = new NetworkService();
service.CreateSdnVnet(session, data);
}
}
}
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -16,6 +16,7 @@ namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>The zone identifier (alphanumeric, hyphens allowed).</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The SDN zone name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Zone { get; set; } = string.Empty;
/// <summary>The zone type.</summary>
@@ -58,10 +59,9 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
WriteVerbose($"Creating SDN zone '{Zone}'...");
var data = new Dictionary<string, string>
var data = new Dictionary<string, object>
{
["zone"] = Zone,
["type"] = Type
@@ -75,7 +75,8 @@ namespace PSProxmoxVE.Cmdlets.Network
if (!string.IsNullOrEmpty(DnsZone)) data["dnszone"] = DnsZone!;
if (!string.IsNullOrEmpty(Ipam)) data["ipam"] = Ipam!;
client.PostAsync("cluster/sdn/zones", data).GetAwaiter().GetResult();
var service = new NetworkService();
service.CreateSdnZone(session, data);
}
}
}
@@ -1,6 +1,5 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -29,10 +28,10 @@ namespace PSProxmoxVE.Cmdlets.Network
return;
var session = GetSession();
using var client = new PveHttpClient(session);
WriteVerbose($"Removing network interface '{Iface}' on node '{Node}'...");
client.DeleteAsync($"nodes/{Uri.EscapeDataString(Node)}/network/{Uri.EscapeDataString(Iface)}").GetAwaiter().GetResult();
WriteVerbose($"Removing network interface '{Iface}' from node '{Node}'...");
var service = new NetworkService();
service.RemoveNetwork(session, Node, Iface);
}
}
}
@@ -1,5 +1,5 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -16,6 +16,7 @@ namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>The SDN VNet containing the subnet.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The SDN VNet name.")]
[ValidatePattern(@"\A[A-Za-z0-9][A-Za-z0-9._-]*\z")]
public string Vnet { get; set; } = string.Empty;
/// <summary>The subnet CIDR to remove (e.g. "10.0.0.0/24").</summary>
@@ -29,12 +30,10 @@ namespace PSProxmoxVE.Cmdlets.Network
var session = GetSession();
RequireVersion(session, "SDN", 6, 2, 8, 0);
using var client = new PveHttpClient(session);
WriteVerbose($"Removing SDN subnet '{Subnet}' from VNet '{Vnet}'...");
client.DeleteAsync(
$"cluster/sdn/vnets/{System.Uri.EscapeDataString(Vnet)}/subnets/{System.Uri.EscapeDataString(Subnet)}")
.GetAwaiter().GetResult();
var service = new NetworkService();
service.RemoveSdnSubnet(session, Vnet, Subnet);
}
}
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
@@ -82,10 +81,9 @@ namespace PSProxmoxVE.Cmdlets.Network
return;
var session = GetSession();
using var client = new PveHttpClient(session);
WriteVerbose($"Updating network interface '{Iface}' on node '{Node}'...");
var data = new Dictionary<string, string>
var data = new Dictionary<string, object>
{
["type"] = Type
};
@@ -109,7 +107,8 @@ namespace PSProxmoxVE.Cmdlets.Network
if (Autostart.IsPresent) data["autostart"] = "1";
if (!string.IsNullOrEmpty(Comments)) data["comments"] = Comments!;
client.PutAsync($"nodes/{Uri.EscapeDataString(Node)}/network/{Uri.EscapeDataString(Iface)}", data).GetAwaiter().GetResult();
var service = new NetworkService();
service.SetNetwork(session, Node, Iface, data);
}
}
}