Files
PSProxmoxVE/src/PSProxmoxVE.Core/Services/BackupService.cs
T
goodolclint-claude[bot] 089bb7c81e refactor: one task-response parser, and the dead code #154 names (part A) (#207)
* refactor: one task-response parser, and the dead code #154 names (part A)

Unifies the 8 byte-similar private ParseTask methods in BackupService,
ContainerService, NetworkService, NodeService, SnapshotService,
StorageService, TemplateService, and VmService into one shared
PveTaskResponse.Parse(json, node) utility, on the variant that stamps
Status = "running" for a bare-UPID response. BackupService, NodeService,
TemplateService, and VmService previously left Status null for that case;
their non--Wait task output now reports "running" like the other four
services already did.

Removes the duplicate ClusterConfigService.GetClusterStatus in favor of
ClusterService's; ClusterConfigService now holds a ClusterService built
from the same injected/default client, and WaitForQuorum and
Get-PveClusterStatus go through it.

Removes dead code named in issue #154 and its fold-in comments: the
never-called PveHttpClient/IPveHttpClient sync wrappers Put/Delete, the
never-thrown PveAuthenticationException, a #pragma around an
already-nullable field, the unreferenced TestHelper mock-handler helpers,
three hand-rolled version-warning blocks (now PveCmdletBase.WarnIfBelowVersion),
an unreachable catch(HttpRequestException) arm in WaitForStatusTransition,
dead ExitStatus-checking branches in ImportPveOvaCmdlet after WaitForTask
(which already throws on failure), and an unreachable int branch in
ApiValueHelper.IsExited.

Fixes the WaitForStatusTransition catch removal's premise: PveHttpClient
read the response body outside its HttpRequestException try block, so a
mid-body stream drop could still escape unwrapped. Moves the body read
inside the try so every HttpRequestException the client can throw becomes
a PveApiException, matching what the removed catch assumed.

Part of #154.

* Add service files: BackupService, ClusterConfigService, ContainerService, NetworkService

* Add service files: NodeService, SnapshotService, StorageService, TemplateService

* Add VmService and Utilities files

* Remove unused PveAuthenticationException (never thrown, caught, or tested)

* Add cmdlet files: GetPveClusterStatus, SDN subnets, PveCmdletBase, SendPveFile, ImportPveOva

* Add test files: BackupServiceTests, ClusterConfigServiceTests, NodeServiceTests

* Add remaining test files: TemplateServiceTests, VmServiceTests, TestHelper, ApiValueHelperTests, PveTaskResponseTests

* Add VmServiceTests

---------

Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
2026-09-03 00:59:51 +00:00

156 lines
6.2 KiB
C#

using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using PSProxmoxVE.Core.Authentication;
using PSProxmoxVE.Core.Client;
using PSProxmoxVE.Core.Models.Backup;
using PSProxmoxVE.Core.Models.Vms;
using PSProxmoxVE.Core.Utilities;
namespace PSProxmoxVE.Core.Services
{
/// <summary>
/// Service for Proxmox VE Backup (vzdump) and backup job API operations.
/// </summary>
public class BackupService : PveServiceBase
{
/// <summary>
/// Initializes a new instance of <see cref="BackupService"/> with no injected client.
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
/// </summary>
public BackupService() { }
/// <summary>
/// Initializes a new instance of <see cref="BackupService"/> with an injected HTTP client.
/// The caller owns the client's lifetime; this service will not dispose it.
/// </summary>
/// <param name="client">The HTTP client to use for all requests.</param>
public BackupService(IPveHttpClient client) : base(client) { }
// -------------------------------------------------------------------------
// Ad-hoc backup (vzdump)
// -------------------------------------------------------------------------
/// <summary>
/// Creates an ad-hoc backup via vzdump. Returns the task UPID.
/// </summary>
/// <param name="session">The authenticated PVE session.</param>
/// <param name="node">The node to run the backup on.</param>
/// <param name="config">Backup configuration parameters.</param>
public PveTask CreateBackup(PveSession session, string node, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
if (config == null) throw new ArgumentNullException(nameof(config));
return Invoke(session, client =>
{
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/vzdump", config)
.GetAwaiter().GetResult();
return PveTaskResponse.Parse(response, node);
});
}
// -------------------------------------------------------------------------
// Backup jobs (scheduled)
// -------------------------------------------------------------------------
/// <summary>
/// Returns all scheduled backup jobs.
/// </summary>
public PveBackupJob[] GetBackupJobs(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
return Invoke(session, client =>
{
var response = client.GetAsync("cluster/backup").GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveBackupJob[]>() ?? Array.Empty<PveBackupJob>();
});
}
/// <summary>
/// Returns a single scheduled backup job by ID.
/// </summary>
public PveBackupJob? GetBackupJob(PveSession session, string id)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
return Invoke(session, client =>
{
var response = client.GetAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return data?.ToObject<PveBackupJob>();
});
}
/// <summary>
/// Creates a scheduled backup job.
/// </summary>
public void CreateBackupJob(PveSession session, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (config == null) throw new ArgumentNullException(nameof(config));
Invoke(session, client =>
{
client.PostAsync("cluster/backup", config).GetAwaiter().GetResult();
});
}
/// <summary>
/// Updates a scheduled backup job.
/// </summary>
public void UpdateBackupJob(PveSession session, string id, Dictionary<string, string> config)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
if (config == null) throw new ArgumentNullException(nameof(config));
Invoke(session, client =>
{
client.PutAsync($"cluster/backup/{Uri.EscapeDataString(id)}", config)
.GetAwaiter().GetResult();
});
}
/// <summary>
/// Removes a scheduled backup job.
/// </summary>
public void RemoveBackupJob(PveSession session, string id)
{
if (session == null) throw new ArgumentNullException(nameof(session));
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
Invoke(session, client =>
{
client.DeleteAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
.GetAwaiter().GetResult();
});
}
// -------------------------------------------------------------------------
// Backup compliance info
// -------------------------------------------------------------------------
/// <summary>
/// Returns the list of guests not covered by any backup job.
/// </summary>
public List<Dictionary<string, object?>> GetNotBackedUp(PveSession session)
{
if (session == null) throw new ArgumentNullException(nameof(session));
return Invoke(session, client =>
{
var response = client.GetAsync("cluster/backup-info/not-backed-up")
.GetAwaiter().GetResult();
var data = JObject.Parse(response)["data"];
return JsonHelper.ToListOfDictionaries(data as JArray);
});
}
}
}