feat: add firewall, backup, and SDN IPAM/DNS/controller cmdlets

Add 35 new cmdlets bringing the total to 118:
- Firewall (21): rules, groups, aliases, IP sets, options at
  cluster/node/VM/container levels
- Backup (5): ad-hoc vzdump and scheduled backup job CRUD
- SDN IPAM/DNS/Controller (9): plugin management for SDN subsystem

Also includes:
- Fix: Remove-PveRole now has ConfirmImpact.High
- Fix: URL-encode snapshot names in API paths
- Refactor: extract auth header strings to constants in PveHttpClient
- Add PSGallery version badge to README
- Full test coverage: 11 JSON fixtures, xUnit model tests, Pester
  unit tests, and integration tests for firewall/backup/OVA import
- Updated manifest, format file, CHANGELOG, README, API coverage docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-21 07:51:49 -05:00
parent 8d5f33f592
commit a72642d203
81 changed files with 7636 additions and 564 deletions
+16
View File
@@ -7,6 +7,22 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
## [Unreleased]
### Added
- Firewall management cmdlets (21): rules, security groups, aliases, IP sets, options at cluster/node/VM/container levels
- Backup/vzdump cmdlets (5): ad-hoc backup creation and scheduled backup job CRUD
- SDN IPAM cmdlets (3): Get/New/Remove-PveSdnIpam for IPAM plugin management
- SDN DNS cmdlets (3): Get/New/Remove-PveSdnDns for DNS plugin management
- SDN Controller cmdlets (3): Get/New/Remove-PveSdnController for controller management
- PSGallery version badge in README
- Integration tests for firewall rules, aliases, IP sets, backup jobs, and OVA import
### Fixed
- `Remove-PveRole` now has `ConfirmImpact.High` for safety
- URL-encode snapshot names in API paths for defense-in-depth
- Extract auth header magic strings to named constants in PveHttpClient
## [0.1.0-preview] - 2026-03-19
### Added
+48
View File
@@ -3,6 +3,7 @@
[![Build](https://github.com/goodolclint/PSProxmoxVE/actions/workflows/build.yml/badge.svg)](https://github.com/goodolclint/PSProxmoxVE/actions/workflows/build.yml)
[![Unit Tests](https://github.com/goodolclint/PSProxmoxVE/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/goodolclint/PSProxmoxVE/actions/workflows/unit-tests.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/goodolclint/PSProxmoxVE/blob/main/LICENSE)
[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/PSProxmoxVE)](https://www.powershellgallery.com/packages/PSProxmoxVE)
A production-grade C# binary PowerShell module for managing Proxmox VE environments.
@@ -325,6 +326,53 @@ SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4
| `Get-PveTask` | Get task status |
| `Wait-PveTask` | Wait for a task to complete |
### Firewall
| Cmdlet | Description |
|---|---|
| `Get-PveFirewallRule` | List firewall rules (cluster/node/VM/container) |
| `New-PveFirewallRule` | Create a firewall rule |
| `Set-PveFirewallRule` | Update a firewall rule |
| `Remove-PveFirewallRule` | Delete a firewall rule |
| `Get-PveFirewallGroup` | List security groups or group rules |
| `New-PveFirewallGroup` | Create a security group |
| `Remove-PveFirewallGroup` | Delete a security group |
| `Get-PveFirewallAlias` | List firewall IP aliases |
| `New-PveFirewallAlias` | Create a firewall IP alias |
| `Set-PveFirewallAlias` | Update a firewall IP alias |
| `Remove-PveFirewallAlias` | Delete a firewall IP alias |
| `Get-PveFirewallIpSet` | List firewall IP sets |
| `New-PveFirewallIpSet` | Create a firewall IP set |
| `Remove-PveFirewallIpSet` | Delete a firewall IP set |
| `Get-PveFirewallIpSetEntry` | List entries in an IP set |
| `New-PveFirewallIpSetEntry` | Add an entry to an IP set |
| `Set-PveFirewallIpSetEntry` | Update an IP set entry |
| `Remove-PveFirewallIpSetEntry` | Remove an entry from an IP set |
| `Get-PveFirewallOptions` | Get firewall options |
| `Set-PveFirewallOptions` | Set firewall options |
| `Get-PveFirewallRef` | List firewall references (aliases, IP sets) |
### Backup
| Cmdlet | Description |
|---|---|
| `New-PveBackup` | Create an ad-hoc backup (vzdump) |
| `Get-PveBackupJob` | List scheduled backup jobs |
| `New-PveBackupJob` | Create a scheduled backup job |
| `Set-PveBackupJob` | Update a scheduled backup job |
| `Remove-PveBackupJob` | Delete a scheduled backup job |
### SDN — IPAM / DNS / Controllers (PVE 8.0+)
| Cmdlet | Description |
|---|---|
| `Get-PveSdnIpam` | List SDN IPAM plugins |
| `New-PveSdnIpam` | Create an SDN IPAM plugin |
| `Remove-PveSdnIpam` | Remove an SDN IPAM plugin |
| `Get-PveSdnDns` | List SDN DNS plugins |
| `New-PveSdnDns` | Create an SDN DNS plugin |
| `Remove-PveSdnDns` | Remove an SDN DNS plugin |
| `Get-PveSdnController` | List SDN controllers |
| `New-PveSdnController` | Create an SDN controller |
| `Remove-PveSdnController` | Remove an SDN controller |
## Known Limitations (v1)
- **No automatic retries**: Failed API calls are not retried. Implement your own retry logic if needed.
+583 -543
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -2,9 +2,9 @@
This document tracks which PVE API areas are implemented in PSProxmoxVE and which are planned for future releases.
**Last updated:** 2026-03-20
**Last updated:** 2026-03-21
**Module version:** 0.1.0-preview
**Total cmdlets:** 75
**Total cmdlets:** 110
## Implemented
@@ -28,17 +28,18 @@ This document tracks which PVE API areas are implemented in PSProxmoxVE and whic
| **Templates** | 4 | VM template conversion, listing, cloning, removal |
| **Cloud-Init** | 3 | `/nodes/{node}/qemu/{vmid}/config` (cloud-init fields), `/nodes/{node}/qemu/{vmid}/cloudinit/regenerate` |
| **Tasks** | 2 | `/nodes/{node}/tasks/{upid}/status` (get, wait) |
| **Firewall** | 21 | `/cluster/firewall/*`, `/nodes/{node}/firewall/*`, `/nodes/{node}/qemu/{vmid}/firewall/*`, `/nodes/{node}/lxc/{vmid}/firewall/*` (rules, groups, aliases, IP sets, options, refs) |
| **Backup** | 5 | `/nodes/{node}/vzdump`, `/cluster/backup/*` (ad-hoc backup, scheduled job CRUD) |
| **SDN IPAM** | 3 | `/cluster/sdn/ipams/*` (CRUD) |
| **SDN DNS** | 3 | `/cluster/sdn/dns/*` (CRUD) |
| **SDN Controllers** | 3 | `/cluster/sdn/controllers/*` (CRUD) |
## Not Yet Implemented
### High-Value Gaps
### Gaps
These are the most impactful areas for real-world automation that are not yet covered.
| Area | Key Endpoints | Use Case | Priority |
|------|--------------|----------|----------|
| **Firewall** | `/cluster/firewall/*`, `/nodes/{node}/firewall/*`, `/nodes/{node}/qemu/{vmid}/firewall/*` | Security automation, rule management, IP sets, aliases | High |
| **Backup / vzdump** | `/nodes/{node}/vzdump`, `/cluster/backup/*` | Disaster recovery, scheduled backups, backup job management | High |
| Area | Key Endpoints | Notes | Priority |
|------|--------------|-------|----------|
| **Pool Management** | `/pools/*` | Multi-tenant environments, resource grouping | Medium |
### Lower Priority Gaps
@@ -56,9 +57,6 @@ These are the most impactful areas for real-world automation that are not yet co
| **Node Management** | `/nodes/{node}/apt/*`, `/nodes/{node}/disks/*`, `/nodes/{node}/services/*` | Package updates, disk management, service control |
| **Metrics** | `/cluster/metrics/*` | External metrics server configuration |
| **Additional VM Agent** | `/nodes/{node}/qemu/{vmid}/agent/*` | File read/write, OS info, suspend/resume via agent |
| **SDN IPAM** | `/cluster/sdn/ipams/*` | IP address management for SDN |
| **SDN DNS** | `/cluster/sdn/dns/*` | DNS integration for SDN |
| **SDN Controllers** | `/cluster/sdn/controllers/*` | SDN controller configuration |
## Contributing New Cmdlets
+7 -3
View File
@@ -31,6 +31,10 @@ namespace PSProxmoxVE.Core.Client
private readonly HttpClient _httpClient;
private bool _disposed;
private const string ApiTokenPrefix = "PVEAPIToken=";
private const string AuthCookieName = "PVEAuthCookie=";
private const string CsrfHeaderName = "CSRFPreventionToken";
/// <summary>
/// Creates an HTTP client authenticated with the specified PVE session.
/// </summary>
@@ -313,14 +317,14 @@ namespace PSProxmoxVE.Core.Client
if (_session.AuthMode == PveAuthMode.ApiToken)
{
request.Headers.TryAddWithoutValidation("Authorization", $"PVEAPIToken={_session.ApiToken}");
request.Headers.TryAddWithoutValidation("Authorization", $"{ApiTokenPrefix}{_session.ApiToken}");
}
else
{
// Ticket auth
request.Headers.Add("Cookie", $"PVEAuthCookie={_session.Ticket}");
request.Headers.Add("Cookie", $"{AuthCookieName}{_session.Ticket}");
if (mutating && !string.IsNullOrEmpty(_session.CsrfToken))
request.Headers.Add("CSRFPreventionToken", _session.CsrfToken);
request.Headers.Add(CsrfHeaderName, _session.CsrfToken);
}
return request;
@@ -0,0 +1,138 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Backup;
/// <summary>
/// Represents a Proxmox VE scheduled backup job as returned by
/// the /cluster/backup endpoint.
/// </summary>
public class PveBackupJob
{
/// <summary>
/// The backup job identifier.
/// </summary>
[JsonPropertyName("id")]
[JsonProperty("id")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// The job type (typically "vzdump").
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string? Type { get; set; }
/// <summary>
/// Whether the job is enabled (1) or disabled (0).
/// </summary>
[JsonPropertyName("enabled")]
[JsonProperty("enabled")]
public int? Enabled { get; set; }
/// <summary>
/// The cron-style schedule (e.g. "0 2 * * *").
/// </summary>
[JsonPropertyName("schedule")]
[JsonProperty("schedule")]
public string? Schedule { get; set; }
/// <summary>
/// The target storage for backup files.
/// </summary>
[JsonPropertyName("storage")]
[JsonProperty("storage")]
public string? Storage { get; set; }
/// <summary>
/// Backup mode: "snapshot", "suspend", or "stop".
/// </summary>
[JsonPropertyName("mode")]
[JsonProperty("mode")]
public string? Mode { get; set; }
/// <summary>
/// Comma-separated list of VM/container IDs to back up, or empty for all.
/// </summary>
[JsonPropertyName("vmid")]
[JsonProperty("vmid")]
public string? VmId { get; set; }
/// <summary>
/// Whether to back up all VMs/containers (1 = all).
/// </summary>
[JsonPropertyName("all")]
[JsonProperty("all")]
public int? All { get; set; }
/// <summary>
/// Compression algorithm (e.g. "zstd", "lzo", "gzip").
/// </summary>
[JsonPropertyName("compress")]
[JsonProperty("compress")]
public string? Compress { get; set; }
/// <summary>
/// Deprecated — maximum number of backup files to keep. Use prune-backups instead.
/// </summary>
[JsonPropertyName("maxfiles")]
[JsonProperty("maxfiles")]
public int? MaxFiles { get; set; }
/// <summary>
/// Retention policy string (e.g. "keep-daily=7,keep-weekly=4").
/// </summary>
[JsonPropertyName("prune-backups")]
[JsonProperty("prune-backups")]
public string? PruneBackups { get; set; }
/// <summary>
/// Template for backup notes.
/// </summary>
[JsonPropertyName("notes-template")]
[JsonProperty("notes-template")]
public string? NotesTemplate { get; set; }
/// <summary>
/// Optional comment describing the backup job.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>
/// Mail notification setting (e.g. "always", "failure").
/// </summary>
[JsonPropertyName("mailnotification")]
[JsonProperty("mailnotification")]
public string? MailNotification { get; set; }
/// <summary>
/// Email address to send notifications to.
/// </summary>
[JsonPropertyName("mailto")]
[JsonProperty("mailto")]
public string? MailTo { get; set; }
/// <summary>
/// Specific node to run the backup on (empty = any node).
/// </summary>
[JsonPropertyName("node")]
[JsonProperty("node")]
public string? Node { get; set; }
/// <summary>
/// Comma-separated list of excluded VM/container IDs.
/// </summary>
[JsonPropertyName("exclude")]
[JsonProperty("exclude")]
public string? Exclude { get; set; }
/// <inheritdoc />
public override string ToString()
{
var state = Enabled == 1 ? "Enabled" : "Disabled";
return $"Backup Job: {Id} | {state} | Schedule: {Schedule ?? "N/A"} | "
+ $"Storage: {Storage ?? "N/A"} | Mode: {Mode ?? "N/A"}";
}
}
@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents a Proxmox VE firewall IP alias as returned by
/// the firewall/aliases endpoints.
/// </summary>
public class PveFirewallAlias
{
/// <summary>
/// The alias name.
/// </summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// The CIDR network or IP address (e.g. "10.0.0.0/8" or "192.168.1.1").
/// </summary>
[JsonPropertyName("cidr")]
[JsonProperty("cidr")]
public string Cidr { get; set; } = string.Empty;
/// <summary>
/// Optional comment describing the alias.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"Alias: {Name} → {Cidr}";
}
}
@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents a Proxmox VE firewall security group as returned by
/// the /cluster/firewall/groups endpoint.
/// </summary>
public class PveFirewallGroup
{
/// <summary>
/// The security group name.
/// </summary>
[JsonPropertyName("group")]
[JsonProperty("group")]
public string Group { get; set; } = string.Empty;
/// <summary>
/// Optional comment describing the group.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>
/// Configuration digest for detecting concurrent modifications.
/// </summary>
[JsonPropertyName("digest")]
[JsonProperty("digest")]
public string? Digest { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"Firewall Group: {Group} | Comment: {Comment ?? "N/A"}";
}
}
@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents a Proxmox VE firewall IP set as returned by
/// the firewall/ipset endpoints.
/// </summary>
public class PveFirewallIpSet
{
/// <summary>
/// The IP set name.
/// </summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Optional comment describing the IP set.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>
/// Configuration digest for detecting concurrent modifications.
/// </summary>
[JsonPropertyName("digest")]
[JsonProperty("digest")]
public string? Digest { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"IP Set: {Name} | Comment: {Comment ?? "N/A"}";
}
}
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents an entry within a Proxmox VE firewall IP set as returned by
/// the firewall/ipset/{name} endpoints.
/// </summary>
public class PveFirewallIpSetEntry
{
/// <summary>
/// The CIDR network or IP address (e.g. "192.168.1.0/24" or "10.0.0.1").
/// </summary>
[JsonPropertyName("cidr")]
[JsonProperty("cidr")]
public string Cidr { get; set; } = string.Empty;
/// <summary>
/// Whether this is an exclusion entry (1 = exclude from set).
/// </summary>
[JsonPropertyName("nomatch")]
[JsonProperty("nomatch")]
public int? NoMatch { get; set; }
/// <summary>
/// Optional comment describing the entry.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <inheritdoc />
public override string ToString()
{
var prefix = NoMatch == 1 ? "!" : "";
return $"{prefix}{Cidr}";
}
}
@@ -0,0 +1,88 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents Proxmox VE firewall options as returned by
/// the firewall/options endpoints at cluster, node, VM, or container level.
/// </summary>
public class PveFirewallOptions
{
/// <summary>
/// Whether the firewall is enabled (1) or disabled (0).
/// </summary>
[JsonPropertyName("enable")]
[JsonProperty("enable")]
public int? Enable { get; set; }
/// <summary>
/// Default policy for incoming traffic (ACCEPT, DROP, REJECT).
/// </summary>
[JsonPropertyName("policy_in")]
[JsonProperty("policy_in")]
public string? PolicyIn { get; set; }
/// <summary>
/// Default policy for outgoing traffic (ACCEPT, DROP, REJECT).
/// </summary>
[JsonPropertyName("policy_out")]
[JsonProperty("policy_out")]
public string? PolicyOut { get; set; }
/// <summary>
/// Log level for incoming traffic (nolog, emerg, alert, crit, err, warning, notice, info, debug).
/// </summary>
[JsonPropertyName("log_level_in")]
[JsonProperty("log_level_in")]
public string? LogLevelIn { get; set; }
/// <summary>
/// Log level for outgoing traffic.
/// </summary>
[JsonPropertyName("log_level_out")]
[JsonProperty("log_level_out")]
public string? LogLevelOut { get; set; }
/// <summary>
/// Whether to allow DHCP traffic (VM/container level).
/// </summary>
[JsonPropertyName("dhcp")]
[JsonProperty("dhcp")]
public int? Dhcp { get; set; }
/// <summary>
/// Whether to allow NDP (IPv6 Neighbor Discovery Protocol).
/// </summary>
[JsonPropertyName("ndp")]
[JsonProperty("ndp")]
public int? Ndp { get; set; }
/// <summary>
/// Whether to allow Router Advertisement.
/// </summary>
[JsonPropertyName("radv")]
[JsonProperty("radv")]
public int? Radv { get; set; }
/// <summary>
/// Whether to enable MAC address filter (VM/container level).
/// </summary>
[JsonPropertyName("macfilter")]
[JsonProperty("macfilter")]
public int? MacFilter { get; set; }
/// <summary>
/// Whether to enable IP filter (VM/container level).
/// </summary>
[JsonPropertyName("ipfilter")]
[JsonProperty("ipfilter")]
public int? IpFilter { get; set; }
/// <inheritdoc />
public override string ToString()
{
var state = Enable == 1 ? "Enabled" : "Disabled";
return $"Firewall: {state} | In: {PolicyIn ?? "N/A"} | Out: {PolicyOut ?? "N/A"}";
}
}
@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents a Proxmox VE firewall reference item (alias, ipset, etc.)
/// as returned by the firewall/refs endpoints.
/// </summary>
public class PveFirewallRef
{
/// <summary>
/// The reference type (e.g. "alias", "ipset").
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// The reference name.
/// </summary>
[JsonPropertyName("name")]
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Optional comment.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"{Type}: {Name}";
}
}
@@ -0,0 +1,111 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Firewall;
/// <summary>
/// Represents a Proxmox VE firewall rule as returned by
/// the firewall/rules endpoints at cluster, node, VM, or container level.
/// </summary>
public class PveFirewallRule
{
/// <summary>
/// Rule position (used for ordering and as identifier for updates/deletes).
/// </summary>
[JsonPropertyName("pos")]
[JsonProperty("pos")]
public int? Pos { get; set; }
/// <summary>
/// Rule type: "in", "out", or "group".
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// Action to take: ACCEPT, DROP, or REJECT.
/// </summary>
[JsonPropertyName("action")]
[JsonProperty("action")]
public string Action { get; set; } = string.Empty;
/// <summary>
/// Whether the rule is enabled (1) or disabled (0).
/// </summary>
[JsonPropertyName("enable")]
[JsonProperty("enable")]
public int? Enable { get; set; }
/// <summary>
/// Source address or alias.
/// </summary>
[JsonPropertyName("source")]
[JsonProperty("source")]
public string? Source { get; set; }
/// <summary>
/// Destination address or alias.
/// </summary>
[JsonPropertyName("dest")]
[JsonProperty("dest")]
public string? Dest { get; set; }
/// <summary>
/// Protocol (e.g. tcp, udp, icmp).
/// </summary>
[JsonPropertyName("proto")]
[JsonProperty("proto")]
public string? Proto { get; set; }
/// <summary>
/// Destination port or port range.
/// </summary>
[JsonPropertyName("dport")]
[JsonProperty("dport")]
public string? Dport { get; set; }
/// <summary>
/// Source port or port range.
/// </summary>
[JsonPropertyName("sport")]
[JsonProperty("sport")]
public string? Sport { get; set; }
/// <summary>
/// Optional comment describing the rule.
/// </summary>
[JsonPropertyName("comment")]
[JsonProperty("comment")]
public string? Comment { get; set; }
/// <summary>
/// Predefined macro name (e.g. "SSH", "HTTP", "DNS").
/// </summary>
[JsonPropertyName("macro")]
[JsonProperty("macro")]
public string? Macro { get; set; }
/// <summary>
/// Log level for matched packets (e.g. "nolog", "info", "warning").
/// </summary>
[JsonPropertyName("log")]
[JsonProperty("log")]
public string? Log { get; set; }
/// <summary>
/// Network interface to match (e.g. "net0").
/// </summary>
[JsonPropertyName("iface")]
[JsonProperty("iface")]
public string? Iface { get; set; }
/// <inheritdoc />
public override string ToString()
{
var action = Macro != null ? $"{Action} ({Macro})" : Action;
return $"Rule {Pos}: {Type} {action} | "
+ $"Src: {Source ?? "any"} → Dst: {Dest ?? "any"} | "
+ $"Proto: {Proto ?? "any"} DPort: {Dport ?? "any"}";
}
}
@@ -0,0 +1,52 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Network;
/// <summary>
/// Represents a Software-Defined Networking (SDN) controller as returned by
/// the /cluster/sdn/controllers endpoint. Requires Proxmox VE 8.0+.
/// </summary>
public class PveSdnController
{
/// <summary>
/// The controller identifier.
/// </summary>
[JsonPropertyName("controller")]
[JsonProperty("controller")]
public string Controller { get; set; } = string.Empty;
/// <summary>
/// The controller type (e.g. "evpn", "bgp").
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// The Autonomous System Number for BGP/EVPN.
/// </summary>
[JsonPropertyName("asn")]
[JsonProperty("asn")]
public int? Asn { get; set; }
/// <summary>
/// Comma-separated list of BGP peer addresses.
/// </summary>
[JsonPropertyName("peers")]
[JsonProperty("peers")]
public string? Peers { get; set; }
/// <summary>
/// The node this controller is configured on.
/// </summary>
[JsonPropertyName("node")]
[JsonProperty("node")]
public string? Node { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"SDN Controller: {Controller} | Type: {Type} | ASN: {Asn?.ToString() ?? "N/A"}";
}
}
@@ -0,0 +1,59 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Network;
/// <summary>
/// Represents a Software-Defined Networking (SDN) DNS plugin as returned by
/// the /cluster/sdn/dns endpoint. Requires Proxmox VE 8.0+.
/// </summary>
public class PveSdnDns
{
/// <summary>
/// The DNS plugin identifier.
/// </summary>
[JsonPropertyName("dns")]
[JsonProperty("dns")]
public string Dns { get; set; } = string.Empty;
/// <summary>
/// The DNS plugin type (e.g. "powerdns").
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// The URL of the DNS service API.
/// </summary>
[JsonPropertyName("url")]
[JsonProperty("url")]
public string? Url { get; set; }
/// <summary>
/// The API key for the DNS service.
/// </summary>
[JsonPropertyName("key")]
[JsonProperty("key")]
public string? Key { get; set; }
/// <summary>
/// The IPv6 reverse zone mask length.
/// </summary>
[JsonPropertyName("reversemaskv6")]
[JsonProperty("reversemaskv6")]
public int? ReverseMaskV6 { get; set; }
/// <summary>
/// The TTL (time-to-live) for DNS records.
/// </summary>
[JsonPropertyName("ttl")]
[JsonProperty("ttl")]
public int? Ttl { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"SDN DNS: {Dns} | Type: {Type} | URL: {Url ?? "N/A"}";
}
}
@@ -0,0 +1,52 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace PSProxmoxVE.Core.Models.Network;
/// <summary>
/// Represents a Software-Defined Networking (SDN) IPAM plugin as returned by
/// the /cluster/sdn/ipams endpoint. Requires Proxmox VE 8.0+.
/// </summary>
public class PveSdnIpam
{
/// <summary>
/// The IPAM plugin identifier.
/// </summary>
[JsonPropertyName("ipam")]
[JsonProperty("ipam")]
public string Ipam { get; set; } = string.Empty;
/// <summary>
/// The IPAM type (e.g. "pve", "netbox", "phpipam").
/// </summary>
[JsonPropertyName("type")]
[JsonProperty("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// The URL of the external IPAM service (for netbox/phpipam types).
/// </summary>
[JsonPropertyName("url")]
[JsonProperty("url")]
public string? Url { get; set; }
/// <summary>
/// The API token for the external IPAM service.
/// </summary>
[JsonPropertyName("token")]
[JsonProperty("token")]
public string? Token { get; set; }
/// <summary>
/// The configuration section identifier.
/// </summary>
[JsonPropertyName("section")]
[JsonProperty("section")]
public int? Section { get; set; }
/// <inheritdoc />
public override string ToString()
{
return $"SDN IPAM: {Ipam} | Type: {Type}";
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Backup;
using PSProxmoxVE.Core.Models.Vms;
namespace PSProxmoxVE.Core.Services
{
/// <summary>
/// Service for Proxmox VE Backup (vzdump) and backup job API operations.
/// </summary>
public class BackupService
{
// -------------------------------------------------------------------------
// Ad-hoc backup (vzdump)
// -------------------------------------------------------------------------
/// <summary>
/// Creates an ad-hoc backup via vzdump. Returns the task UPID.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The node to run the backup on.</param>
/// <param name="config">Backup configuration parameters.</param>
public PveTask CreateBackup(PveSession session, string node, Dictionary<string, string> 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 response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/vzdump", config)
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
// -------------------------------------------------------------------------
// Backup jobs (scheduled)
// -------------------------------------------------------------------------
/// <summary>
/// Returns all scheduled backup jobs.
/// </summary>
public PveBackupJob[] GetBackupJobs(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var response = client.GetAsync("cluster/backup").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveBackupJob[]>() ?? Array.Empty<PveBackupJob>();
}
/// <summary>
/// Returns a single scheduled backup job by ID.
/// </summary>
public PveBackupJob? GetBackupJob(PveSession session, string id)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveBackupJob>();
}
/// <summary>
/// Creates a scheduled backup job.
/// </summary>
public void CreateBackupJob(PveSession session, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PostAsync("cluster/backup", config).GetAwaiter().GetResult();
}
/// <summary>
/// Updates a scheduled backup job.
/// </summary>
public void UpdateBackupJob(PveSession session, string id, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/backup/{Uri.EscapeDataString(id)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Removes a scheduled backup job.
/// </summary>
public void RemoveBackupJob(PveSession session, string id)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
using var client = new PveHttpClient(session);
client.DeleteAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
.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;
}
}
}
@@ -173,7 +173,7 @@ namespace PSProxmoxVE.Core.Services
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}")
var response = client.DeleteAsync($"nodes/{node}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
@@ -192,7 +192,7 @@ namespace PSProxmoxVE.Core.Services
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")
var response = client.PostAsync($"nodes/{node}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
@@ -0,0 +1,478 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Firewall;
namespace PSProxmoxVE.Core.Services
{
/// <summary>
/// Service for Proxmox VE Firewall API operations.
/// Supports firewall management at cluster, node, VM, and container levels.
/// </summary>
public class FirewallService
{
// -------------------------------------------------------------------------
// Rules
// -------------------------------------------------------------------------
/// <summary>
/// Returns firewall rules at the specified level.
/// </summary>
public PveFirewallRule[] GetRules(PveSession session, string level, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
var response = client.GetAsync($"{basePath}/rules").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
}
/// <summary>
/// Creates a firewall rule at the specified level.
/// </summary>
public void CreateRule(PveSession session, string level, Dictionary<string, string> config,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (config == null) throw new ArgumentNullException(nameof(config));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.PostAsync($"{basePath}/rules", config).GetAwaiter().GetResult();
}
/// <summary>
/// Updates a firewall rule at the specified position.
/// </summary>
public void UpdateRule(PveSession session, string level, int pos, Dictionary<string, string> config,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (config == null) throw new ArgumentNullException(nameof(config));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.PutAsync($"{basePath}/rules/{pos}", config).GetAwaiter().GetResult();
}
/// <summary>
/// Removes a firewall rule at the specified position.
/// </summary>
public void RemoveRule(PveSession session, string level, int pos,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.DeleteAsync($"{basePath}/rules/{pos}").GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Security Groups (cluster level only)
// -------------------------------------------------------------------------
/// <summary>
/// Returns all firewall security groups.
/// </summary>
public PveFirewallGroup[] GetGroups(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
using var client = new PveHttpClient(session);
var response = client.GetAsync("cluster/firewall/groups").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallGroup[]>() ?? Array.Empty<PveFirewallGroup>();
}
/// <summary>
/// Creates a firewall security group.
/// </summary>
public void CreateGroup(PveSession session, string name, string? comment = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var formData = new Dictionary<string, string> { ["group"] = name };
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PostAsync("cluster/firewall/groups", formData).GetAwaiter().GetResult();
}
/// <summary>
/// Removes a firewall security group.
/// </summary>
public void RemoveGroup(PveSession session, string name)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
using var client = new PveHttpClient(session);
client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(name)}")
.GetAwaiter().GetResult();
}
/// <summary>
/// Returns firewall rules within a security group.
/// </summary>
public PveFirewallRule[] GetGroupRules(PveSession session, string group)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
using var client = new PveHttpClient(session);
var response = client.GetAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
}
/// <summary>
/// Creates a rule within a security group.
/// </summary>
public void CreateGroupRule(PveSession session, string group, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PostAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates a rule within a security group.
/// </summary>
public void UpdateGroupRule(PveSession session, string group, int pos, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
if (config == null) throw new ArgumentNullException(nameof(config));
using var client = new PveHttpClient(session);
client.PutAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}", config)
.GetAwaiter().GetResult();
}
/// <summary>
/// Removes a rule from a security group.
/// </summary>
public void RemoveGroupRule(PveSession session, string group, int pos)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
using var client = new PveHttpClient(session);
client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Aliases
// -------------------------------------------------------------------------
/// <summary>
/// Returns firewall aliases at the specified level.
/// </summary>
public PveFirewallAlias[] GetAliases(PveSession session, string level, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
var response = client.GetAsync($"{basePath}/aliases").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallAlias[]>() ?? Array.Empty<PveFirewallAlias>();
}
/// <summary>
/// Creates a firewall alias.
/// </summary>
public void CreateAlias(PveSession session, string level, string name, string cidr,
string? comment = null, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
var basePath = BuildBasePath(level, node, vmid);
var formData = new Dictionary<string, string>
{
["name"] = name,
["cidr"] = cidr
};
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PostAsync($"{basePath}/aliases", formData).GetAwaiter().GetResult();
}
/// <summary>
/// Updates a firewall alias.
/// </summary>
public void UpdateAlias(PveSession session, string level, string name,
string? cidr = null, string? comment = null, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var basePath = BuildBasePath(level, node, vmid);
var formData = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(cidr))
formData["cidr"] = cidr!;
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PutAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}", formData)
.GetAwaiter().GetResult();
}
/// <summary>
/// Removes a firewall alias.
/// </summary>
public void RemoveAlias(PveSession session, string level, string name,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.DeleteAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// IP Sets
// -------------------------------------------------------------------------
/// <summary>
/// Returns firewall IP sets at the specified level.
/// </summary>
public PveFirewallIpSet[] GetIpSets(PveSession session, string level, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
var response = client.GetAsync($"{basePath}/ipset").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallIpSet[]>() ?? Array.Empty<PveFirewallIpSet>();
}
/// <summary>
/// Creates a firewall IP set.
/// </summary>
public void CreateIpSet(PveSession session, string level, string name,
string? comment = null, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var basePath = BuildBasePath(level, node, vmid);
var formData = new Dictionary<string, string> { ["name"] = name };
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PostAsync($"{basePath}/ipset", formData).GetAwaiter().GetResult();
}
/// <summary>
/// Removes a firewall IP set.
/// </summary>
public void RemoveIpSet(PveSession session, string level, string name,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.DeleteAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// IP Set Entries
// -------------------------------------------------------------------------
/// <summary>
/// Returns entries within an IP set.
/// </summary>
public PveFirewallIpSetEntry[] GetIpSetEntries(PveSession session, string level, string name,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
var response = client.GetAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallIpSetEntry[]>() ?? Array.Empty<PveFirewallIpSetEntry>();
}
/// <summary>
/// Adds an entry to an IP set.
/// </summary>
public void AddIpSetEntry(PveSession session, string level, string name, string cidr,
bool nomatch = false, string? comment = null, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
var basePath = BuildBasePath(level, node, vmid);
var formData = new Dictionary<string, string> { ["cidr"] = cidr };
if (nomatch)
formData["nomatch"] = "1";
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PostAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}", formData)
.GetAwaiter().GetResult();
}
/// <summary>
/// Updates an entry in an IP set.
/// </summary>
public void UpdateIpSetEntry(PveSession session, string level, string name, string cidr,
bool? nomatch = null, string? comment = null, string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
var basePath = BuildBasePath(level, node, vmid);
var formData = new Dictionary<string, string>();
if (nomatch.HasValue)
formData["nomatch"] = nomatch.Value ? "1" : "0";
if (!string.IsNullOrEmpty(comment))
formData["comment"] = comment!;
using var client = new PveHttpClient(session);
client.PutAsync(
$"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}",
formData).GetAwaiter().GetResult();
}
/// <summary>
/// Removes an entry from an IP set.
/// </summary>
public void RemoveIpSetEntry(PveSession session, string level, string name, string cidr,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.DeleteAsync(
$"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Options
// -------------------------------------------------------------------------
/// <summary>
/// Returns firewall options at the specified level.
/// </summary>
public PveFirewallOptions GetOptions(PveSession session, string level,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
var response = client.GetAsync($"{basePath}/options").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveFirewallOptions>() ?? new PveFirewallOptions();
}
/// <summary>
/// Sets firewall options at the specified level.
/// </summary>
public void SetOptions(PveSession session, string level, Dictionary<string, string> config,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (config == null) throw new ArgumentNullException(nameof(config));
var basePath = BuildBasePath(level, node, vmid);
using var client = new PveHttpClient(session);
client.PutAsync($"{basePath}/options", config).GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Refs
// -------------------------------------------------------------------------
/// <summary>
/// Returns firewall references (aliases, IP sets) at the specified level.
/// </summary>
public PveFirewallRef[] GetRefs(PveSession session, string level, string? type = null,
string? node = null, int? vmid = null)
{
if (session == null) throw new ArgumentNullException(nameof(session));
var basePath = BuildBasePath(level, node, vmid);
var resource = $"{basePath}/refs";
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<PveFirewallRef[]>() ?? Array.Empty<PveFirewallRef>();
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
/// <summary>
/// Builds the API base path for the specified firewall level.
/// </summary>
private static string BuildBasePath(string level, string? node, int? vmid)
{
switch (level.ToLowerInvariant())
{
case "cluster":
return "cluster/firewall";
case "node":
if (string.IsNullOrWhiteSpace(node))
throw new ArgumentException("Node is required for node-level firewall operations.", nameof(node));
return $"nodes/{Uri.EscapeDataString(node)}/firewall";
case "vm":
if (string.IsNullOrWhiteSpace(node))
throw new ArgumentException("Node is required for VM-level firewall operations.", nameof(node));
if (!vmid.HasValue)
throw new ArgumentException("VmId is required for VM-level firewall operations.", nameof(vmid));
return $"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid.Value}/firewall";
case "container":
if (string.IsNullOrWhiteSpace(node))
throw new ArgumentException("Node is required for container-level firewall operations.", nameof(node));
if (!vmid.HasValue)
throw new ArgumentException("VmId is required for container-level firewall operations.", nameof(vmid));
return $"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid.Value}/firewall";
default:
throw new ArgumentException($"Invalid firewall level: {level}. Must be Cluster, Node, Vm, or Container.", nameof(level));
}
}
}
}
@@ -304,6 +304,141 @@ namespace PSProxmoxVE.Core.Services
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// SDN IPAM
// -------------------------------------------------------------------------
/// <summary>
/// Returns all SDN IPAM plugins. Requires PVE 8.0+.
/// </summary>
public PveSdnIpam[] GetSdnIpams(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
using var client = new PveHttpClient(session);
var response = client.GetAsync("cluster/sdn/ipams").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveSdnIpam[]>() ?? Array.Empty<PveSdnIpam>();
}
/// <summary>
/// Creates an SDN IPAM plugin. Requires PVE 8.0+.
/// </summary>
public void CreateSdnIpam(PveSession session, Dictionary<string, string> 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);
client.PostAsync("cluster/sdn/ipams", config).GetAwaiter().GetResult();
}
/// <summary>
/// Removes an SDN IPAM plugin. Requires PVE 8.0+.
/// </summary>
public void RemoveSdnIpam(PveSession session, string ipam)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam));
using var client = new PveHttpClient(session);
client.DeleteAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// SDN DNS
// -------------------------------------------------------------------------
/// <summary>
/// Returns all SDN DNS plugins. Requires PVE 8.0+.
/// </summary>
public PveSdnDns[] GetSdnDnsPlugins(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
using var client = new PveHttpClient(session);
var response = client.GetAsync("cluster/sdn/dns").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveSdnDns[]>() ?? Array.Empty<PveSdnDns>();
}
/// <summary>
/// Creates an SDN DNS plugin. Requires PVE 8.0+.
/// </summary>
public void CreateSdnDnsPlugin(PveSession session, Dictionary<string, string> 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);
client.PostAsync("cluster/sdn/dns", config).GetAwaiter().GetResult();
}
/// <summary>
/// Removes an SDN DNS plugin. Requires PVE 8.0+.
/// </summary>
public void RemoveSdnDnsPlugin(PveSession session, string dns)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns));
using var client = new PveHttpClient(session);
client.DeleteAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// SDN Controllers
// -------------------------------------------------------------------------
/// <summary>
/// Returns all SDN controllers. Requires PVE 8.0+.
/// </summary>
public PveSdnController[] GetSdnControllers(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
using var client = new PveHttpClient(session);
var response = client.GetAsync("cluster/sdn/controllers").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveSdnController[]>() ?? Array.Empty<PveSdnController>();
}
/// <summary>
/// Creates an SDN controller. Requires PVE 8.0+.
/// </summary>
public void CreateSdnController(PveSession session, Dictionary<string, string> 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);
client.PostAsync("cluster/sdn/controllers", config).GetAwaiter().GetResult();
}
/// <summary>
/// Removes an SDN controller. Requires PVE 8.0+.
/// </summary>
public void RemoveSdnController(PveSession session, string controller)
{
if (session == null) throw new ArgumentNullException(nameof(session));
RequireSdn(session);
if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller));
using var client = new PveHttpClient(session);
client.DeleteAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}")
.GetAwaiter().GetResult();
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
@@ -84,7 +84,7 @@ namespace PSProxmoxVE.Core.Services
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}")
var response = client.DeleteAsync($"nodes/{node}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
@@ -107,7 +107,7 @@ namespace PSProxmoxVE.Core.Services
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")
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
.GetAwaiter().GetResult();
return ParseTask(response, node);
}
@@ -0,0 +1,46 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Backup;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
{
/// <summary>
/// <para type="synopsis">Lists Proxmox VE backup jobs.</para>
/// <para type="description">
/// Returns scheduled backup job configurations from the Proxmox VE cluster.
/// Optionally filter by job ID.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveBackupJob")]
[OutputType(typeof(PveBackupJob))]
public sealed class GetPveBackupJobCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The backup job ID to retrieve. When omitted, all jobs are returned.</para>
/// </summary>
[Parameter(Mandatory = false, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The backup job ID.")]
public string? Id { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new BackupService();
if (!string.IsNullOrEmpty(Id))
{
WriteVerbose($"Getting backup job '{Id}'...");
var job = service.GetBackupJob(session, Id!);
WriteObject(job);
}
else
{
WriteVerbose("Getting all backup jobs...");
var jobs = service.GetBackupJobs(session);
foreach (var job in jobs)
{
WriteObject(job);
}
}
}
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
{
/// <summary>
/// <para type="synopsis">Creates an ad-hoc backup of a virtual machine via vzdump.</para>
/// <para type="description">
/// Triggers an immediate backup (vzdump) of the specified VM on the given node.
/// Returns a PveTask representing the backup operation. Use -Wait to block until
/// the backup completes.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveBackup", SupportsShouldProcess = true)]
[OutputType(typeof(PveTask))]
public sealed class NewPveBackupCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The node on which the VM resides.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
public string Node { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The ID of the VM to back up.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
/// <summary>
/// <para type="description">The target storage for the backup.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The target storage for the backup.")]
public string? Storage { get; set; }
/// <summary>
/// <para type="description">The backup mode (snapshot, suspend, or stop).</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The backup mode.")]
[ValidateSet("snapshot", "suspend", "stop")]
public string? Mode { get; set; }
/// <summary>
/// <para type="description">The compression algorithm to use.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The compression algorithm.")]
[ValidateSet("zstd", "lzo", "gzip", "none")]
public string? Compress { get; set; }
/// <summary>
/// <para type="description">When specified, waits for the backup task to complete before returning.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
public SwitchParameter Wait { get; set; }
/// <summary>
/// <para type="description">Maximum seconds to wait when -Wait is specified. Default 3600.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 3600).")]
public int Timeout { get; set; } = 3600;
protected override void ProcessRecord()
{
if (!ShouldProcess($"VM {VmId} on node '{Node}'", "New-PveBackup"))
return;
var session = GetSession();
var service = new BackupService();
var formData = new Dictionary<string, string>
{
["vmid"] = VmId.ToString()
};
if (!string.IsNullOrEmpty(Storage)) formData["storage"] = Storage!;
if (!string.IsNullOrEmpty(Mode)) formData["mode"] = Mode!;
if (!string.IsNullOrEmpty(Compress)) formData["compress"] = Compress!;
WriteVerbose($"Creating backup of VM {VmId} on node '{Node}'...");
var task = service.CreateBackup(session, Node, formData);
if (Wait.IsPresent)
{
var taskService = new TaskService();
task = taskService.WaitForTask(session, task.Node ?? Node, task.Upid, TimeSpan.FromSeconds(Timeout));
}
WriteObject(task);
}
}
}
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
{
/// <summary>
/// <para type="synopsis">Creates a scheduled backup job on Proxmox VE.</para>
/// <para type="description">
/// Creates a new scheduled backup job in the Proxmox VE cluster configuration.
/// Specify a cron-style schedule and target storage. Use VmId for specific VMs
/// or -All to back up all VMs.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveBackupJob", SupportsShouldProcess = true)]
public sealed class NewPveBackupJobCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">Cron-style schedule expression (e.g., "0 2 * * *").</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "Cron-style schedule expression (e.g., '0 2 * * *').")]
public string Schedule { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The target storage for backups.</para>
/// </summary>
[Parameter(Mandatory = true, HelpMessage = "The target storage for backups.")]
public string Storage { get; set; } = string.Empty;
/// <summary>
/// <para type="description">The backup mode (snapshot, suspend, or stop).</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The backup mode.")]
[ValidateSet("snapshot", "suspend", "stop")]
public string? Mode { get; set; }
/// <summary>
/// <para type="description">Comma-separated list of VM IDs to include in the backup job.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of VM IDs to back up.")]
public string? VmId { get; set; }
/// <summary>
/// <para type="description">When specified, backs up all VMs on the node.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Back up all VMs.")]
public SwitchParameter All { get; set; }
/// <summary>
/// <para type="description">The compression algorithm to use.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The compression algorithm.")]
[ValidateSet("zstd", "lzo", "gzip", "none")]
public string? Compress { get; set; }
/// <summary>
/// <para type="description">The node to restrict the backup job to.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The node to restrict the backup job to.")]
public string? Node { get; set; }
/// <summary>
/// <para type="description">Email address(es) to send notifications to.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Email address(es) for notifications.")]
public string? MailTo { get; set; }
/// <summary>
/// <para type="description">When to send email notifications (always or failure).</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "When to send email notifications.")]
[ValidateSet("always", "failure")]
public string? MailNotification { get; set; }
/// <summary>
/// <para type="description">Whether the backup job is enabled. Default is true.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Whether the backup job is enabled.")]
public SwitchParameter Enabled { get; set; } = true;
/// <summary>
/// <para type="description">A comment or description for the backup job.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "A comment or description for the backup job.")]
public string? Comment { get; set; }
/// <summary>
/// <para type="description">Prune-backups configuration string (e.g., "keep-daily=7,keep-weekly=4").</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Prune-backups configuration string.")]
public string? PruneBackups { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess("cluster backup configuration", "New-PveBackupJob"))
return;
var session = GetSession();
var service = new BackupService();
var config = new Dictionary<string, string>
{
["schedule"] = Schedule,
["storage"] = Storage,
["enabled"] = Enabled.IsPresent ? "1" : "0"
};
if (!string.IsNullOrEmpty(Mode)) config["mode"] = Mode!;
if (!string.IsNullOrEmpty(VmId)) config["vmid"] = VmId!;
if (All.IsPresent) config["all"] = "1";
if (!string.IsNullOrEmpty(Compress)) config["compress"] = Compress!;
if (!string.IsNullOrEmpty(Node)) config["node"] = Node!;
if (!string.IsNullOrEmpty(MailTo)) config["mailto"] = MailTo!;
if (!string.IsNullOrEmpty(MailNotification)) config["mailnotification"] = MailNotification!;
if (!string.IsNullOrEmpty(Comment)) config["comment"] = Comment!;
if (!string.IsNullOrEmpty(PruneBackups)) config["prune-backups"] = PruneBackups!;
WriteVerbose("Creating backup job...");
service.CreateBackupJob(session, config);
}
}
}
@@ -0,0 +1,36 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
{
/// <summary>
/// <para type="synopsis">Removes a scheduled backup job from Proxmox VE.</para>
/// <para type="description">
/// Deletes a backup job from the Proxmox VE cluster configuration.
/// This operation is destructive and requires confirmation unless -Force is specified.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveBackupJob",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
public sealed class RemovePveBackupJobCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The backup job ID to remove.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The backup job ID.")]
public string Id { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"backup job '{Id}'", "Remove-PveBackupJob"))
return;
var session = GetSession();
var service = new BackupService();
WriteVerbose($"Removing backup job '{Id}'...");
service.RemoveBackupJob(session, Id);
}
}
}
@@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Backup
{
/// <summary>
/// <para type="synopsis">Updates a scheduled backup job on Proxmox VE.</para>
/// <para type="description">
/// Modifies properties of an existing backup job. Only specified parameters are
/// updated; omitted parameters retain their current values.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Set, "PveBackupJob", SupportsShouldProcess = true)]
public sealed class SetPveBackupJobCmdlet : PveCmdletBase
{
/// <summary>
/// <para type="description">The backup job ID to update.</para>
/// </summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The backup job ID.")]
public string Id { get; set; } = string.Empty;
/// <summary>
/// <para type="description">Updated cron-style schedule expression.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Cron-style schedule expression (e.g., '0 2 * * *').")]
public string? Schedule { get; set; }
/// <summary>
/// <para type="description">Updated target storage for backups.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The target storage for backups.")]
public string? Storage { get; set; }
/// <summary>
/// <para type="description">Updated backup mode (snapshot, suspend, or stop).</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The backup mode.")]
[ValidateSet("snapshot", "suspend", "stop")]
public string? Mode { get; set; }
/// <summary>
/// <para type="description">Updated comma-separated list of VM IDs.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of VM IDs to back up.")]
public string? VmId { get; set; }
/// <summary>
/// <para type="description">When specified, backs up all VMs.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Back up all VMs.")]
public SwitchParameter All { get; set; }
/// <summary>
/// <para type="description">Updated compression algorithm.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The compression algorithm.")]
[ValidateSet("zstd", "lzo", "gzip", "none")]
public string? Compress { get; set; }
/// <summary>
/// <para type="description">Updated node restriction.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "The node to restrict the backup job to.")]
public string? Node { get; set; }
/// <summary>
/// <para type="description">Updated email address(es) for notifications.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Email address(es) for notifications.")]
public string? MailTo { get; set; }
/// <summary>
/// <para type="description">Updated notification policy.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "When to send email notifications.")]
[ValidateSet("always", "failure")]
public string? MailNotification { get; set; }
/// <summary>
/// <para type="description">Enable or disable the backup job.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Enable or disable the backup job.")]
public SwitchParameter Enabled { get; set; }
/// <summary>
/// <para type="description">Updated comment or description.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "A comment or description for the backup job.")]
public string? Comment { get; set; }
/// <summary>
/// <para type="description">Updated prune-backups configuration string.</para>
/// </summary>
[Parameter(Mandatory = false, HelpMessage = "Prune-backups configuration string.")]
public string? PruneBackups { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"backup job '{Id}'", "Set-PveBackupJob"))
return;
var session = GetSession();
var service = new BackupService();
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Schedule)) config["schedule"] = Schedule!;
if (!string.IsNullOrEmpty(Storage)) config["storage"] = Storage!;
if (!string.IsNullOrEmpty(Mode)) config["mode"] = Mode!;
if (!string.IsNullOrEmpty(VmId)) config["vmid"] = VmId!;
if (All.IsPresent) config["all"] = "1";
if (!string.IsNullOrEmpty(Compress)) config["compress"] = Compress!;
if (!string.IsNullOrEmpty(Node)) config["node"] = Node!;
if (!string.IsNullOrEmpty(MailTo)) config["mailto"] = MailTo!;
if (!string.IsNullOrEmpty(MailNotification)) config["mailnotification"] = MailNotification!;
if (MyInvocation.BoundParameters.ContainsKey("Enabled")) config["enabled"] = Enabled.IsPresent ? "1" : "0";
if (!string.IsNullOrEmpty(Comment)) config["comment"] = Comment!;
if (!string.IsNullOrEmpty(PruneBackups)) config["prune-backups"] = PruneBackups!;
WriteVerbose($"Updating backup job '{Id}'...");
service.UpdateBackupJob(session, Id, config);
}
}
}
@@ -0,0 +1,70 @@
using System;
using System.Linq;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallAlias")]
[OutputType(typeof(PveFirewallAlias))]
public class GetPveFirewallAliasCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Optional alias name to filter by.")]
public string? Name { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Getting firewall aliases at level '{level}'...");
var aliases = service.GetAliases(session, level, Node, vmid);
if (!string.IsNullOrEmpty(Name))
{
var filtered = aliases.Where(a => string.Equals(a.Name, Name, StringComparison.OrdinalIgnoreCase)).ToArray();
WriteObject(filtered, true);
}
else
{
WriteObject(aliases, true);
}
}
}
}
@@ -0,0 +1,34 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallGroup")]
[OutputType(typeof(PveFirewallGroup), typeof(PveFirewallRule))]
public class GetPveFirewallGroupCmdlet : PveCmdletBase
{
[Parameter(Mandatory = false, Position = 0, HelpMessage = "The security group name. If specified, returns rules within the group.")]
public string? Group { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new FirewallService();
if (!string.IsNullOrEmpty(Group))
{
WriteVerbose($"Getting firewall rules for security group '{Group}'...");
var rules = service.GetGroupRules(session, Group!);
WriteObject(rules, true);
}
else
{
WriteVerbose("Getting firewall security groups...");
var groups = service.GetGroups(session);
WriteObject(groups, true);
}
}
}
}
@@ -0,0 +1,70 @@
using System;
using System.Linq;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallIpSet")]
[OutputType(typeof(PveFirewallIpSet))]
public class GetPveFirewallIpSetCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Optional IP set name to filter by.")]
public string? Name { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Getting firewall IP sets at level '{level}'...");
var ipSets = service.GetIpSets(session, level, Node, vmid);
if (!string.IsNullOrEmpty(Name))
{
var filtered = ipSets.Where(s => string.Equals(s.Name, Name, StringComparison.OrdinalIgnoreCase)).ToArray();
WriteObject(filtered, true);
}
else
{
WriteObject(ipSets, true);
}
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallIpSetEntry")]
[OutputType(typeof(PveFirewallIpSetEntry))]
public class GetPveFirewallIpSetEntryCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The IP set name.")]
public string Name { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Getting entries for IP set '{Name}' at level '{level}'...");
var entries = service.GetIpSetEntries(session, level, Name, Node, vmid);
WriteObject(entries, true);
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallOptions")]
[OutputType(typeof(PveFirewallOptions))]
public class GetPveFirewallOptionsCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Getting firewall options at level '{level}'...");
var options = service.GetOptions(session, level, Node, vmid);
WriteObject(options);
}
}
}
@@ -0,0 +1,60 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallRef")]
[OutputType(typeof(PveFirewallRef))]
public class GetPveFirewallRefCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Optional type filter for references.")]
public string? Type { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Getting firewall references at level '{level}'...");
var refs = service.GetRefs(session, level, Type, Node, vmid);
WriteObject(refs, true);
}
}
}
@@ -0,0 +1,70 @@
using System;
using System.Linq;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Get, "PveFirewallRule")]
[OutputType(typeof(PveFirewallRule))]
public class GetPveFirewallRuleCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Optional rule position to filter by.")]
public int? Position { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Getting firewall rules at level '{level}'...");
var rules = service.GetRules(session, level, Node, vmid);
if (Position.HasValue)
{
var filtered = rules.Where(r => r.Pos == Position.Value).ToArray();
WriteObject(filtered, true);
}
else
{
WriteObject(rules, true);
}
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.New, "PveFirewallAlias", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class NewPveFirewallAliasCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The alias name.")]
public string Name { get; set; } = string.Empty;
[Parameter(Mandatory = true, HelpMessage = "The CIDR network address.")]
public string Cidr { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "Optional comment for the alias.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall alias '{Name}' ({Level})", "Create"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Creating firewall alias '{Name}' at level '{level}'...");
service.CreateAlias(session, level, Name, Cidr, Comment, Node, vmid);
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.New, "PveFirewallGroup", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class NewPveFirewallGroupCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The security group name.")]
public string Group { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "Optional comment for the security group.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"firewall security group '{Group}'", "Create"))
return;
var session = GetSession();
var service = new FirewallService();
WriteVerbose($"Creating firewall security group '{Group}'...");
service.CreateGroup(session, Group, Comment);
}
}
}
@@ -0,0 +1,65 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.New, "PveFirewallIpSet", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class NewPveFirewallIpSetCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The IP set name.")]
public string Name { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "Optional comment for the IP set.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall IP set '{Name}' ({Level})", "Create"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Creating firewall IP set '{Name}' at level '{level}'...");
service.CreateIpSet(session, level, Name, Comment, Node, vmid);
}
}
}
@@ -0,0 +1,71 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.New, "PveFirewallIpSetEntry", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class NewPveFirewallIpSetEntryCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The IP set name.")]
public string Name { get; set; } = string.Empty;
[Parameter(Mandatory = true, HelpMessage = "The CIDR network address to add.")]
public string Cidr { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "Exclude this entry from the IP set match.")]
public SwitchParameter NoMatch { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Optional comment for the entry.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"IP set entry '{Cidr}' in '{Name}' ({Level})", "Create"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Adding entry '{Cidr}' to IP set '{Name}' at level '{level}'...");
service.AddIpSetEntry(session, level, Name, Cidr, NoMatch.IsPresent, Comment, Node, vmid);
}
}
}
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.New, "PveFirewallRule", SupportsShouldProcess = true)]
[OutputType(typeof(PveFirewallRule))]
public class NewPveFirewallRuleCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The rule type: in, out, or group.")]
[ValidateSet("in", "out", "group")]
public string Type { get; set; } = string.Empty;
[Parameter(Mandatory = true, HelpMessage = "The rule action: ACCEPT, DROP, or REJECT.")]
[ValidateSet("ACCEPT", "DROP", "REJECT")]
public string Action { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "Enable the rule.")]
public SwitchParameter Enable { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Source address or alias.")]
public string? Source { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Destination address or alias.")]
public string? Dest { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Protocol (e.g., tcp, udp, icmp).")]
public string? Proto { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Destination port.")]
public string? Dport { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Source port.")]
public string? Sport { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Rule comment.")]
public string? Comment { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Macro name.")]
public string? Macro { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Log level.")]
public string? Log { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Network interface.")]
public string? Iface { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall rule ({Level})", "Create"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
var config = new Dictionary<string, string>
{
["type"] = Type,
["action"] = Action
};
if (Enable.IsPresent)
config["enable"] = "1";
if (!string.IsNullOrEmpty(Source))
config["source"] = Source!;
if (!string.IsNullOrEmpty(Dest))
config["dest"] = Dest!;
if (!string.IsNullOrEmpty(Proto))
config["proto"] = Proto!;
if (!string.IsNullOrEmpty(Dport))
config["dport"] = Dport!;
if (!string.IsNullOrEmpty(Sport))
config["sport"] = Sport!;
if (!string.IsNullOrEmpty(Comment))
config["comment"] = Comment!;
if (!string.IsNullOrEmpty(Macro))
config["macro"] = Macro!;
if (!string.IsNullOrEmpty(Log))
config["log"] = Log!;
if (!string.IsNullOrEmpty(Iface))
config["iface"] = Iface!;
WriteVerbose($"Creating firewall rule at level '{level}'...");
service.CreateRule(session, level, config, Node, vmid);
}
}
}
@@ -0,0 +1,62 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Remove, "PveFirewallAlias", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public class RemovePveFirewallAliasCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The alias name to remove.")]
public string Name { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall alias '{Name}' ({Level})", "Remove"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Removing firewall alias '{Name}' at level '{level}'...");
service.RemoveAlias(session, level, Name, Node, vmid);
}
}
}
@@ -0,0 +1,27 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Remove, "PveFirewallGroup", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public class RemovePveFirewallGroupCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The security group name to remove.")]
public string Group { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"firewall security group '{Group}'", "Remove"))
return;
var session = GetSession();
var service = new FirewallService();
WriteVerbose($"Removing firewall security group '{Group}'...");
service.RemoveGroup(session, Group);
}
}
}
@@ -0,0 +1,62 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Remove, "PveFirewallIpSet", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public class RemovePveFirewallIpSetCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The IP set name to remove.")]
public string Name { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall IP set '{Name}' ({Level})", "Remove"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Removing firewall IP set '{Name}' at level '{level}'...");
service.RemoveIpSet(session, level, Name, Node, vmid);
}
}
}
@@ -0,0 +1,65 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Remove, "PveFirewallIpSetEntry", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public class RemovePveFirewallIpSetEntryCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The IP set name.")]
public string Name { get; set; } = string.Empty;
[Parameter(Mandatory = true, HelpMessage = "The CIDR network address to remove.")]
public string Cidr { get; set; } = string.Empty;
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"IP set entry '{Cidr}' from '{Name}' ({Level})", "Remove"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Removing entry '{Cidr}' from IP set '{Name}' at level '{level}'...");
service.RemoveIpSetEntry(session, level, Name, Cidr, Node, vmid);
}
}
}
@@ -0,0 +1,62 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Remove, "PveFirewallRule", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(void))]
public class RemovePveFirewallRuleCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The rule position to remove.")]
public int Position { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall rule at position {Position} ({Level})", "Remove"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Removing firewall rule at position {Position} ({level})...");
service.RemoveRule(session, level, Position, Node, vmid);
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Set, "PveFirewallAlias", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class SetPveFirewallAliasCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The alias name to update.")]
public string Name { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The updated CIDR network address.")]
public string? Cidr { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Updated comment for the alias.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall alias '{Name}' ({Level})", "Update"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
WriteVerbose($"Updating firewall alias '{Name}' at level '{level}'...");
service.UpdateAlias(session, level, Name, Cidr, Comment, Node, vmid);
}
}
}
@@ -0,0 +1,72 @@
using System;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Set, "PveFirewallIpSetEntry", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class SetPveFirewallIpSetEntryCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The IP set name.")]
public string Name { get; set; } = string.Empty;
[Parameter(Mandatory = true, HelpMessage = "The CIDR network address to update.")]
public string Cidr { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "Exclude this entry from the IP set match.")]
public SwitchParameter NoMatch { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Updated comment for the entry.")]
public string? Comment { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"IP set entry '{Cidr}' in '{Name}' ({Level})", "Update"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
bool? nomatch = NoMatch.IsPresent ? true : (bool?)null;
WriteVerbose($"Updating entry '{Cidr}' in IP set '{Name}' at level '{level}'...");
service.UpdateIpSetEntry(session, level, Name, Cidr, nomatch, Comment, Node, vmid);
}
}
}
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Set, "PveFirewallOptions", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class SetPveFirewallOptionsCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable or disable the firewall.")]
public SwitchParameter Enable { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Default policy for incoming traffic: ACCEPT, DROP, or REJECT.")]
[ValidateSet("ACCEPT", "DROP", "REJECT")]
public string? PolicyIn { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Default policy for outgoing traffic: ACCEPT, DROP, or REJECT.")]
[ValidateSet("ACCEPT", "DROP", "REJECT")]
public string? PolicyOut { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Log level for incoming traffic.")]
public string? LogLevelIn { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Log level for outgoing traffic.")]
public string? LogLevelOut { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable DHCP.")]
public SwitchParameter Dhcp { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable NDP.")]
public SwitchParameter Ndp { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable MAC address filter.")]
public SwitchParameter MacFilter { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable IP filter.")]
public SwitchParameter IpFilter { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall options ({Level})", "Update"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
var config = new Dictionary<string, string>();
if (Enable.IsPresent)
config["enable"] = "1";
if (!string.IsNullOrEmpty(PolicyIn))
config["policy_in"] = PolicyIn!;
if (!string.IsNullOrEmpty(PolicyOut))
config["policy_out"] = PolicyOut!;
if (!string.IsNullOrEmpty(LogLevelIn))
config["log_level_in"] = LogLevelIn!;
if (!string.IsNullOrEmpty(LogLevelOut))
config["log_level_out"] = LogLevelOut!;
if (Dhcp.IsPresent)
config["dhcp"] = "1";
if (Ndp.IsPresent)
config["ndp"] = "1";
if (MacFilter.IsPresent)
config["macfilter"] = "1";
if (IpFilter.IsPresent)
config["ipfilter"] = "1";
WriteVerbose($"Setting firewall options at level '{level}'...");
service.SetOptions(session, level, config, Node, vmid);
}
}
}
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Firewall;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Firewall
{
[Cmdlet(VerbsCommon.Set, "PveFirewallRule", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public class SetPveFirewallRuleCmdlet : PveCmdletBase
{
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The firewall level: Cluster, Node, Vm, or Container.")]
[ValidateSet("Cluster", "Node", "Vm", "Container")]
public string Level { get; set; } = string.Empty;
[Parameter(Mandatory = false, HelpMessage = "The node name. Required when Level is Node, Vm, or Container.")]
public string? Node { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The VM/Container ID. Required when Level is Vm or Container.")]
[ValidateRange(100, 999999999)]
public int VmId { get; set; }
[Parameter(Mandatory = true, HelpMessage = "The rule position to update.")]
public int Position { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The rule type: in, out, or group.")]
[ValidateSet("in", "out", "group")]
public string? Type { get; set; }
[Parameter(Mandatory = false, HelpMessage = "The rule action: ACCEPT, DROP, or REJECT.")]
[ValidateSet("ACCEPT", "DROP", "REJECT")]
public string? Action { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Enable the rule.")]
public SwitchParameter Enable { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Source address or alias.")]
public string? Source { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Destination address or alias.")]
public string? Dest { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Protocol (e.g., tcp, udp, icmp).")]
public string? Proto { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Destination port.")]
public string? Dport { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Source port.")]
public string? Sport { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Rule comment.")]
public string? Comment { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Macro name.")]
public string? Macro { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Log level.")]
public string? Log { get; set; }
[Parameter(Mandatory = false, HelpMessage = "Network interface.")]
public string? Iface { get; set; }
protected override void ProcessRecord()
{
var level = Level;
if (!string.Equals(level, "Cluster", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(Node))
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("Node is required when Level is not Cluster."),
"NodeRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (string.Equals(level, "Vm", StringComparison.OrdinalIgnoreCase) ||
string.Equals(level, "Container", StringComparison.OrdinalIgnoreCase))
{
if (VmId == 0)
{
ThrowTerminatingError(new ErrorRecord(
new PSArgumentException("VmId is required when Level is Vm or Container."),
"VmIdRequired", ErrorCategory.InvalidArgument, null));
return;
}
}
if (!ShouldProcess($"firewall rule at position {Position} ({Level})", "Update"))
return;
var session = GetSession();
var service = new FirewallService();
int? vmid = VmId > 0 ? VmId : (int?)null;
var config = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Type))
config["type"] = Type!;
if (!string.IsNullOrEmpty(Action))
config["action"] = Action!;
if (Enable.IsPresent)
config["enable"] = "1";
if (!string.IsNullOrEmpty(Source))
config["source"] = Source!;
if (!string.IsNullOrEmpty(Dest))
config["dest"] = Dest!;
if (!string.IsNullOrEmpty(Proto))
config["proto"] = Proto!;
if (!string.IsNullOrEmpty(Dport))
config["dport"] = Dport!;
if (!string.IsNullOrEmpty(Sport))
config["sport"] = Sport!;
if (!string.IsNullOrEmpty(Comment))
config["comment"] = Comment!;
if (!string.IsNullOrEmpty(Macro))
config["macro"] = Macro!;
if (!string.IsNullOrEmpty(Log))
config["log"] = Log!;
if (!string.IsNullOrEmpty(Iface))
config["iface"] = Iface!;
WriteVerbose($"Updating firewall rule at position {Position} ({level})...");
service.UpdateRule(session, level, Position, config, Node, vmid);
}
}
}
@@ -0,0 +1,41 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Lists SDN controllers in Proxmox VE.</para>
/// <para type="description">
/// Returns Software-Defined Networking controller definitions.
/// Optionally filters by controller identifier.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveSdnController")]
[OutputType(typeof(PveSdnController))]
public class GetPveSdnControllerCmdlet : PveCmdletBase
{
/// <summary>Optional controller identifier filter.</summary>
[Parameter(Mandatory = false, Position = 0, HelpMessage = "Filter by controller identifier.")]
public string? Controller { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new NetworkService();
WriteVerbose("Getting SDN controllers...");
var controllers = service.GetSdnControllers(session);
foreach (var item in controllers)
{
if (!string.IsNullOrEmpty(Controller) &&
!string.Equals(item.Controller, Controller, System.StringComparison.OrdinalIgnoreCase))
continue;
WriteObject(item);
}
}
}
}
@@ -0,0 +1,41 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Lists SDN DNS plugins in Proxmox VE.</para>
/// <para type="description">
/// Returns Software-Defined Networking DNS plugin definitions.
/// Optionally filters by DNS identifier.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveSdnDns")]
[OutputType(typeof(PveSdnDns))]
public class GetPveSdnDnsCmdlet : PveCmdletBase
{
/// <summary>Optional DNS plugin identifier filter.</summary>
[Parameter(Mandatory = false, Position = 0, HelpMessage = "Filter by DNS plugin identifier.")]
public string? Dns { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new NetworkService();
WriteVerbose("Getting SDN DNS plugins...");
var plugins = service.GetSdnDnsPlugins(session);
foreach (var item in plugins)
{
if (!string.IsNullOrEmpty(Dns) &&
!string.Equals(item.Dns, Dns, System.StringComparison.OrdinalIgnoreCase))
continue;
WriteObject(item);
}
}
}
}
@@ -0,0 +1,41 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Models.Network;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Lists SDN IPAM plugins in Proxmox VE.</para>
/// <para type="description">
/// Returns Software-Defined Networking IPAM plugin definitions.
/// Optionally filters by IPAM identifier.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Get, "PveSdnIpam")]
[OutputType(typeof(PveSdnIpam))]
public class GetPveSdnIpamCmdlet : PveCmdletBase
{
/// <summary>Optional IPAM identifier filter.</summary>
[Parameter(Mandatory = false, Position = 0, HelpMessage = "Filter by IPAM plugin identifier.")]
public string? Ipam { get; set; }
protected override void ProcessRecord()
{
var session = GetSession();
var service = new NetworkService();
WriteVerbose("Getting SDN IPAM plugins...");
var ipams = service.GetSdnIpams(session);
foreach (var item in ipams)
{
if (!string.IsNullOrEmpty(Ipam) &&
!string.Equals(item.Ipam, Ipam, System.StringComparison.OrdinalIgnoreCase))
continue;
WriteObject(item);
}
}
}
}
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Creates a new SDN controller in Proxmox VE.</para>
/// <para type="description">
/// Adds a new Software-Defined Networking controller.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveSdnController", SupportsShouldProcess = true)]
public class NewPveSdnControllerCmdlet : PveCmdletBase
{
/// <summary>The controller identifier.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The controller identifier.")]
public string Controller { get; set; } = string.Empty;
/// <summary>The controller type.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The controller type.")]
[ValidateSet("evpn", "bgp")]
public string Type { get; set; } = string.Empty;
/// <summary>The Autonomous System Number for BGP/EVPN.</summary>
[Parameter(Mandatory = false, HelpMessage = "The Autonomous System Number for BGP/EVPN.")]
public int? Asn { get; set; }
/// <summary>Comma-separated list of BGP peer addresses.</summary>
[Parameter(Mandatory = false, HelpMessage = "Comma-separated list of BGP peer addresses.")]
public string? Peers { get; set; }
/// <summary>The node this controller is configured on.</summary>
[Parameter(Mandatory = false, HelpMessage = "The node this controller is configured on.")]
public string? Node { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"SDN Controller '{Controller}'", "Create PVE SDN Controller"))
return;
var session = GetSession();
var service = new NetworkService();
WriteVerbose($"Creating SDN controller '{Controller}' of type '{Type}'...");
var data = new Dictionary<string, string>
{
["controller"] = Controller,
["type"] = Type
};
if (Asn.HasValue) data["asn"] = Asn.Value.ToString();
if (!string.IsNullOrEmpty(Peers)) data["peers"] = Peers!;
if (!string.IsNullOrEmpty(Node)) data["node"] = Node!;
service.CreateSdnController(session, data);
}
}
}
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Creates a new SDN DNS plugin in Proxmox VE.</para>
/// <para type="description">
/// Adds a new Software-Defined Networking DNS plugin.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveSdnDns", SupportsShouldProcess = true)]
public class NewPveSdnDnsCmdlet : PveCmdletBase
{
/// <summary>The DNS plugin identifier.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The DNS plugin identifier.")]
public string Dns { get; set; } = string.Empty;
/// <summary>The DNS plugin type.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The DNS plugin type.")]
[ValidateSet("powerdns")]
public string Type { get; set; } = string.Empty;
/// <summary>The URL of the DNS service API.</summary>
[Parameter(Mandatory = true, HelpMessage = "The URL of the DNS service API.")]
public string Url { get; set; } = string.Empty;
/// <summary>The API key for the DNS service.</summary>
[Parameter(Mandatory = true, HelpMessage = "The API key for the DNS service.")]
public string Key { get; set; } = string.Empty;
/// <summary>The IPv6 reverse zone mask length.</summary>
[Parameter(Mandatory = false, HelpMessage = "The IPv6 reverse zone mask length.")]
public int? ReverseMaskV6 { get; set; }
/// <summary>The TTL (time-to-live) for DNS records.</summary>
[Parameter(Mandatory = false, HelpMessage = "The TTL (time-to-live) for DNS records.")]
public int? Ttl { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"SDN DNS '{Dns}'", "Create PVE SDN DNS"))
return;
var session = GetSession();
var service = new NetworkService();
WriteVerbose($"Creating SDN DNS plugin '{Dns}' of type '{Type}'...");
var data = new Dictionary<string, string>
{
["dns"] = Dns,
["type"] = Type,
["url"] = Url,
["key"] = Key
};
if (ReverseMaskV6.HasValue) data["reversemaskv6"] = ReverseMaskV6.Value.ToString();
if (Ttl.HasValue) data["ttl"] = Ttl.Value.ToString();
service.CreateSdnDnsPlugin(session, data);
}
}
}
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Creates a new SDN IPAM plugin in Proxmox VE.</para>
/// <para type="description">
/// Adds a new Software-Defined Networking IPAM plugin.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.New, "PveSdnIpam", SupportsShouldProcess = true)]
public class NewPveSdnIpamCmdlet : PveCmdletBase
{
/// <summary>The IPAM plugin identifier.</summary>
[Parameter(Mandatory = true, Position = 0, HelpMessage = "The IPAM plugin identifier.")]
public string Ipam { get; set; } = string.Empty;
/// <summary>The IPAM plugin type.</summary>
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The IPAM plugin type.")]
[ValidateSet("pve", "netbox", "phpipam")]
public string Type { get; set; } = string.Empty;
/// <summary>The URL of the external IPAM service.</summary>
[Parameter(Mandatory = false, HelpMessage = "The URL of the external IPAM service.")]
public string? Url { get; set; }
/// <summary>The API token for the external IPAM service.</summary>
[Parameter(Mandatory = false, HelpMessage = "The API token for the external IPAM service.")]
public string? Token { get; set; }
/// <summary>The configuration section identifier.</summary>
[Parameter(Mandatory = false, HelpMessage = "The configuration section identifier.")]
public int? Section { get; set; }
protected override void ProcessRecord()
{
if (!ShouldProcess($"SDN IPAM '{Ipam}'", "Create PVE SDN IPAM"))
return;
var session = GetSession();
var service = new NetworkService();
WriteVerbose($"Creating SDN IPAM plugin '{Ipam}' of type '{Type}'...");
var data = new Dictionary<string, string>
{
["ipam"] = Ipam,
["type"] = Type
};
if (!string.IsNullOrEmpty(Url)) data["url"] = Url!;
if (!string.IsNullOrEmpty(Token)) data["token"] = Token!;
if (Section.HasValue) data["section"] = Section.Value.ToString();
service.CreateSdnIpam(session, data);
}
}
}
@@ -0,0 +1,32 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Removes an SDN controller from Proxmox VE.</para>
/// <para type="description">
/// Deletes the specified Software-Defined Networking controller.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveSdnController", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public class RemovePveSdnControllerCmdlet : PveCmdletBase
{
/// <summary>The controller identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The controller identifier to remove.")]
public string Controller { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"SDN Controller '{Controller}'", "Remove PVE SDN Controller"))
return;
var session = GetSession();
var service = new NetworkService();
WriteVerbose($"Removing SDN controller '{Controller}'...");
service.RemoveSdnController(session, Controller);
}
}
}
@@ -0,0 +1,32 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Removes an SDN DNS plugin from Proxmox VE.</para>
/// <para type="description">
/// Deletes the specified Software-Defined Networking DNS plugin.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveSdnDns", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public class RemovePveSdnDnsCmdlet : PveCmdletBase
{
/// <summary>The DNS plugin identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The DNS plugin identifier to remove.")]
public string Dns { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"SDN DNS '{Dns}'", "Remove PVE SDN DNS"))
return;
var session = GetSession();
var service = new NetworkService();
WriteVerbose($"Removing SDN DNS plugin '{Dns}'...");
service.RemoveSdnDnsPlugin(session, Dns);
}
}
}
@@ -0,0 +1,32 @@
using System.Management.Automation;
using PSProxmoxVE.Core.Services;
namespace PSProxmoxVE.Cmdlets.Network
{
/// <summary>
/// <para type="synopsis">Removes an SDN IPAM plugin from Proxmox VE.</para>
/// <para type="description">
/// Deletes the specified Software-Defined Networking IPAM plugin.
/// Requires Proxmox VE 8.0 or later.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveSdnIpam", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public class RemovePveSdnIpamCmdlet : PveCmdletBase
{
/// <summary>The IPAM plugin identifier to remove.</summary>
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, HelpMessage = "The IPAM plugin identifier to remove.")]
public string Ipam { get; set; } = string.Empty;
protected override void ProcessRecord()
{
if (!ShouldProcess($"SDN IPAM '{Ipam}'", "Remove PVE SDN IPAM"))
return;
var session = GetSession();
var service = new NetworkService();
WriteVerbose($"Removing SDN IPAM plugin '{Ipam}'...");
service.RemoveSdnIpam(session, Ipam);
}
}
}
@@ -10,7 +10,7 @@ namespace PSProxmoxVE.Cmdlets.Users
/// Built-in roles cannot be removed.
/// </para>
/// </summary>
[Cmdlet(VerbsCommon.Remove, "PveRole", SupportsShouldProcess = true)]
[Cmdlet(VerbsCommon.Remove, "PveRole", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public class RemovePveRoleCmdlet : PveCmdletBase
{
/// <summary>The role identifier to remove.</summary>
+502
View File
@@ -442,5 +442,507 @@
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Firewall.PveFirewallRule -->
<View>
<Name>PSProxmoxVE.Core.Models.Firewall.PveFirewallRule</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Firewall.PveFirewallRule</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Pos</Label>
<Width>5</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Type</Label>
<Width>6</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Action</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Proto</Label>
<Width>6</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Dport</Label>
<Width>12</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Source</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Dest</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Enable</Label>
<Width>7</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Pos</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Action</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Proto</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Dport</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Source</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Dest</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Enable -eq 1) { 'Yes' } else { 'No' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Firewall.PveFirewallGroup -->
<View>
<Name>PSProxmoxVE.Core.Models.Firewall.PveFirewallGroup</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Firewall.PveFirewallGroup</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Group</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Group</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Firewall.PveFirewallAlias -->
<View>
<Name>PSProxmoxVE.Core.Models.Firewall.PveFirewallAlias</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Firewall.PveFirewallAlias</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Cidr</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Cidr</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Firewall.PveFirewallIpSet -->
<View>
<Name>PSProxmoxVE.Core.Models.Firewall.PveFirewallIpSet</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Firewall.PveFirewallIpSet</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Firewall.PveFirewallIpSetEntry -->
<View>
<Name>PSProxmoxVE.Core.Models.Firewall.PveFirewallIpSetEntry</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Firewall.PveFirewallIpSetEntry</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Cidr</Label>
<Width>25</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>NoMatch</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Cidr</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.NoMatch -eq 1) { 'Yes' } else { 'No' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Firewall.PveFirewallOptions -->
<View>
<Name>PSProxmoxVE.Core.Models.Firewall.PveFirewallOptions</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Firewall.PveFirewallOptions</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Enable</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PolicyIn</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PolicyOut</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>LogLevelIn</Label>
<Width>12</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>LogLevelOut</Label>
<Width>12</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<ScriptBlock>if ($_.Enable -eq 1) { 'Yes' } else { 'No' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>PolicyIn</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>PolicyOut</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>LogLevelIn</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>LogLevelOut</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Backup.PveBackupJob -->
<View>
<Name>PSProxmoxVE.Core.Models.Backup.PveBackupJob</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Backup.PveBackupJob</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Id</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Enabled</Label>
<Width>8</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Schedule</Label>
<Width>16</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Storage</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Mode</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>VmId</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comment</Label>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Id</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>if ($_.Enabled -eq 1) { 'Yes' } else { 'No' }</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Schedule</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Storage</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Mode</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>VmId</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Comment</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Network.PveSdnIpam -->
<View>
<Name>PSProxmoxVE.Core.Models.Network.PveSdnIpam</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Network.PveSdnIpam</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Ipam</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Type</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Ipam</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Network.PveSdnDns -->
<View>
<Name>PSProxmoxVE.Core.Models.Network.PveSdnDns</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Network.PveSdnDns</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Dns</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Type</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Url</Label>
<Width>40</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Dns</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Url</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<!-- PSProxmoxVE.Core.Models.Network.PveSdnController -->
<View>
<Name>PSProxmoxVE.Core.Models.Network.PveSdnController</Name>
<ViewSelectedBy>
<TypeName>PSProxmoxVE.Core.Models.Network.PveSdnController</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Controller</Label>
<Width>20</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Type</Label>
<Width>10</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Asn</Label>
<Width>10</Width>
<Alignment>Right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Peers</Label>
<Width>30</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Node</Label>
<Width>15</Width>
<Alignment>Left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Controller</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Type</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Asn</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Peers</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Node</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
+46 -1
View File
@@ -170,7 +170,52 @@
# Tasks
'Get-PveTask',
'Wait-PveTask'
'Wait-PveTask',
# Firewall
'Get-PveFirewallRule',
'New-PveFirewallRule',
'Set-PveFirewallRule',
'Remove-PveFirewallRule',
'Get-PveFirewallGroup',
'New-PveFirewallGroup',
'Remove-PveFirewallGroup',
'Get-PveFirewallAlias',
'New-PveFirewallAlias',
'Set-PveFirewallAlias',
'Remove-PveFirewallAlias',
'Get-PveFirewallIpSet',
'New-PveFirewallIpSet',
'Remove-PveFirewallIpSet',
'Get-PveFirewallIpSetEntry',
'New-PveFirewallIpSetEntry',
'Set-PveFirewallIpSetEntry',
'Remove-PveFirewallIpSetEntry',
'Get-PveFirewallOptions',
'Set-PveFirewallOptions',
'Get-PveFirewallRef',
# Backup
'New-PveBackup',
'Get-PveBackupJob',
'New-PveBackupJob',
'Set-PveBackupJob',
'Remove-PveBackupJob',
# SDN — IPAM
'Get-PveSdnIpam',
'New-PveSdnIpam',
'Remove-PveSdnIpam',
# SDN — DNS
'Get-PveSdnDns',
'New-PveSdnDns',
'Remove-PveSdnDns',
# SDN — Controller
'Get-PveSdnController',
'New-PveSdnController',
'Remove-PveSdnController'
)
# Variables to export from this module
@@ -0,0 +1 @@
{"data":[{"id":"backup-1a2b3c","type":"vzdump","enabled":1,"schedule":"0 2 * * *","storage":"local","mode":"snapshot","compress":"zstd","vmid":"100,101","comment":"Nightly backup"},{"id":"backup-4d5e6f","type":"vzdump","enabled":0,"schedule":"0 4 * * 0","storage":"nfs-backup","mode":"stop","all":1,"comment":"Weekly full"}]}
@@ -0,0 +1 @@
{"data":[{"name":"local_network","cidr":"10.0.0.0/8","comment":"RFC1918 private"},{"name":"dns_server","cidr":"8.8.8.8","comment":"Google DNS"}]}
@@ -0,0 +1 @@
{"data":[{"group":"webservers","comment":"HTTP/HTTPS servers","digest":"abc123"},{"group":"dbservers","comment":"Database servers","digest":"def456"}]}
@@ -0,0 +1 @@
{"data":[{"cidr":"192.168.1.0/24","nomatch":0,"comment":"Local subnet"},{"cidr":"10.0.0.0/8","nomatch":1,"comment":"Exclude private"}]}
@@ -0,0 +1 @@
{"data":[{"name":"blocklist","comment":"Blocked IPs","digest":"aaa111"},{"name":"allowlist","comment":"Allowed IPs","digest":"bbb222"}]}
@@ -0,0 +1 @@
{"data":{"enable":1,"policy_in":"DROP","policy_out":"ACCEPT","log_level_in":"nolog","log_level_out":"nolog"}}
@@ -0,0 +1 @@
{"data":[{"type":"alias","name":"local_network","comment":"RFC1918 private"},{"type":"ipset","name":"blocklist","comment":"Blocked IPs"}]}
@@ -0,0 +1 @@
{"data":[{"pos":0,"type":"in","action":"ACCEPT","proto":"tcp","dport":"80","enable":1,"comment":"Allow HTTP","log":"nolog"},{"pos":1,"type":"in","action":"ACCEPT","proto":"tcp","dport":"443","enable":1,"comment":"Allow HTTPS"},{"pos":2,"type":"out","action":"DROP","enable":0,"comment":"Block outbound (disabled)"}]}
@@ -0,0 +1 @@
{"data":[{"controller":"evpn1","type":"evpn","asn":65000,"peers":"10.0.0.1,10.0.0.2","node":"pve1"}]}
@@ -0,0 +1 @@
{"data":[{"dns":"powerdns1","type":"powerdns","url":"http://dns.example.com:8081","key":"secret123","ttl":3600}]}
@@ -0,0 +1 @@
{"data":[{"ipam":"pve","type":"pve"},{"ipam":"netbox1","type":"netbox","url":"https://netbox.example.com","token":"abc123","section":1}]}
@@ -0,0 +1,126 @@
using Xunit;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Models.Backup;
namespace PSProxmoxVE.Core.Tests.Models
{
public class BackupModelTests
{
[Fact]
public void PveBackupJob_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal(2, jobs.Length);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_FirstJob_HasCorrectId()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal("backup-1a2b3c", jobs[0].Id);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_FirstJob_IsEnabled()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal(1, jobs[0].Enabled);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_FirstJob_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal("vzdump", jobs[0].Type);
Assert.Equal("0 2 * * *", jobs[0].Schedule);
Assert.Equal("local", jobs[0].Storage);
Assert.Equal("snapshot", jobs[0].Mode);
Assert.Equal("zstd", jobs[0].Compress);
Assert.Equal("100,101", jobs[0].VmId);
Assert.Equal("Nightly backup", jobs[0].Comment);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_FirstJob_AllIsNull()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Null(jobs[0].All);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_SecondJob_IsDisabled()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal(0, jobs[1].Enabled);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_SecondJob_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal("backup-4d5e6f", jobs[1].Id);
Assert.Equal("vzdump", jobs[1].Type);
Assert.Equal("0 4 * * 0", jobs[1].Schedule);
Assert.Equal("nfs-backup", jobs[1].Storage);
Assert.Equal("stop", jobs[1].Mode);
Assert.Equal("Weekly full", jobs[1].Comment);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_SecondJob_BacksUpAll()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Equal(1, jobs[1].All);
Assert.Null(jobs[1].VmId);
}
[Fact]
public void PveBackupJob_Deserialize_Pve9_SecondJob_OptionalFieldsAreNull()
{
var json = TestHelper.LoadFixture("pve9_backup_jobs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var jobs = data.ToObject<PveBackupJob[]>();
Assert.NotNull(jobs);
Assert.Null(jobs[1].Compress);
Assert.Null(jobs[1].MaxFiles);
Assert.Null(jobs[1].PruneBackups);
Assert.Null(jobs[1].MailNotification);
Assert.Null(jobs[1].MailTo);
Assert.Null(jobs[1].Node);
Assert.Null(jobs[1].Exclude);
}
}
}
@@ -0,0 +1,330 @@
using Xunit;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Models.Firewall;
namespace PSProxmoxVE.Core.Tests.Models
{
public class FirewallModelTests
{
// ── PveFirewallRule ──────────────────────────────────────────────
[Fact]
public void PveFirewallRule_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_firewall_rules.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var rules = data.ToObject<PveFirewallRule[]>();
Assert.NotNull(rules);
Assert.Equal(3, rules.Length);
}
[Fact]
public void PveFirewallRule_Deserialize_Pve9_FirstRule_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_rules.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var rules = data.ToObject<PveFirewallRule[]>();
Assert.NotNull(rules);
Assert.Equal(0, rules[0].Pos);
Assert.Equal("in", rules[0].Type);
Assert.Equal("ACCEPT", rules[0].Action);
Assert.Equal("tcp", rules[0].Proto);
Assert.Equal("80", rules[0].Dport);
Assert.Equal(1, rules[0].Enable);
Assert.Equal("Allow HTTP", rules[0].Comment);
}
[Fact]
public void PveFirewallRule_Deserialize_Pve9_FirstRule_HasLogLevel()
{
var json = TestHelper.LoadFixture("pve9_firewall_rules.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var rules = data.ToObject<PveFirewallRule[]>();
Assert.NotNull(rules);
Assert.NotNull(rules[0].Log);
Assert.Equal("nolog", rules[0].Log);
}
[Fact]
public void PveFirewallRule_Deserialize_Pve9_SecondRule_LogIsNull()
{
var json = TestHelper.LoadFixture("pve9_firewall_rules.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var rules = data.ToObject<PveFirewallRule[]>();
Assert.NotNull(rules);
Assert.Null(rules[1].Log);
}
[Fact]
public void PveFirewallRule_Deserialize_Pve9_ThirdRule_IsOutboundDrop()
{
var json = TestHelper.LoadFixture("pve9_firewall_rules.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var rules = data.ToObject<PveFirewallRule[]>();
Assert.NotNull(rules);
Assert.Equal(2, rules[2].Pos);
Assert.Equal("out", rules[2].Type);
Assert.Equal("DROP", rules[2].Action);
Assert.Equal(0, rules[2].Enable);
}
[Fact]
public void PveFirewallRule_Deserialize_Pve9_ThirdRule_NullableFieldsAreNull()
{
var json = TestHelper.LoadFixture("pve9_firewall_rules.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var rules = data.ToObject<PveFirewallRule[]>();
Assert.NotNull(rules);
Assert.Null(rules[2].Proto);
Assert.Null(rules[2].Dport);
Assert.Null(rules[2].Sport);
Assert.Null(rules[2].Source);
Assert.Null(rules[2].Dest);
Assert.Null(rules[2].Macro);
Assert.Null(rules[2].Iface);
}
// ── PveFirewallGroup ────────────────────────────────────────────
[Fact]
public void PveFirewallGroup_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_firewall_groups.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var groups = data.ToObject<PveFirewallGroup[]>();
Assert.NotNull(groups);
Assert.Equal(2, groups.Length);
}
[Fact]
public void PveFirewallGroup_Deserialize_Pve9_FirstGroup_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_groups.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var groups = data.ToObject<PveFirewallGroup[]>();
Assert.NotNull(groups);
Assert.Equal("webservers", groups[0].Group);
Assert.Equal("HTTP/HTTPS servers", groups[0].Comment);
Assert.Equal("abc123", groups[0].Digest);
}
[Fact]
public void PveFirewallGroup_Deserialize_Pve9_SecondGroup_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_groups.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var groups = data.ToObject<PveFirewallGroup[]>();
Assert.NotNull(groups);
Assert.Equal("dbservers", groups[1].Group);
Assert.Equal("Database servers", groups[1].Comment);
Assert.Equal("def456", groups[1].Digest);
}
// ── PveFirewallAlias ────────────────────────────────────────────
[Fact]
public void PveFirewallAlias_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_firewall_aliases.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var aliases = data.ToObject<PveFirewallAlias[]>();
Assert.NotNull(aliases);
Assert.Equal(2, aliases.Length);
}
[Fact]
public void PveFirewallAlias_Deserialize_Pve9_FirstAlias_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_aliases.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var aliases = data.ToObject<PveFirewallAlias[]>();
Assert.NotNull(aliases);
Assert.Equal("local_network", aliases[0].Name);
Assert.Equal("10.0.0.0/8", aliases[0].Cidr);
Assert.Equal("RFC1918 private", aliases[0].Comment);
}
[Fact]
public void PveFirewallAlias_Deserialize_Pve9_SecondAlias_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_aliases.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var aliases = data.ToObject<PveFirewallAlias[]>();
Assert.NotNull(aliases);
Assert.Equal("dns_server", aliases[1].Name);
Assert.Equal("8.8.8.8", aliases[1].Cidr);
Assert.Equal("Google DNS", aliases[1].Comment);
}
// ── PveFirewallIpSet ────────────────────────────────────────────
[Fact]
public void PveFirewallIpSet_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_firewall_ipsets.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipsets = data.ToObject<PveFirewallIpSet[]>();
Assert.NotNull(ipsets);
Assert.Equal(2, ipsets.Length);
}
[Fact]
public void PveFirewallIpSet_Deserialize_Pve9_FirstIpSet_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_ipsets.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipsets = data.ToObject<PveFirewallIpSet[]>();
Assert.NotNull(ipsets);
Assert.Equal("blocklist", ipsets[0].Name);
Assert.Equal("Blocked IPs", ipsets[0].Comment);
Assert.Equal("aaa111", ipsets[0].Digest);
}
[Fact]
public void PveFirewallIpSet_Deserialize_Pve9_SecondIpSet_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_ipsets.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipsets = data.ToObject<PveFirewallIpSet[]>();
Assert.NotNull(ipsets);
Assert.Equal("allowlist", ipsets[1].Name);
Assert.Equal("Allowed IPs", ipsets[1].Comment);
Assert.Equal("bbb222", ipsets[1].Digest);
}
// ── PveFirewallIpSetEntry ───────────────────────────────────────
[Fact]
public void PveFirewallIpSetEntry_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_firewall_ipset_entries.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var entries = data.ToObject<PveFirewallIpSetEntry[]>();
Assert.NotNull(entries);
Assert.Equal(2, entries.Length);
}
[Fact]
public void PveFirewallIpSetEntry_Deserialize_Pve9_FirstEntry_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_ipset_entries.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var entries = data.ToObject<PveFirewallIpSetEntry[]>();
Assert.NotNull(entries);
Assert.Equal("192.168.1.0/24", entries[0].Cidr);
Assert.Equal(0, entries[0].NoMatch);
Assert.Equal("Local subnet", entries[0].Comment);
}
[Fact]
public void PveFirewallIpSetEntry_Deserialize_Pve9_SecondEntry_IsExclusion()
{
var json = TestHelper.LoadFixture("pve9_firewall_ipset_entries.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var entries = data.ToObject<PveFirewallIpSetEntry[]>();
Assert.NotNull(entries);
Assert.Equal("10.0.0.0/8", entries[1].Cidr);
Assert.Equal(1, entries[1].NoMatch);
Assert.Equal("Exclude private", entries[1].Comment);
}
// ── PveFirewallOptions ──────────────────────────────────────────
[Fact]
public void PveFirewallOptions_Deserialize_Pve9_ReturnsNotNull()
{
var json = TestHelper.LoadFixture("pve9_firewall_options.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var options = data.ToObject<PveFirewallOptions>();
Assert.NotNull(options);
}
[Fact]
public void PveFirewallOptions_Deserialize_Pve9_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_firewall_options.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var options = data.ToObject<PveFirewallOptions>();
Assert.NotNull(options);
Assert.Equal(1, options.Enable);
Assert.Equal("DROP", options.PolicyIn);
Assert.Equal("ACCEPT", options.PolicyOut);
Assert.Equal("nolog", options.LogLevelIn);
Assert.Equal("nolog", options.LogLevelOut);
}
[Fact]
public void PveFirewallOptions_Deserialize_Pve9_OptionalFieldsAreNull()
{
var json = TestHelper.LoadFixture("pve9_firewall_options.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var options = data.ToObject<PveFirewallOptions>();
Assert.NotNull(options);
Assert.Null(options.Dhcp);
Assert.Null(options.Ndp);
Assert.Null(options.Radv);
Assert.Null(options.MacFilter);
Assert.Null(options.IpFilter);
}
// ── PveFirewallRef ──────────────────────────────────────────────
[Fact]
public void PveFirewallRef_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_firewall_refs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var refs = data.ToObject<PveFirewallRef[]>();
Assert.NotNull(refs);
Assert.Equal(2, refs.Length);
}
[Fact]
public void PveFirewallRef_Deserialize_Pve9_FirstRef_IsAlias()
{
var json = TestHelper.LoadFixture("pve9_firewall_refs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var refs = data.ToObject<PveFirewallRef[]>();
Assert.NotNull(refs);
Assert.Equal("alias", refs[0].Type);
Assert.Equal("local_network", refs[0].Name);
Assert.Equal("RFC1918 private", refs[0].Comment);
}
[Fact]
public void PveFirewallRef_Deserialize_Pve9_SecondRef_IsIpSet()
{
var json = TestHelper.LoadFixture("pve9_firewall_refs.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var refs = data.ToObject<PveFirewallRef[]>();
Assert.NotNull(refs);
Assert.Equal("ipset", refs[1].Type);
Assert.Equal("blocklist", refs[1].Name);
Assert.Equal("Blocked IPs", refs[1].Comment);
}
}
}
@@ -142,5 +142,125 @@ namespace PSProxmoxVE.Core.Tests.Models
// Comments not present in fixture entries
Assert.Null(vnets[0].Comments);
}
// ── PveSdnIpam ─────────────────────────────────────────────────
[Fact]
public void PveSdnIpam_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_sdn_ipams.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipams = data.ToObject<PveSdnIpam[]>();
Assert.NotNull(ipams);
Assert.Equal(2, ipams.Length);
}
[Fact]
public void PveSdnIpam_Deserialize_Pve9_FirstIpam_IsPveType()
{
var json = TestHelper.LoadFixture("pve9_sdn_ipams.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipams = data.ToObject<PveSdnIpam[]>();
Assert.NotNull(ipams);
Assert.Equal("pve", ipams[0].Ipam);
Assert.Equal("pve", ipams[0].Type);
}
[Fact]
public void PveSdnIpam_Deserialize_Pve9_FirstIpam_OptionalFieldsAreNull()
{
var json = TestHelper.LoadFixture("pve9_sdn_ipams.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipams = data.ToObject<PveSdnIpam[]>();
Assert.NotNull(ipams);
Assert.Null(ipams[0].Url);
Assert.Null(ipams[0].Token);
Assert.Null(ipams[0].Section);
}
[Fact]
public void PveSdnIpam_Deserialize_Pve9_SecondIpam_HasNetboxProperties()
{
var json = TestHelper.LoadFixture("pve9_sdn_ipams.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var ipams = data.ToObject<PveSdnIpam[]>();
Assert.NotNull(ipams);
Assert.Equal("netbox1", ipams[1].Ipam);
Assert.Equal("netbox", ipams[1].Type);
Assert.Equal("https://netbox.example.com", ipams[1].Url);
Assert.Equal("abc123", ipams[1].Token);
Assert.Equal(1, ipams[1].Section);
}
// ── PveSdnDns ──────────────────────────────────────────────────
[Fact]
public void PveSdnDns_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_sdn_dns.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var dnsEntries = data.ToObject<PveSdnDns[]>();
Assert.NotNull(dnsEntries);
Assert.Single(dnsEntries);
}
[Fact]
public void PveSdnDns_Deserialize_Pve9_FirstEntry_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_sdn_dns.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var dnsEntries = data.ToObject<PveSdnDns[]>();
Assert.NotNull(dnsEntries);
Assert.Equal("powerdns1", dnsEntries[0].Dns);
Assert.Equal("powerdns", dnsEntries[0].Type);
Assert.Equal("http://dns.example.com:8081", dnsEntries[0].Url);
Assert.Equal("secret123", dnsEntries[0].Key);
Assert.Equal(3600, dnsEntries[0].Ttl);
}
[Fact]
public void PveSdnDns_Deserialize_Pve9_FirstEntry_OptionalFieldsAreNull()
{
var json = TestHelper.LoadFixture("pve9_sdn_dns.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var dnsEntries = data.ToObject<PveSdnDns[]>();
Assert.NotNull(dnsEntries);
Assert.Null(dnsEntries[0].ReverseMaskV6);
}
// ── PveSdnController ───────────────────────────────────────────
[Fact]
public void PveSdnController_Deserialize_Pve9_ReturnsCorrectCount()
{
var json = TestHelper.LoadFixture("pve9_sdn_controllers.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var controllers = data.ToObject<PveSdnController[]>();
Assert.NotNull(controllers);
Assert.Single(controllers);
}
[Fact]
public void PveSdnController_Deserialize_Pve9_FirstController_HasCorrectProperties()
{
var json = TestHelper.LoadFixture("pve9_sdn_controllers.json");
var data = JObject.Parse(json)["data"];
Assert.NotNull(data);
var controllers = data.ToObject<PveSdnController[]>();
Assert.NotNull(controllers);
Assert.Equal("evpn1", controllers[0].Controller);
Assert.Equal("evpn", controllers[0].Type);
Assert.Equal(65000, controllers[0].Asn);
Assert.Equal("10.0.0.1,10.0.0.2", controllers[0].Peers);
Assert.Equal("pve1", controllers[0].Node);
}
}
}
@@ -0,0 +1,291 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for backup cmdlets:
New-PveBackup, Get-PveBackupJob, New-PveBackupJob, Set-PveBackupJob, Remove-PveBackupJob.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @('New-PveBackup', 'Get-PveBackupJob', 'New-PveBackupJob', 'Set-PveBackupJob', 'Remove-PveBackupJob')) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'Backup cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'New-PveBackup' }
@{ cmdName = 'Get-PveBackupJob' }
@{ cmdName = 'New-PveBackupJob' }
@{ cmdName = 'Set-PveBackupJob' }
@{ cmdName = 'Remove-PveBackupJob' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ---------------------------------------------------------------------------
# New-PveBackup
# ---------------------------------------------------------------------------
Describe 'New-PveBackup' {
BeforeAll { $script:Cmd = Get-Command 'New-PveBackup' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Parameter metadata' {
It 'Should have VmId parameter' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd.Parameters.ContainsKey('VmId') | Should -BeTrue
}
It 'Should have Storage parameter' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd.Parameters.ContainsKey('Storage') | Should -BeTrue
}
It 'Should have Node parameter' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd.Parameters.ContainsKey('Node') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'New-PveBackup'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveBackup'
{ New-PveBackup -VmId 100 -Storage 'local' -Node 'pve' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Get-PveBackupJob
# ---------------------------------------------------------------------------
Describe 'Get-PveBackupJob' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveBackupJob' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveBackupJob'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveBackupJob'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Id parameter' {
Skip-IfMissing 'Get-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('Id') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveBackupJob'
{ Get-PveBackupJob -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveBackupJob
# ---------------------------------------------------------------------------
Describe 'New-PveBackupJob' {
BeforeAll { $script:Cmd = Get-Command 'New-PveBackupJob' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveBackupJob'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Storage should be Mandatory' {
Skip-IfMissing 'New-PveBackupJob'
$isMandatory = $script:Cmd.Parameters['Storage'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Schedule should be Mandatory' {
Skip-IfMissing 'New-PveBackupJob'
$isMandatory = $script:Cmd.Parameters['Schedule'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Optional parameters' {
It 'Should have VmId parameter' {
Skip-IfMissing 'New-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('VmId') | Should -BeTrue
}
It 'Should have Mode parameter' {
Skip-IfMissing 'New-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('Mode') | Should -BeTrue
}
It 'Should have Compress parameter' {
Skip-IfMissing 'New-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('Compress') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveBackupJob'
{ New-PveBackupJob -Storage 'local' -Schedule 'daily' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Set-PveBackupJob
# ---------------------------------------------------------------------------
Describe 'Set-PveBackupJob' {
BeforeAll { $script:Cmd = Get-Command 'Set-PveBackupJob' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Set-PveBackupJob'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'Set-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Id should be Mandatory' {
Skip-IfMissing 'Set-PveBackupJob'
$isMandatory = $script:Cmd.Parameters['Id'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Set-PveBackupJob'
{ Set-PveBackupJob -Id 'backup-test' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveBackupJob
# ---------------------------------------------------------------------------
Describe 'Remove-PveBackupJob' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveBackupJob' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveBackupJob'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveBackupJob'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveBackupJob'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Id should be Mandatory' {
Skip-IfMissing 'Remove-PveBackupJob'
$isMandatory = $script:Cmd.Parameters['Id'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveBackupJob'
{ Remove-PveBackupJob -Id 'backup-test' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
@@ -0,0 +1,230 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for firewall alias cmdlets:
Get-PveFirewallAlias, New-PveFirewallAlias, Set-PveFirewallAlias, Remove-PveFirewallAlias.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @('Get-PveFirewallAlias', 'New-PveFirewallAlias', 'Set-PveFirewallAlias', 'Remove-PveFirewallAlias')) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'Firewall alias cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'Get-PveFirewallAlias' }
@{ cmdName = 'New-PveFirewallAlias' }
@{ cmdName = 'Set-PveFirewallAlias' }
@{ cmdName = 'Remove-PveFirewallAlias' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallAlias
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallAlias' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallAlias' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallAlias'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallAlias'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Name parameter' {
Skip-IfMissing 'Get-PveFirewallAlias'
$script:Cmd.Parameters.ContainsKey('Name') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallAlias'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallAlias'
{ Get-PveFirewallAlias -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveFirewallAlias
# ---------------------------------------------------------------------------
Describe 'New-PveFirewallAlias' {
BeforeAll { $script:Cmd = Get-Command 'New-PveFirewallAlias' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveFirewallAlias'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveFirewallAlias'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'New-PveFirewallAlias'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Cidr should be Mandatory' {
Skip-IfMissing 'New-PveFirewallAlias'
$isMandatory = $script:Cmd.Parameters['Cidr'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Optional parameters' {
It 'Should have Comment parameter' {
Skip-IfMissing 'New-PveFirewallAlias'
$script:Cmd.Parameters.ContainsKey('Comment') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveFirewallAlias'
{ New-PveFirewallAlias -Name 'testalias' -Cidr '10.0.0.0/24' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Set-PveFirewallAlias
# ---------------------------------------------------------------------------
Describe 'Set-PveFirewallAlias' {
BeforeAll { $script:Cmd = Get-Command 'Set-PveFirewallAlias' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Set-PveFirewallAlias'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'Set-PveFirewallAlias'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'Set-PveFirewallAlias'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Cidr should be Mandatory' {
Skip-IfMissing 'Set-PveFirewallAlias'
$isMandatory = $script:Cmd.Parameters['Cidr'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Set-PveFirewallAlias'
{ Set-PveFirewallAlias -Name 'testalias' -Cidr '10.0.0.0/24' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveFirewallAlias
# ---------------------------------------------------------------------------
Describe 'Remove-PveFirewallAlias' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveFirewallAlias' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveFirewallAlias'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveFirewallAlias'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveFirewallAlias'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'Remove-PveFirewallAlias'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveFirewallAlias'
{ Remove-PveFirewallAlias -Name 'testalias' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
@@ -0,0 +1,176 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for firewall group cmdlets:
Get-PveFirewallGroup, New-PveFirewallGroup, Remove-PveFirewallGroup.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @('Get-PveFirewallGroup', 'New-PveFirewallGroup', 'Remove-PveFirewallGroup')) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'Firewall group cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'Get-PveFirewallGroup' }
@{ cmdName = 'New-PveFirewallGroup' }
@{ cmdName = 'Remove-PveFirewallGroup' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallGroup
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallGroup' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallGroup' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallGroup'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallGroup'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Group parameter' {
Skip-IfMissing 'Get-PveFirewallGroup'
$script:Cmd.Parameters.ContainsKey('Group') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallGroup'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallGroup'
{ Get-PveFirewallGroup -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveFirewallGroup
# ---------------------------------------------------------------------------
Describe 'New-PveFirewallGroup' {
BeforeAll { $script:Cmd = Get-Command 'New-PveFirewallGroup' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveFirewallGroup'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveFirewallGroup'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Group should be Mandatory' {
Skip-IfMissing 'New-PveFirewallGroup'
$isMandatory = $script:Cmd.Parameters['Group'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Optional parameters' {
It 'Should have Comment parameter' {
Skip-IfMissing 'New-PveFirewallGroup'
$script:Cmd.Parameters.ContainsKey('Comment') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveFirewallGroup'
{ New-PveFirewallGroup -Group 'testgroup' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveFirewallGroup
# ---------------------------------------------------------------------------
Describe 'Remove-PveFirewallGroup' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveFirewallGroup' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveFirewallGroup'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveFirewallGroup'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveFirewallGroup'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Group should be Mandatory' {
Skip-IfMissing 'Remove-PveFirewallGroup'
$isMandatory = $script:Cmd.Parameters['Group'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveFirewallGroup'
{ Remove-PveFirewallGroup -Group 'testgroup' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
@@ -0,0 +1,376 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for firewall IP set cmdlets:
Get-PveFirewallIpSet, New-PveFirewallIpSet, Remove-PveFirewallIpSet,
Get-PveFirewallIpSetEntry, New-PveFirewallIpSetEntry, Set-PveFirewallIpSetEntry,
Remove-PveFirewallIpSetEntry.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @(
'Get-PveFirewallIpSet', 'New-PveFirewallIpSet', 'Remove-PveFirewallIpSet',
'Get-PveFirewallIpSetEntry', 'New-PveFirewallIpSetEntry', 'Set-PveFirewallIpSetEntry',
'Remove-PveFirewallIpSetEntry'
)) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'Firewall IP set cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'Get-PveFirewallIpSet' }
@{ cmdName = 'New-PveFirewallIpSet' }
@{ cmdName = 'Remove-PveFirewallIpSet' }
@{ cmdName = 'Get-PveFirewallIpSetEntry' }
@{ cmdName = 'New-PveFirewallIpSetEntry' }
@{ cmdName = 'Set-PveFirewallIpSetEntry' }
@{ cmdName = 'Remove-PveFirewallIpSetEntry' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallIpSet
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallIpSet' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallIpSet' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallIpSet'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallIpSet'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Name parameter' {
Skip-IfMissing 'Get-PveFirewallIpSet'
$script:Cmd.Parameters.ContainsKey('Name') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallIpSet'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallIpSet'
{ Get-PveFirewallIpSet -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveFirewallIpSet
# ---------------------------------------------------------------------------
Describe 'New-PveFirewallIpSet' {
BeforeAll { $script:Cmd = Get-Command 'New-PveFirewallIpSet' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveFirewallIpSet'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveFirewallIpSet'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'New-PveFirewallIpSet'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Optional parameters' {
It 'Should have Comment parameter' {
Skip-IfMissing 'New-PveFirewallIpSet'
$script:Cmd.Parameters.ContainsKey('Comment') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveFirewallIpSet'
{ New-PveFirewallIpSet -Name 'testipset' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveFirewallIpSet
# ---------------------------------------------------------------------------
Describe 'Remove-PveFirewallIpSet' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveFirewallIpSet' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveFirewallIpSet'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveFirewallIpSet'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveFirewallIpSet'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'Remove-PveFirewallIpSet'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveFirewallIpSet'
{ Remove-PveFirewallIpSet -Name 'testipset' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallIpSetEntry
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallIpSetEntry' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallIpSetEntry' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallIpSetEntry'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallIpSetEntry'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'Get-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Parameter metadata' {
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallIpSetEntry'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallIpSetEntry'
{ Get-PveFirewallIpSetEntry -Name 'testipset' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveFirewallIpSetEntry
# ---------------------------------------------------------------------------
Describe 'New-PveFirewallIpSetEntry' {
BeforeAll { $script:Cmd = Get-Command 'New-PveFirewallIpSetEntry' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveFirewallIpSetEntry'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveFirewallIpSetEntry'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'New-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Cidr should be Mandatory' {
Skip-IfMissing 'New-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Cidr'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveFirewallIpSetEntry'
{ New-PveFirewallIpSetEntry -Name 'testipset' -Cidr '10.0.0.1' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Set-PveFirewallIpSetEntry
# ---------------------------------------------------------------------------
Describe 'Set-PveFirewallIpSetEntry' {
BeforeAll { $script:Cmd = Get-Command 'Set-PveFirewallIpSetEntry' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Set-PveFirewallIpSetEntry'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'Set-PveFirewallIpSetEntry'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'Set-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Cidr should be Mandatory' {
Skip-IfMissing 'Set-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Cidr'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Set-PveFirewallIpSetEntry'
{ Set-PveFirewallIpSetEntry -Name 'testipset' -Cidr '10.0.0.1' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveFirewallIpSetEntry
# ---------------------------------------------------------------------------
Describe 'Remove-PveFirewallIpSetEntry' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveFirewallIpSetEntry' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveFirewallIpSetEntry'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveFirewallIpSetEntry'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveFirewallIpSetEntry'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Name should be Mandatory' {
Skip-IfMissing 'Remove-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Name'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Cidr should be Mandatory' {
Skip-IfMissing 'Remove-PveFirewallIpSetEntry'
$isMandatory = $script:Cmd.Parameters['Cidr'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveFirewallIpSetEntry'
{ Remove-PveFirewallIpSetEntry -Name 'testipset' -Cidr '10.0.0.1' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
@@ -0,0 +1,175 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for firewall options/ref cmdlets:
Get-PveFirewallOptions, Set-PveFirewallOptions, Get-PveFirewallRef.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @('Get-PveFirewallOptions', 'Set-PveFirewallOptions', 'Get-PveFirewallRef')) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'Firewall options/ref cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'Get-PveFirewallOptions' }
@{ cmdName = 'Set-PveFirewallOptions' }
@{ cmdName = 'Get-PveFirewallRef' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallOptions
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallOptions' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallOptions' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallOptions'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallOptions'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have mandatory Level parameter with ValidateSet' {
Skip-IfMissing 'Get-PveFirewallOptions'
$param = $script:Cmd.Parameters['Level']
$param | Should -Not -BeNullOrEmpty
$isMandatory = $param.ParameterSets.Values | Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
$validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
$validateSet | Should -Not -BeNullOrEmpty
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallOptions'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallOptions'
{ Get-PveFirewallOptions -Level 'cluster' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Set-PveFirewallOptions
# ---------------------------------------------------------------------------
Describe 'Set-PveFirewallOptions' {
BeforeAll { $script:Cmd = Get-Command 'Set-PveFirewallOptions' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Set-PveFirewallOptions'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'Set-PveFirewallOptions'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Parameter metadata' {
It 'Should have mandatory Level parameter with ValidateSet' {
Skip-IfMissing 'Set-PveFirewallOptions'
$param = $script:Cmd.Parameters['Level']
$param | Should -Not -BeNullOrEmpty
$isMandatory = $param.ParameterSets.Values | Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
$validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
$validateSet | Should -Not -BeNullOrEmpty
}
It 'Should have Enable parameter' {
Skip-IfMissing 'Set-PveFirewallOptions'
$script:Cmd.Parameters.ContainsKey('Enable') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Set-PveFirewallOptions'
{ Set-PveFirewallOptions -Level 'cluster' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallRef
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallRef' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallRef' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallRef'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallRef'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallRef'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
It 'Should have optional Type parameter' {
Skip-IfMissing 'Get-PveFirewallRef'
$script:Cmd.Parameters.ContainsKey('Type') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallRef'
{ Get-PveFirewallRef -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
@@ -0,0 +1,243 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for firewall rule cmdlets:
Get-PveFirewallRule, New-PveFirewallRule, Set-PveFirewallRule, Remove-PveFirewallRule.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @('Get-PveFirewallRule', 'New-PveFirewallRule', 'Set-PveFirewallRule', 'Remove-PveFirewallRule')) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'Firewall rule cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'Get-PveFirewallRule' }
@{ cmdName = 'New-PveFirewallRule' }
@{ cmdName = 'Set-PveFirewallRule' }
@{ cmdName = 'Remove-PveFirewallRule' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ---------------------------------------------------------------------------
# Get-PveFirewallRule
# ---------------------------------------------------------------------------
Describe 'Get-PveFirewallRule' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveFirewallRule' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveFirewallRule'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveFirewallRule'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have mandatory Level parameter with ValidateSet' {
Skip-IfMissing 'Get-PveFirewallRule'
$param = $script:Cmd.Parameters['Level']
$param | Should -Not -BeNullOrEmpty
$isMandatory = $param.ParameterSets.Values | Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
$validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
$validateSet | Should -Not -BeNullOrEmpty
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveFirewallRule'
{ Get-PveFirewallRule -Level 'cluster' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveFirewallRule
# ---------------------------------------------------------------------------
Describe 'New-PveFirewallRule' {
BeforeAll { $script:Cmd = Get-Command 'New-PveFirewallRule' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveFirewallRule'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Parameter metadata' {
It 'Should have mandatory Level parameter with ValidateSet' {
Skip-IfMissing 'New-PveFirewallRule'
$param = $script:Cmd.Parameters['Level']
$param | Should -Not -BeNullOrEmpty
$isMandatory = $param.ParameterSets.Values | Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
$validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
$validateSet | Should -Not -BeNullOrEmpty
}
It 'Should have Action parameter' {
Skip-IfMissing 'New-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('Action') | Should -BeTrue
}
It 'Should have Type parameter' {
Skip-IfMissing 'New-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('Type') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveFirewallRule'
{ New-PveFirewallRule -Level 'cluster' -Action 'ACCEPT' -Type 'in' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Set-PveFirewallRule
# ---------------------------------------------------------------------------
Describe 'Set-PveFirewallRule' {
BeforeAll { $script:Cmd = Get-Command 'Set-PveFirewallRule' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Set-PveFirewallRule'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'Set-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Parameter metadata' {
It 'Should have mandatory Level parameter with ValidateSet' {
Skip-IfMissing 'Set-PveFirewallRule'
$param = $script:Cmd.Parameters['Level']
$param | Should -Not -BeNullOrEmpty
$isMandatory = $param.ParameterSets.Values | Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
$validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
$validateSet | Should -Not -BeNullOrEmpty
}
It 'Should have Pos parameter' {
Skip-IfMissing 'Set-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('Pos') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Set-PveFirewallRule'
{ Set-PveFirewallRule -Level 'cluster' -Pos 0 -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveFirewallRule
# ---------------------------------------------------------------------------
Describe 'Remove-PveFirewallRule' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveFirewallRule' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveFirewallRule'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveFirewallRule'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Parameter metadata' {
It 'Should have mandatory Level parameter with ValidateSet' {
Skip-IfMissing 'Remove-PveFirewallRule'
$param = $script:Cmd.Parameters['Level']
$param | Should -Not -BeNullOrEmpty
$isMandatory = $param.ParameterSets.Values | Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
$validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
$validateSet | Should -Not -BeNullOrEmpty
}
It 'Should have Pos parameter' {
Skip-IfMissing 'Remove-PveFirewallRule'
$script:Cmd.Parameters.ContainsKey('Pos') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveFirewallRule'
{ Remove-PveFirewallRule -Level 'cluster' -Pos 0 -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
@@ -79,6 +79,8 @@ BeforeAll {
$script:CreatedRoles = [System.Collections.Generic.List[string]]::new()
$script:TestVmId = $null
$script:TestContainerId = $null
$script:FirewallTestRulePos = $null
$script:BackupTestJobId = $null
# Helper functions for skip logic
function script:Skip-IfNoTarget {
@@ -159,6 +161,32 @@ AfterAll {
}
catch { <# non-fatal #> }
}
# Cleanup: firewall test artifacts
try {
$rules = Get-PveFirewallRule -Level Cluster -ErrorAction SilentlyContinue
$testRules = $rules | Where-Object { $_.Comment -like 'pester-test*' }
foreach ($r in $testRules) {
Remove-PveFirewallRule -Level Cluster -Position $r.Pos -Confirm:$false -ErrorAction SilentlyContinue
}
} catch { }
# Cleanup: firewall aliases
try {
Remove-PveFirewallAlias -Level Cluster -Name 'pester-alias' -Confirm:$false -ErrorAction SilentlyContinue
} catch { }
# Cleanup: firewall IP sets
try {
Remove-PveFirewallIpSet -Level Cluster -Name 'pester-ipset' -Confirm:$false -ErrorAction SilentlyContinue
} catch { }
# Cleanup: backup jobs
try {
if ($script:BackupTestJobId) {
Remove-PveBackupJob -Id $script:BackupTestJobId -Confirm:$false -ErrorAction SilentlyContinue
}
} catch { }
}
# ===========================================================================
@@ -1343,6 +1371,161 @@ Describe 'Integration Tests' -Tag 'Integration' {
}
}
# -----------------------------------------------------------------------
Context 'OVA Import' {
BeforeAll {
function Skip-IfNoOva {
if (-not $script:OvaPath) {
Set-ItResult -Skipped -Because 'PVETEST_OVA_PATH is not set'
}
}
}
It 'Should import a VM from OVA file' {
Skip-IfNoOva
$vmId = 9900 + (Get-Random -Maximum 99)
$result = Import-PveOva -Node $script:Node -Path $script:OvaPath -Storage $script:Storage -NewVmId $vmId -ErrorAction Stop
$result | Should -Not -BeNullOrEmpty
$script:CreatedVmIds.Add($vmId) | Out-Null
}
It 'Should find the imported VM' {
Skip-IfNoOva
if ($script:CreatedVmIds.Count -eq 0 -or $script:CreatedVmIds[-1] -lt 9900) {
Set-ItResult -Skipped -Because 'OVA import did not create a VM'
}
$vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:CreatedVmIds[-1] }
$vm | Should -Not -BeNullOrEmpty
}
}
# -----------------------------------------------------------------------
Context 'Firewall — Cluster Rules' {
It 'Should create a firewall rule' {
Skip-IfNoTarget
{ New-PveFirewallRule -Level Cluster -Type in -Action ACCEPT -Proto tcp -Dport '8080' -Comment 'pester-test-rule' -Enable -ErrorAction Stop } | Should -Not -Throw
}
It 'Should list firewall rules and find the test rule' {
Skip-IfNoTarget
$rules = Get-PveFirewallRule -Level Cluster
$rules | Should -Not -BeNullOrEmpty
$testRule = $rules | Where-Object { $_.Comment -eq 'pester-test-rule' }
$testRule | Should -Not -BeNullOrEmpty
$script:FirewallTestRulePos = $testRule.Pos
}
It 'Should update the firewall rule' {
Skip-IfNoTarget
if ($null -eq $script:FirewallTestRulePos) {
Set-ItResult -Skipped -Because 'No test rule was created'
}
{ Set-PveFirewallRule -Level Cluster -Position $script:FirewallTestRulePos -Comment 'pester-test-updated' -ErrorAction Stop } | Should -Not -Throw
}
It 'Should remove the firewall rule' {
Skip-IfNoTarget
if ($null -eq $script:FirewallTestRulePos) {
Set-ItResult -Skipped -Because 'No test rule was created'
}
{ Remove-PveFirewallRule -Level Cluster -Position $script:FirewallTestRulePos -Confirm:$false -ErrorAction Stop } | Should -Not -Throw
}
It 'Should get firewall options' {
Skip-IfNoTarget
$opts = Get-PveFirewallOptions -Level Cluster
$opts | Should -Not -BeNullOrEmpty
}
}
# -----------------------------------------------------------------------
Context 'Backup Jobs' {
It 'Should create a backup job' {
Skip-IfNoTarget
{ New-PveBackupJob -Schedule '0 3 * * 6' -Storage $script:Storage -Mode snapshot -Comment 'pester-test-backup' -ErrorAction Stop } | Should -Not -Throw
}
It 'Should list backup jobs and find the test job' {
Skip-IfNoTarget
$jobs = Get-PveBackupJob
$testJob = $jobs | Where-Object { $_.Comment -eq 'pester-test-backup' }
$testJob | Should -Not -BeNullOrEmpty
$script:BackupTestJobId = $testJob.Id
}
It 'Should update the backup job' {
Skip-IfNoTarget
if (-not $script:BackupTestJobId) {
Set-ItResult -Skipped -Because 'No test backup job was created'
}
{ Set-PveBackupJob -Id $script:BackupTestJobId -Comment 'pester-test-updated' -ErrorAction Stop } | Should -Not -Throw
}
It 'Should remove the backup job' {
Skip-IfNoTarget
if (-not $script:BackupTestJobId) {
Set-ItResult -Skipped -Because 'No test backup job was created'
}
{ Remove-PveBackupJob -Id $script:BackupTestJobId -Confirm:$false -ErrorAction Stop } | Should -Not -Throw
}
It 'Should verify the backup job was removed' {
Skip-IfNoTarget
if (-not $script:BackupTestJobId) {
Set-ItResult -Skipped -Because 'No test backup job was created'
}
$jobs = Get-PveBackupJob
$testJob = $jobs | Where-Object { $_.Id -eq $script:BackupTestJobId }
$testJob | Should -BeNullOrEmpty
}
}
# -----------------------------------------------------------------------
Context 'Firewall — Aliases and IP Sets' {
It 'Should create a firewall alias' {
Skip-IfNoTarget
{ New-PveFirewallAlias -Level Cluster -Name 'pester-alias' -Cidr '192.168.99.0/24' -Comment 'test alias' -ErrorAction Stop } | Should -Not -Throw
}
It 'Should list and find the alias' {
Skip-IfNoTarget
$aliases = Get-PveFirewallAlias -Level Cluster
$aliases | Where-Object { $_.Name -eq 'pester-alias' } | Should -Not -BeNullOrEmpty
}
It 'Should remove the alias' {
Skip-IfNoTarget
{ Remove-PveFirewallAlias -Level Cluster -Name 'pester-alias' -Confirm:$false -ErrorAction Stop } | Should -Not -Throw
}
It 'Should create an IP set' {
Skip-IfNoTarget
{ New-PveFirewallIpSet -Level Cluster -Name 'pester-ipset' -Comment 'test ipset' -ErrorAction Stop } | Should -Not -Throw
}
It 'Should add an entry to the IP set' {
Skip-IfNoTarget
{ New-PveFirewallIpSetEntry -Level Cluster -Name 'pester-ipset' -Cidr '10.99.0.0/16' -Comment 'test entry' -ErrorAction Stop } | Should -Not -Throw
}
It 'Should list IP set entries' {
Skip-IfNoTarget
$entries = Get-PveFirewallIpSetEntry -Level Cluster -Name 'pester-ipset'
$entries | Should -Not -BeNullOrEmpty
($entries | Where-Object { $_.Cidr -like '10.99.0.0*' }) | Should -Not -BeNullOrEmpty
}
It 'Should remove the IP set entry' {
Skip-IfNoTarget
{ Remove-PveFirewallIpSetEntry -Level Cluster -Name 'pester-ipset' -Cidr '10.99.0.0/16' -Confirm:$false -ErrorAction Stop } | Should -Not -Throw
}
It 'Should remove the IP set' {
Skip-IfNoTarget
{ Remove-PveFirewallIpSet -Level Cluster -Name 'pester-ipset' -Confirm:$false -ErrorAction Stop } | Should -Not -Throw
}
}
# -----------------------------------------------------------------------
Context 'Disconnect' {
It 'Should disconnect from PVE server (Disconnect-PveServer)' {
@@ -0,0 +1,466 @@
#Requires -Module Pester
<#
.SYNOPSIS
Pester 5 tests for SDN IPAM, DNS, and controller cmdlets:
Get-PveSdnIpam, New-PveSdnIpam, Remove-PveSdnIpam,
Get-PveSdnDns, New-PveSdnDns, Remove-PveSdnDns,
Get-PveSdnController, New-PveSdnController, Remove-PveSdnController.
All tests are fully offline — no live Proxmox VE target is required.
If a cmdlet is not yet compiled the test is marked Skipped.
#>
BeforeAll {
. $PSScriptRoot/../_TestHelper.ps1
$script:Availability = @{}
foreach ($name in @(
'Get-PveSdnIpam', 'New-PveSdnIpam', 'Remove-PveSdnIpam',
'Get-PveSdnDns', 'New-PveSdnDns', 'Remove-PveSdnDns',
'Get-PveSdnController', 'New-PveSdnController', 'Remove-PveSdnController'
)) {
$script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue)
}
function Skip-IfMissing([string]$Name) {
if (-not $script:Availability[$Name]) {
Set-ItResult -Skipped -Because "$Name is not yet implemented in this build"
}
}
}
# ---------------------------------------------------------------------------
# Manifest contract
# ---------------------------------------------------------------------------
Describe 'SDN IPAM/DNS/Controller cmdlets — manifest declarations' {
BeforeAll {
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
}
It "<cmdName> should be declared in CmdletsToExport" -TestCases @(
@{ cmdName = 'Get-PveSdnIpam' }
@{ cmdName = 'New-PveSdnIpam' }
@{ cmdName = 'Remove-PveSdnIpam' }
@{ cmdName = 'Get-PveSdnDns' }
@{ cmdName = 'New-PveSdnDns' }
@{ cmdName = 'Remove-PveSdnDns' }
@{ cmdName = 'Get-PveSdnController' }
@{ cmdName = 'New-PveSdnController' }
@{ cmdName = 'Remove-PveSdnController' }
) {
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
$script:Manifest.CmdletsToExport | Should -Contain $cmdName
}
}
# ===========================================================================
# IPAM cmdlets
# ===========================================================================
# ---------------------------------------------------------------------------
# Get-PveSdnIpam
# ---------------------------------------------------------------------------
Describe 'Get-PveSdnIpam' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveSdnIpam' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveSdnIpam'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveSdnIpam'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Ipam parameter' {
Skip-IfMissing 'Get-PveSdnIpam'
$script:Cmd.Parameters.ContainsKey('Ipam') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveSdnIpam'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveSdnIpam'
{ Get-PveSdnIpam -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveSdnIpam
# ---------------------------------------------------------------------------
Describe 'New-PveSdnIpam' {
BeforeAll { $script:Cmd = Get-Command 'New-PveSdnIpam' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveSdnIpam'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveSdnIpam'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Ipam should be Mandatory' {
Skip-IfMissing 'New-PveSdnIpam'
$isMandatory = $script:Cmd.Parameters['Ipam'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Type should be Mandatory' {
Skip-IfMissing 'New-PveSdnIpam'
$isMandatory = $script:Cmd.Parameters['Type'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveSdnIpam'
{ New-PveSdnIpam -Ipam 'testipam' -Type 'pve' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveSdnIpam
# ---------------------------------------------------------------------------
Describe 'Remove-PveSdnIpam' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveSdnIpam' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveSdnIpam'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveSdnIpam'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveSdnIpam'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Ipam should be Mandatory' {
Skip-IfMissing 'Remove-PveSdnIpam'
$isMandatory = $script:Cmd.Parameters['Ipam'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveSdnIpam'
{ Remove-PveSdnIpam -Ipam 'testipam' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ===========================================================================
# DNS cmdlets
# ===========================================================================
# ---------------------------------------------------------------------------
# Get-PveSdnDns
# ---------------------------------------------------------------------------
Describe 'Get-PveSdnDns' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveSdnDns' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveSdnDns'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveSdnDns'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Dns parameter' {
Skip-IfMissing 'Get-PveSdnDns'
$script:Cmd.Parameters.ContainsKey('Dns') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveSdnDns'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveSdnDns'
{ Get-PveSdnDns -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveSdnDns
# ---------------------------------------------------------------------------
Describe 'New-PveSdnDns' {
BeforeAll { $script:Cmd = Get-Command 'New-PveSdnDns' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveSdnDns'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveSdnDns'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Dns should be Mandatory' {
Skip-IfMissing 'New-PveSdnDns'
$isMandatory = $script:Cmd.Parameters['Dns'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Type should be Mandatory' {
Skip-IfMissing 'New-PveSdnDns'
$isMandatory = $script:Cmd.Parameters['Type'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveSdnDns'
{ New-PveSdnDns -Dns 'testdns' -Type 'powerdns' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveSdnDns
# ---------------------------------------------------------------------------
Describe 'Remove-PveSdnDns' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveSdnDns' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveSdnDns'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveSdnDns'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveSdnDns'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Dns should be Mandatory' {
Skip-IfMissing 'Remove-PveSdnDns'
$isMandatory = $script:Cmd.Parameters['Dns'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveSdnDns'
{ Remove-PveSdnDns -Dns 'testdns' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ===========================================================================
# Controller cmdlets
# ===========================================================================
# ---------------------------------------------------------------------------
# Get-PveSdnController
# ---------------------------------------------------------------------------
Describe 'Get-PveSdnController' {
BeforeAll { $script:Cmd = Get-Command 'Get-PveSdnController' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Get-PveSdnController'
$script:Cmd | Should -Not -BeNullOrEmpty
}
It 'Should be a CmdletInfo (binary cmdlet)' {
Skip-IfMissing 'Get-PveSdnController'
$script:Cmd.CommandType | Should -Be 'Cmdlet'
}
}
Context 'Parameter metadata' {
It 'Should have optional Controller parameter' {
Skip-IfMissing 'Get-PveSdnController'
$script:Cmd.Parameters.ContainsKey('Controller') | Should -BeTrue
}
It 'Should have Session parameter' {
Skip-IfMissing 'Get-PveSdnController'
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Get-PveSdnController'
{ Get-PveSdnController -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# New-PveSdnController
# ---------------------------------------------------------------------------
Describe 'New-PveSdnController' {
BeforeAll { $script:Cmd = Get-Command 'New-PveSdnController' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'New-PveSdnController'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess support' {
It 'Should support WhatIf' {
Skip-IfMissing 'New-PveSdnController'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
}
Context 'Required parameters' {
It 'Controller should be Mandatory' {
Skip-IfMissing 'New-PveSdnController'
$isMandatory = $script:Cmd.Parameters['Controller'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
It 'Type should be Mandatory' {
Skip-IfMissing 'New-PveSdnController'
$isMandatory = $script:Cmd.Parameters['Type'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'New-PveSdnController'
{ New-PveSdnController -Controller 'testctrl' -Type 'evpn' -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}
# ---------------------------------------------------------------------------
# Remove-PveSdnController
# ---------------------------------------------------------------------------
Describe 'Remove-PveSdnController' {
BeforeAll { $script:Cmd = Get-Command 'Remove-PveSdnController' -ErrorAction SilentlyContinue }
Context 'Command existence' {
It 'Should be available after module import' {
Skip-IfMissing 'Remove-PveSdnController'
$script:Cmd | Should -Not -BeNullOrEmpty
}
}
Context 'ShouldProcess / ConfirmImpact' {
It 'Should support WhatIf' {
Skip-IfMissing 'Remove-PveSdnController'
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
}
It 'Should declare ConfirmImpact High' {
Skip-IfMissing 'Remove-PveSdnController'
$attr = $script:Cmd.ImplementingType.GetCustomAttributes(
[System.Management.Automation.CmdletAttribute], $false) |
Select-Object -First 1
$attr.ConfirmImpact | Should -Be ([System.Management.Automation.ConfirmImpact]::High)
}
}
Context 'Required parameters' {
It 'Controller should be Mandatory' {
Skip-IfMissing 'Remove-PveSdnController'
$isMandatory = $script:Cmd.Parameters['Controller'].ParameterSets.Values |
Where-Object { $_.IsMandatory }
$isMandatory | Should -Not -BeNullOrEmpty
}
}
Context 'Without active session' {
It 'Should throw when no session is active' {
Skip-IfMissing 'Remove-PveSdnController'
{ Remove-PveSdnController -Controller 'testctrl' -Confirm:$false -ErrorAction Stop } |
Should -Throw '*No active Proxmox VE session*'
}
}
}