Make installer runtime acceptance transactional
This commit is contained in:
@@ -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 +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")]
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user