mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
- Options: VersionCommand, VersionTimeoutSeconds, MinimumVersion, EnforceMinimumVersion - Version probe service and parser; startup enforcement when EnforceMinimumVersion - Save flow: block save on probe failure (VersionProbeFailed) or version below minimum (UnsupportedVersion) - Status API and Dnsmasq page: show version with tooltip, link to CHANGELOG - Readiness: GET /healthz/ready (version + dnsmasq running when StatusCommand set) - MapReadyHealthCheck extension; Dockerfile HEALTHCHECK + curl; compose healthcheck and service_healthy - Docs: README and agents.md updated
220 lines
9.1 KiB
C#
220 lines
9.1 KiB
C#
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Config.Abstractions;
|
|
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Reload.Abstractions;
|
|
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Validation.Abstractions;
|
|
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Version.Abstractions;
|
|
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Abstractions;
|
|
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig;
|
|
|
|
public sealed class EffectiveConfigSaveService : IEffectiveConfigSaveService
|
|
{
|
|
private readonly IDnsmasqConfigSetService _configSetService;
|
|
private readonly IDnsmasqConfigService _configService;
|
|
private readonly IConfigSetCache _configSetCache;
|
|
private readonly IConfigValidationService _validationService;
|
|
private readonly IReloadService _reloadService;
|
|
private readonly IDnsmasqVersionService _versionService;
|
|
private readonly ILogger<EffectiveConfigSaveService> _logger;
|
|
|
|
public EffectiveConfigSaveService(
|
|
IDnsmasqConfigSetService configSetService,
|
|
IDnsmasqConfigService configService,
|
|
IConfigSetCache configSetCache,
|
|
IConfigValidationService validationService,
|
|
IReloadService reloadService,
|
|
IDnsmasqVersionService versionService,
|
|
ILogger<EffectiveConfigSaveService> logger)
|
|
{
|
|
_configSetService = configSetService;
|
|
_configService = configService;
|
|
_configSetCache = configSetCache;
|
|
_validationService = validationService;
|
|
_reloadService = reloadService;
|
|
_versionService = versionService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<EffectiveConfigSaveResult> SaveAsync(
|
|
IReadOnlyList<PendingEffectiveConfigChange> changes,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (changes.Count == 0)
|
|
return EffectiveConfigSaveResult.NoChanges();
|
|
|
|
var version = await _versionService.GetVersionInfoAsync(ct);
|
|
if (!version.ProbeSucceeded)
|
|
{
|
|
return new EffectiveConfigSaveResult(
|
|
BackupCreated: false,
|
|
BackupPath: null,
|
|
Saved: false,
|
|
Validated: false,
|
|
ValidationExitCode: -1,
|
|
ValidationStdOut: null,
|
|
ValidationStdErr: null,
|
|
Restarted: false,
|
|
RestartExitCode: -1,
|
|
RestartStdOut: null,
|
|
RestartStdErr: null,
|
|
ErrorCode: EffectiveConfigSaveResult.ErrorCodes.VersionProbeFailed,
|
|
UserMessage: string.IsNullOrWhiteSpace(version.Error)
|
|
? "Cannot save: dnsmasq version could not be determined."
|
|
: $"Cannot save: dnsmasq version could not be determined. {version.Error}");
|
|
}
|
|
if (!version.IsSupported)
|
|
{
|
|
return new EffectiveConfigSaveResult(
|
|
BackupCreated: false,
|
|
BackupPath: null,
|
|
Saved: false,
|
|
Validated: false,
|
|
ValidationExitCode: -1,
|
|
ValidationStdOut: null,
|
|
ValidationStdErr: null,
|
|
Restarted: false,
|
|
RestartExitCode: -1,
|
|
RestartStdOut: null,
|
|
RestartStdErr: null,
|
|
ErrorCode: EffectiveConfigSaveResult.ErrorCodes.UnsupportedVersion,
|
|
UserMessage: $"Installed dnsmasq {version.InstalledVersion} is below required {version.MinimumVersion}.");
|
|
}
|
|
|
|
var set = await _configSetService.GetConfigSetAsync(ct);
|
|
if (string.IsNullOrWhiteSpace(set.ManagedFilePath))
|
|
{
|
|
_logger.LogWarning("Save skipped: managed config path is not configured");
|
|
return new EffectiveConfigSaveResult(
|
|
false, null, false, false, -1, null, null, false, -1, null, null,
|
|
EffectiveConfigSaveResult.ErrorCodes.MissingManagedPath, "Managed config path is not configured.");
|
|
}
|
|
|
|
var managedPath = set.ManagedFilePath!;
|
|
var backupPath = BuildBackupPath(managedPath);
|
|
|
|
try
|
|
{
|
|
CreateBackupIfSourceExists(managedPath, backupPath);
|
|
await _configService.ApplyEffectiveConfigChangesAsync(changes, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to write effective config");
|
|
var backupExists = File.Exists(backupPath);
|
|
return new EffectiveConfigSaveResult(
|
|
backupExists,
|
|
backupExists ? backupPath : null,
|
|
false,
|
|
false,
|
|
-1,
|
|
null,
|
|
null,
|
|
false,
|
|
-1,
|
|
null,
|
|
ex.Message,
|
|
EffectiveConfigSaveResult.ErrorCodes.WriteFailed,
|
|
"Failed to write config.");
|
|
}
|
|
|
|
var backupCreated = File.Exists(backupPath);
|
|
var validateResult = await _validationService.ValidateAsync(ct);
|
|
|
|
if (!validateResult.Success)
|
|
{
|
|
_logger.LogWarning("Config saved but validation failed: exit {ExitCode}, stderr: {Stderr}", validateResult.ExitCode, validateResult.StdErr);
|
|
return new EffectiveConfigSaveResult(
|
|
BackupCreated: backupCreated,
|
|
BackupPath: backupCreated ? backupPath : null,
|
|
Saved: true,
|
|
Validated: false,
|
|
ValidationExitCode: validateResult.ExitCode,
|
|
ValidationStdOut: validateResult.StdOut,
|
|
ValidationStdErr: validateResult.StdErr,
|
|
Restarted: false,
|
|
RestartExitCode: -1,
|
|
RestartStdOut: null,
|
|
RestartStdErr: null,
|
|
ErrorCode: EffectiveConfigSaveResult.ErrorCodes.ValidateFailed,
|
|
UserMessage: "Saved, but validation failed. Restart was not attempted.");
|
|
}
|
|
|
|
var restartResult = await _reloadService.ReloadAsync(ct);
|
|
|
|
if (!restartResult.Success)
|
|
_logger.LogWarning("Config saved but restart command failed: exit {ExitCode}, stderr: {Stderr}", restartResult.ExitCode, restartResult.StdErr);
|
|
|
|
return new EffectiveConfigSaveResult(
|
|
BackupCreated: backupCreated,
|
|
BackupPath: backupCreated ? backupPath : null,
|
|
Saved: true,
|
|
Validated: true,
|
|
ValidationExitCode: 0,
|
|
ValidationStdOut: validateResult.StdOut,
|
|
ValidationStdErr: validateResult.StdErr,
|
|
Restarted: restartResult.Success,
|
|
RestartExitCode: restartResult.ExitCode,
|
|
RestartStdOut: restartResult.StdOut,
|
|
RestartStdErr: restartResult.StdErr,
|
|
ErrorCode: restartResult.Success ? null : EffectiveConfigSaveResult.ErrorCodes.RestartFailed,
|
|
UserMessage: restartResult.Success
|
|
? "Saved, validated, and dnsmasq restarted."
|
|
: "Saved and validated, but the restart command failed.");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<EffectiveConfigRestoreResult> RestoreAsync(string backupPath, CancellationToken ct = default)
|
|
{
|
|
var set = await _configSetService.GetConfigSetAsync(ct);
|
|
if (string.IsNullOrWhiteSpace(set.ManagedFilePath))
|
|
{
|
|
_logger.LogWarning("Restore skipped: managed config path is not configured");
|
|
return new EffectiveConfigRestoreResult(
|
|
false, false, -1, null, "Managed config path is not configured.");
|
|
}
|
|
|
|
if (!File.Exists(backupPath))
|
|
{
|
|
_logger.LogWarning("Restore skipped: backup file not found: {Path}", backupPath);
|
|
return new EffectiveConfigRestoreResult(
|
|
false, false, -1, null, "Backup file not found.");
|
|
}
|
|
|
|
var managedPath = set.ManagedFilePath!;
|
|
File.Copy(backupPath, managedPath, overwrite: true);
|
|
_configSetCache.Invalidate();
|
|
_logger.LogInformation("Restored managed config from backup: {BackupPath}", backupPath);
|
|
|
|
var restartResult = await _reloadService.ReloadAsync(ct);
|
|
|
|
if (!restartResult.Success)
|
|
_logger.LogWarning("Restore completed but restart command failed: exit {ExitCode}, stderr: {Stderr}", restartResult.ExitCode, restartResult.StdErr);
|
|
|
|
return new EffectiveConfigRestoreResult(
|
|
Restored: true,
|
|
Restarted: restartResult.Success,
|
|
RestartExitCode: restartResult.ExitCode,
|
|
RestartStdErr: restartResult.StdErr,
|
|
UserMessage: restartResult.Success
|
|
? "Backup restored and dnsmasq restarted."
|
|
: "Backup restored, but the restart command still failed.");
|
|
}
|
|
|
|
private static string BuildBackupPath(string managedPath) =>
|
|
$"{managedPath}.bak.{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
|
|
|
private static void CreateBackupIfSourceExists(string managedPath, string backupPath)
|
|
{
|
|
if (!File.Exists(managedPath))
|
|
return;
|
|
|
|
var dir = Path.GetDirectoryName(backupPath);
|
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
|
Directory.CreateDirectory(dir);
|
|
|
|
File.Copy(managedPath, backupPath, overwrite: false);
|
|
}
|
|
}
|