feat: add container snapshot and SDN subnet cmdlets

Container snapshots (4 cmdlets):
- Get-PveContainerSnapshot, New-PveContainerSnapshot
- Remove-PveContainerSnapshot, Restore-PveContainerSnapshot
- Service methods in ContainerService
- Pester unit tests

SDN subnet management (3 cmdlets):
- Get-PveSdnSubnet, New-PveSdnSubnet, Remove-PveSdnSubnet
- PveSdnSubnet model and NetworkService methods
- Pester unit tests

Also updates manifest (73 cmdlets), README cmdlet reference, and CHANGELOG.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-20 12:21:40 -05:00
parent 0bf275ea9f
commit ffec75b461
11 changed files with 932 additions and 9 deletions
@@ -113,6 +113,90 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Snapshots
// -------------------------------------------------------------------------
/// <summary>
/// Returns all snapshots for a container.
/// </summary>
public PveSnapshot[] GetContainerSnapshots(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}/snapshot")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
}
/// <summary>
/// Creates a snapshot of a container. Returns the task UPID.
/// </summary>
public PveTask CreateContainerSnapshot(
PveSession session,
string node,
int vmid,
string snapname,
string? description = null)
{
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
};
if (!string.IsNullOrEmpty(description))
formData["description"] = description!;
using var client = new PveHttpClient(session);
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/snapshot", formData)
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
/// <summary>
/// Removes a snapshot from a container. Returns the task UPID.
/// </summary>
public PveTask RemoveContainerSnapshot(
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}/lxc/{vmid}/snapshot/{snapname}")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
/// <summary>
/// Rolls a container back to a snapshot. Returns the task UPID.
/// </summary>
public PveTask RollbackContainerSnapshot(
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}/lxc/{vmid}/snapshot/{snapname}/rollback")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
@@ -239,6 +239,71 @@ namespace PSProxmoxVE.Core.Services
client.DeleteAsync($"cluster/sdn/vnets/{vnet}").GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// SDN Subnets — requires PVE 8.0+
// -------------------------------------------------------------------------
/// <summary>
/// Returns all subnets for an SDN VNet. Requires PVE 8.0+.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="vnet">The SDN VNet identifier.</param>
public PveSdnSubnet[] GetSdnSubnets(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);
var response = client.GetAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveSdnSubnet[]>() ?? Array.Empty<PveSdnSubnet>();
}
/// <summary>
/// Creates an SDN subnet on a VNet. Requires PVE 8.0+.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="vnet">The SDN VNet identifier.</param>
/// <param name="config">Subnet configuration parameters including "subnet" (CIDR).</param>
public void CreateSdnSubnet(
PveSession session,
string vnet,
Dictionary<string, object> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
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.PostAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets", formData)
.GetAwaiter().GetResult();
}
/// <summary>
/// Removes an SDN subnet from a VNet. Requires PVE 8.0+.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="vnet">The SDN VNet identifier.</param>
/// <param name="subnet">The subnet CIDR to remove.</param>
public void RemoveSdnSubnet(PveSession session, string vnet, string subnet)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet));
using var client = new PveHttpClient(session);
client.DeleteAsync(
$"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------