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}");
+ }
+ }
+}