mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
feat(services): implement service layer for all domains
Add service classes for nodes, VMs, containers, storage, snapshots, network/SDN, users/roles/permissions, templates, cloud-init, tasks, and cluster. SDN services enforce PVE 8.0+ version guard. Task service supports polling with configurable timeout and progress callbacks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for managing Cloud-Init configuration on Proxmox VE QEMU/KVM VMs.
|
||||
/// All operations target the /nodes/{node}/qemu/{vmid}/config endpoint.
|
||||
/// </summary>
|
||||
public class CloudInitService
|
||||
{
|
||||
// Cloud-Init field names as used in the PVE API
|
||||
private static readonly string[] CloudInitFields =
|
||||
{
|
||||
"ciuser", "cipassword", "sshkeys",
|
||||
"ipconfig0", "ipconfig1", "ipconfig2", "ipconfig3",
|
||||
"nameserver", "searchdomain", "cicustom"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Cloud-Init specific configuration fields for a VM.
|
||||
/// Internally fetches the full VM config and extracts the CI fields.
|
||||
/// </summary>
|
||||
public PveCloudInitConfig GetCloudInitConfig(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/config")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
if (data == null) return new PveCloudInitConfig();
|
||||
|
||||
// Extract only the Cloud-Init fields into a reduced JObject for deserialization
|
||||
var ciObj = new JObject();
|
||||
foreach (var field in CloudInitFields)
|
||||
{
|
||||
if (data[field] != null)
|
||||
ciObj[field] = data[field];
|
||||
}
|
||||
|
||||
return ciObj.ToObject<PveCloudInitConfig>() ?? new PveCloudInitConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates Cloud-Init configuration fields on a VM. Only fields present in
|
||||
/// <paramref name="config"/> are changed; unspecified fields are left as-is.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pass only the Cloud-Init keys you want to change, for example:
|
||||
/// <c>new Dictionary<string, object> { ["ciuser"] = "ubuntu", ["ipconfig0"] = "ip=dhcp" }</c>
|
||||
/// </remarks>
|
||||
public void SetCloudInitConfig(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
client.PutAsync($"nodes/{node}/qemu/{vmid}/config", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers regeneration of the Cloud-Init ISO drive attached to the VM.
|
||||
/// This is done by calling the cloudinit dump endpoint, then touching the config to
|
||||
/// force PVE to rebuild the CD-ROM image on next start.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The raw Cloud-Init dump (user-data) string as reported by the PVE API.
|
||||
/// </returns>
|
||||
public string RegenerateCloudInitImage(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
|
||||
// Request a Cloud-Init dump (user-data section) — this causes PVE to rebuild the image
|
||||
var dumpResponse = client.GetAsync($"nodes/{node}/qemu/{vmid}/cloudinit/dump?type=user")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(dumpResponse)["data"];
|
||||
return data?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Cluster;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE cluster-level API operations.
|
||||
/// </summary>
|
||||
public class ClusterService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the current cluster status. The response is a mixed array of
|
||||
/// "cluster" and "node" type entries.
|
||||
/// </summary>
|
||||
public PveClusterStatus[] GetClusterStatus(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("cluster/status").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
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.Containers;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE Linux Container (LXC) API operations.
|
||||
/// </summary>
|
||||
public class ContainerService
|
||||
{
|
||||
private readonly NodeService _nodeService = new NodeService();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Read operations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns containers. If <paramref name="node"/> is null, queries every cluster node.
|
||||
/// </summary>
|
||||
public PveContainer[] GetContainers(PveSession session, string? node = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
if (node != null)
|
||||
return GetContainersOnNode(session, node);
|
||||
|
||||
var nodes = _nodeService.GetNodes(session);
|
||||
var all = new List<PveContainer>();
|
||||
foreach (var n in nodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var containers = GetContainersOnNode(session, n.Name);
|
||||
foreach (var ct in containers)
|
||||
ct.Node ??= n.Name;
|
||||
all.AddRange(containers);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip offline/inaccessible nodes
|
||||
}
|
||||
}
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
private PveContainer[] GetContainersOnNode(PveSession session, string node)
|
||||
{
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/lxc").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveContainer[]>() ?? Array.Empty<PveContainer>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single container by its ID on the specified node.
|
||||
/// </summary>
|
||||
public PveContainer GetContainer(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var containers = GetContainersOnNode(session, node);
|
||||
var ct = containers.FirstOrDefault(c => c.VmId == vmid);
|
||||
if (ct == null)
|
||||
throw new InvalidOperationException($"Container {vmid} not found on node '{node}'.");
|
||||
ct.Node ??= node;
|
||||
return ct;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the full configuration of a container.
|
||||
/// </summary>
|
||||
public PveContainerConfig GetContainerConfig(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/lxc/{vmid}/config")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveContainerConfig>() ?? new PveContainerConfig();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuration mutation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Updates one or more container configuration settings.
|
||||
/// </summary>
|
||||
public void SetContainerConfig(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
client.PutAsync($"nodes/{node}/lxc/{vmid}/config", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new container. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask CreateContainer(
|
||||
PveSession session,
|
||||
string node,
|
||||
Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
var response = client.PostAsync($"nodes/{node}/lxc", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>Starts a container. Returns the task UPID.</summary>
|
||||
public PveTask StartContainer(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "start");
|
||||
|
||||
/// <summary>Stops a container (hard stop). Returns the task UPID.</summary>
|
||||
public PveTask StopContainer(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "stop");
|
||||
|
||||
/// <summary>Gracefully shuts down a container. Returns the task UPID.</summary>
|
||||
public PveTask ShutdownContainer(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
int? timeoutSeconds = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var formData = new Dictionary<string, string>();
|
||||
if (timeoutSeconds.HasValue)
|
||||
formData["timeout"] = timeoutSeconds.Value.ToString();
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/status/shutdown", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>Removes a container. Returns the task UPID.</summary>
|
||||
public PveTask RemoveContainer(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
bool purge = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.DeleteAsync($"nodes/{node}/lxc/{vmid}{purgeParam}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>Clones a container. Returns the task UPID.</summary>
|
||||
public PveTask CloneContainer(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
int newid,
|
||||
string? hostname = null,
|
||||
string? targetNode = null,
|
||||
bool full = true)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["newid"] = newid.ToString(),
|
||||
["full"] = full ? "1" : "0"
|
||||
};
|
||||
if (!string.IsNullOrEmpty(hostname)) formData["hostname"] = hostname!;
|
||||
if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!;
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/clone", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>Migrates a container to another node. Returns the task UPID.</summary>
|
||||
public PveTask MigrateContainer(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
string targetNode,
|
||||
bool online = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(targetNode)) throw new ArgumentNullException(nameof(targetNode));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["target"] = targetNode,
|
||||
["online"] = online ? "1" : "0"
|
||||
};
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/migrate", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private PveTask PostStatus(PveSession session, string node, int vmid, string action)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/status/{action}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Exceptions;
|
||||
using PSProxmoxVE.Core.Models.Network;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE node network and SDN (Software-Defined Networking) API operations.
|
||||
/// SDN methods require PVE 8.0 or later and will throw <see cref="PveVersionException"/> otherwise.
|
||||
/// </summary>
|
||||
public class NetworkService
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Node network interfaces
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns network interface configurations for a node.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// Optional interface type filter (e.g. "bridge", "bond", "eth", "vlan", "alias").
|
||||
/// </param>
|
||||
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!)}";
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveNetwork[]>() ?? Array.Empty<PveNetwork>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a network interface on a node. Changes are pending until
|
||||
/// <see cref="ApplyNetworkConfig"/> is called. Returns the new interface config.
|
||||
/// </summary>
|
||||
public PveNetwork CreateNetwork(
|
||||
PveSession session,
|
||||
string node,
|
||||
Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
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<PveNetwork>() ?? new PveNetwork();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing network interface on a node. Changes are pending until
|
||||
/// <see cref="ApplyNetworkConfig"/> is called.
|
||||
/// </summary>
|
||||
public void SetNetwork(
|
||||
PveSession session,
|
||||
string node,
|
||||
string iface,
|
||||
Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
client.PutAsync($"nodes/{node}/network/{iface}", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a network interface from a node. Changes are pending until
|
||||
/// <see cref="ApplyNetworkConfig"/> is called.
|
||||
/// </summary>
|
||||
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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.DeleteAsync($"nodes/{node}/network/{iface}").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies pending network configuration changes on a node. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask ApplyNetworkConfig(PveSession session, string node)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PutAsync($"nodes/{node}/network")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SDN — requires PVE 8.0+
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns all SDN zone definitions. Requires PVE 8.0+.
|
||||
/// </summary>
|
||||
public PveSdnZone[] GetSdnZones(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
RequireSdn(session);
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("cluster/sdn/zones").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveSdnZone[]>() ?? Array.Empty<PveSdnZone>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all SDN VNet definitions. Requires PVE 8.0+.
|
||||
/// </summary>
|
||||
public PveSdnVnet[] GetSdnVnets(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
RequireSdn(session);
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("cluster/sdn/vnets").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveSdnVnet[]>() ?? Array.Empty<PveSdnVnet>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an SDN zone. Requires PVE 8.0+.
|
||||
/// </summary>
|
||||
public PveSdnZone CreateSdnZone(
|
||||
PveSession session,
|
||||
Dictionary<string, object> config)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
RequireSdn(session);
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
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<PveSdnZone>() ?? new PveSdnZone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an SDN zone. Requires PVE 8.0+.
|
||||
/// </summary>
|
||||
public void RemoveSdnZone(PveSession session, string zone)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
RequireSdn(session);
|
||||
if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.DeleteAsync($"cluster/sdn/zones/{zone}").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an SDN VNet. Requires PVE 8.0+.
|
||||
/// </summary>
|
||||
public PveSdnVnet CreateSdnVnet(
|
||||
PveSession session,
|
||||
Dictionary<string, object> config)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
RequireSdn(session);
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
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<PveSdnVnet>() ?? new PveSdnVnet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an SDN VNet. Requires PVE 8.0+.
|
||||
/// </summary>
|
||||
public void RemoveSdnVnet(PveSession session, string vnet)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
RequireSdn(session);
|
||||
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.DeleteAsync($"cluster/sdn/vnets/{vnet}").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Throws <see cref="PveVersionException"/> if the session's server version is below 8.0.
|
||||
/// </summary>
|
||||
private static void RequireSdn(PveSession session)
|
||||
{
|
||||
if (session.ServerVersion != null && !session.ServerVersion.IsAtLeast(8, 0))
|
||||
throw new PveVersionException(8, 0, session.ServerVersion);
|
||||
}
|
||||
|
||||
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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Nodes;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for node-level and cluster-version Proxmox VE API operations.
|
||||
/// </summary>
|
||||
public class NodeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all cluster nodes.
|
||||
/// </summary>
|
||||
public PveNode[] GetNodes(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("nodes").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveNode[]>() ?? Array.Empty<PveNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns detailed status for a specific node.
|
||||
/// </summary>
|
||||
public PveNodeStatus GetNodeStatus(PveSession session, string node)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/status").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveNodeStatus>() ?? new PveNodeStatus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Proxmox VE version running on the server.
|
||||
/// </summary>
|
||||
public PveVersion GetVersion(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("version").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
var versionStr = data?["version"]?.ToString();
|
||||
if (string.IsNullOrEmpty(versionStr))
|
||||
throw new InvalidOperationException("Failed to retrieve PVE version from API response.");
|
||||
return PveVersion.Parse(versionStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE VM snapshot API operations.
|
||||
/// All operations apply to QEMU/KVM VMs via the /nodes/{node}/qemu/{vmid}/snapshot endpoints.
|
||||
/// </summary>
|
||||
public class SnapshotService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all snapshots for a VM.
|
||||
/// </summary>
|
||||
public PveSnapshot[] GetSnapshots(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/snapshot")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a snapshot of a VM. Returns the task UPID.
|
||||
/// </summary>
|
||||
/// <param name="snapname">Snapshot name (alphanumeric, no spaces).</param>
|
||||
/// <param name="description">Optional description.</param>
|
||||
/// <param name="vmstate">Whether to save VM RAM state (live snapshot). Default false.</param>
|
||||
public PveTask CreateSnapshot(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
string snapname,
|
||||
string? description = null,
|
||||
bool vmstate = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["snapname"] = snapname,
|
||||
["vmstate"] = vmstate ? "1" : "0"
|
||||
};
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
formData["description"] = description!;
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/snapshot", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a snapshot from a VM. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask RemoveSnapshot(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
string snapname)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.DeleteAsync($"nodes/{node}/qemu/{vmid}/snapshot/{snapname}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls a VM back to a snapshot. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask RollbackSnapshot(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
string snapname)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/snapshot/{snapname}/rollback")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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.Storage;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE storage API operations.
|
||||
/// </summary>
|
||||
public class StorageService
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Read operations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns storage definitions. If <paramref name="node"/> is null, returns
|
||||
/// the cluster-wide storage list; otherwise returns storage visible on that node.
|
||||
/// </summary>
|
||||
public PveStorage[] GetStorages(PveSession session, string? node = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
var resource = node != null
|
||||
? $"nodes/{node}/storage"
|
||||
: "storage";
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveStorage[]>() ?? Array.Empty<PveStorage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the contents of a storage volume on a specific node.
|
||||
/// </summary>
|
||||
/// <param name="contentType">
|
||||
/// Optional content type filter (e.g. "iso", "vztmpl", "images", "backup").
|
||||
/// </param>
|
||||
public PveStorageContent[] GetStorageContent(
|
||||
PveSession session,
|
||||
string node,
|
||||
string storage,
|
||||
string? contentType = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||
|
||||
var resource = $"nodes/{node}/storage/{storage}/content";
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
resource += $"?content={Uri.EscapeDataString(contentType!)}";
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveStorageContent[]>() ?? Array.Empty<PveStorageContent>();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Upload / download
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Uploads an ISO (or other file) to a storage on a node. Returns the task UPID.
|
||||
/// </summary>
|
||||
/// <param name="checksum">Optional checksum value.</param>
|
||||
/// <param name="checksumAlgorithm">Optional checksum algorithm (e.g. "sha256").</param>
|
||||
/// <param name="progressCallback">Optional callback with (bytesSent, totalBytes).</param>
|
||||
public PveTask UploadIso(
|
||||
PveSession session,
|
||||
string node,
|
||||
string storage,
|
||||
string filePath,
|
||||
string? checksum = null,
|
||||
string? checksumAlgorithm = null,
|
||||
Action<long, long>? progressCallback = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||
if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException(nameof(filePath));
|
||||
|
||||
var formFields = new Dictionary<string, string>
|
||||
{
|
||||
["content"] = "iso"
|
||||
};
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.UploadFileAsync(
|
||||
$"nodes/{node}/storage/{storage}/upload",
|
||||
filePath,
|
||||
formFields,
|
||||
checksum,
|
||||
checksumAlgorithm,
|
||||
progressCallback)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file from a URL directly to a storage on a node. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask DownloadUrl(
|
||||
PveSession session,
|
||||
string node,
|
||||
string storage,
|
||||
string url,
|
||||
string filename,
|
||||
string contentType)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||
if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url));
|
||||
if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException(nameof(filename));
|
||||
if (string.IsNullOrWhiteSpace(contentType)) throw new ArgumentNullException(nameof(contentType));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["url"] = url,
|
||||
["filename"] = filename,
|
||||
["content"] = contentType
|
||||
};
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/storage/{storage}/download-url", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Storage CRUD
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new storage definition at the cluster level. Returns the task UPID or
|
||||
/// null if the API returns no task (some storage types apply immediately).
|
||||
/// </summary>
|
||||
public PveStorage CreateStorage(PveSession session, Dictionary<string, object> config)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
var response = client.PostAsync("storage", formData).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveStorage>() ?? new PveStorage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a cluster-level storage definition.
|
||||
/// </summary>
|
||||
public void RemoveStorage(PveSession session, string storage)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.DeleteAsync($"storage/{storage}").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Exceptions;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for querying and waiting on Proxmox VE asynchronous tasks (UPIDs).
|
||||
/// </summary>
|
||||
public class TaskService
|
||||
{
|
||||
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(10);
|
||||
private static readonly TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(2);
|
||||
private static readonly TimeSpan MinPollInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current status of a task identified by its UPID.
|
||||
/// </summary>
|
||||
public PveTask GetTask(PveSession session, string node, string upid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var encodedUpid = Uri.EscapeDataString(upid);
|
||||
var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/status")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
var task = data?.ToObject<PveTask>() ?? new PveTask { Upid = upid };
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all log lines for a task identified by its UPID.
|
||||
/// </summary>
|
||||
public PveTaskLog[] GetTaskLog(PveSession session, string node, string upid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var encodedUpid = Uri.EscapeDataString(upid);
|
||||
var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/log")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveTaskLog[]>() ?? Array.Empty<PveTaskLog>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls the task status until it completes, throws on timeout or failure.
|
||||
/// </summary>
|
||||
/// <param name="session">Active PVE session.</param>
|
||||
/// <param name="node">Node name where the task is running.</param>
|
||||
/// <param name="upid">Task UPID.</param>
|
||||
/// <param name="timeout">Maximum time to wait. Defaults to 10 minutes.</param>
|
||||
/// <param name="pollInterval">Interval between status polls. Defaults to 2 seconds. Minimum 1 second.</param>
|
||||
/// <param name="progressCallback">Optional callback invoked on each poll with the current task.</param>
|
||||
/// <returns>The completed <see cref="PveTask"/>.</returns>
|
||||
/// <exception cref="PveTaskTimeoutException">Thrown when the task does not complete within <paramref name="timeout"/>.</exception>
|
||||
/// <exception cref="PveTaskFailedException">Thrown when the task completes with a non-OK exit status.</exception>
|
||||
public PveTask WaitForTask(
|
||||
PveSession session,
|
||||
string node,
|
||||
string upid,
|
||||
TimeSpan? timeout = null,
|
||||
TimeSpan? pollInterval = null,
|
||||
Action<PveTask>? progressCallback = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
||||
|
||||
var effectiveTimeout = timeout ?? DefaultTimeout;
|
||||
var effectivePoll = pollInterval ?? DefaultPollInterval;
|
||||
if (effectivePoll < MinPollInterval)
|
||||
effectivePoll = MinPollInterval;
|
||||
|
||||
var deadline = DateTime.UtcNow.Add(effectiveTimeout);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var task = GetTask(session, node, upid);
|
||||
progressCallback?.Invoke(task);
|
||||
|
||||
if (string.Equals(task.Status, "stopped", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.Equals(task.ExitStatus, "OK", StringComparison.OrdinalIgnoreCase))
|
||||
throw new PveTaskFailedException(upid, task.ExitStatus ?? "(no exit status)");
|
||||
return task;
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
throw new PveTaskTimeoutException(upid, effectiveTimeout);
|
||||
|
||||
Thread.Sleep(effectivePoll);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE VM template operations.
|
||||
/// Templates are VMs with the "template" flag set to 1.
|
||||
/// </summary>
|
||||
public class TemplateService
|
||||
{
|
||||
private readonly VmService _vmService = new VmService();
|
||||
|
||||
/// <summary>
|
||||
/// Returns all VM templates. If <paramref name="node"/> is null, searches all cluster nodes.
|
||||
/// </summary>
|
||||
public PveVm[] GetTemplates(PveSession session, string? node = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
var vms = _vmService.GetVms(session, node);
|
||||
return vms.Where(v => v.Template == 1).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an existing VM into a template. Returns the task UPID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The VM must be stopped and must not already be a template.
|
||||
/// Once converted, this operation cannot be reversed via the API.
|
||||
/// </remarks>
|
||||
public PveTask CreateTemplate(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/template")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a VM template (delegates to <see cref="VmService.RemoveVm"/>).
|
||||
/// </summary>
|
||||
/// <param name="purge">
|
||||
/// If true, also removes all associated backup files and jobs.
|
||||
/// </param>
|
||||
public PveTask RemoveTemplate(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
bool purge = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
return _vmService.RemoveVm(session, node, vmid, purge);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
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.Users;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE access control — users, roles, and permissions (ACLs).
|
||||
/// </summary>
|
||||
public class UserService
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Users
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Returns all users.</summary>
|
||||
public PveUser[] GetUsers(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("access/users").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveUser[]>() ?? Array.Empty<PveUser>();
|
||||
}
|
||||
|
||||
/// <summary>Returns a single user by their user ID (e.g. "admin@pam").</summary>
|
||||
public PveUser GetUser(PveSession session, string userId)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var encodedId = Uri.EscapeDataString(userId);
|
||||
var response = client.GetAsync($"access/users/{encodedId}").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
var user = data?.ToObject<PveUser>() ?? new PveUser();
|
||||
// The single-user endpoint may not echo back the userid
|
||||
if (string.IsNullOrEmpty(user.UserId))
|
||||
user.UserId = userId;
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new user account.
|
||||
/// </summary>
|
||||
/// <param name="userId">User ID in "username@realm" format.</param>
|
||||
/// <param name="config">Additional fields (password, email, firstname, lastname, etc.).</param>
|
||||
public void CreateUser(
|
||||
PveSession session,
|
||||
string userId,
|
||||
Dictionary<string, object>? config = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||
|
||||
var formData = new Dictionary<string, string> { ["userid"] = userId };
|
||||
if (config != null)
|
||||
{
|
||||
foreach (var kvp in config)
|
||||
formData[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.PostAsync("access/users", formData).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>Removes a user account.</summary>
|
||||
public void RemoveUser(PveSession session, string userId)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var encodedId = Uri.EscapeDataString(userId);
|
||||
client.DeleteAsync($"access/users/{encodedId}").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>Updates one or more properties of an existing user.</summary>
|
||||
public void SetUser(
|
||||
PveSession session,
|
||||
string userId,
|
||||
Dictionary<string, object> config)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var encodedId = Uri.EscapeDataString(userId);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
client.PutAsync($"access/users/{encodedId}", formData).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Roles
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Returns all roles.</summary>
|
||||
public PveRole[] GetRoles(PveSession session)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync("access/roles").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveRole[]>() ?? Array.Empty<PveRole>();
|
||||
}
|
||||
|
||||
/// <summary>Creates a new role.</summary>
|
||||
/// <param name="roleId">Role name.</param>
|
||||
/// <param name="privileges">Comma-separated list of privilege strings.</param>
|
||||
public void CreateRole(PveSession session, string roleId, string? privileges = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId));
|
||||
|
||||
var formData = new Dictionary<string, string> { ["roleid"] = roleId };
|
||||
if (!string.IsNullOrEmpty(privileges))
|
||||
formData["privs"] = privileges!;
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.PostAsync("access/roles", formData).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>Removes a role.</summary>
|
||||
public void RemoveRole(PveSession session, string roleId)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.DeleteAsync($"access/roles/{Uri.EscapeDataString(roleId)}")
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Permissions / ACLs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns the effective permissions (resolved privilege set) for a user or token.
|
||||
/// </summary>
|
||||
/// <param name="userId">Optional user ID to query; defaults to the authenticated user.</param>
|
||||
/// <param name="path">Optional path to restrict the query.</param>
|
||||
public PvePermission[] GetPermissions(
|
||||
PveSession session,
|
||||
string? userId = null,
|
||||
string? path = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
var resource = "access/permissions";
|
||||
var queryParts = new List<string>();
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
queryParts.Add($"userid={Uri.EscapeDataString(userId!)}");
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
queryParts.Add($"path={Uri.EscapeDataString(path!)}");
|
||||
if (queryParts.Count > 0)
|
||||
resource += "?" + string.Join("&", queryParts);
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
// /access/permissions returns an object keyed by path, not an array
|
||||
if (data == null) return Array.Empty<PvePermission>();
|
||||
if (data.Type == JTokenType.Array)
|
||||
return data.ToObject<PvePermission[]>() ?? Array.Empty<PvePermission>();
|
||||
|
||||
// Unwrap path-keyed object into flat list
|
||||
var result = new List<PvePermission>();
|
||||
foreach (var prop in ((JObject)data).Properties())
|
||||
{
|
||||
var perm = new PvePermission { Path = prop.Name };
|
||||
result.Add(perm);
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets (adds or updates) an ACL entry.
|
||||
/// </summary>
|
||||
/// <param name="path">Access control path (e.g. "/", "/nodes/pve", "/vms/100").</param>
|
||||
/// <param name="roles">Comma-separated role IDs.</param>
|
||||
/// <param name="users">Comma-separated user IDs.</param>
|
||||
/// <param name="groups">Comma-separated group names.</param>
|
||||
/// <param name="propagate">Whether to propagate the permission to sub-paths.</param>
|
||||
/// <param name="delete">If true, removes the specified ACL entries.</param>
|
||||
public void SetPermission(
|
||||
PveSession session,
|
||||
string path,
|
||||
string roles,
|
||||
string? users = null,
|
||||
string? groups = null,
|
||||
bool propagate = true,
|
||||
bool delete = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
|
||||
if (string.IsNullOrWhiteSpace(roles)) throw new ArgumentNullException(nameof(roles));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["path"] = path,
|
||||
["roles"] = roles,
|
||||
["propagate"] = propagate ? "1" : "0",
|
||||
["delete"] = delete ? "1" : "0"
|
||||
};
|
||||
if (!string.IsNullOrEmpty(users)) formData["users"] = users!;
|
||||
if (!string.IsNullOrEmpty(groups)) formData["groups"] = groups!;
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
client.PutAsync("access/acl", formData).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
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.Nodes;
|
||||
using PSProxmoxVE.Core.Models.Vms;
|
||||
|
||||
namespace PSProxmoxVE.Core.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for Proxmox VE QEMU/KVM virtual machine API operations.
|
||||
/// </summary>
|
||||
public class VmService
|
||||
{
|
||||
private readonly NodeService _nodeService = new NodeService();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Read operations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns VMs. If <paramref name="node"/> is null, queries every cluster node.
|
||||
/// </summary>
|
||||
public PveVm[] GetVms(PveSession session, string? node = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
|
||||
if (node != null)
|
||||
return GetVmsOnNode(session, node);
|
||||
|
||||
// Query all nodes and aggregate
|
||||
var nodes = _nodeService.GetNodes(session);
|
||||
var all = new List<PveVm>();
|
||||
foreach (var n in nodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vms = GetVmsOnNode(session, n.Name);
|
||||
// Stamp the node name in case it wasn't returned by the API
|
||||
foreach (var vm in vms)
|
||||
vm.Node ??= n.Name;
|
||||
all.AddRange(vms);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip nodes that are offline or inaccessible
|
||||
}
|
||||
}
|
||||
return all.ToArray();
|
||||
}
|
||||
|
||||
private PveVm[] GetVmsOnNode(PveSession session, string node)
|
||||
{
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/qemu").GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveVm[]>() ?? Array.Empty<PveVm>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single VM by its ID on the specified node.
|
||||
/// </summary>
|
||||
public PveVm GetVm(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var vms = GetVmsOnNode(session, node);
|
||||
var vm = vms.FirstOrDefault(v => v.VmId == vmid);
|
||||
if (vm == null)
|
||||
throw new InvalidOperationException($"VM {vmid} not found on node '{node}'.");
|
||||
vm.Node ??= node;
|
||||
return vm;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the full configuration of a VM.
|
||||
/// </summary>
|
||||
public PveVmConfig GetVmConfig(PveSession session, string node, int vmid)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/config")
|
||||
.GetAwaiter().GetResult();
|
||||
var data = JObject.Parse(response)["data"];
|
||||
return data?.ToObject<PveVmConfig>() ?? new PveVmConfig();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuration mutation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Updates one or more VM configuration settings. Changes are applied immediately (POST).
|
||||
/// </summary>
|
||||
public void SetVmConfig(PveSession session, string node, int vmid, Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
client.PutAsync($"nodes/{node}/qemu/{vmid}/config", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new VM. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask CreateVm(PveSession session, string node, Dictionary<string, object> 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));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var formData = config.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>Starts a VM. Returns the task UPID.</summary>
|
||||
public PveTask StartVm(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "start");
|
||||
|
||||
/// <summary>Stops a VM (hard power-off). Returns the task UPID.</summary>
|
||||
public PveTask StopVm(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "stop");
|
||||
|
||||
/// <summary>Gracefully shuts down a VM. Returns the task UPID.</summary>
|
||||
public PveTask ShutdownVm(PveSession session, string node, int vmid, int? timeoutSeconds = null)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var formData = new Dictionary<string, string>();
|
||||
if (timeoutSeconds.HasValue)
|
||||
formData["timeout"] = timeoutSeconds.Value.ToString();
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/status/shutdown", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>Resets a VM (hard reset). Returns the task UPID.</summary>
|
||||
public PveTask ResetVm(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "reset");
|
||||
|
||||
/// <summary>Suspends a VM (writes RAM state to disk). Returns the task UPID.</summary>
|
||||
public PveTask SuspendVm(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "suspend");
|
||||
|
||||
/// <summary>Resumes a suspended VM. Returns the task UPID.</summary>
|
||||
public PveTask ResumeVm(PveSession session, string node, int vmid)
|
||||
=> PostStatus(session, node, vmid, "resume");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Removal / migration / clone
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Removes a VM. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask RemoveVm(PveSession session, string node, int vmid, bool purge = false)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.DeleteAsync($"nodes/{node}/qemu/{vmid}{purgeParam}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones a VM. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask CloneVm(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
int newid,
|
||||
string? name = null,
|
||||
string? targetNode = null,
|
||||
bool full = true)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["newid"] = newid.ToString(),
|
||||
["full"] = full ? "1" : "0"
|
||||
};
|
||||
if (!string.IsNullOrEmpty(name)) formData["name"] = name!;
|
||||
if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!;
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/clone", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrates a VM to another node. Returns the task UPID.
|
||||
/// </summary>
|
||||
public PveTask MigrateVm(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
string targetNode,
|
||||
bool online = true)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(targetNode)) throw new ArgumentNullException(nameof(targetNode));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["target"] = targetNode,
|
||||
["online"] = online ? "1" : "0"
|
||||
};
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/migrate", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes a disk attached to a VM. Returns the task UPID.
|
||||
/// </summary>
|
||||
/// <param name="disk">Disk identifier, e.g. "scsi0" or "virtio0".</param>
|
||||
/// <param name="size">
|
||||
/// New absolute size (e.g. "32G") or relative increase with "+" prefix (e.g. "+10G").
|
||||
/// </param>
|
||||
public PveTask ResizeDisk(
|
||||
PveSession session,
|
||||
string node,
|
||||
int vmid,
|
||||
string disk,
|
||||
string size)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
if (string.IsNullOrWhiteSpace(disk)) throw new ArgumentNullException(nameof(disk));
|
||||
if (string.IsNullOrWhiteSpace(size)) throw new ArgumentNullException(nameof(size));
|
||||
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["disk"] = disk,
|
||||
["size"] = size
|
||||
};
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PutAsync($"nodes/{node}/qemu/{vmid}/resize", formData)
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private PveTask PostStatus(PveSession session, string node, int vmid, string action)
|
||||
{
|
||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||
|
||||
using var client = new PveHttpClient(session);
|
||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/status/{action}")
|
||||
.GetAwaiter().GetResult();
|
||||
return ParseTask(response, node);
|
||||
}
|
||||
|
||||
private static PveTask ParseTask(string response, string node)
|
||||
{
|
||||
var data = JObject.Parse(response)["data"];
|
||||
// Many endpoints return the UPID string directly as the data value
|
||||
if (data?.Type == JTokenType.String)
|
||||
return new PveTask { Upid = data.ToString(), Node = node };
|
||||
|
||||
var task = data?.ToObject<PveTask>() ?? new PveTask();
|
||||
task.Node = node;
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user