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
+2
View File
@@ -5,6 +5,8 @@
<add key="Server" value="." />
<add key="ServicePrefix" value="BizTalk" />
<add key="EnvironmentName" value="" />
<!-- Stabiler Checkmk-Servicevertrag; nur mit geplanter Service Discovery aktivieren. -->
<add key="IncludeEnvironmentInServiceName" value="false" />
<!-- Provider/Consumer-Datei. Der Scheduled Task schreibt, LocalSystem liest. -->
<add key="SnapshotPath" value="%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" />
@@ -52,6 +52,7 @@
<Compile Include="MonitoringOptions.cs" />
<Compile Include="Models.cs" />
<Compile Include="Program.cs" />
<Compile Include="RuntimeValidator.cs" />
<Compile Include="SqlConnectivityProbe.cs" />
<Compile Include="SnapshotStore.cs" />
<Compile Include="WmiBizTalkProbe.cs" />
@@ -62,7 +62,8 @@ namespace BizTalkCheckmkPulse
}
var now = DateTime.UtcNow;
state.RefreshRequired = catalog == null
state.RefreshRequired = _options.ForceEndpointCatalogRefresh
|| catalog == null
|| catalog.SynchronizedUtc > now.AddMinutes(5)
|| now - catalog.SynchronizedUtc >= TimeSpan.FromHours(_options.EndpointDiscoveryIntervalHours);
if (state.RefreshRequired)
@@ -4,6 +4,7 @@ using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace BizTalkCheckmkPulse
{
@@ -49,6 +50,10 @@ namespace BizTalkCheckmkPulse
public int EndpointMaxCount { get; set; }
public bool Collect { get; set; }
public bool SelfTest { get; set; }
public bool ForceEndpointCatalogRefresh { get; set; }
public bool RuntimeValidation { get; set; }
public DateTime? RuntimeValidationNotBeforeUtc { get; set; }
public string ExpectedRuntimeIdentity { get; set; }
/// <summary>
/// Setzt konservative Standardwerte fuer BizTalk Server 2020.
@@ -91,6 +96,7 @@ namespace BizTalkCheckmkPulse
EndpointProbeTimeoutMilliseconds = 3000;
EndpointProbeMaxConcurrency = 16;
EndpointMaxCount = 100;
ExpectedRuntimeIdentity = string.Empty;
}
/// <summary>
@@ -182,6 +188,33 @@ namespace BizTalkCheckmkPulse
{
options.Collect = true;
}
else if (EqualsAny(arg, "--force-endpoint-refresh", "/force-endpoint-refresh"))
{
options.ForceEndpointCatalogRefresh = true;
}
else if (EqualsAny(arg, "--validate-runtime", "/validate-runtime"))
{
options.RuntimeValidation = true;
}
else if (EqualsAny(arg, "--validation-not-before-utc", "/validation-not-before-utc") && i + 1 < args.Length)
{
DateTime value;
if (!DateTime.TryParseExact(
args[++i],
"o",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out value))
{
throw new ConfigurationErrorsException("Runtime validation timestamp must use the round-trip UTC format.");
}
options.RuntimeValidationNotBeforeUtc = value;
}
else if (EqualsAny(arg, "--expected-identity-base64", "/expected-identity-base64") && i + 1 < args.Length)
{
options.ExpectedRuntimeIdentity = new UTF8Encoding(false, true)
.GetString(Convert.FromBase64String(args[++i]));
}
else if (EqualsAny(arg, "--consume", "/consume"))
{
options.Collect = false;
@@ -214,6 +247,18 @@ namespace BizTalkCheckmkPulse
/// </summary>
private static void Validate(MonitoringOptions options)
{
if (options.ForceEndpointCatalogRefresh && !options.Collect)
{
throw new ConfigurationErrorsException("ForceEndpointCatalogRefresh is valid only in provider mode.");
}
if (options.RuntimeValidation
&& (!options.RuntimeValidationNotBeforeUtc.HasValue
|| string.IsNullOrWhiteSpace(options.ExpectedRuntimeIdentity)))
{
throw new ConfigurationErrorsException("Runtime validation requires a start timestamp and expected identity.");
}
if (string.IsNullOrWhiteSpace(options.SnapshotPath) || !Path.IsPathRooted(options.SnapshotPath))
{
throw new ConfigurationErrorsException("SnapshotPath must be an absolute path.");
+37 -6
View File
@@ -37,6 +37,11 @@ namespace BizTalkCheckmkPulse
return 0;
}
if (options.RuntimeValidation)
{
return RunRuntimeValidation(options, formatter);
}
return options.Collect
? RunProvider(options, formatter)
: RunConsumer(options, formatter);
@@ -51,7 +56,14 @@ namespace BizTalkCheckmkPulse
Console.WriteLine(line);
}
return options != null && options.Collect ? 1 : 0;
var strictFailureMode = options != null
? options.Collect || options.RuntimeValidation
: (args ?? new string[0]).Any(x =>
string.Equals(x, "--collect", StringComparison.OrdinalIgnoreCase)
|| string.Equals(x, "/collect", StringComparison.OrdinalIgnoreCase)
|| string.Equals(x, "--validate-runtime", StringComparison.OrdinalIgnoreCase)
|| string.Equals(x, "/validate-runtime", StringComparison.OrdinalIgnoreCase));
return strictFailureMode ? 1 : 0;
}
}
@@ -99,13 +111,9 @@ namespace BizTalkCheckmkPulse
+ " send_ports_not_started="
+ result.SendPorts.Count(x => x.Status != 3)
+ " endpoint_candidates_active="
+ result.EndpointConnectivity.ActiveCandidates
+ result.EndpointCandidates.Count(x => x.Active)
+ " endpoints_active="
+ result.EndpointConnectivity.Active
+ " endpoints_unsupported="
+ result.EndpointConnectivity.UnsupportedActive
+ " endpoints_excluded="
+ result.EndpointConnectivity.ExcludedActive
+ " endpoints_failed="
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
+ " endpoints_unresolved="
@@ -137,6 +145,29 @@ namespace BizTalkCheckmkPulse
}
}
private static int RunRuntimeValidation(MonitoringOptions options, CheckmkLocalFormatter formatter)
{
var validation = RuntimeValidator.Validate(
options,
formatter,
options.RuntimeValidationNotBeforeUtc.Value,
options.ExpectedRuntimeIdentity,
DateTime.UtcNow);
if (!validation.IsSuccess)
{
Console.Error.WriteLine("Runtime validation failed: " + validation.Error);
return 1;
}
Console.WriteLine(
"RUNTIME_VALIDATION_V1 generatedUtc=" + validation.GeneratedUtc.ToString("o")
+ " catalogUtc=" + (validation.CatalogSynchronizedUtc.HasValue
? validation.CatalogSynchronizedUtc.Value.ToString("o")
: "disabled")
+ " stableServices=" + validation.StableServiceCount);
return 0;
}
private static void EnsureProviderIdentity()
{
using (var identity = WindowsIdentity.GetCurrent())
@@ -6,5 +6,5 @@ using System.Reflection;
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
[assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
[assembly: AssemblyVersion("2.2.3.0")]
[assembly: AssemblyFileVersion("2.2.3.0")]
[assembly: AssemblyVersion("2.2.4.0")]
[assembly: AssemblyFileVersion("2.2.4.0")]
+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 };
}
}
}
+6 -4
View File
@@ -114,7 +114,7 @@ namespace BizTalkCheckmkPulse
}
var expectedMachine = DecodeHeader(headerLines[2], "machineBase64=");
DecodeHeader(headerLines[3], "identityBase64=");
var identity = DecodeHeader(headerLines[3], "identityBase64=");
var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256=");
if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
{
@@ -159,7 +159,7 @@ namespace BizTalkCheckmkPulse
+ "s.");
}
return SnapshotReadResult.Success(lines, generatedUtc);
return SnapshotReadResult.Success(lines, generatedUtc, identity);
}
catch (FileNotFoundException)
{
@@ -330,14 +330,16 @@ namespace BizTalkCheckmkPulse
public string Error { get; private set; }
public IReadOnlyList<string> Lines { get; private set; }
public DateTime GeneratedUtc { get; private set; }
public string Identity { get; private set; }
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc)
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc, string identity)
{
return new SnapshotReadResult
{
IsSuccess = true,
Lines = lines,
GeneratedUtc = generatedUtc
GeneratedUtc = generatedUtc,
Identity = identity
};
}