using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using PSProxmoxVE.Core.Authentication; using PSProxmoxVE.Core.Client; using PSProxmoxVE.Core.Models.Network; using PSProxmoxVE.Core.Models.Vms; namespace PSProxmoxVE.Core.Services { /// /// Service for Proxmox VE node network and SDN (Software-Defined Networking) API operations. /// SDN methods require PVE 8.0 or later. Version checks are performed at the cmdlet layer. /// public class NetworkService { private readonly IPveHttpClient? _injectedClient; /// Initializes a new instance that creates its own HTTP clients. public NetworkService() { } /// Initializes a new instance that uses the supplied HTTP client for all requests. /// The HTTP client to use. The caller owns its lifetime. public NetworkService(IPveHttpClient client) { _injectedClient = client ?? throw new ArgumentNullException(nameof(client)); } // ------------------------------------------------------------------------- // Node network interfaces // ------------------------------------------------------------------------- /// /// Returns network interface configurations for a node. /// /// The authenticated PVE session. /// The cluster node name. /// /// Optional interface type filter (e.g. "bridge", "bond", "eth", "vlan", "alias"). /// public PveNetwork[] GetNetworks(PveSession session, string node, string? type = null) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); var resource = $"nodes/{node}/network"; if (!string.IsNullOrEmpty(type)) resource += $"?type={Uri.EscapeDataString(type!)}"; IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync(resource).GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates a network interface on a node. Changes are pending until /// is called. Returns the new interface config. /// /// The authenticated PVE session. /// The cluster node name. /// Network interface configuration parameters. public PveNetwork CreateNetwork( PveSession session, string node, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); var response = client.PostAsync($"nodes/{node}/network", formData) .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveNetwork(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an existing network interface on a node. Changes are pending until /// is called. /// /// The authenticated PVE session. /// The cluster node name. /// The network interface name. /// Network interface configuration parameters to update. public void SetNetwork( PveSession session, string node, string iface, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PutAsync($"nodes/{node}/network/{iface}", formData) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes a network interface from a node. Changes are pending until /// is called. /// /// The authenticated PVE session. /// The cluster node name. /// The network interface name to remove. public void RemoveNetwork(PveSession session, string node, string iface) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"nodes/{node}/network/{iface}").GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Applies pending network configuration changes on a node. Returns the task UPID. /// /// The authenticated PVE session. /// The cluster node name. public PveTask ApplyNetworkConfig(PveSession session, string node) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.PutAsync($"nodes/{node}/network") .GetAwaiter().GetResult(); return ParseTask(response, node); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // SDN — requires PVE 8.0+ // ------------------------------------------------------------------------- /// /// Returns all SDN zone definitions. Requires PVE 8.0+. /// /// The authenticated PVE session. public PveSdnZone[] GetSdnZones(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/sdn/zones").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Returns all SDN VNet definitions. Requires PVE 8.0+. /// /// The authenticated PVE session. public PveSdnVnet[] GetSdnVnets(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/sdn/vnets").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates an SDN zone. Requires PVE 8.0+. /// /// The authenticated PVE session. /// SDN zone configuration parameters. public PveSdnZone CreateSdnZone( PveSession session, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); var response = client.PostAsync("cluster/sdn/zones", formData) .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveSdnZone(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes an SDN zone. Requires PVE 8.0+. /// /// The authenticated PVE session. /// The SDN zone identifier to remove. public void RemoveSdnZone(PveSession session, string zone) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"cluster/sdn/zones/{zone}").GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates an SDN VNet. Requires PVE 8.0+. /// /// The authenticated PVE session. /// SDN VNet configuration parameters. public PveSdnVnet CreateSdnVnet( PveSession session, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); var response = client.PostAsync("cluster/sdn/vnets", formData) .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? new PveSdnVnet(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes an SDN VNet. Requires PVE 8.0+. /// /// The authenticated PVE session. /// The SDN VNet identifier to remove. public void RemoveSdnVnet(PveSession session, string vnet) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"cluster/sdn/vnets/{vnet}").GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // SDN Subnets — requires PVE 8.0+ // ------------------------------------------------------------------------- /// /// Returns all subnets for an SDN VNet. Requires PVE 8.0+. /// /// The authenticated PVE session. /// The SDN VNet identifier. public PveSdnSubnet[] GetSdnSubnets(PveSession session, string vnet) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets") .GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates an SDN subnet on a VNet. Requires PVE 8.0+. /// /// The authenticated PVE session. /// The SDN VNet identifier. /// Subnet configuration parameters including "subnet" (CIDR). public void CreateSdnSubnet( PveSession session, string vnet, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var formData = config.ToDictionary( kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? string.Empty); client.PostAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets", formData) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes an SDN subnet from a VNet. Requires PVE 8.0+. /// /// The authenticated PVE session. /// The SDN VNet identifier. /// The subnet CIDR to remove. public void RemoveSdnSubnet(PveSession session, string vnet, string subnet) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync( $"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}") .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // SDN IPAM // ------------------------------------------------------------------------- /// /// Returns all SDN IPAM plugins. Requires PVE 8.0+. /// public PveSdnIpam[] GetSdnIpams(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/sdn/ipams").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates an SDN IPAM plugin. Requires PVE 8.0+. /// public void CreateSdnIpam(PveSession session, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PostAsync("cluster/sdn/ipams", config).GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes an SDN IPAM plugin. Requires PVE 8.0+. /// public void RemoveSdnIpam(PveSession session, string ipam) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}") .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // SDN DNS // ------------------------------------------------------------------------- /// /// Returns all SDN DNS plugins. Requires PVE 8.0+. /// public PveSdnDns[] GetSdnDnsPlugins(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/sdn/dns").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates an SDN DNS plugin. Requires PVE 8.0+. /// public void CreateSdnDnsPlugin(PveSession session, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PostAsync("cluster/sdn/dns", config).GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes an SDN DNS plugin. Requires PVE 8.0+. /// public void RemoveSdnDnsPlugin(PveSession session, string dns) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}") .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // SDN Controllers // ------------------------------------------------------------------------- /// /// Returns all SDN controllers. Requires PVE 8.0+. /// public PveSdnController[] GetSdnControllers(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { var response = client.GetAsync("cluster/sdn/controllers").GetAwaiter().GetResult(); var data = JObject.Parse(response)["data"]; return data?.ToObject() ?? Array.Empty(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Creates an SDN controller. Requires PVE 8.0+. /// public void CreateSdnController(PveSession session, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PostAsync("cluster/sdn/controllers", config).GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Removes an SDN controller. Requires PVE 8.0+. /// public void RemoveSdnController(PveSession session, string controller) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.DeleteAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}") .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // SDN Update operations // ------------------------------------------------------------------------- /// /// Applies pending SDN configuration changes cluster-wide. /// public void ApplySdn(PveSession session) { if (session == null) throw new ArgumentNullException(nameof(session)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync("cluster/sdn", new Dictionary()) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an SDN zone configuration. /// public void UpdateSdnZone(PveSession session, string zone, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}", config) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an SDN VNet configuration. /// public void UpdateSdnVnet(PveSession session, string vnet, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}", config) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an SDN subnet configuration. /// public void UpdateSdnSubnet(PveSession session, string vnet, string subnet, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet)); if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync( $"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}", config).GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an SDN controller configuration. /// public void UpdateSdnController(PveSession session, string controller, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}", config) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an SDN IPAM plugin configuration. /// public void UpdateSdnIpam(PveSession session, string ipam, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}", config) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } /// /// Updates an SDN DNS plugin configuration. /// public void UpdateSdnDns(PveSession session, string dns, Dictionary config) { if (session == null) throw new ArgumentNullException(nameof(session)); if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns)); if (config == null) throw new ArgumentNullException(nameof(config)); IPveHttpClient client = _injectedClient ?? new PveHttpClient(session); try { client.PutAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}", config) .GetAwaiter().GetResult(); } finally { if (_injectedClient == null) client.Dispose(); } } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- private static PveTask ParseTask(string response, string node) { var data = JObject.Parse(response)["data"]; if (data?.Type == JTokenType.String) return new PveTask { Upid = data.ToString(), Node = node }; var task = data?.ToObject() ?? new PveTask(); task.Node = node; return task; } } }