Make installer runtime acceptance transactional

This commit is contained in:
2026-08-10 18:28:49 +02:00
parent 8fe1b57173
commit 448be8dc49
20 changed files with 874 additions and 65 deletions
+179
View File
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Validiert die vom ersten Providerlauf erzeugten Runtime-Artefakte mit denselben
/// Parsern und Grenzen, die auch der laufende Consumer verwendet.
/// </summary>
internal static class RuntimeValidator
{
public static RuntimeValidationResult Validate(
MonitoringOptions options,
CheckmkLocalFormatter formatter,
DateTime notBeforeUtc,
string expectedIdentity,
DateTime utcNow)
{
if (options == null) throw new ArgumentNullException("options");
if (formatter == null) throw new ArgumentNullException("formatter");
var snapshot = new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Read(utcNow, TimeSpan.FromSeconds(options.SnapshotMaxAgeSeconds));
if (!snapshot.IsSuccess)
{
return RuntimeValidationResult.Failed("Snapshot ist ungueltig: " + snapshot.Error);
}
var boundary = notBeforeUtc.ToUniversalTime();
if (snapshot.GeneratedUtc < boundary)
{
return RuntimeValidationResult.Failed(
"Snapshot stammt nicht aus dem gestarteten Providerlauf. generatedUtc="
+ snapshot.GeneratedUtc.ToString("o") + ", requiredUtc=" + boundary.ToString("o") + ".");
}
if (!string.Equals(snapshot.Identity, expectedIdentity, StringComparison.OrdinalIgnoreCase))
{
return RuntimeValidationResult.Failed(
"Snapshot wurde von einer unerwarteten Identitaet erzeugt. expected="
+ expectedIdentity + ", actual=" + (snapshot.Identity ?? "(leer)") + ".");
}
string serviceError;
var expectedServices = formatter.FormatSelfTest().Select(ExtractServiceName).ToArray();
if (!ContainsExactStableContract(snapshot.Lines, expectedServices, out serviceError))
{
return RuntimeValidationResult.Failed(serviceError);
}
DateTime? catalogUtc = null;
if (options.ProbeEndpointConnectivity)
{
EndpointCatalog catalog;
try
{
catalog = new EndpointCatalogStore(
options.EndpointCatalogPath,
options.EndpointCatalogMaxBytes,
options.EndpointCatalogMaxEntries)
.Read(options.EnvironmentName);
}
catch (Exception ex)
{
return RuntimeValidationResult.Failed(
"Endpoint-Katalog ist ungueltig: " + ex.GetType().Name + ": " + ex.Message);
}
if (catalog.SynchronizedUtc < boundary)
{
return RuntimeValidationResult.Failed(
"Endpoint-Katalog wurde beim Installationslauf nicht aktualisiert. synchronizedUtc="
+ catalog.SynchronizedUtc.ToString("o") + ", requiredUtc=" + boundary.ToString("o") + ".");
}
if (catalog.SynchronizedUtc > utcNow.ToUniversalTime().AddMinutes(5))
{
return RuntimeValidationResult.Failed("Endpoint-Katalog-Zeitstempel liegt unplausibel in der Zukunft.");
}
catalogUtc = catalog.SynchronizedUtc;
}
return RuntimeValidationResult.Success(
snapshot.GeneratedUtc,
catalogUtc,
expectedServices.Length);
}
internal static bool ContainsExactStableContract(
IEnumerable<string> lines,
IEnumerable<string> expectedServices,
out string error)
{
string[] actual;
try
{
actual = (lines ?? Enumerable.Empty<string>()).Select(ExtractServiceName).ToArray();
}
catch (Exception ex)
{
error = "Snapshot-Servicevertrag kann nicht gelesen werden: " + ex.Message;
return false;
}
if (actual.Distinct(StringComparer.Ordinal).Count() != actual.Length)
{
error = "Snapshot enthaelt doppelte Checkmk-Servicenamen.";
return false;
}
var actualSet = new HashSet<string>(actual, StringComparer.Ordinal);
var expected = (expectedServices ?? Enumerable.Empty<string>()).ToArray();
var missing = expected
.Where(x => !actualSet.Contains(x))
.OrderBy(x => x, StringComparer.Ordinal)
.ToArray();
if (missing.Length != 0)
{
error = "Snapshot enthaelt nicht alle stabilen Checkmk-Services. Fehlend=["
+ string.Join(", ", missing) + "].";
return false;
}
var unknown = (lines ?? Enumerable.Empty<string>())
.Where(x => x != null && x.StartsWith("3 ", StringComparison.Ordinal))
.Select(ExtractServiceName)
.Where(x => expected.Contains(x, StringComparer.Ordinal))
.OrderBy(x => x, StringComparer.Ordinal)
.ToArray();
if (unknown.Length != 0)
{
error = "Snapshot enthaelt unzuverlaessige UNKNOWN-Zustaende in stabilen Services. UNKNOWN=["
+ string.Join(", ", unknown) + "].";
return false;
}
error = string.Empty;
return true;
}
private static string ExtractServiceName(string line)
{
var firstQuote = (line ?? string.Empty).IndexOf('"');
var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1);
if (firstQuote < 0 || secondQuote <= firstQuote + 1)
throw new InvalidOperationException("Ungueltige Local-Check-Zeile.");
return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
}
}
internal sealed class RuntimeValidationResult
{
private RuntimeValidationResult() { }
public bool IsSuccess { get; private set; }
public string Error { get; private set; }
public DateTime GeneratedUtc { get; private set; }
public DateTime? CatalogSynchronizedUtc { get; private set; }
public int StableServiceCount { get; private set; }
public static RuntimeValidationResult Success(DateTime generatedUtc, DateTime? catalogUtc, int serviceCount)
{
return new RuntimeValidationResult
{
IsSuccess = true,
GeneratedUtc = generatedUtc,
CatalogSynchronizedUtc = catalogUtc,
StableServiceCount = serviceCount
};
}
public static RuntimeValidationResult Failed(string error)
{
return new RuntimeValidationResult { Error = error };
}
}
}