using ZA.CoreService.ESBCertificateManager.Models;
namespace ZA.CoreService.ESBCertificateManager.Services;
///
/// Orchestriert nur noch Container-Neustart (kein Deploy/TLS/SQL).
///
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 sonicConnections)
{
_settings = settings;
_restartExecutor =
new RestartExecutor(sonicConnections);
_localCertificateDeployer =
new LocalCertificateDeployer();
}
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList selectedTargets)
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
public async Task RestartOnlyAsync(
IReadOnlyList selectedTargets,
IProgress? progress,
CancellationToken cancellationToken = default)
{
DateTimeOffset startedAt = DateTimeOffset.Now;
List 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 DeployAsync(
string certificateFilePath,
string certificateFingerprint,
IReadOnlyList selectedTargets,
IProgress? 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 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 RestartTargetOnlyAsync(
DeploymentTarget target,
RunLogger logger,
IProgress? progress,
CancellationToken cancellationToken)
{
List 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 DeployTargetAsync(
string certificateFilePath,
DeploymentTarget target,
RunLogger logger,
IProgress? progress,
CancellationToken cancellationToken)
{
List 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);