mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
feat(cmdlets): implement connection and node cmdlets
Add ModuleState, PveCmdletBase with -Session parameter and session validation. Implement Connect-PveServer, Disconnect-PveServer, Test-PveConnection, Get-PveNode, and Get-PveNodeStatus with pipeline support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Establishes an authenticated session to a Proxmox VE server.</para>
|
||||
/// <para type="description">
|
||||
/// Connect-PveServer authenticates against the Proxmox VE API using either a
|
||||
/// PSCredential (username/password) or a pre-generated API token, and stores the
|
||||
/// resulting session in module state for use by subsequent cmdlets.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommunications.Connect, "PveServer",
|
||||
DefaultParameterSetName = ParameterSetCredential)]
|
||||
[OutputType(typeof(PveSession))]
|
||||
public sealed class ConnectPveServerCmdlet : PSCmdlet
|
||||
{
|
||||
private const string ParameterSetCredential = "Credential";
|
||||
private const string ParameterSetApiToken = "ApiToken";
|
||||
|
||||
/// <summary>Hostname or IP address of the Proxmox VE server.</summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string Server { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>API port. Defaults to 8006.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
[ValidateRange(1, 65535)]
|
||||
public int Port { get; set; } = 8006;
|
||||
|
||||
/// <summary>Username and password credential. Username must include a realm, e.g. root@pam.</summary>
|
||||
[Parameter(Mandatory = true, ParameterSetName = ParameterSetCredential)]
|
||||
[ValidateNotNull]
|
||||
public PSCredential? Credential { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Proxmox VE API token in the format USER@REALM!TOKENID=UUID,
|
||||
/// e.g. root@pam!mytoken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = true, ParameterSetName = ParameterSetApiToken)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string? ApiToken { get; set; }
|
||||
|
||||
/// <summary>When specified, skips TLS certificate validation for the server.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public SwitchParameter SkipCertificateCheck { get; set; }
|
||||
|
||||
/// <summary>When specified, writes the resulting PveSession object to the pipeline.</summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public SwitchParameter PassThru { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
PveSession session;
|
||||
|
||||
switch (ParameterSetName)
|
||||
{
|
||||
case ParameterSetCredential:
|
||||
{
|
||||
if (Credential is null)
|
||||
ThrowTerminatingError(new ErrorRecord(
|
||||
new ArgumentNullException(nameof(Credential)),
|
||||
"CredentialRequired",
|
||||
ErrorCategory.InvalidArgument,
|
||||
null));
|
||||
|
||||
var username = Credential!.UserName;
|
||||
var password = Credential.GetNetworkCredential().Password;
|
||||
|
||||
try
|
||||
{
|
||||
session = PveAuthenticator.AuthenticateWithCredentials(
|
||||
Server, Port, SkipCertificateCheck.IsPresent, username, password);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThrowTerminatingError(new ErrorRecord(
|
||||
ex,
|
||||
"PveAuthenticationFailed",
|
||||
ErrorCategory.AuthenticationError,
|
||||
Server));
|
||||
return; // unreachable — satisfies compiler
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ParameterSetApiToken:
|
||||
{
|
||||
try
|
||||
{
|
||||
session = PveAuthenticator.AuthenticateWithApiToken(
|
||||
Server, Port, SkipCertificateCheck.IsPresent, ApiToken!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThrowTerminatingError(new ErrorRecord(
|
||||
ex,
|
||||
"PveAuthenticationFailed",
|
||||
ErrorCategory.AuthenticationError,
|
||||
Server));
|
||||
return; // unreachable — satisfies compiler
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
ThrowTerminatingError(new ErrorRecord(
|
||||
new InvalidOperationException($"Unknown parameter set: {ParameterSetName}"),
|
||||
"UnknownParameterSet",
|
||||
ErrorCategory.InvalidOperation,
|
||||
null));
|
||||
return;
|
||||
}
|
||||
|
||||
ModuleState.ActiveSession = session;
|
||||
|
||||
WriteVerbose($"Connected to {Server}:{Port} as {session.AuthMode} (PVE {session.ServerVersion}).");
|
||||
|
||||
if (PassThru.IsPresent)
|
||||
WriteObject(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Closes the active Proxmox VE session.</para>
|
||||
/// <para type="description">
|
||||
/// Disconnect-PveServer invalidates the module-level session. For ticket-based
|
||||
/// sessions it additionally attempts a best-effort DELETE against the
|
||||
/// /access/ticket endpoint to invalidate the server-side ticket. Errors from
|
||||
/// that request are silently ignored so that the local session is always cleared.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommunications.Disconnect, "PveServer",
|
||||
SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)]
|
||||
public sealed class DisconnectPveServerCmdlet : PSCmdlet
|
||||
{
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = ModuleState.ActiveSession;
|
||||
|
||||
if (session is null)
|
||||
{
|
||||
WriteWarning("No active Proxmox VE session to disconnect.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ShouldProcess($"{session.Hostname}:{session.Port}", "Disconnect"))
|
||||
return;
|
||||
|
||||
// Best-effort server-side ticket invalidation for ticket-based auth.
|
||||
if (session.AuthMode == PveAuthMode.Ticket)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new PveHttpClient(session);
|
||||
client.DeleteAsync("/api2/json/access/ticket").GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Non-fatal — we still clear the local session below.
|
||||
WriteVerbose($"Server-side ticket invalidation failed (ignored): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
ModuleState.ActiveSession = null;
|
||||
|
||||
WriteVerbose($"Disconnected from {session.Hostname}:{session.Port}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Tests whether an active, non-expired Proxmox VE session exists.</para>
|
||||
/// <para type="description">
|
||||
/// Without -Detailed, returns $true if a session is present and not expired, otherwise $false.
|
||||
/// With -Detailed, returns the PveSession object (or nothing if there is no active session).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsDiagnostic.Test, "PveConnection")]
|
||||
[OutputType(typeof(bool), ParameterSetName = new[] { "Default" })]
|
||||
[OutputType(typeof(PveSession), ParameterSetName = new[] { "Detailed" })]
|
||||
public sealed class TestPveConnectionCmdlet : PSCmdlet
|
||||
{
|
||||
/// <summary>
|
||||
/// When specified, writes the PveSession object instead of a boolean.
|
||||
/// Nothing is written if no session is active.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false, ParameterSetName = "Detailed")]
|
||||
public SwitchParameter Detailed { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = ModuleState.ActiveSession;
|
||||
var isConnected = session is not null && !session.IsExpired;
|
||||
|
||||
if (Detailed.IsPresent)
|
||||
{
|
||||
if (isConnected)
|
||||
WriteObject(session);
|
||||
// If not connected, write nothing — caller can check $null.
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteObject(isConnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management.Automation;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Nodes;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Nodes
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Returns one or more Proxmox VE cluster nodes.</para>
|
||||
/// <para type="description">
|
||||
/// Get-PveNode retrieves all nodes visible to the authenticated session.
|
||||
/// Use -Name to filter to a specific node by exact name.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveNode")]
|
||||
[OutputType(typeof(PveNode))]
|
||||
public sealed class GetPveNodeCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>Optional node name filter. When specified, only the matching node is returned.</summary>
|
||||
[Parameter(Mandatory = false, Position = 0)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string? Name { get; set; }
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = GetSession();
|
||||
|
||||
string responseBody;
|
||||
try
|
||||
{
|
||||
using var client = new PveHttpClient(session);
|
||||
responseBody = client.GetAsync("/api2/json/nodes").GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThrowTerminatingError(new ErrorRecord(
|
||||
ex,
|
||||
"GetPveNodeFailed",
|
||||
ErrorCategory.ConnectionError,
|
||||
session.Hostname));
|
||||
return;
|
||||
}
|
||||
|
||||
var json = JObject.Parse(responseBody);
|
||||
var dataToken = json["data"] ?? throw new InvalidOperationException("Response did not contain a 'data' field.");
|
||||
|
||||
var nodes = dataToken.ToObject<List<PveNode>>(
|
||||
JsonSerializer.CreateDefault()) ?? new List<PveNode>();
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (Name is null || string.Equals(node.Name, Name, StringComparison.OrdinalIgnoreCase))
|
||||
WriteObject(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Management.Automation;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PSProxmoxVE.Core.Client;
|
||||
using PSProxmoxVE.Core.Models.Nodes;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets.Nodes
|
||||
{
|
||||
/// <summary>
|
||||
/// <para type="synopsis">Returns detailed status information for a Proxmox VE node.</para>
|
||||
/// <para type="description">
|
||||
/// Get-PveNodeStatus retrieves CPU, memory, disk, swap, network and uptime statistics
|
||||
/// for the specified node from the /nodes/{node}/status endpoint.
|
||||
/// The -Node parameter accepts values from the pipeline by property name, so you can
|
||||
/// pipe output from Get-PveNode directly.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Cmdlet(VerbsCommon.Get, "PveNodeStatus")]
|
||||
[OutputType(typeof(PveNodeStatus))]
|
||||
public sealed class GetPveNodeStatusCmdlet : PveCmdletBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the node to query. Accepts pipeline input via the PveNode.Name property.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
|
||||
[ValidateNotNullOrEmpty]
|
||||
public string Node { get; set; } = string.Empty;
|
||||
|
||||
protected override void ProcessRecord()
|
||||
{
|
||||
var session = GetSession();
|
||||
|
||||
var resource = $"/api2/json/nodes/{Uri.EscapeDataString(Node)}/status";
|
||||
|
||||
string responseBody;
|
||||
try
|
||||
{
|
||||
using var client = new PveHttpClient(session);
|
||||
responseBody = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ThrowTerminatingError(new ErrorRecord(
|
||||
ex,
|
||||
"GetPveNodeStatusFailed",
|
||||
ErrorCategory.ConnectionError,
|
||||
Node));
|
||||
return;
|
||||
}
|
||||
|
||||
var json = JObject.Parse(responseBody);
|
||||
var dataToken = json["data"] ?? throw new InvalidOperationException("Response did not contain a 'data' field.");
|
||||
|
||||
var status = dataToken.ToObject<PveNodeStatus>(JsonSerializer.CreateDefault())
|
||||
?? throw new InvalidOperationException("Failed to deserialize node status.");
|
||||
|
||||
// The /nodes/{node}/status response does not include the node name; populate it.
|
||||
if (string.IsNullOrEmpty(status.Name))
|
||||
status.Name = Node;
|
||||
|
||||
WriteObject(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Management.Automation;
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
using PSProxmoxVE.Core.Exceptions;
|
||||
|
||||
namespace PSProxmoxVE.Cmdlets
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all PSProxmoxVE cmdlets. Provides optional -Session parameter
|
||||
/// and a helper method to resolve and validate the active session.
|
||||
/// </summary>
|
||||
public abstract class PveCmdletBase : PSCmdlet
|
||||
{
|
||||
/// <summary>
|
||||
/// An explicit PveSession to use for this cmdlet invocation.
|
||||
/// When omitted, the module-level session stored by Connect-PveServer is used.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
public PveSession? Session { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the session to use for this cmdlet.
|
||||
/// Resolution order: -Session parameter → ModuleState.ActiveSession.
|
||||
/// Throws <see cref="PveNotConnectedException"/> if no session is available,
|
||||
/// or <see cref="PveSessionExpiredException"/> if the session ticket has expired.
|
||||
/// </summary>
|
||||
protected PveSession GetSession()
|
||||
{
|
||||
var session = Session ?? ModuleState.ActiveSession;
|
||||
|
||||
if (session is null)
|
||||
throw new PveNotConnectedException();
|
||||
|
||||
if (session.IsExpired)
|
||||
throw new PveSessionExpiredException();
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using PSProxmoxVE.Core.Authentication;
|
||||
|
||||
namespace PSProxmoxVE
|
||||
{
|
||||
/// <summary>Module-scoped state container for the active PVE session</summary>
|
||||
internal static class ModuleState
|
||||
{
|
||||
internal static PveSession? ActiveSession { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user