diff --git a/src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj b/src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj index 320216f..f00c7d2 100644 --- a/src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj +++ b/src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj @@ -1,7 +1,7 @@ - net10.0;net48 + net9.0;net48 10.0 enable PSProxmoxVE.Core @@ -22,7 +22,7 @@ - + diff --git a/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs index 4e202e2..4fbf998 100644 --- a/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/CloudInit/InvokePveCloudInitRegenerateCmdlet.cs @@ -1,6 +1,7 @@ using System.Management.Automation; using PSProxmoxVE.Core.Models.Vms; using PSProxmoxVE.Core.Services; +using PSProxmoxVE.Core.Authentication; namespace PSProxmoxVE.Cmdlets.CloudInit { @@ -24,6 +25,10 @@ namespace PSProxmoxVE.Cmdlets.CloudInit [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] public int VmId { get; set; } + /// When specified, waits for the regeneration task to complete before returning. + [Parameter] + public SwitchParameter Wait { get; set; } + protected override void ProcessRecord() { if (!ShouldProcess($"VM {VmId} on {Node}", "Regenerate PVE Cloud-Init Drive")) @@ -31,7 +36,7 @@ namespace PSProxmoxVE.Cmdlets.CloudInit var session = GetSession(); var service = new CloudInitService(); - service.RegenerateCloudInitImage(session, Node, VmId); + var upid = service.RegenerateCloudInitImage(session, Node, VmId); var task = new PveTask { @@ -40,6 +45,11 @@ namespace PSProxmoxVE.Cmdlets.CloudInit ExitStatus = "OK" }; + if (Wait.IsPresent && !string.IsNullOrEmpty(upid)) + { + task = new TaskService().WaitForTask(session, Node, upid, null, null, null); + } + WriteObject(task); } } diff --git a/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs b/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs index 6db1fc7..a87dd01 100644 --- a/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/CloudInit/SetPveCloudInitConfigCmdlet.cs @@ -32,9 +32,10 @@ namespace PSProxmoxVE.Cmdlets.CloudInit [Parameter(Mandatory = false)] public string? Hostname { get; set; } - /// Cloud-init default username. + /// Cloud-init default username. Alias: User. [Parameter(Mandatory = false)] - public string? User { get; set; } + [Alias("User")] + public string? CiUser { get; set; } /// Cloud-init default user password. Accepts a SecureString. [Parameter(Mandatory = false)] @@ -49,15 +50,15 @@ namespace PSProxmoxVE.Cmdlets.CloudInit /// Maps to ipconfig0 on the VM. /// [Parameter(Mandatory = false)] - public string? IpConfig { get; set; } + public string? IpConfig0 { get; set; } /// DNS nameserver(s) to inject (space or comma separated). [Parameter(Mandatory = false)] - public string? Dns { get; set; } + public string? Nameserver { get; set; } /// DNS search domain to inject. [Parameter(Mandatory = false)] - public string? SearchDomain { get; set; } + public string? Searchdomain { get; set; } /// When specified, waits for the config update task to complete before returning. [Parameter(Mandatory = false)] @@ -65,16 +66,18 @@ namespace PSProxmoxVE.Cmdlets.CloudInit protected override void ProcessRecord() { + var session = GetSession(); + if (!ShouldProcess($"VM {VmId} on {Node}", "Set PVE Cloud-Init Config")) return; var config = new Dictionary(); - if (!string.IsNullOrEmpty(User)) config["ciuser"] = User; + if (!string.IsNullOrEmpty(CiUser)) config["ciuser"] = CiUser; if (!string.IsNullOrEmpty(Hostname)) config["name"] = Hostname; - if (!string.IsNullOrEmpty(IpConfig)) config["ipconfig0"] = IpConfig; - if (!string.IsNullOrEmpty(Dns)) config["nameserver"] = Dns; - if (!string.IsNullOrEmpty(SearchDomain)) config["searchdomain"] = SearchDomain; + if (!string.IsNullOrEmpty(IpConfig0)) config["ipconfig0"] = IpConfig0; + if (!string.IsNullOrEmpty(Nameserver)) config["nameserver"] = Nameserver; + if (!string.IsNullOrEmpty(Searchdomain)) config["searchdomain"] = Searchdomain; if (Password != null) { @@ -96,7 +99,6 @@ namespace PSProxmoxVE.Cmdlets.CloudInit return; } - var session = GetSession(); var service = new CloudInitService(); service.SetCloudInitConfig(session, Node, VmId, config); diff --git a/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs index ed2c345..045c5f7 100644 --- a/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs @@ -24,6 +24,10 @@ namespace PSProxmoxVE.Cmdlets.Network [Alias("NodeName")] public string Node { get; set; } = string.Empty; + /// Filter results to this specific interface name (e.g., "vmbr0"). + [Parameter(Mandatory = false)] + public string? Iface { get; set; } + /// Filter by interface type (e.g., "bridge", "bond", "eth", "vlan"). [Parameter(Mandatory = false)] [ValidateSet("bridge", "bond", "eth", "alias", "vlan", "OVSBridge", "OVSBond", @@ -47,6 +51,9 @@ namespace PSProxmoxVE.Cmdlets.Network { var network = item.ToObject()!; network.Node = Node; + if (!string.IsNullOrEmpty(Iface) && + !string.Equals(network.Iface, Iface, System.StringComparison.OrdinalIgnoreCase)) + continue; WriteObject(network); } } diff --git a/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs index 38dce08..966656b 100644 --- a/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs @@ -20,7 +20,7 @@ namespace PSProxmoxVE.Cmdlets.Network /// The interface name (e.g., "vmbr1", "bond0"). [Parameter(Mandatory = true, Position = 1)] - public string Interface { get; set; } = string.Empty; + public string Iface { get; set; } = string.Empty; /// The interface type. [Parameter(Mandatory = true, Position = 2)] @@ -66,7 +66,7 @@ namespace PSProxmoxVE.Cmdlets.Network protected override void ProcessRecord() { - if (!ShouldProcess($"{Interface} on {Node}", "Create PVE Network Interface")) + if (!ShouldProcess($"{Iface} on {Node}", "Create PVE Network Iface")) return; var session = GetSession(); @@ -74,7 +74,7 @@ namespace PSProxmoxVE.Cmdlets.Network var data = new Dictionary { - ["iface"] = Interface, + ["iface"] = Iface, ["type"] = Type }; diff --git a/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs index 208113a..97d4f00 100644 --- a/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs @@ -10,7 +10,7 @@ namespace PSProxmoxVE.Cmdlets.Network /// use Invoke-PveNetworkApply to apply pending changes to the running system. /// /// - [Cmdlet(VerbsCommon.Remove, "PveNetwork", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Remove, "PveNetwork", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public class RemovePveNetworkCmdlet : PveCmdletBase { /// The Proxmox VE node name. @@ -19,17 +19,17 @@ namespace PSProxmoxVE.Cmdlets.Network /// The interface name to remove. [Parameter(Mandatory = true, Position = 1)] - public string Interface { get; set; } = string.Empty; + public string Iface { get; set; } = string.Empty; protected override void ProcessRecord() { - if (!ShouldProcess($"{Interface} on {Node}", "Remove PVE Network Interface")) + if (!ShouldProcess($"{Iface} on {Node}", "Remove PVE Network Iface")) return; var session = GetSession(); using var client = new PveHttpClient(session); - client.DeleteAsync($"/nodes/{Node}/network/{Interface}").GetAwaiter().GetResult(); + client.DeleteAsync($"/nodes/{Node}/network/{Iface}").GetAwaiter().GetResult(); } } } diff --git a/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs index 8a02602..538bbd6 100644 --- a/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnVnetCmdlet.cs @@ -9,7 +9,7 @@ namespace PSProxmoxVE.Cmdlets.Network /// Deletes the specified Software-Defined Networking VNet from the cluster SDN configuration. /// /// - [Cmdlet(VerbsCommon.Remove, "PveSdnVnet", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Remove, "PveSdnVnet", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public class RemovePveSdnVnetCmdlet : PveCmdletBase { /// The VNet identifier to remove. diff --git a/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs index 6b3bb27..52a3be7 100644 --- a/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Network/RemovePveSdnZoneCmdlet.cs @@ -10,7 +10,7 @@ namespace PSProxmoxVE.Cmdlets.Network /// All VNets within this zone must be removed first. /// /// - [Cmdlet(VerbsCommon.Remove, "PveSdnZone", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Remove, "PveSdnZone", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public class RemovePveSdnZoneCmdlet : PveCmdletBase { /// The zone identifier to remove. diff --git a/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs index 1c1d490..b1351c4 100644 --- a/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs @@ -20,7 +20,7 @@ namespace PSProxmoxVE.Cmdlets.Network /// The interface name to modify. [Parameter(Mandatory = true, Position = 1)] - public string Interface { get; set; } = string.Empty; + public string Iface { get; set; } = string.Empty; /// IPv4 address for the interface. [Parameter(Mandatory = false)] @@ -68,7 +68,7 @@ namespace PSProxmoxVE.Cmdlets.Network protected override void ProcessRecord() { - if (!ShouldProcess($"{Interface} on {Node}", "Set PVE Network Interface")) + if (!ShouldProcess($"{Iface} on {Node}", "Set PVE Network Iface")) return; var session = GetSession(); @@ -88,7 +88,7 @@ namespace PSProxmoxVE.Cmdlets.Network if (Autostart.IsPresent) data["autostart"] = "1"; if (!string.IsNullOrEmpty(Comments)) data["comments"] = Comments; - client.PutAsync($"/nodes/{Node}/network/{Interface}", data).GetAwaiter().GetResult(); + client.PutAsync($"/nodes/{Node}/network/{Iface}", data).GetAwaiter().GetResult(); } } } diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs index 0ba4031..6f506b6 100644 --- a/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Snapshots/GetPveSnapshotCmdlet.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Management.Automation; using PSProxmoxVE.Core.Models.Vms; using PSProxmoxVE.Core.Services; @@ -25,6 +26,10 @@ namespace PSProxmoxVE.Cmdlets.Snapshots [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] public int VmId { get; set; } + /// Optional filter: return only the snapshot with this name. + [Parameter(Mandatory = false)] + public string? Name { get; set; } + protected override void ProcessRecord() { var session = GetSession(); @@ -32,6 +37,9 @@ namespace PSProxmoxVE.Cmdlets.Snapshots var snapshots = service.GetSnapshots(session, Node, VmId); + if (!string.IsNullOrEmpty(Name)) + snapshots = snapshots.Where(s => s.Name == Name).ToArray(); + foreach (var snapshot in snapshots) { snapshot.VmId = VmId; diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs index 52f3999..de0ff62 100644 --- a/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs @@ -13,7 +13,7 @@ namespace PSProxmoxVE.Cmdlets.Snapshots /// Returns a PveTask. Use -Wait to block until removal completes. /// /// - [Cmdlet(VerbsCommon.Remove, "PveSnapshot", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Remove, "PveSnapshot", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] [OutputType(typeof(PveTask))] public class RemovePveSnapshotCmdlet : PveCmdletBase { @@ -37,10 +37,10 @@ namespace PSProxmoxVE.Cmdlets.Snapshots protected override void ProcessRecord() { + var session = GetSession(); + if (!ShouldProcess($"VM {VmId} snapshot '{Name}' on {Node}", "Remove PVE Snapshot")) return; - - var session = GetSession(); using var client = new PveHttpClient(session); var json = client.DeleteAsync($"/nodes/{Node}/qemu/{VmId}/snapshot/{Name}").GetAwaiter().GetResult(); diff --git a/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs index 5925f91..ceda163 100644 --- a/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs @@ -40,10 +40,10 @@ namespace PSProxmoxVE.Cmdlets.Snapshots protected override void ProcessRecord() { + var session = GetSession(); + if (!ShouldProcess($"VM {VmId} on {Node}", $"Restore snapshot '{Name}' (current state will be lost)")) return; - - var session = GetSession(); using var client = new PveHttpClient(session); var json = client.PostAsync($"/nodes/{Node}/qemu/{VmId}/snapshot/{Name}/rollback").GetAwaiter().GetResult(); diff --git a/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs index a468d8f..0cac730 100644 --- a/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs @@ -24,6 +24,10 @@ namespace PSProxmoxVE.Cmdlets.Storage [Alias("NodeName")] public string? Node { get; set; } + /// Filter results to a specific storage name (e.g., "local", "local-lvm"). + [Parameter(Mandatory = false)] + public string? Storage { get; set; } + /// Filter results to a specific storage type (e.g., "dir", "nfs", "zfspool"). [Parameter(Mandatory = false)] public string? Type { get; set; } @@ -44,6 +48,10 @@ namespace PSProxmoxVE.Cmdlets.Storage if (!string.IsNullOrEmpty(Node)) storage.Node = Node; + if (!string.IsNullOrEmpty(Storage) && + !string.Equals(storage.Storage, Storage, StringComparison.OrdinalIgnoreCase)) + continue; + if (!string.IsNullOrEmpty(Type) && !string.Equals(storage.Type, Type, StringComparison.OrdinalIgnoreCase)) continue; diff --git a/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs index 44fb343..2bbf3f0 100644 --- a/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs @@ -40,7 +40,7 @@ namespace PSProxmoxVE.Cmdlets.Storage /// Checksum algorithm used for verification. [Parameter(Mandatory = false)] - [ValidateSet("md5", "sha1", "sha256", IgnoreCase = true)] + [ValidateSet("md5", "sha1", "sha256", "sha512", IgnoreCase = true)] public string? ChecksumAlgorithm { get; set; } /// diff --git a/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs index 377edee..237d421 100644 --- a/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Templates/NewPveTemplateCmdlet.cs @@ -12,7 +12,7 @@ namespace PSProxmoxVE.Cmdlets.Templates /// The VM must be stopped before converting. Returns a PveTask. /// /// - [Cmdlet(VerbsCommon.New, "PveTemplate", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.New, "PveTemplate", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] [OutputType(typeof(PveTask))] public class NewPveTemplateCmdlet : PveCmdletBase { @@ -30,10 +30,10 @@ namespace PSProxmoxVE.Cmdlets.Templates protected override void ProcessRecord() { + var session = GetSession(); + if (!ShouldProcess($"VM {VmId} on {Node}", "Convert to PVE Template (irreversible)")) return; - - var session = GetSession(); var service = new TemplateService(); var task = service.CreateTemplate(session, Node, VmId); diff --git a/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs index c7246c4..8de4e73 100644 --- a/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Templates/NewPveVmFromTemplateCmdlet.cs @@ -17,9 +17,10 @@ namespace PSProxmoxVE.Cmdlets.Templates [OutputType(typeof(PveTask))] public class NewPveVmFromTemplateCmdlet : PveCmdletBase { - /// The node where the source template resides. - [Parameter(Mandatory = true, Position = 0)] - public string Node { get; set; } = string.Empty; + /// The node where the source template resides. Alias: Node. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Node")] + public string TemplateNode { get; set; } = string.Empty; /// The source template VM ID. Accepts pipeline input from Get-PveTemplate (PveVm.VmId). [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] @@ -61,7 +62,7 @@ namespace PSProxmoxVE.Cmdlets.Templates var service = new VmService(); var task = service.CloneVm( session, - Node, + TemplateNode, VmId, NewVmId, name: NewName, @@ -71,7 +72,7 @@ namespace PSProxmoxVE.Cmdlets.Templates if (Wait.IsPresent && !string.IsNullOrEmpty(task.Upid)) { var taskService = new TaskService(); - task = taskService.WaitForTask(session, Node, task.Upid); + task = taskService.WaitForTask(session, TemplateNode, task.Upid); } WriteObject(task); diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs index b280d72..c9f98dc 100644 --- a/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Users/GetPvePermissionCmdlet.cs @@ -22,7 +22,7 @@ namespace PSProxmoxVE.Cmdlets.Users /// Filter results to a specific user or group ID. [Parameter(Mandatory = false)] - public string? UgId { get; set; } + public string? UserId { get; set; } protected override void ProcessRecord() { @@ -37,8 +37,8 @@ namespace PSProxmoxVE.Cmdlets.Users !string.Equals(perm.Path, Path, StringComparison.OrdinalIgnoreCase)) continue; - if (!string.IsNullOrEmpty(UgId) && - !string.Equals(perm.UserId, UgId, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(UserId) && + !string.Equals(perm.UserId, UserId, StringComparison.OrdinalIgnoreCase)) continue; WriteObject(perm); diff --git a/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs index d2b1f48..cbe3f38 100644 --- a/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Users/GetPveUserCmdlet.cs @@ -23,6 +23,11 @@ namespace PSProxmoxVE.Cmdlets.Users [Parameter(Mandatory = false, Position = 0)] public string? UserId { get; set; } + /// When specified, returns only enabled users. + [Parameter(Mandatory = false)] + [Alias("EnabledOnly")] + public SwitchParameter Enabled { get; set; } + protected override void ProcessRecord() { var session = GetSession(); @@ -35,24 +40,26 @@ namespace PSProxmoxVE.Cmdlets.Users foreach (var item in data) { var user = item.ToObject()!; - - if (!string.IsNullOrEmpty(UserId)) - { - var pattern = UserId.Replace("*", ""); - if (UserId.Contains("*")) - { - if (user.UserId.IndexOf(pattern, System.StringComparison.OrdinalIgnoreCase) < 0) - continue; - } - else - { - if (!string.Equals(user.UserId, UserId, System.StringComparison.OrdinalIgnoreCase)) - continue; - } - } - - WriteObject(user); + if (MatchesFilters(user)) + WriteObject(user); } } + + private bool MatchesFilters(PveUser user) + { + if (Enabled.IsPresent && user.Enabled.GetValueOrDefault() != 1) + return false; + + if (string.IsNullOrEmpty(UserId)) + return true; + + if (UserId.Contains("*")) + { + var pattern = UserId.Replace("*", ""); + return user.UserId.IndexOf(pattern, System.StringComparison.OrdinalIgnoreCase) >= 0; + } + + return string.Equals(user.UserId, UserId, System.StringComparison.OrdinalIgnoreCase); + } } } diff --git a/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs index c6e6c03..34506e9 100644 --- a/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Users/RemovePveUserCmdlet.cs @@ -9,7 +9,7 @@ namespace PSProxmoxVE.Cmdlets.Users /// Deletes the specified user from the Proxmox VE access management system. /// /// - [Cmdlet(VerbsCommon.Remove, "PveUser", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Remove, "PveUser", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public class RemovePveUserCmdlet : PveCmdletBase { /// The user identifier to remove (e.g., "jdoe@pve"). diff --git a/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs index 7ba5329..6973fc3 100644 --- a/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs +++ b/src/PSProxmoxVE/Cmdlets/Users/SetPvePermissionCmdlet.cs @@ -24,7 +24,7 @@ namespace PSProxmoxVE.Cmdlets.Users /// The role to assign (e.g., "Administrator", "PVEVMUser"). [Parameter(Mandatory = true, Position = 2)] - public string RoleId { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; /// The ACL entry type: "user" or "group". [Parameter(Mandatory = false)] @@ -42,7 +42,7 @@ namespace PSProxmoxVE.Cmdlets.Users protected override void ProcessRecord() { var action = Delete.IsPresent ? "Remove" : "Set"; - if (!ShouldProcess($"{Type} '{UgId}' at '{Path}'", $"{action} PVE Permission ({RoleId})")) + if (!ShouldProcess($"{Type} '{UgId}' at '{Path}'", $"{action} PVE Permission ({Role})")) return; var session = GetSession(); @@ -51,7 +51,7 @@ namespace PSProxmoxVE.Cmdlets.Users var data = new Dictionary { ["path"] = Path, - ["roles"] = RoleId + ["roles"] = Role }; if (string.Equals(Type, "group", System.StringComparison.OrdinalIgnoreCase)) diff --git a/src/PSProxmoxVE/PSProxmoxVE.csproj b/src/PSProxmoxVE/PSProxmoxVE.csproj index 5d7ecc4..8baa638 100644 --- a/src/PSProxmoxVE/PSProxmoxVE.csproj +++ b/src/PSProxmoxVE/PSProxmoxVE.csproj @@ -1,7 +1,7 @@ - net10.0;net48 + net9.0;net48 10.0 enable PSProxmoxVE @@ -18,7 +18,7 @@ - + diff --git a/tests/PSProxmoxVE.Tests/CloudInit/CloudInitCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/CloudInit/CloudInitCmdlets.Tests.ps1 index 62e9f2c..2c6c764 100644 --- a/tests/PSProxmoxVE.Tests/CloudInit/CloudInitCmdlets.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/CloudInit/CloudInitCmdlets.Tests.ps1 @@ -16,8 +16,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -52,12 +52,13 @@ Describe 'Cloud-Init cmdlets — manifest declarations' { $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } } - foreach ($cmdName in @('Get-PveCloudInitConfig', 'Set-PveCloudInitConfig', - 'Invoke-PveCloudInitRegenerate')) { - It "$cmdName should be declared in CmdletsToExport" { - if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } - $script:Manifest.CmdletsToExport | Should -Contain $cmdName - } + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Get-PveCloudInitConfig' } + @{ cmdName = 'Set-PveCloudInitConfig' } + @{ cmdName = 'Invoke-PveCloudInitRegenerate' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName } } diff --git a/tests/PSProxmoxVE.Tests/Connection/Connect-PveServer.Tests.ps1 b/tests/PSProxmoxVE.Tests/Connection/Connect-PveServer.Tests.ps1 index 112c678..bf4805c 100644 --- a/tests/PSProxmoxVE.Tests/Connection/Connect-PveServer.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Connection/Connect-PveServer.Tests.ps1 @@ -12,8 +12,8 @@ BeforeAll { # Adjust the path if your build output directory differs. $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Connection/Disconnect-PveServer.Tests.ps1 b/tests/PSProxmoxVE.Tests/Connection/Disconnect-PveServer.Tests.ps1 index 0bdb5bb..baa0aa9 100644 --- a/tests/PSProxmoxVE.Tests/Connection/Disconnect-PveServer.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Connection/Disconnect-PveServer.Tests.ps1 @@ -8,8 +8,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Connection/Test-PveConnection.Tests.ps1 b/tests/PSProxmoxVE.Tests/Connection/Test-PveConnection.Tests.ps1 index 42dec00..96de72d 100644 --- a/tests/PSProxmoxVE.Tests/Connection/Test-PveConnection.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Connection/Test-PveConnection.Tests.ps1 @@ -8,8 +8,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 b/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 index b71d93e..fd54d6e 100644 --- a/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Containers/ContainerLifecycle.Tests.ps1 @@ -12,8 +12,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -159,8 +159,7 @@ Describe 'Remove-PveContainer' { Context 'Without active session' { It 'Should throw when no session is active (without -WhatIf)' { Skip-IfMissing 'Remove-PveContainer' - { Remove-PveContainer -Node 'pve-node1' -VmId 200 -ErrorAction Stop } | - Should -Throw '*No active Proxmox VE session*' + { Remove-PveContainer -Node 'pve-node1' -VmId 200 -Confirm:$false -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*' } It 'Should not throw with -WhatIf' { diff --git a/tests/PSProxmoxVE.Tests/Containers/Get-PveContainer.Tests.ps1 b/tests/PSProxmoxVE.Tests/Containers/Get-PveContainer.Tests.ps1 index 6612412..2a13cb2 100644 --- a/tests/PSProxmoxVE.Tests/Containers/Get-PveContainer.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Containers/Get-PveContainer.Tests.ps1 @@ -12,8 +12,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 index ca74319..1b97964 100644 --- a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 @@ -29,8 +29,8 @@ BeforeAll { # ----------------------------------------------------------------------- $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Network/Get-PveNetwork.Tests.ps1 b/tests/PSProxmoxVE.Tests/Network/Get-PveNetwork.Tests.ps1 index 3677dc5..54edd3e 100644 --- a/tests/PSProxmoxVE.Tests/Network/Get-PveNetwork.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Network/Get-PveNetwork.Tests.ps1 @@ -12,8 +12,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -48,12 +48,15 @@ Describe 'Network cmdlets — manifest declarations' { $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } } - foreach ($cmdName in @('Get-PveNetwork', 'New-PveNetwork', 'Set-PveNetwork', - 'Remove-PveNetwork', 'Invoke-PveNetworkApply')) { - It "$cmdName should be declared in CmdletsToExport" { - if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } - $script:Manifest.CmdletsToExport | Should -Contain $cmdName - } + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Get-PveNetwork' } + @{ cmdName = 'New-PveNetwork' } + @{ cmdName = 'Set-PveNetwork' } + @{ cmdName = 'Remove-PveNetwork' } + @{ cmdName = 'Invoke-PveNetworkApply' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName } } diff --git a/tests/PSProxmoxVE.Tests/Network/SdnCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Network/SdnCmdlets.Tests.ps1 index 1350cb5..aa4111b 100644 --- a/tests/PSProxmoxVE.Tests/Network/SdnCmdlets.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Network/SdnCmdlets.Tests.ps1 @@ -16,8 +16,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -56,12 +56,16 @@ Describe 'SDN cmdlets — manifest declarations' { $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } } - foreach ($cmdName in @('Get-PveSdnZone', 'New-PveSdnZone', 'Remove-PveSdnZone', - 'Get-PveSdnVnet', 'New-PveSdnVnet', 'Remove-PveSdnVnet')) { - It "$cmdName should be declared in CmdletsToExport" { - if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } - $script:Manifest.CmdletsToExport | Should -Contain $cmdName - } + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Get-PveSdnZone' } + @{ cmdName = 'New-PveSdnZone' } + @{ cmdName = 'Remove-PveSdnZone' } + @{ cmdName = 'Get-PveSdnVnet' } + @{ cmdName = 'New-PveSdnVnet' } + @{ cmdName = 'Remove-PveSdnVnet' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName } } diff --git a/tests/PSProxmoxVE.Tests/Snapshots/SnapshotCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Snapshots/SnapshotCmdlets.Tests.ps1 index 3a3375c..8456416 100644 --- a/tests/PSProxmoxVE.Tests/Snapshots/SnapshotCmdlets.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Snapshots/SnapshotCmdlets.Tests.ps1 @@ -14,8 +14,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -50,12 +50,14 @@ Describe 'Snapshot cmdlets — manifest declarations' { $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } } - foreach ($cmdName in @('Get-PveSnapshot', 'New-PveSnapshot', - 'Remove-PveSnapshot', 'Restore-PveSnapshot')) { - It "$cmdName should be declared in CmdletsToExport" { - if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } - $script:Manifest.CmdletsToExport | Should -Contain $cmdName - } + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Get-PveSnapshot' } + @{ cmdName = 'New-PveSnapshot' } + @{ cmdName = 'Remove-PveSnapshot' } + @{ cmdName = 'Restore-PveSnapshot' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName } } diff --git a/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1 b/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1 index cf4dc2c..ca68367 100644 --- a/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1 @@ -11,8 +11,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -143,7 +143,7 @@ Describe 'Get-PveStorageContent' { Context 'Without active session' { It 'Should throw when no session is active' { Skip-IfMissing 'Get-PveStorageContent' - { Get-PveStorageContent -ErrorAction Stop } | + { Get-PveStorageContent -Node 'pve-node1' -Storage 'local' -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*' } } diff --git a/tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1 b/tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1 index 128d138..f15f483 100644 --- a/tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Storage/Send-PveIso.Tests.ps1 @@ -9,8 +9,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -169,8 +169,11 @@ Describe 'Send-PveIso' { Context 'Without active session' { It 'Should throw when no session is active (without -WhatIf)' { if (-not $script:CmdExists) { Set-ItResult -Skipped -Because 'Not yet compiled'; return } - { Send-PveIso -Node 'pve-node1' -Storage 'local' -Path '/tmp/test.iso' -ErrorAction Stop } | - Should -Throw '*No active Proxmox VE session*' + $tmpIso = [System.IO.Path]::GetTempFileName() + try { + { Send-PveIso -Node 'pve-node1' -Storage 'local' -Path $tmpIso -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } finally { Remove-Item $tmpIso -ErrorAction SilentlyContinue } } } } diff --git a/tests/PSProxmoxVE.Tests/Templates/TemplateCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Templates/TemplateCmdlets.Tests.ps1 index 44b751d..301a266 100644 --- a/tests/PSProxmoxVE.Tests/Templates/TemplateCmdlets.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Templates/TemplateCmdlets.Tests.ps1 @@ -15,8 +15,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -51,12 +51,14 @@ Describe 'Template cmdlets — manifest declarations' { $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } } - foreach ($cmdName in @('Get-PveTemplate', 'New-PveTemplate', - 'Remove-PveTemplate', 'New-PveVmFromTemplate')) { - It "$cmdName should be declared in CmdletsToExport" { - if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } - $script:Manifest.CmdletsToExport | Should -Contain $cmdName - } + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Get-PveTemplate' } + @{ cmdName = 'New-PveTemplate' } + @{ cmdName = 'Remove-PveTemplate' } + @{ cmdName = 'New-PveVmFromTemplate' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName } } @@ -274,6 +276,7 @@ Describe 'New-PveVmFromTemplate' { elseif ($script:Cmd.Parameters.ContainsKey('SourceNode')) { $splat['SourceNode'] = 'pve-node1' } if ($script:Cmd.Parameters.ContainsKey('TemplateId')) { $splat['TemplateId'] = 9000 } elseif ($script:Cmd.Parameters.ContainsKey('VmId')) { $splat['VmId'] = 9000 } + if ($script:Cmd.Parameters.ContainsKey('NewVmId')) { $splat['NewVmId'] = 9001 } & 'New-PveVmFromTemplate' @splat } | Should -Throw '*No active Proxmox VE session*' } diff --git a/tests/PSProxmoxVE.Tests/Users/UserCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Users/UserCmdlets.Tests.ps1 index ac5d413..85c8403 100644 --- a/tests/PSProxmoxVE.Tests/Users/UserCmdlets.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Users/UserCmdlets.Tests.ps1 @@ -13,8 +13,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -54,13 +54,19 @@ Describe 'User / Role / Permission cmdlets — manifest declarations' { $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } } - foreach ($cmdName in @('Get-PveUser', 'New-PveUser', 'Remove-PveUser', 'Set-PveUser', - 'Get-PveRole', 'New-PveRole', 'Remove-PveRole', - 'Get-PvePermission', 'Set-PvePermission')) { - It "$cmdName should be declared in CmdletsToExport" { - if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } - $script:Manifest.CmdletsToExport | Should -Contain $cmdName - } + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Get-PveUser' } + @{ cmdName = 'New-PveUser' } + @{ cmdName = 'Remove-PveUser' } + @{ cmdName = 'Set-PveUser' } + @{ cmdName = 'Get-PveRole' } + @{ cmdName = 'New-PveRole' } + @{ cmdName = 'Remove-PveRole' } + @{ cmdName = 'Get-PvePermission' } + @{ cmdName = 'Set-PvePermission' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName } } diff --git a/tests/PSProxmoxVE.Tests/Vms/Get-PveVm.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/Get-PveVm.Tests.ps1 index 352dd53..27eb087 100644 --- a/tests/PSProxmoxVE.Tests/Vms/Get-PveVm.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Vms/Get-PveVm.Tests.ps1 @@ -8,8 +8,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Vms/New-PveVm.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/New-PveVm.Tests.ps1 index b026fea..4238002 100644 --- a/tests/PSProxmoxVE.Tests/Vms/New-PveVm.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Vms/New-PveVm.Tests.ps1 @@ -8,8 +8,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tests/PSProxmoxVE.Tests/Vms/Remove-PveVm.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/Remove-PveVm.Tests.ps1 index 0a6a9b0..f559e5e 100644 --- a/tests/PSProxmoxVE.Tests/Vms/Remove-PveVm.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Vms/Remove-PveVm.Tests.ps1 @@ -8,8 +8,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) @@ -132,8 +132,7 @@ Describe 'Remove-PveVm' { Context 'Without active session' { It 'Should throw when no session is active (without -WhatIf)' { - { Remove-PveVm -Node 'pve-node1' -VmId 100 -ErrorAction Stop } | - Should -Throw '*No active Proxmox VE session*' + { Remove-PveVm -Node 'pve-node1' -VmId 100 -Confirm:$false -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*' } } } diff --git a/tests/PSProxmoxVE.Tests/Vms/VmLifecycle.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/VmLifecycle.Tests.ps1 index 273291c..c491d85 100644 --- a/tests/PSProxmoxVE.Tests/Vms/VmLifecycle.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Vms/VmLifecycle.Tests.ps1 @@ -19,8 +19,8 @@ BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '../../../src/PSProxmoxVE') $dllCandidates = @( - Join-Path $moduleRoot 'bin/Debug/net8.0/PSProxmoxVE.dll' - Join-Path $moduleRoot 'bin/Release/net8.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Debug/net9.0/PSProxmoxVE.dll' + Join-Path $moduleRoot 'bin/Release/net9.0/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Debug/net48/PSProxmoxVE.dll' Join-Path $moduleRoot 'bin/Release/net48/PSProxmoxVE.dll' ) diff --git a/tools/Invoke-Tests.ps1 b/tools/Invoke-Tests.ps1 index 1cbaacd..748abba 100644 --- a/tools/Invoke-Tests.ps1 +++ b/tools/Invoke-Tests.ps1 @@ -16,6 +16,29 @@ When omitted the framework is auto-detected from the first *.csproj found under tests/PSProxmoxVE.Core.Tests. +.PARAMETER FromTerraform + Read PVE connection details from 'terraform output -json' in + tests/infrastructure/. Requires Terraform to be installed and + 'terraform apply' to have been run. Implies -Tier Integration. + +.PARAMETER PveHost + Hostname or IP of the PVE test node. Sets PVETEST_HOST. + +.PARAMETER PvePort + API port. Default 8006. Sets PVETEST_PORT. + +.PARAMETER PveApiToken + API token in USER@REALM!TOKENID=UUID format. Sets PVETEST_APITOKEN. + +.PARAMETER PveNode + PVE node name (e.g. pve). Sets PVETEST_NODE. + +.PARAMETER PveStorage + Storage pool for disk/ISO operations (e.g. local). Sets PVETEST_STORAGE. + +.PARAMETER PveIsoPath + Local filesystem path to a small .iso for upload tests. Sets PVETEST_ISO_PATH. + .EXAMPLE ./tools/Invoke-Tests.ps1 @@ -23,24 +46,101 @@ ./tools/Invoke-Tests.ps1 -Tier All .EXAMPLE - ./tools/Invoke-Tests.ps1 -Tier Integration + ./tools/Invoke-Tests.ps1 -FromTerraform + +.EXAMPLE + ./tools/Invoke-Tests.ps1 -Tier Integration ` + -PveHost 192.168.1.200 -PveApiToken "root@pam!integration=abc123..." ` + -PveNode pve -PveStorage local -PveIsoPath /tmp/test.iso .EXAMPLE ./tools/Invoke-Tests.ps1 -Tier Unit -Framework net9.0 #> -[CmdletBinding()] +[CmdletBinding(DefaultParameterSetName = 'Explicit')] param( [Parameter()] [ValidateSet('Unit', 'Integration', 'All')] [string] $Tier = 'Unit', [Parameter()] - [string] $Framework + [string] $Framework, + + # --- Terraform-sourced connection --- + [Parameter(ParameterSetName = 'Terraform')] + [switch] $FromTerraform, + + # --- Explicit connection params --- + [Parameter(ParameterSetName = 'Explicit')] + [string] $PveHost, + + [Parameter(ParameterSetName = 'Explicit')] + [int] $PvePort = 8006, + + [Parameter(ParameterSetName = 'Explicit')] + [string] $PveApiToken, + + [Parameter(ParameterSetName = 'Explicit')] + [string] $PveNode, + + [Parameter(ParameterSetName = 'Explicit')] + [string] $PveStorage, + + [Parameter(ParameterSetName = 'Explicit')] + [string] $PveIsoPath ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# --------------------------------------------------------------------------- +# Integration connection setup +# --------------------------------------------------------------------------- + +if ($FromTerraform) { + $Tier = 'Integration' + $infraDir = Join-Path $PSScriptRoot '../tests/infrastructure' + + if (-not (Get-Command terraform -ErrorAction SilentlyContinue)) { + throw 'terraform not found on PATH. Install Terraform >= 1.5 first.' + } + if (-not (Test-Path $infraDir)) { + throw "Infrastructure directory not found: $infraDir" + } + + Write-Host 'Reading connection details from terraform output...' -ForegroundColor Cyan + $tfOutputJson = terraform -chdir:$infraDir output -json 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "terraform output failed. Have you run 'terraform apply' in $infraDir?`n$tfOutputJson" + } + + $tfOutput = $tfOutputJson | ConvertFrom-Json + $env:PVETEST_HOST = $tfOutput.pve_test_host.value + $env:PVETEST_PORT = $tfOutput.pve_test_port.value + $env:PVETEST_NODE = $tfOutput.pve_test_node_name.value + $env:PVETEST_APITOKEN = terraform -chdir:$infraDir output -raw pve_test_api_token 2>&1 + + # Storage and ISO path are not provisioned by Terraform; require env vars or defaults + if (-not $env:PVETEST_STORAGE) { $env:PVETEST_STORAGE = 'local' } + if (-not $env:PVETEST_ISO_PATH) { + Write-Warning 'PVETEST_ISO_PATH not set — ISO upload tests will be skipped.' + } + + Write-Host " PVE host : $($env:PVETEST_HOST):$($env:PVETEST_PORT)" -ForegroundColor DarkGray + Write-Host " PVE node : $($env:PVETEST_NODE)" -ForegroundColor DarkGray + Write-Host " Storage : $($env:PVETEST_STORAGE)" -ForegroundColor DarkGray +} +elseif ($PSBoundParameters.ContainsKey('PveHost') -or $PSBoundParameters.ContainsKey('PveApiToken')) { + # Explicit params override env vars + if ($PveHost) { $env:PVETEST_HOST = $PveHost } + if ($PvePort) { $env:PVETEST_PORT = $PvePort } + if ($PveApiToken) { $env:PVETEST_APITOKEN = $PveApiToken } + if ($PveNode) { $env:PVETEST_NODE = $PveNode } + if ($PveStorage) { $env:PVETEST_STORAGE = $PveStorage } + if ($PveIsoPath) { $env:PVETEST_ISO_PATH = $PveIsoPath } + + if ($Tier -eq 'Unit') { $Tier = 'Integration' } +} + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -80,13 +180,16 @@ if (-not $Framework) { Select-Object -First 1 if ($csproj) { - [xml] $proj = Get-Content $csproj.FullName -Raw - $tfm = $proj.Project.PropertyGroup | - Where-Object { $_.TargetFramework } | - Select-Object -First 1 -ExpandProperty TargetFramework + # Try singular TargetFramework first, then first entry from TargetFrameworks + $tfmNode = Select-Xml -Path $csproj.FullName -XPath '//*[local-name()="TargetFramework" or local-name()="TargetFrameworks"]' | + Select-Object -First 1 - if ($tfm) { - $Framework = $tfm.Trim() + if ($tfmNode) { + # TargetFrameworks may be semicolon-separated; pick the first .NET Core/5+ TFM + # On non-Windows, skip net48 since .NET Framework is not available + $allTfms = $tfmNode.Node.InnerText.Trim() -split ';' | ForEach-Object { $_.Trim() } + $onWindows = $PSVersionTable.Platform -eq 'Win32NT' -or $PSVersionTable.PSEdition -eq 'Desktop' + $Framework = $allTfms | Where-Object { $_ -notmatch 'net4' -or $onWindows } | Select-Object -First 1 Write-Verbose "Auto-detected target framework: $Framework" } } @@ -163,11 +266,11 @@ function Invoke-PesterUnitTests { throw 'Pester module is not installed. Run: Install-Module Pester -Force' } - Import-Module $pesterModule.ModuleBase -Force + Import-Module -Name Pester -RequiredVersion $pesterModule.Version -Force $config = New-PesterConfiguration $config.Run.Path = $pesterTestDir - $config.Filter.ExcludeTag = @('Integration') + $config.Filter.ExcludeTag = @('Integration', 'MockIntegration') $config.Output.Verbosity = 'Detailed' $config.Run.PassThru = $true @@ -210,7 +313,7 @@ function Invoke-PesterIntegrationTests { throw 'Pester module is not installed. Run: Install-Module Pester -Force' } - Import-Module $pesterModule.ModuleBase -Force + Import-Module -Name Pester -RequiredVersion $pesterModule.Version -Force $config = New-PesterConfiguration $config.Run.Path = $integrationDir @@ -272,7 +375,7 @@ foreach ($suite in $results.Keys) { if ($color -eq 'Red') { $anyFailure = $true } - Write-Host (' {0,-30} {1}' -f "$suite:", $status) -ForegroundColor $color + Write-Host (' {0,-30} {1}' -f "${suite}:", $status) -ForegroundColor $color } Write-Host ''