346 lines
12 KiB
C#
346 lines
12 KiB
C#
using ZA.CoreService.ESBCertificateManager.Models;
|
||
|
||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||
|
||
/// <summary>
|
||
/// Orchestriert nur noch Container-Neustart (kein Deploy/TLS/SQL).
|
||
/// </summary>
|
||
public sealed class DeploymentOrchestrator
|
||
{
|
||
private readonly RestartExecutor _restartExecutor;
|
||
private readonly LocalCertificateDeployer _localCertificateDeployer;
|
||
private readonly PreflightValidator _preflightValidator = new();
|
||
private readonly AppSettings _settings;
|
||
|
||
public DeploymentOrchestrator(
|
||
AppSettings settings,
|
||
IReadOnlyList<SonicConnection> sonicConnections)
|
||
{
|
||
_settings = settings;
|
||
|
||
_restartExecutor =
|
||
new RestartExecutor(sonicConnections);
|
||
|
||
_localCertificateDeployer =
|
||
new LocalCertificateDeployer();
|
||
}
|
||
|
||
|
||
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
|
||
|
||
public async Task<DeploymentRunResult> RestartOnlyAsync(
|
||
IReadOnlyList<DeploymentTarget> selectedTargets,
|
||
IProgress<TargetProgressUpdate>? progress,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
DateTimeOffset startedAt = DateTimeOffset.Now;
|
||
List<TargetStepResult> results = [];
|
||
|
||
using RunLogger logger = new(_settings.LogDirectory);
|
||
logger.Write($"Neustart gestartet für {selectedTargets.Count} Ziel(e).");
|
||
|
||
foreach (DeploymentTarget target in selectedTargets)
|
||
{
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
results.Add(await RestartTargetOnlyAsync(target, logger, progress, cancellationToken));
|
||
}
|
||
|
||
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
||
DeploymentRunResult runResult = new()
|
||
{
|
||
TargetResults = results,
|
||
StartedAt = startedAt,
|
||
FinishedAt = finishedAt,
|
||
CertificateFilePath = string.Empty,
|
||
CertificateFingerprint = string.Empty
|
||
};
|
||
|
||
logger.Write(
|
||
$"Neustart beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}");
|
||
|
||
return runResult;
|
||
}
|
||
public async Task<DeploymentRunResult> DeployAsync(
|
||
string certificateFilePath,
|
||
string certificateFingerprint,
|
||
IReadOnlyList<DeploymentTarget> selectedTargets,
|
||
IProgress<TargetProgressUpdate>? progress,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(certificateFilePath))
|
||
{
|
||
throw new ArgumentException(
|
||
"Es wurde kein Zertifikatsdateipfad angegeben.",
|
||
nameof(certificateFilePath));
|
||
}
|
||
|
||
if (!File.Exists(certificateFilePath))
|
||
{
|
||
throw new FileNotFoundException(
|
||
"Die ausgewählte Zertifikatsdatei wurde nicht gefunden.",
|
||
certificateFilePath);
|
||
}
|
||
|
||
if (selectedTargets.Count == 0)
|
||
{
|
||
throw new ArgumentException(
|
||
"Es wurde kein Bereitstellungsziel ausgewählt.",
|
||
nameof(selectedTargets));
|
||
}
|
||
|
||
DateTimeOffset startedAt = DateTimeOffset.Now;
|
||
List<TargetStepResult> results = [];
|
||
|
||
using RunLogger logger = new(_settings.LogDirectory);
|
||
|
||
logger.Write(
|
||
$"Deployment gestartet für {selectedTargets.Count} Ziel(e). " +
|
||
$"Zertifikat={Path.GetFileName(certificateFilePath)}");
|
||
|
||
foreach (DeploymentTarget target in selectedTargets)
|
||
{
|
||
cancellationToken.ThrowIfCancellationRequested();
|
||
|
||
TargetStepResult result = await DeployTargetAsync(
|
||
certificateFilePath,
|
||
target,
|
||
logger,
|
||
progress,
|
||
cancellationToken);
|
||
|
||
results.Add(result);
|
||
}
|
||
|
||
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
||
|
||
DeploymentRunResult runResult = new()
|
||
{
|
||
TargetResults = results,
|
||
StartedAt = startedAt,
|
||
FinishedAt = finishedAt,
|
||
CertificateFilePath = certificateFilePath,
|
||
CertificateFingerprint = certificateFingerprint
|
||
};
|
||
|
||
logger.Write(
|
||
$"Deployment beendet. Erfolg={runResult.OverallSuccess}; " +
|
||
$"Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; " +
|
||
$"Log={logger.LogFilePath}");
|
||
|
||
return runResult;
|
||
}
|
||
private async Task<TargetStepResult> RestartTargetOnlyAsync(
|
||
DeploymentTarget target,
|
||
RunLogger logger,
|
||
IProgress<TargetProgressUpdate>? progress,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
List<string> steps = [];
|
||
DateTimeOffset targetStart = DateTimeOffset.Now;
|
||
progress?.Report(new TargetProgressUpdate(target.Id, "Neustart…", false));
|
||
|
||
(bool restartOk, string restartStatus, string? restartDetail) =
|
||
await _restartExecutor.ExecuteAsync(target, cancellationToken);
|
||
|
||
steps.Add($"{restartStatus}: {restartDetail}");
|
||
logger.Write($"[{target.Name}] Restart: {restartStatus} | {restartDetail}");
|
||
|
||
TargetStepResult targetResult = new()
|
||
{
|
||
TargetId = target.Id,
|
||
TargetName = target.Name,
|
||
Success = restartOk,
|
||
StatusText = restartStatus,
|
||
Detail = restartDetail,
|
||
Steps = steps,
|
||
StartedAt = targetStart,
|
||
FinishedAt = DateTimeOffset.Now,
|
||
CopySucceeded = true,
|
||
RestartSucceeded = restartOk,
|
||
TlsSucceeded = true,
|
||
ObservedFingerprint = null
|
||
};
|
||
|
||
progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk));
|
||
return targetResult;
|
||
}
|
||
private async Task<TargetStepResult> DeployTargetAsync(
|
||
string certificateFilePath,
|
||
DeploymentTarget target,
|
||
RunLogger logger,
|
||
IProgress<TargetProgressUpdate>? progress,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
List<string> steps = [];
|
||
DateTimeOffset targetStart = DateTimeOffset.Now;
|
||
|
||
try
|
||
{
|
||
progress?.Report(new TargetProgressUpdate(
|
||
target.Id,
|
||
$"Zertifikat wird nach '{target.TargetDirectory}' kopiert ...",
|
||
false));
|
||
|
||
string deployedFilePath =
|
||
await _localCertificateDeployer.DeployAsync(
|
||
certificateFilePath,
|
||
target.TargetDirectory,
|
||
target.CertificateFileName,
|
||
target.BackupEnabled,
|
||
target.BackupDirectoryName,
|
||
cancellationToken);
|
||
|
||
string copyStatus =
|
||
$"Zertifikat kopiert: {Path.GetFileName(deployedFilePath)}";
|
||
|
||
steps.Add(copyStatus);
|
||
logger.Write(
|
||
$"[{target.Name}] {copyStatus} | Ziel={deployedFilePath}");
|
||
|
||
progress?.Report(new TargetProgressUpdate(
|
||
target.Id,
|
||
copyStatus,
|
||
true));
|
||
|
||
// Nach dem Dateitausch Container neu starten, damit Sonic
|
||
// das neue Zertifikat lädt. Ohne Neustart bleibt oft das Alte aktiv.
|
||
bool restartNeeded =
|
||
target.RestartType is RestartType.SonicContainer
|
||
or RestartType.SonicContainerWithXapi
|
||
|| !string.IsNullOrWhiteSpace(target.ContainerName);
|
||
|
||
if (!restartNeeded)
|
||
{
|
||
return new TargetStepResult
|
||
{
|
||
TargetId = target.Id,
|
||
TargetName = target.Name,
|
||
Success = true,
|
||
StatusText = "Kopieren erfolgreich (kein Neustart)",
|
||
Detail = deployedFilePath,
|
||
Steps = steps,
|
||
StartedAt = targetStart,
|
||
FinishedAt = DateTimeOffset.Now,
|
||
CopySucceeded = true,
|
||
RestartSucceeded = true,
|
||
TlsSucceeded = true,
|
||
ObservedFingerprint = null
|
||
};
|
||
}
|
||
|
||
progress?.Report(new TargetProgressUpdate(
|
||
target.Id,
|
||
"Zertifikat kopiert – Container-Neustart …",
|
||
true));
|
||
|
||
(bool restartOk, string restartStatus, string? restartDetail) =
|
||
await _restartExecutor.ExecuteAsync(
|
||
target,
|
||
cancellationToken);
|
||
|
||
string restartStep =
|
||
$"{restartStatus}: {restartDetail}";
|
||
|
||
steps.Add(restartStep);
|
||
logger.Write(
|
||
$"[{target.Name}] Restart nach Deploy: {restartStep}");
|
||
|
||
progress?.Report(new TargetProgressUpdate(
|
||
target.Id,
|
||
restartOk
|
||
? "Austausch + Neustart OK"
|
||
: restartStatus,
|
||
restartOk));
|
||
|
||
return new TargetStepResult
|
||
{
|
||
TargetId = target.Id,
|
||
TargetName = target.Name,
|
||
Success = restartOk,
|
||
StatusText = restartOk
|
||
? "Austausch + Neustart OK"
|
||
: $"Kopiert, Neustart fehlgeschlagen: {restartStatus}",
|
||
Detail =
|
||
$"Datei={deployedFilePath}"
|
||
+ Environment.NewLine
|
||
+ (restartDetail ?? string.Empty),
|
||
Steps = steps,
|
||
StartedAt = targetStart,
|
||
FinishedAt = DateTimeOffset.Now,
|
||
CopySucceeded = true,
|
||
RestartSucceeded = restartOk,
|
||
TlsSucceeded = true,
|
||
ObservedFingerprint = null
|
||
};
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
string errorStatus = $"Kopieren fehlgeschlagen: {ex.Message}";
|
||
|
||
steps.Add(errorStatus);
|
||
logger.Write($"[{target.Name}] {errorStatus}");
|
||
|
||
progress?.Report(new TargetProgressUpdate(
|
||
target.Id,
|
||
errorStatus,
|
||
false));
|
||
|
||
return new TargetStepResult
|
||
{
|
||
TargetId = target.Id,
|
||
TargetName = target.Name,
|
||
Success = false,
|
||
StatusText = "Kopieren fehlgeschlagen",
|
||
Detail = ex.Message,
|
||
Steps = steps,
|
||
StartedAt = targetStart,
|
||
FinishedAt = DateTimeOffset.Now,
|
||
|
||
CopySucceeded = false,
|
||
RestartSucceeded = false,
|
||
TlsSucceeded = false,
|
||
ObservedFingerprint = null
|
||
};
|
||
}
|
||
}
|
||
private static string? CreateCertificateBackup(string currentCertificatePath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(currentCertificatePath) ||
|
||
!File.Exists(currentCertificatePath))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
string certificateDirectory =
|
||
Path.GetDirectoryName(currentCertificatePath)
|
||
?? throw new InvalidOperationException(
|
||
$"Kein Zielordner für Zertifikat gefunden: {currentCertificatePath}");
|
||
|
||
string backupDirectory = Path.Combine(certificateDirectory, "Backup");
|
||
Directory.CreateDirectory(backupDirectory);
|
||
|
||
string fileNameWithoutExtension =
|
||
Path.GetFileNameWithoutExtension(currentCertificatePath);
|
||
|
||
string extension = Path.GetExtension(currentCertificatePath);
|
||
|
||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||
|
||
string backupFileName =
|
||
$"{fileNameWithoutExtension}_{timestamp}{extension}";
|
||
|
||
string backupPath = Path.Combine(backupDirectory, backupFileName);
|
||
|
||
File.Copy(currentCertificatePath, backupPath, overwrite: false);
|
||
|
||
return backupPath;
|
||
}
|
||
}
|
||
|
||
public readonly record struct TargetProgressUpdate(int TargetId, string StatusText, bool? SuccessHint);
|