diff --git a/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs new file mode 100644 index 0000000..a468d8f --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageCmdlet.cs @@ -0,0 +1,59 @@ +using System; +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Storage; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Lists storage definitions on a Proxmox VE node or cluster. + /// + /// Returns storage definitions visible to the specified node. When Node is omitted the + /// cluster-level storage list is queried. Results can be filtered by Type and ContentType. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveStorage")] + [OutputType(typeof(PveStorage))] + public class GetPveStorageCmdlet : PveCmdletBase + { + /// + /// The Proxmox VE node name. Accepts pipeline input from Get-PveNode (PveNode.Name). + /// When omitted the cluster-wide storage list is used. + /// + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true)] + [Alias("NodeName")] + public string? Node { get; set; } + + /// Filter results to a specific storage type (e.g., "dir", "nfs", "zfspool"). + [Parameter(Mandatory = false)] + public string? Type { get; set; } + + /// Filter results to storages that support the given content type (e.g., "iso", "backup"). + [Parameter(Mandatory = false)] + public string? ContentType { get; set; } + + protected override void ProcessRecord() + { + var session = GetSession(); + var service = new StorageService(); + + var storages = service.GetStorages(session, Node); + + foreach (var storage in storages) + { + if (!string.IsNullOrEmpty(Node)) + storage.Node = Node; + + if (!string.IsNullOrEmpty(Type) && + !string.Equals(storage.Type, Type, StringComparison.OrdinalIgnoreCase)) + continue; + + if (!string.IsNullOrEmpty(ContentType) && storage.Content != null && + !storage.Content.Contains(ContentType)) + continue; + + WriteObject(storage); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageContentCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageContentCmdlet.cs new file mode 100644 index 0000000..7382fbb --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/GetPveStorageContentCmdlet.cs @@ -0,0 +1,47 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Models.Storage; +using PSProxmoxVE.Core.Services; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Lists content items within a Proxmox VE storage. + /// + /// Returns volume entries (ISOs, templates, backups, disk images) stored in the + /// specified storage on the given node. Optionally filter by content type. + /// + /// + [Cmdlet(VerbsCommon.Get, "PveStorageContent")] + [OutputType(typeof(PveStorageContent))] + public class GetPveStorageContentCmdlet : PveCmdletBase + { + /// The Proxmox VE node name that hosts the storage. + [Parameter(Mandatory = true, Position = 0)] + public string Node { get; set; } = string.Empty; + + /// + /// The storage identifier. Accepts pipeline input from Get-PveStorage (PveStorage.Storage). + /// + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] + public string Storage { get; set; } = string.Empty; + + /// Filter results to a specific content type (e.g., "iso", "vztmpl", "backup", "images"). + [Parameter(Mandatory = false)] + public string? ContentType { get; set; } + + protected override void ProcessRecord() + { + var session = GetSession(); + var service = new StorageService(); + + var items = service.GetStorageContent(session, Node, Storage, ContentType); + + foreach (var content in items) + { + content.Storage = Storage; + content.Node = Node; + WriteObject(content); + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/InvokePveStorageDownloadCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/InvokePveStorageDownloadCmdlet.cs new file mode 100644 index 0000000..099d3af --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/InvokePveStorageDownloadCmdlet.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using Newtonsoft.Json.Linq; +using PSProxmoxVE.Core.Client; +using PSProxmoxVE.Core.Models.Vms; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Downloads a file from a URL directly into a Proxmox VE storage. + /// + /// Instructs the Proxmox VE node to download a file (e.g., an ISO or container template) + /// from an external URL and save it to the specified storage. Returns a PveTask. + /// + /// + [Cmdlet(VerbsLifecycle.Invoke, "PveStorageDownload", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public class InvokePveStorageDownloadCmdlet : PveCmdletBase + { + /// The Proxmox VE node that will perform the download. + [Parameter(Mandatory = true, Position = 0)] + public string Node { get; set; } = string.Empty; + + /// The target storage identifier. + [Parameter(Mandatory = true, Position = 1)] + public string Storage { get; set; } = string.Empty; + + /// The URL to download the file from. + [Parameter(Mandatory = true, Position = 2)] + public string Url { get; set; } = string.Empty; + + /// The filename to save the downloaded file as on the storage. + [Parameter(Mandatory = true, Position = 3)] + public string Filename { get; set; } = string.Empty; + + /// The content type category for the downloaded file. Defaults to "iso". + [Parameter(Mandatory = false)] + [ValidateSet("iso", "vztmpl", "backup", "import", IgnoreCase = true)] + public string ContentType { get; set; } = "iso"; + + /// When specified, waits for the download task to complete before returning. + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + protected override void ProcessRecord() + { + if (!ShouldProcess($"{Node}/{Storage}/{Filename}", $"Download from {Url}")) + return; + + var session = GetSession(); + using var client = new PveHttpClient(session); + + var resource = $"/nodes/{Node}/storage/{Storage}/download-url"; + var data = new Dictionary + { + ["url"] = Url, + ["filename"] = Filename, + ["content"] = ContentType + }; + + var json = client.PostAsync(resource, data).GetAwaiter().GetResult(); + var root = JObject.Parse(json); + var upid = root["data"]?.ToString() ?? string.Empty; + + var task = new PveTask { Upid = upid, Node = Node, Status = "running" }; + + if (Wait.IsPresent && !string.IsNullOrEmpty(upid)) + { + task = WaitForTask(client, Node, upid); + } + + WriteObject(task); + } + + private static PveTask WaitForTask(PveHttpClient client, string node, string upid) + { + var encodedUpid = Uri.EscapeDataString(upid); + var statusResource = $"/nodes/{node}/tasks/{encodedUpid}/status"; + + while (true) + { + System.Threading.Thread.Sleep(2000); + var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult(); + var statusRoot = JObject.Parse(statusJson); + var data = statusRoot["data"]; + var status = data?["status"]?.ToString(); + var exitStatus = data?["exitstatus"]?.ToString(); + + if (status == "stopped") + { + return new PveTask + { + Upid = upid, + Node = node, + Status = status, + ExitStatus = exitStatus + }; + } + } + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs new file mode 100644 index 0000000..13d1ab3 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Management.Automation; +using Newtonsoft.Json.Linq; +using PSProxmoxVE.Core.Client; +using PSProxmoxVE.Core.Models.Storage; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Creates a new storage definition in Proxmox VE. + /// + /// Adds a new storage backend to the Proxmox VE cluster configuration. + /// The storage will be available cluster-wide after creation. + /// + /// + [Cmdlet(VerbsCommon.New, "PveStorage", SupportsShouldProcess = true)] + [OutputType(typeof(PveStorage))] + public class NewPveStorageCmdlet : PveCmdletBase + { + /// The unique storage identifier/name. + [Parameter(Mandatory = true, Position = 0)] + public string Storage { get; set; } = string.Empty; + + /// The storage type (e.g., "dir", "nfs", "lvm", "zfspool", "cephfs", "rbd"). + [Parameter(Mandatory = true, Position = 1)] + [ValidateSet("dir", "nfs", "lvm", "lvmthin", "zfspool", "zfs", "cephfs", "rbd", + "iscsi", "iscsidirect", "glusterfs", "cifs", "pbs", IgnoreCase = true)] + public string Type { get; set; } = string.Empty; + + /// Comma-separated list of content types to support (e.g., "iso,vztmpl,backup"). + [Parameter(Mandatory = false)] + public string? Content { get; set; } + + /// Base directory path (for "dir" type storages). + [Parameter(Mandatory = false)] + public string? Path { get; set; } + + /// NFS/CIFS server hostname or IP address. + [Parameter(Mandatory = false)] + public string? Server { get; set; } + + /// NFS export path or CIFS share name. + [Parameter(Mandatory = false)] + public string? Export { get; set; } + + /// LVM volume group name (for "lvm"/"lvmthin" types). + [Parameter(Mandatory = false)] + public string? VgName { get; set; } + + /// LVM thin pool name (for "lvmthin" type). + [Parameter(Mandatory = false)] + public string? ThinPool { get; set; } + + /// ZFS pool name (for "zfspool" type). + [Parameter(Mandatory = false)] + public string? Pool { get; set; } + + /// Ceph pool name (for "rbd"/"cephfs" types). + [Parameter(Mandatory = false)] + public string? CephPool { get; set; } + + /// Monitor list for Ceph storages (comma-separated host:port pairs). + [Parameter(Mandatory = false)] + public string? MonHost { get; set; } + + /// Whether this storage is shared across cluster nodes. + [Parameter(Mandatory = false)] + public SwitchParameter Shared { get; set; } + + /// Whether this storage is enabled. Defaults to enabled. + [Parameter(Mandatory = false)] + public SwitchParameter Disable { get; set; } + + /// Limit nodes that can access this storage (comma-separated node names). + [Parameter(Mandatory = false)] + public string? Nodes { get; set; } + + protected override void ProcessRecord() + { + if (!ShouldProcess(Storage, "Create PVE Storage")) + return; + + var session = GetSession(); + using var client = new PveHttpClient(session); + + var data = new Dictionary + { + ["storage"] = Storage, + ["type"] = Type + }; + + if (!string.IsNullOrEmpty(Content)) data["content"] = Content; + if (!string.IsNullOrEmpty(Path)) data["path"] = Path; + if (!string.IsNullOrEmpty(Server)) data["server"] = Server; + if (!string.IsNullOrEmpty(Export)) data["export"] = Export; + if (!string.IsNullOrEmpty(VgName)) data["vgname"] = VgName; + if (!string.IsNullOrEmpty(ThinPool)) data["thinpool"] = ThinPool; + if (!string.IsNullOrEmpty(Pool)) data["pool"] = Pool; + if (!string.IsNullOrEmpty(CephPool)) data["pool"] = CephPool; + if (!string.IsNullOrEmpty(MonHost)) data["monhost"] = MonHost; + if (!string.IsNullOrEmpty(Nodes)) data["nodes"] = Nodes; + if (Shared.IsPresent) data["shared"] = "1"; + if (Disable.IsPresent) data["disable"] = "1"; + + client.PostAsync("/storage", data).GetAwaiter().GetResult(); + + // Return the storage object representing what was created + var storage = new PveStorage + { + Storage = Storage, + Type = Type, + Content = Content, + Enabled = Disable.IsPresent ? 0 : 1, + Shared = Shared.IsPresent ? 1 : 0 + }; + WriteObject(storage); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageCmdlet.cs new file mode 100644 index 0000000..f0ed2f1 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/RemovePveStorageCmdlet.cs @@ -0,0 +1,33 @@ +using System.Management.Automation; +using PSProxmoxVE.Core.Client; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Removes a storage definition from Proxmox VE. + /// + /// Deletes the specified storage definition from the Proxmox VE cluster configuration. + /// This does not delete the underlying data — only the storage reference is removed. + /// + /// + [Cmdlet(VerbsCommon.Remove, "PveStorage", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + public class RemovePveStorageCmdlet : PveCmdletBase + { + /// The storage identifier to remove. + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + public string Storage { get; set; } = string.Empty; + + protected override void ProcessRecord() + { + if (!ShouldProcess(Storage, "Remove PVE Storage")) + return; + + var session = GetSession(); + using var client = new PveHttpClient(session); + + client.DeleteAsync($"/storage/{Storage}").GetAwaiter().GetResult(); + } + } +} diff --git a/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs new file mode 100644 index 0000000..44fb343 --- /dev/null +++ b/src/PSProxmoxVE/Cmdlets/Storage/SendPveIsoCmdlet.cs @@ -0,0 +1,144 @@ +using System; +using System.IO; +using System.Management.Automation; +using Newtonsoft.Json.Linq; +using PSProxmoxVE.Core.Client; +using PSProxmoxVE.Core.Models.Vms; + +namespace PSProxmoxVE.Cmdlets.Storage +{ + /// + /// Uploads a local ISO file to a Proxmox VE storage. + /// + /// Uploads an ISO image from the local filesystem to the specified node/storage using + /// the Proxmox VE upload API. Streams the file in 4 MB chunks and reports progress + /// via Write-Progress. Returns a PveTask representing the upload job. + /// + /// + [Cmdlet(VerbsCommunications.Send, "PveIso", SupportsShouldProcess = true)] + [OutputType(typeof(PveTask))] + public class SendPveIsoCmdlet : PveCmdletBase + { + /// The Proxmox VE node to upload to. + [Parameter(Mandatory = true, Position = 0)] + public string Node { get; set; } = string.Empty; + + /// The target storage identifier (must support "iso" content). + [Parameter(Mandatory = true, Position = 1)] + public string Storage { get; set; } = string.Empty; + + /// + /// The full local path to the ISO file to upload. The file must exist. + /// + [Parameter(Mandatory = true, Position = 2)] + [FileExistsValidation] + public string Path { get; set; } = string.Empty; + + /// Optional checksum value to verify the uploaded file. + [Parameter(Mandatory = false)] + public string? Checksum { get; set; } + + /// Checksum algorithm used for verification. + [Parameter(Mandatory = false)] + [ValidateSet("md5", "sha1", "sha256", IgnoreCase = true)] + public string? ChecksumAlgorithm { get; set; } + + /// + /// When specified, waits for the upload task to complete before returning. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + protected override void ProcessRecord() + { + var fileName = System.IO.Path.GetFileName(Path); + if (!ShouldProcess($"{Node}/{Storage}/{fileName}", "Upload ISO")) + return; + + var session = GetSession(); + using var client = new PveHttpClient(session); + + var resource = $"/nodes/{Node}/storage/{Storage}/upload"; + + var activityId = 1; + var progressRecord = new ProgressRecord(activityId, + $"Uploading ISO to {Node}/{Storage}", + $"Uploading {fileName}..."); + + var json = client.UploadFileAsync( + resource, + Path, + formFields: new System.Collections.Generic.Dictionary + { + ["content"] = "iso" + }, + checksum: Checksum, + checksumAlgorithm: ChecksumAlgorithm, + progressCallback: (bytesSent, total) => + { + if (total > 0) + { + var pct = (int)((bytesSent * 100L) / total); + progressRecord.PercentComplete = pct; + progressRecord.StatusDescription = $"{bytesSent / 1024 / 1024} MB / {total / 1024 / 1024} MB"; + WriteProgress(progressRecord); + } + } + ).GetAwaiter().GetResult(); + + progressRecord.RecordType = ProgressRecordType.Completed; + WriteProgress(progressRecord); + + var root = JObject.Parse(json); + var upid = root["data"]?.ToString() ?? string.Empty; + + var task = new PveTask { Upid = upid, Node = Node, Status = "running" }; + + if (Wait.IsPresent && !string.IsNullOrEmpty(upid)) + { + task = WaitForTask(client, Node, upid); + } + + WriteObject(task); + } + + private static PveTask WaitForTask(PveHttpClient client, string node, string upid) + { + var encodedUpid = Uri.EscapeDataString(upid); + var statusResource = $"/nodes/{node}/tasks/{encodedUpid}/status"; + + while (true) + { + System.Threading.Thread.Sleep(2000); + var statusJson = client.GetAsync(statusResource).GetAwaiter().GetResult(); + var statusRoot = JObject.Parse(statusJson); + var data = statusRoot["data"]; + var status = data?["status"]?.ToString(); + var exitStatus = data?["exitstatus"]?.ToString(); + + if (status == "stopped") + { + return new PveTask + { + Upid = upid, + Node = node, + Status = status, + ExitStatus = exitStatus + }; + } + } + } + } + + /// Validates that a file path exists on disk. + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + internal sealed class FileExistsValidationAttribute : ValidateArgumentsAttribute + { + protected override void Validate(object arguments, EngineIntrinsics engineIntrinsics) + { + var path = arguments as string; + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + throw new ValidationMetadataException($"File not found: {path}"); + } + } +}