diff --git a/src/PSProxmoxVE.Core/Authentication/PveAuthMode.cs b/src/PSProxmoxVE.Core/Authentication/PveAuthMode.cs
new file mode 100644
index 0000000..2691ba3
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Authentication/PveAuthMode.cs
@@ -0,0 +1,9 @@
+namespace PSProxmoxVE.Core.Authentication
+{
+ /// Authentication mode for Proxmox VE API
+ public enum PveAuthMode
+ {
+ Ticket,
+ ApiToken
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Authentication/PveAuthenticator.cs b/src/PSProxmoxVE.Core/Authentication/PveAuthenticator.cs
new file mode 100644
index 0000000..47ee43a
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Authentication/PveAuthenticator.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json.Linq;
+using PSProxmoxVE.Core.Client;
+
+namespace PSProxmoxVE.Core.Authentication
+{
+ /// Provides methods to authenticate against a Proxmox VE server
+ public static class PveAuthenticator
+ {
+ // Pattern: USER@REALM!TOKENID=UUID
+ private static readonly Regex ApiTokenRegex = new Regex(
+ @"^[^@]+@[^!]+![^=]+=.+$",
+ RegexOptions.Compiled);
+
+ ///
+ /// Authenticates using username and password, obtaining a ticket and CSRF token.
+ /// The username must be in the form user@realm (e.g. root@pam).
+ ///
+ public static PveSession AuthenticateWithCredentials(
+ string hostname,
+ int port,
+ bool skipCertificateCheck,
+ string username,
+ string password)
+ {
+ if (string.IsNullOrWhiteSpace(hostname))
+ throw new ArgumentException("Hostname cannot be null or empty.", nameof(hostname));
+ if (string.IsNullOrWhiteSpace(username))
+ throw new ArgumentException("Username cannot be null or empty.", nameof(username));
+ if (!username.Contains("@"))
+ throw new ArgumentException("Username must include a realm (e.g. 'root@pam').", nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ var formData = new Dictionary
+ {
+ { "username", username },
+ { "password", password }
+ };
+
+ string responseBody;
+ using (var httpClient = new PveHttpClient(hostname, port, skipCertificateCheck))
+ {
+ responseBody = httpClient.Post("/api2/json/access/ticket", formData);
+ }
+
+ var json = JObject.Parse(responseBody);
+ var data = json["data"] ?? throw new InvalidOperationException("Response did not contain a 'data' field.");
+
+ var ticket = data["ticket"]?.Value()
+ ?? throw new InvalidOperationException("Response did not contain a ticket.");
+ var csrfToken = data["CSRFPreventionToken"]?.Value()
+ ?? throw new InvalidOperationException("Response did not contain a CSRFPreventionToken.");
+
+ var ticketExpiry = DateTime.UtcNow.AddHours(2);
+
+ var session = new PveSession(hostname, port, skipCertificateCheck, ticket, csrfToken, ticketExpiry);
+
+ session.ServerVersion = GetVersion(session);
+
+ return session;
+ }
+
+ ///
+ /// Authenticates using a Proxmox VE API token.
+ /// The token must be in the format USER@REALM!TOKENID=UUID (e.g. root@pam!mytoken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
+ ///
+ public static PveSession AuthenticateWithApiToken(
+ string hostname,
+ int port,
+ bool skipCertificateCheck,
+ string apiToken)
+ {
+ if (string.IsNullOrWhiteSpace(hostname))
+ throw new ArgumentException("Hostname cannot be null or empty.", nameof(hostname));
+ if (string.IsNullOrWhiteSpace(apiToken))
+ throw new ArgumentException("API token cannot be null or empty.", nameof(apiToken));
+ if (!ApiTokenRegex.IsMatch(apiToken))
+ throw new ArgumentException(
+ "API token must be in the format 'USER@REALM!TOKENID=UUID' (e.g. root@pam!mytoken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).",
+ nameof(apiToken));
+
+ var session = new PveSession(hostname, port, skipCertificateCheck, apiToken);
+
+ session.ServerVersion = GetVersion(session);
+
+ return session;
+ }
+
+ /// Retrieves and parses the server version from the PVE API
+ private static PveVersion GetVersion(PveSession session)
+ {
+ string responseBody;
+ using (var httpClient = new PveHttpClient(session))
+ {
+ responseBody = httpClient.Get("/api2/json/version");
+ }
+
+ var json = JObject.Parse(responseBody);
+ var data = json["data"] ?? throw new InvalidOperationException("Version response did not contain a 'data' field.");
+
+ var versionString = data["version"]?.Value()
+ ?? throw new InvalidOperationException("Version response did not contain a 'version' field.");
+
+ return PveVersion.Parse(versionString);
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Authentication/PveSession.cs b/src/PSProxmoxVE.Core/Authentication/PveSession.cs
new file mode 100644
index 0000000..cdbb587
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Authentication/PveSession.cs
@@ -0,0 +1,66 @@
+using System;
+using PSProxmoxVE.Core.Models;
+
+namespace PSProxmoxVE.Core.Authentication
+{
+ /// Represents an authenticated session to a Proxmox VE server
+ public class PveSession
+ {
+ public string Hostname { get; }
+ public int Port { get; }
+ public bool SkipCertificateCheck { get; }
+ public PveAuthMode AuthMode { get; }
+ public string? ApiToken { get; }
+ public string? Ticket { get; }
+ public string? CsrfToken { get; }
+ public DateTime TicketExpiry { get; }
+ public PveVersion ServerVersion { get; internal set; }
+
+ /// Returns true if the ticket has expired (only relevant for Ticket auth mode)
+ public bool IsExpired
+ {
+ get
+ {
+ if (AuthMode == PveAuthMode.ApiToken)
+ return false;
+ return DateTime.UtcNow >= TicketExpiry;
+ }
+ }
+
+ /// Base URL for the Proxmox VE API
+ public string BaseUrl => $"https://{Hostname}:{Port}/api2/json";
+
+ /// Creates a session using ticket-based authentication
+ internal PveSession(
+ string hostname,
+ int port,
+ bool skipCertificateCheck,
+ string ticket,
+ string csrfToken,
+ DateTime ticketExpiry)
+ {
+ Hostname = hostname;
+ Port = port;
+ SkipCertificateCheck = skipCertificateCheck;
+ AuthMode = PveAuthMode.Ticket;
+ Ticket = ticket;
+ CsrfToken = csrfToken;
+ TicketExpiry = ticketExpiry;
+ }
+
+ /// Creates a session using API token authentication
+ internal PveSession(
+ string hostname,
+ int port,
+ bool skipCertificateCheck,
+ string apiToken)
+ {
+ Hostname = hostname;
+ Port = port;
+ SkipCertificateCheck = skipCertificateCheck;
+ AuthMode = PveAuthMode.ApiToken;
+ ApiToken = apiToken;
+ TicketExpiry = DateTime.MaxValue;
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Authentication/PveVersion.cs b/src/PSProxmoxVE.Core/Authentication/PveVersion.cs
new file mode 100644
index 0000000..ebcdac3
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Authentication/PveVersion.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace PSProxmoxVE.Core.Authentication
+{
+ /// Represents a Proxmox VE server version (major.minor only)
+ public class PveVersion
+ {
+ public int Major { get; }
+ public int Minor { get; }
+
+ public PveVersion(int major, int minor)
+ {
+ Major = major;
+ Minor = minor;
+ }
+
+ /// Parse version from PVE API format "major.minor-patchlevel" e.g. "9.1-1"
+ public static PveVersion Parse(string versionString)
+ {
+ if (string.IsNullOrWhiteSpace(versionString))
+ throw new ArgumentException("Version string cannot be null or empty.", nameof(versionString));
+
+ // Strip patchlevel: "9.1-1" -> "9.1"
+ var dashIndex = versionString.IndexOf('-');
+ var majorMinor = dashIndex >= 0 ? versionString.Substring(0, dashIndex) : versionString;
+
+ var parts = majorMinor.Split('.');
+ if (parts.Length < 2)
+ throw new FormatException($"Invalid PVE version format: '{versionString}'. Expected 'major.minor' or 'major.minor-patchlevel'.");
+
+ if (!int.TryParse(parts[0], out var major) || !int.TryParse(parts[1], out var minor))
+ throw new FormatException($"Invalid PVE version format: '{versionString}'. Major and minor must be integers.");
+
+ return new PveVersion(major, minor);
+ }
+
+ public bool IsAtLeast(int major, int minor)
+ {
+ return Major > major || (Major == major && Minor >= minor);
+ }
+
+ public override string ToString() => $"{Major}.{Minor}";
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveApiException.cs b/src/PSProxmoxVE.Core/Exceptions/PveApiException.cs
new file mode 100644
index 0000000..1d76a80
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveApiException.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Net;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when the Proxmox VE API returns an error response
+ public class PveApiException : Exception
+ {
+ public HttpStatusCode StatusCode { get; }
+ public string Resource { get; }
+ public string HttpMethod { get; }
+
+ public PveApiException(HttpStatusCode statusCode, string message, string resource, string httpMethod)
+ : base($"PVE API error ({(int)statusCode} {statusCode}) on {httpMethod} {resource}: {message}")
+ {
+ StatusCode = statusCode;
+ Resource = resource;
+ HttpMethod = httpMethod;
+ }
+
+ public PveApiException(HttpStatusCode statusCode, string message, string resource, string httpMethod, Exception innerException)
+ : base($"PVE API error ({(int)statusCode} {statusCode}) on {httpMethod} {resource}: {message}", innerException)
+ {
+ StatusCode = statusCode;
+ Resource = resource;
+ HttpMethod = httpMethod;
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs b/src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs
new file mode 100644
index 0000000..6d4112c
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveAuthenticationException.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when authentication to a Proxmox VE server fails
+ public class PveAuthenticationException : Exception
+ {
+ public PveAuthenticationException(string message)
+ : base(message)
+ {
+ }
+
+ public PveAuthenticationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveNotConnectedException.cs b/src/PSProxmoxVE.Core/Exceptions/PveNotConnectedException.cs
new file mode 100644
index 0000000..296bedd
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveNotConnectedException.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when an operation is attempted without an active Proxmox VE session
+ public class PveNotConnectedException : Exception
+ {
+ public PveNotConnectedException()
+ : base("No active Proxmox VE session. Run Connect-PveServer first.")
+ {
+ }
+
+ public PveNotConnectedException(Exception innerException)
+ : base("No active Proxmox VE session. Run Connect-PveServer first.", innerException)
+ {
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveSessionExpiredException.cs b/src/PSProxmoxVE.Core/Exceptions/PveSessionExpiredException.cs
new file mode 100644
index 0000000..eb78e03
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveSessionExpiredException.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when the Proxmox VE session ticket has expired
+ public class PveSessionExpiredException : Exception
+ {
+ public PveSessionExpiredException()
+ : base("Your Proxmox VE session has expired. Please run Connect-PveServer to establish a new session.")
+ {
+ }
+
+ public PveSessionExpiredException(Exception innerException)
+ : base("Your Proxmox VE session has expired. Please run Connect-PveServer to establish a new session.", innerException)
+ {
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveTaskFailedException.cs b/src/PSProxmoxVE.Core/Exceptions/PveTaskFailedException.cs
new file mode 100644
index 0000000..0b99ffd
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveTaskFailedException.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when a Proxmox VE task completes with a failed exit status
+ public class PveTaskFailedException : Exception
+ {
+ public string Upid { get; }
+ public string ExitStatus { get; }
+
+ public PveTaskFailedException(string upid, string exitStatus)
+ : base($"Task {upid} failed with exit status: {exitStatus}")
+ {
+ Upid = upid;
+ ExitStatus = exitStatus;
+ }
+
+ public PveTaskFailedException(string upid, string exitStatus, Exception innerException)
+ : base($"Task {upid} failed with exit status: {exitStatus}", innerException)
+ {
+ Upid = upid;
+ ExitStatus = exitStatus;
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveTaskTimeoutException.cs b/src/PSProxmoxVE.Core/Exceptions/PveTaskTimeoutException.cs
new file mode 100644
index 0000000..3d5d725
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveTaskTimeoutException.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when a Proxmox VE task does not complete within the allowed timeout period
+ public class PveTaskTimeoutException : Exception
+ {
+ public string Upid { get; }
+ public TimeSpan Timeout { get; }
+
+ public PveTaskTimeoutException(string upid, TimeSpan timeout)
+ : base($"Task {upid} did not complete within {timeout.TotalSeconds} seconds.")
+ {
+ Upid = upid;
+ Timeout = timeout;
+ }
+
+ public PveTaskTimeoutException(string upid, TimeSpan timeout, Exception innerException)
+ : base($"Task {upid} did not complete within {timeout.TotalSeconds} seconds.", innerException)
+ {
+ Upid = upid;
+ Timeout = timeout;
+ }
+ }
+}
diff --git a/src/PSProxmoxVE.Core/Exceptions/PveVersionException.cs b/src/PSProxmoxVE.Core/Exceptions/PveVersionException.cs
new file mode 100644
index 0000000..e2606a8
--- /dev/null
+++ b/src/PSProxmoxVE.Core/Exceptions/PveVersionException.cs
@@ -0,0 +1,29 @@
+using System;
+using PSProxmoxVE.Core.Authentication;
+
+namespace PSProxmoxVE.Core.Exceptions
+{
+ /// Exception thrown when the connected Proxmox VE server does not meet the minimum version requirement for an operation
+ public class PveVersionException : Exception
+ {
+ public int RequiredMajor { get; }
+ public int RequiredMinor { get; }
+ public PveVersion ActualVersion { get; }
+
+ public PveVersionException(int requiredMajor, int requiredMinor, PveVersion actualVersion)
+ : base($"This operation requires Proxmox VE {requiredMajor}.{requiredMinor} or later. Connected server is version {actualVersion}.")
+ {
+ RequiredMajor = requiredMajor;
+ RequiredMinor = requiredMinor;
+ ActualVersion = actualVersion;
+ }
+
+ public PveVersionException(int requiredMajor, int requiredMinor, PveVersion actualVersion, Exception innerException)
+ : base($"This operation requires Proxmox VE {requiredMajor}.{requiredMinor} or later. Connected server is version {actualVersion}.", innerException)
+ {
+ RequiredMajor = requiredMajor;
+ RequiredMinor = requiredMinor;
+ ActualVersion = actualVersion;
+ }
+ }
+}