mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-08 21:43:12 +00:00
feat(cmdlets): implement storage and ISO upload cmdlets
Add Get-PveStorage, Get-PveStorageContent, Send-PveIso (with manual multipart upload and Write-Progress), Invoke-PveStorageDownload, New-PveStorage, and Remove-PveStorage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Models.Storage;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Lists storage definitions on a Proxmox VE node or cluster.</para>
|
||||
/// <para type="description">
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveStorage")]
|
||||
[OutputType(typeof(PveStorage))]
|
||||
public class GetPveStorageCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The Proxmox VE node name. Accepts pipeline input from Get-PveNode (PveNode.Name).
|
||||
/// When omitted the cluster-wide storage list is used.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true)]
|
||||
[Alias("NodeName")]
|
||||
public string? Node { get; set; }
|
||||
|
||||
/// <summary>Filter results to a specific storage type (e.g., "dir", "nfs", "zfspool").</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>Filter results to storages that support the given content type (e.g., "iso", "backup").</summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Models.Storage;
|
||||
using PSProxmoxVE.Core.Services;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Lists content items within a Proxmox VE storage.</para>
|
||||
/// <para type="description">
|
||||
/// Returns volume entries (ISOs, templates, backups, disk images) stored in the
|
||||
/// specified storage on the given node. Optionally filter by content type.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveStorageContent")]
|
||||
[OutputType(typeof(PveStorageContent))]
|
||||
public class GetPveStorageContentCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node name that hosts the storage.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The storage identifier. Accepts pipeline input from Get-PveStorage (PveStorage.Storage).
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
|
||||
public string Storage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Filter results to a specific content type (e.g., "iso", "vztmpl", "backup", "images").</summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Downloads a file from a URL directly into a Proxmox VE storage.</para>
|
||||
/// <para type="description">
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsLifecycle.Invoke, "PveStorageDownload", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(PveTask))]
|
||||
public class InvokePveStorageDownloadCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node that will perform the download.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The target storage identifier.</summary>
|
||||
[Parameter(Mandatory = true, Position = 1)]
|
||||
public string Storage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The URL to download the file from.</summary>
|
||||
[Parameter(Mandatory = true, Position = 2)]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The filename to save the downloaded file as on the storage.</summary>
|
||||
[Parameter(Mandatory = true, Position = 3)]
|
||||
public string Filename { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The content type category for the downloaded file. Defaults to "iso".</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
[ValidateSet("iso", "vztmpl", "backup", "import", IgnoreCase = true)]
|
||||
public string ContentType { get; set; } = "iso";
|
||||
|
||||
/// <summary>When specified, waits for the download task to complete before returning.</summary>
|
||||
[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<string, string>
|
||||
{
|
||||
["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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Creates a new storage definition in Proxmox VE.</para>
|
||||
/// <para type="description">
|
||||
/// Adds a new storage backend to the Proxmox VE cluster configuration.
|
||||
/// The storage will be available cluster-wide after creation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.New, "PveStorage", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(PveStorage))]
|
||||
public class NewPveStorageCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The unique storage identifier/name.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Storage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The storage type (e.g., "dir", "nfs", "lvm", "zfspool", "cephfs", "rbd").</summary>
|
||||
[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;
|
||||
|
||||
/// <summary>Comma-separated list of content types to support (e.g., "iso,vztmpl,backup").</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>Base directory path (for "dir" type storages).</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Path { get; set; }
|
||||
|
||||
/// <summary>NFS/CIFS server hostname or IP address.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Server { get; set; }
|
||||
|
||||
/// <summary>NFS export path or CIFS share name.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Export { get; set; }
|
||||
|
||||
/// <summary>LVM volume group name (for "lvm"/"lvmthin" types).</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? VgName { get; set; }
|
||||
|
||||
/// <summary>LVM thin pool name (for "lvmthin" type).</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? ThinPool { get; set; }
|
||||
|
||||
/// <summary>ZFS pool name (for "zfspool" type).</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Pool { get; set; }
|
||||
|
||||
/// <summary>Ceph pool name (for "rbd"/"cephfs" types).</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? CephPool { get; set; }
|
||||
|
||||
/// <summary>Monitor list for Ceph storages (comma-separated host:port pairs).</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? MonHost { get; set; }
|
||||
|
||||
/// <summary>Whether this storage is shared across cluster nodes.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public SwitchParameter Shared { get; set; }
|
||||
|
||||
/// <summary>Whether this storage is enabled. Defaults to enabled.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public SwitchParameter Disable { get; set; }
|
||||
|
||||
/// <summary>Limit nodes that can access this storage (comma-separated node names).</summary>
|
||||
[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<string, string>
|
||||
{
|
||||
["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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Removes a storage definition from Proxmox VE.</para>
|
||||
/// <para type="description">
|
||||
/// Deletes the specified storage definition from the Proxmox VE cluster configuration.
|
||||
/// This does not delete the underlying data — only the storage reference is removed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Remove, "PveStorage",
|
||||
SupportsShouldProcess = true,
|
||||
ConfirmImpact = ConfirmImpact.High)]
|
||||
public class RemovePveStorageCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The storage identifier to remove.</summary>
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Uploads a local ISO file to a Proxmox VE storage.</para>
|
||||
/// <para type="description">
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommunications.Send, "PveIso", SupportsShouldProcess = true)]
|
||||
[OutputType(typeof(PveTask))]
|
||||
public class SendPveIsoCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>The Proxmox VE node to upload to.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The target storage identifier (must support "iso" content).</summary>
|
||||
[Parameter(Mandatory = true, Position = 1)]
|
||||
public string Storage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The full local path to the ISO file to upload. The file must exist.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = true, Position = 2)]
|
||||
[FileExistsValidation]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Optional checksum value to verify the uploaded file.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public string? Checksum { get; set; }
|
||||
|
||||
/// <summary>Checksum algorithm used for verification.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
[ValidateSet("md5", "sha1", "sha256", IgnoreCase = true)]
|
||||
public string? ChecksumAlgorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When specified, waits for the upload task to complete before returning.
|
||||
/// </summary>
|
||||
[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<string, string>
|
||||
{
|
||||
["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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Validates that a file path exists on disk.</summary>
|
||||
[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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user