Fix empty Sonic discovery: KnownContainers fallback and clearer domain status.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1829,23 +1829,41 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid mit entdeckten Containern befüllen
|
// Grid mit entdeckten + konfigurierten Containern befüllen
|
||||||
_loadedTargets = result.DiscoveredTargets;
|
_loadedTargets = result.DiscoveredTargets;
|
||||||
BindTargetsToGrid(result.DiscoveredTargets);
|
BindTargetsToGrid(result.DiscoveredTargets);
|
||||||
|
|
||||||
int configured = result.DiscoveredTargets.Count(t => !string.IsNullOrWhiteSpace(t.TargetDirectory));
|
int total = result.DiscoveredTargets.Count;
|
||||||
int discovered = result.RawContainerNames.Count;
|
int remote = result.RawContainerNames.Count;
|
||||||
|
string domain = string.IsNullOrWhiteSpace(result.DomainName)
|
||||||
|
? selectedConnection
|
||||||
|
: result.DomainName;
|
||||||
|
|
||||||
lblSonicStatus.Text = discovered > 0
|
if (total == 0)
|
||||||
? $"{discovered} Container gefunden, {configured} konfiguriert"
|
{
|
||||||
: $"Verbindung ok – keine Container in Domain '{selectedConnection}'";
|
lblSonicStatus.Text = $"Verb. ok – 0 Container in Domain '{domain}'";
|
||||||
lblSonicStatus.ForeColor = discovered > 0 ? GreenColor : GoldColor;
|
lblSonicStatus.ForeColor = GoldColor;
|
||||||
|
SetStatus(
|
||||||
|
result.ErrorMessage
|
||||||
|
?? $"Sonic Domain '{domain}': keine Container. KnownContainers/SonicHome in appsettings prüfen.",
|
||||||
|
isError: true);
|
||||||
|
}
|
||||||
|
else if (remote == 0)
|
||||||
|
{
|
||||||
|
lblSonicStatus.Text = $"Verb. ok – Domain '{domain}': 0 remote, {total} aus Config";
|
||||||
|
lblSonicStatus.ForeColor = GoldColor;
|
||||||
|
SetStatus(
|
||||||
|
$"Domain '{domain}': Remote leer – {total} Ziel(e) aus appsettings/KnownContainers. "
|
||||||
|
+ (result.ErrorMessage ?? string.Empty),
|
||||||
|
isError: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lblSonicStatus.Text = $"Domain '{domain}': {remote} remote, {total} gesamt";
|
||||||
|
lblSonicStatus.ForeColor = GreenColor;
|
||||||
|
SetStatus($"Sonic Domain '{domain}': {total} Ziel(e) geladen ({remote} remote).", isError: false);
|
||||||
|
}
|
||||||
|
|
||||||
SetStatus(
|
|
||||||
$"Sonic {selectedConnection}: {discovered} Container geladen.",
|
|
||||||
isError: false);
|
|
||||||
|
|
||||||
// Nicht-konfigurierte Ziele visuell markieren
|
|
||||||
MarkUnconfiguredRows();
|
MarkUnconfiguredRows();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ public sealed class SonicConnection
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0";
|
public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bekannte Container-Namen dieser Domain (Fallback, wenn Remote-Discovery nichts findet).
|
||||||
|
/// z.B. [ "ESB", "DomainManager" ] oder vollqualifiziert [ "proalpha-test.ESB" ].
|
||||||
|
/// </summary>
|
||||||
|
public List<string> KnownContainers { get; init; } = [];
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// HTTP REST API (ManagementMode = HttpApi)
|
// HTTP REST API (ManagementMode = HttpApi)
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
@@ -127,21 +133,63 @@ public sealed class SonicConnection
|
|||||||
"""
|
"""
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
$sonicHome = '{sonicHome}'
|
$sonicHome = '{sonicHome}'
|
||||||
$names = @()
|
$domain = '{domain}'
|
||||||
|
$names = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
# Windows-Dienste mit Sonic/ESB im Namen
|
function Add-Name([string]$n) {
|
||||||
$names += @(Get-Service -ErrorAction SilentlyContinue |
|
if ([string]::IsNullOrWhiteSpace($n)) { return }
|
||||||
Where-Object { $_.DisplayName -match 'Sonic|ESB|MQ' -or $_.Name -match 'Sonic|ESB|MQ' } |
|
$n = $n.Trim()
|
||||||
Select-Object -ExpandProperty Name)
|
if (-not $names.Contains($n)) { [void]$names.Add($n) }
|
||||||
|
|
||||||
# Container-Cache-Ordner unter SonicHome (Domain.Container.cache)
|
|
||||||
if (Test-Path -LiteralPath $sonicHome) {
|
|
||||||
$names += @(Get-ChildItem -LiteralPath $sonicHome -Directory -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.Name -like '*.cache' } |
|
|
||||||
ForEach-Object { $_.Name -replace '\.cache$', '' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$names | Where-Object { $_ } | Sort-Object -Unique
|
if (-not (Test-Path -LiteralPath $sonicHome)) {
|
||||||
|
Write-Output "WARN:SonicHomeNichtGefunden:$sonicHome"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Output "INFO:SonicHomeOk:$sonicHome"
|
||||||
|
|
||||||
|
# *.cache Ordner (Domain.Container.cache) – 1–2 Ebenen
|
||||||
|
Get-ChildItem -LiteralPath $sonicHome -Directory -ErrorAction SilentlyContinue |
|
||||||
|
ForEach-Object {
|
||||||
|
if ($_.Name -like '*.cache') {
|
||||||
|
Add-Name ($_.Name -replace '\.cache$', '')
|
||||||
|
}
|
||||||
|
Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Name -like '*.cache' } |
|
||||||
|
ForEach-Object { Add-Name ($_.Name -replace '\.cache$', '') }
|
||||||
|
}
|
||||||
|
|
||||||
|
# container.xml / Containers-Verzeichnisse
|
||||||
|
Get-ChildItem -LiteralPath $sonicHome -Recurse -Filter 'container.xml' -File -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -First 40 |
|
||||||
|
ForEach-Object {
|
||||||
|
$parent = Split-Path $_.DirectoryName -Leaf
|
||||||
|
Add-Name $parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Windows-Dienste
|
||||||
|
Get-Service -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.DisplayName -match 'Sonic|ESB|MQ|Aurea' -or $_.Name -match 'Sonic|ESB|MQ|Aurea' } |
|
||||||
|
ForEach-Object {
|
||||||
|
Add-Name $_.Name
|
||||||
|
Add-Name $_.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prozess-Hinweise (laufende Container)
|
||||||
|
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.CommandLine -match 'sonic|mf\.framework|container\.xml' } |
|
||||||
|
ForEach-Object {
|
||||||
|
if ($_.CommandLine -match '([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)\.cache') {
|
||||||
|
Add-Name $Matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($names.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($domain)) {
|
||||||
|
Write-Output "WARN:KeineContainerGefunden Domain=$domain SonicHome=$sonicHome"
|
||||||
|
}
|
||||||
|
|
||||||
|
$names | Sort-Object -Unique
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ using ZA.CoreService.ESBCertificateManager.Models;
|
|||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fragt alle konfigurierten Sonic-Management-Verbindungen nach ihren Containern
|
/// Fragt konfigurierte Sonic-Verbindungen nach Containern ab
|
||||||
/// und gleicht die gefundenen Container mit den bereits konfigurierten Deployment-Zielen ab.
|
/// und merged Remote-Treffer mit KnownContainers / Sample-Zielen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SonicContainerDiscovery
|
public sealed class SonicContainerDiscovery
|
||||||
{
|
{
|
||||||
@@ -18,10 +18,6 @@ public sealed class SonicContainerDiscovery
|
|||||||
public bool HasConnections => _connections.Count > 0;
|
public bool HasConnections => _connections.Count > 0;
|
||||||
public IReadOnlyList<SonicConnection> Connections => _connections;
|
public IReadOnlyList<SonicConnection> Connections => _connections;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verbindet sich mit der angegebenen Sonic-Instanz, liest die Container-Liste
|
|
||||||
/// und reichert sie mit konfigurierten Ziel-Metadaten an.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<SonicDiscoveryResult> DiscoverAsync(
|
public async Task<SonicDiscoveryResult> DiscoverAsync(
|
||||||
string connectionName,
|
string connectionName,
|
||||||
IReadOnlyList<DeploymentTarget> knownTargets,
|
IReadOnlyList<DeploymentTarget> knownTargets,
|
||||||
@@ -45,28 +41,116 @@ public sealed class SonicContainerDiscovery
|
|||||||
$"Management-Konsole nicht erreichbar: {pingError}");
|
$"Management-Konsole nicht erreichbar: {pingError}");
|
||||||
}
|
}
|
||||||
|
|
||||||
(bool listOk, IReadOnlyList<string> containerNames, string? listError) =
|
(bool listOk, IReadOnlyList<string> remoteNames, string? listError) =
|
||||||
await client.GetContainersAsync(cancellationToken);
|
await client.GetContainersAsync(cancellationToken);
|
||||||
|
|
||||||
|
List<string> diagnostics = [];
|
||||||
|
List<string> remoteContainers = [];
|
||||||
|
|
||||||
|
foreach (string line in remoteNames)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| line.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
diagnostics.Add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteContainers.Add(line);
|
||||||
|
}
|
||||||
|
|
||||||
if (!listOk)
|
if (!listOk)
|
||||||
{
|
{
|
||||||
// Fallback: CheckConnection erfolgreich, aber /containers nicht gefunden –
|
diagnostics.Add($"Listen-Fehler: {listError}");
|
||||||
// dies kann passieren wenn die API-Pfade abweichen. Container-Namen sind dann leer.
|
remoteContainers = [];
|
||||||
containerNames = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DeploymentTarget> discovered = BuildTargets(connection, containerNames, knownTargets);
|
// Fallback: explizit in appsettings hinterlegte Container
|
||||||
|
List<string> mergedNames = MergeContainerNames(
|
||||||
|
remoteContainers,
|
||||||
|
connection.KnownContainers,
|
||||||
|
knownTargets
|
||||||
|
.Where(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(t => t.ContainerName)
|
||||||
|
.Where(n => !string.IsNullOrWhiteSpace(n)));
|
||||||
|
|
||||||
|
List<DeploymentTarget> discovered = BuildTargets(connection, mergedNames, knownTargets);
|
||||||
|
|
||||||
|
string? hint = BuildHint(connection, remoteContainers.Count, connection.KnownContainers.Count, diagnostics, listOk, listError);
|
||||||
|
|
||||||
return new SonicDiscoveryResult
|
return new SonicDiscoveryResult
|
||||||
{
|
{
|
||||||
ConnectionName = connectionName,
|
ConnectionName = connectionName,
|
||||||
|
DomainName = connection.DomainName,
|
||||||
Success = true,
|
Success = true,
|
||||||
ErrorMessage = listOk ? null : $"Container-Liste konnte nicht geladen werden: {listError}",
|
ErrorMessage = hint,
|
||||||
DiscoveredTargets = discovered,
|
DiscoveredTargets = discovered,
|
||||||
RawContainerNames = containerNames
|
RawContainerNames = remoteContainers,
|
||||||
|
ResolvedPath = resolvedPath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<string> MergeContainerNames(
|
||||||
|
IEnumerable<string> remote,
|
||||||
|
IEnumerable<string> knownConfigured,
|
||||||
|
IEnumerable<string> knownFromTargets)
|
||||||
|
{
|
||||||
|
List<string> result = [];
|
||||||
|
|
||||||
|
foreach (string name in remote.Concat(knownConfigured).Concat(knownFromTargets))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Any(existing => ContainerNamesMatch(existing, name, domain: null)))
|
||||||
|
{
|
||||||
|
result.Add(name.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildHint(
|
||||||
|
SonicConnection connection,
|
||||||
|
int remoteCount,
|
||||||
|
int knownConfigCount,
|
||||||
|
List<string> diagnostics,
|
||||||
|
bool listOk,
|
||||||
|
string? listError)
|
||||||
|
{
|
||||||
|
List<string> parts = [];
|
||||||
|
|
||||||
|
if (!listOk && !string.IsNullOrWhiteSpace(listError))
|
||||||
|
{
|
||||||
|
parts.Add(listError!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteCount == 0)
|
||||||
|
{
|
||||||
|
parts.Add(
|
||||||
|
$"Remote hat 0 Container geliefert (Domain '{connection.DomainName}', SonicHome='{connection.SonicHome}').");
|
||||||
|
|
||||||
|
if (knownConfigCount > 0)
|
||||||
|
{
|
||||||
|
parts.Add($"Fallback: {knownConfigCount} KnownContainers aus appsettings.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parts.Add("Tipp: KnownContainers in appsettings setzen oder SonicHome korrigieren.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string d in diagnostics.Take(3))
|
||||||
|
{
|
||||||
|
parts.Add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.Count == 0 ? null : string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<DeploymentTarget> BuildTargets(
|
private static List<DeploymentTarget> BuildTargets(
|
||||||
SonicConnection connection,
|
SonicConnection connection,
|
||||||
IReadOnlyList<string> containerNames,
|
IReadOnlyList<string> containerNames,
|
||||||
@@ -77,19 +161,19 @@ public sealed class SonicContainerDiscovery
|
|||||||
|
|
||||||
foreach (string containerName in containerNames)
|
foreach (string containerName in containerNames)
|
||||||
{
|
{
|
||||||
// Bekanntes, voll-konfiguriertes Ziel suchen (nach ContainerName + SonicConnectionName)
|
|
||||||
DeploymentTarget? existing = knownTargets.FirstOrDefault(t =>
|
DeploymentTarget? existing = knownTargets.FirstOrDefault(t =>
|
||||||
string.Equals(t.ContainerName, containerName, StringComparison.OrdinalIgnoreCase) &&
|
string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase));
|
&& ContainerNamesMatch(t.ContainerName, containerName, connection.DomainName));
|
||||||
|
|
||||||
if (existing is not null)
|
if (existing is not null)
|
||||||
{
|
{
|
||||||
result.Add(existing);
|
if (!result.Any(r => r.Id == existing.Id))
|
||||||
|
{
|
||||||
|
result.Add(existing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Minimales Ziel aus der Discovery erzeugen.
|
|
||||||
// TargetDirectory/CertificateFileName sind leer → Preflight-Validator zeigt Warnung.
|
|
||||||
result.Add(new DeploymentTarget
|
result.Add(new DeploymentTarget
|
||||||
{
|
{
|
||||||
Id = syntheticId--,
|
Id = syntheticId--,
|
||||||
@@ -106,15 +190,16 @@ public sealed class SonicContainerDiscovery
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Konfigurierte Ziele dieser Verbindung, die NICHT in der Discovery aufgetaucht sind,
|
|
||||||
// trotzdem anzeigen (könnten offline / gestoppt sein).
|
|
||||||
foreach (DeploymentTarget known in knownTargets)
|
foreach (DeploymentTarget known in knownTargets)
|
||||||
{
|
{
|
||||||
if (!string.Equals(known.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(known.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
bool alreadyAdded = result.Any(r =>
|
bool alreadyAdded = result.Any(r =>
|
||||||
string.Equals(r.ContainerName, known.ContainerName, StringComparison.OrdinalIgnoreCase));
|
ContainerNamesMatch(r.ContainerName, known.ContainerName, connection.DomainName)
|
||||||
|
|| r.Id == known.Id);
|
||||||
|
|
||||||
if (!alreadyAdded)
|
if (!alreadyAdded)
|
||||||
{
|
{
|
||||||
@@ -124,13 +209,44 @@ public sealed class SonicContainerDiscovery
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool ContainerNamesMatch(string? a, string? b, string? domain)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string aShort = StripDomainPrefix(a, domain);
|
||||||
|
string bShort = StripDomainPrefix(b, domain);
|
||||||
|
return string.Equals(aShort, bShort, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripDomainPrefix(string name, string? domain)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(domain)
|
||||||
|
&& name.StartsWith(domain + ".", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return name[(domain.Length + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
int dot = name.IndexOf('.');
|
||||||
|
return dot > 0 ? name[(dot + 1)..] : name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SonicDiscoveryResult
|
public sealed class SonicDiscoveryResult
|
||||||
{
|
{
|
||||||
public required string ConnectionName { get; init; }
|
public required string ConnectionName { get; init; }
|
||||||
|
public string DomainName { get; init; } = string.Empty;
|
||||||
public bool Success { get; init; }
|
public bool Success { get; init; }
|
||||||
public string? ErrorMessage { get; init; }
|
public string? ErrorMessage { get; init; }
|
||||||
|
public string? ResolvedPath { get; init; }
|
||||||
public IReadOnlyList<DeploymentTarget> DiscoveredTargets { get; init; } = [];
|
public IReadOnlyList<DeploymentTarget> DiscoveredTargets { get; init; } = [];
|
||||||
public IReadOnlyList<string> RawContainerNames { get; init; } = [];
|
public IReadOnlyList<string> RawContainerNames { get; init; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -185,9 +185,11 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
|
|
||||||
List<string> names = (output ?? string.Empty)
|
List<string> names = (output ?? string.Empty)
|
||||||
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
.Where(l => l.Length > 0 && !l.StartsWith("WARNING", StringComparison.OrdinalIgnoreCase))
|
.Where(l => l.Length > 0)
|
||||||
|
.Where(l => !l.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// INFO:/WARN:-Zeilen aus dem Script bewusst durchreichen (Diagnose in Discovery)
|
||||||
return (true, names, null);
|
return (true, names, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
"Password": "Administrator",
|
"Password": "Administrator",
|
||||||
"ManagementMode": "WinRm",
|
"ManagementMode": "WinRm",
|
||||||
"SonicHome": "C:\\Sonic\\MQ10.0",
|
"SonicHome": "C:\\Sonic\\MQ10.0",
|
||||||
|
"KnownContainers": [
|
||||||
|
"ESB",
|
||||||
|
"DomainManager"
|
||||||
|
],
|
||||||
"WinRmPort": 5985,
|
"WinRmPort": 5985,
|
||||||
"TimeoutSeconds": 90,
|
"TimeoutSeconds": 90,
|
||||||
"PostRestartDelaySeconds": 15,
|
"PostRestartDelaySeconds": 15,
|
||||||
|
|||||||
Reference in New Issue
Block a user