Separate BizTalk collection from Checkmk agent

This commit is contained in:
2026-07-30 15:50:42 +02:00
parent e3d7f6a780
commit d15bb6539b
22 changed files with 2051 additions and 988 deletions
+9 -1
View File
@@ -6,8 +6,16 @@
<add key="ServicePrefix" value="BizTalk" />
<add key="EnvironmentName" value="" />
<!-- Provider/Consumer-Datei. Der Scheduled Task schreibt, LocalSystem liest. -->
<add key="SnapshotPath" value="%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" />
<!-- Bei minuetlicher Provider-Ausfuehrung nach drei Minuten als UNKNOWN bewerten. -->
<add key="SnapshotMaxAgeSeconds" value="180" />
<add key="SnapshotMaxBytes" value="1048576" />
<add key="LogDirectory" value="%ProgramData%\BizTalkCheckmkPulse\logs" />
<add key="LogRetentionDays" value="30" />
<add key="QueryTimeoutSeconds" value="25" />
<!-- Testet integrierte Windows-Anmeldung an den per WMI ermittelten BizTalk-Datenbanken. -->
<!-- Testet die Anmeldung des privilegierten Provider-Kontos an den ermittelten BizTalk-Datenbanken. -->
<add key="ProbeSqlConnectivity" value="true" />
<add key="SqlConnectionTimeoutSeconds" value="5" />
<add key="WarnResumableThreshold" value="1" />
@@ -43,10 +43,12 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CheckmkLocalFormatter.cs" />
<Compile Include="EventLogProbe.cs" />
<Compile Include="FileLogger.cs" />
<Compile Include="MonitoringOptions.cs" />
<Compile Include="Models.cs" />
<Compile Include="Program.cs" />
<Compile Include="SqlConnectivityProbe.cs" />
<Compile Include="SnapshotStore.cs" />
<Compile Include="WmiBizTalkProbe.cs" />
<Compile Include="WmiHelpers.cs" />
</ItemGroup>
@@ -83,6 +83,22 @@ namespace BizTalkCheckmkPulse
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", action);
}
/// <summary>
/// Liefert stabile UNKNOWN-Services, wenn der Consumer keinen gueltigen Provider-Snapshot lesen kann.
/// </summary>
public IEnumerable<string> FormatSnapshotFailure(string reason)
{
var detail = "Privilegierter BizTalk-Datensnapshot nicht verfuegbar: "
+ SanitizeDetail(reason)
+ " Massnahme: Scheduled Task 'BizTalk Checkmk Pulse Provider', Provider-Log, Snapshot-Alter und ACL pruefen.";
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Platform"), "-", detail);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("SQL Access"), "-", detail);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Suspended Instances"), "-", detail);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Host Instances"), "-", detail);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Runtime Artifacts"), "-", detail);
yield return BuildLine(CheckState.Unknown, _options.ServiceName("Event Log"), "-", detail);
}
/// <summary>
/// Formatiert Erreichbarkeit und Basisdaten des BizTalk-WMI-Providers.
/// </summary>
@@ -96,6 +112,7 @@ namespace BizTalkCheckmkPulse
detail.Append(available ? "BizTalk WMI and platform data reachable" : "BizTalk WMI or required platform data not readable");
detail.Append(", server=").Append(EmptyAsUnknown(result.Platform.ServerName));
AppendOptional(detail, "group", result.Platform.GroupName);
AppendOptional(detail, "read_only_group", result.Platform.ReadOnlyUserGroup);
AppendOptional(detail, "operator_group", result.Platform.OperatorGroup);
AppendOptional(detail, "mgmt_db", JoinDb(result.Platform.ManagementDbServer, result.Platform.ManagementDbName));
AppendOptional(detail, "msgbox_db", JoinDb(result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName));
@@ -307,7 +324,7 @@ namespace BizTalkCheckmkPulse
CheckState.Unknown,
_options.ServiceName("Event Log"),
"-",
"Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Checkmk-Agentkontos und den Windows Event Log Dienst pruefen.");
"Application Event Log konnte nicht gelesen werden: " + result.EventLog.Failure + " Massnahme: lokalen Event-Log-Zugriff des Provider-Kontos und den Windows Event Log Dienst pruefen.");
}
var state = _options.EventLogCritThreshold > 0 && result.EventLog.Errors >= _options.EventLogCritThreshold
+150
View File
@@ -0,0 +1,150 @@
using System;
using System.Globalization;
using System.IO;
using System.Security.Principal;
using System.Text;
using System.Threading;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Kleine, ausfallsichere Tagesdatei-Protokollierung ohne externe Abhaengigkeiten.
/// Logging-Fehler duerfen weder Provider noch Checkmk-Ausgabe blockieren.
/// </summary>
internal sealed class FileLogger
{
private readonly string _directory;
private readonly int _retentionDays;
private readonly string _component;
public FileLogger(string directory, int retentionDays, string component)
{
_directory = directory;
_retentionDays = retentionDays;
_component = string.IsNullOrWhiteSpace(component) ? "application" : component;
}
public void Info(string message)
{
Write("INFO", message, null);
}
public void Warning(string message)
{
Write("WARN", message, null);
}
public void Error(string message, Exception exception)
{
Write("ERROR", message, exception);
}
public void Prune()
{
try
{
if (!Directory.Exists(_directory))
{
return;
}
var cutoff = DateTime.UtcNow.Date.AddDays(-_retentionDays);
foreach (var file in Directory.GetFiles(_directory, "biztalk-checkmk-pulse-*.log"))
{
try
{
if (File.GetLastWriteTimeUtc(file) < cutoff)
{
File.Delete(file);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
private void Write(string level, string message, Exception exception)
{
try
{
Directory.CreateDirectory(_directory);
var path = Path.Combine(
_directory,
"biztalk-checkmk-pulse-" + DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture) + ".log");
var line = new StringBuilder()
.Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture))
.Append(" level=").Append(level)
.Append(" component=").Append(_component)
.Append(" pid=").Append(System.Diagnostics.Process.GetCurrentProcess().Id)
.Append(" identity=").Append(CurrentIdentity())
.Append(" message=").Append(SingleLine(message));
if (exception != null)
{
line.Append(" exception=").Append(SingleLine(exception.ToString()));
}
line.Append(Environment.NewLine);
var bytes = new UTF8Encoding(false).GetBytes(line.ToString());
for (var attempt = 0; attempt < 3; attempt++)
{
try
{
using (var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read))
{
stream.Write(bytes, 0, bytes.Length);
stream.Flush();
}
return;
}
catch (IOException)
{
if (attempt == 2)
{
return;
}
Thread.Sleep(25 * (attempt + 1));
}
}
}
catch (Exception)
{
// Logging bleibt bewusst best effort.
}
}
private static string CurrentIdentity()
{
try
{
var identity = WindowsIdentity.GetCurrent();
return identity == null ? "(unknown)" : SingleLine(identity.Name);
}
catch (Exception)
{
return "(unknown)";
}
}
private static string SingleLine(string value)
{
return (value ?? string.Empty)
.Replace("\r", "\\r")
.Replace("\n", "\\n")
.Replace("\t", " ");
}
}
}
+1
View File
@@ -125,6 +125,7 @@ namespace BizTalkCheckmkPulse
public string ServerName { get; set; }
public string GroupName { get; set; }
public string OperatorGroup { get; set; }
public string ReadOnlyUserGroup { get; set; }
public string ManagementDbServer { get; set; }
public string ManagementDbName { get; set; }
public string MessageBoxDbServer { get; set; }
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
namespace BizTalkCheckmkPulse
@@ -27,6 +28,12 @@ namespace BizTalkCheckmkPulse
public int EventLogWarnThreshold { get; set; }
public int EventLogCritThreshold { get; set; }
public IReadOnlyList<string> EventLogSources { get; set; }
public string SnapshotPath { get; set; }
public int SnapshotMaxAgeSeconds { get; set; }
public int SnapshotMaxBytes { get; set; }
public string LogDirectory { get; set; }
public int LogRetentionDays { get; set; }
public bool Collect { get; set; }
public bool SelfTest { get; set; }
/// <summary>
@@ -50,6 +57,12 @@ namespace BizTalkCheckmkPulse
EventLogWarnThreshold = 1;
EventLogCritThreshold = 10;
EventLogSources = new[] { "BizTalk Server", "XLANG/s", "ENTSSO", "BizTalk Server Application", "BizTalk Server EDI" };
var commonData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
SnapshotPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "biztalk-checkmk-pulse.snapshot");
SnapshotMaxAgeSeconds = 180;
SnapshotMaxBytes = 1048576;
LogDirectory = Path.Combine(commonData, "BizTalkCheckmkPulse", "logs");
LogRetentionDays = 30;
}
/// <summary>
@@ -91,8 +104,17 @@ namespace BizTalkCheckmkPulse
options.EventLogWarnThreshold = ReadInt(settings, "EventLogWarnThreshold", options.EventLogWarnThreshold, 0, 1000000);
options.EventLogCritThreshold = ReadInt(settings, "EventLogCritThreshold", options.EventLogCritThreshold, 0, 1000000);
options.EventLogSources = ReadSources(ReadString(settings, "EventLogSources", string.Join("|", options.EventLogSources)));
options.SnapshotPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "SnapshotPath", options.SnapshotPath));
options.SnapshotMaxAgeSeconds = ReadInt(settings, "SnapshotMaxAgeSeconds", options.SnapshotMaxAgeSeconds, 60, 86400);
options.SnapshotMaxBytes = ReadInt(settings, "SnapshotMaxBytes", options.SnapshotMaxBytes, 4096, 16777216);
options.LogDirectory = Environment.ExpandEnvironmentVariables(ReadString(settings, "LogDirectory", options.LogDirectory));
options.LogRetentionDays = ReadInt(settings, "LogRetentionDays", options.LogRetentionDays, 1, 365);
ApplyArguments(options, args ?? new string[0]);
if (!options.SelfTest)
{
Validate(options);
}
return options;
}
@@ -110,6 +132,14 @@ namespace BizTalkCheckmkPulse
{
options.SelfTest = true;
}
else if (EqualsAny(arg, "--collect", "/collect"))
{
options.Collect = true;
}
else if (EqualsAny(arg, "--consume", "/consume"))
{
options.Collect = false;
}
else if (EqualsAny(arg, "--server", "/server") && i + 1 < args.Length)
{
options.Server = args[++i];
@@ -133,6 +163,22 @@ namespace BizTalkCheckmkPulse
}
}
/// <summary>
/// Verhindert unsichere oder mehrdeutige Laufzeitpfade.
/// </summary>
private static void Validate(MonitoringOptions options)
{
if (string.IsNullOrWhiteSpace(options.SnapshotPath) || !Path.IsPathRooted(options.SnapshotPath))
{
throw new ConfigurationErrorsException("SnapshotPath must be an absolute path.");
}
if (string.IsNullOrWhiteSpace(options.LogDirectory) || !Path.IsPathRooted(options.LogDirectory))
{
throw new ConfigurationErrorsException("LogDirectory must be an absolute path.");
}
}
/// <summary>
/// Liest einen getrimmten Konfigurationswert.
/// </summary>
+141 -25
View File
@@ -1,19 +1,22 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Text;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Einstiegspunkt des Checkmk Local Checks.
/// Einstiegspunkt fuer privilegierten Provider und unprivilegierten Checkmk-Consumer.
/// </summary>
internal static class Program
{
/// <summary>
/// Laedt die Konfiguration, fuehrt die Probes aus und schreibt Checkmk-Zeilen nach STDOUT.
/// Laedt die Konfiguration und fuehrt je nach Modus Provider oder Snapshot-Consumer aus.
/// </summary>
/// <param name="args">Kommandozeilenargumente wie <c>--self-test</c> oder <c>--environment</c>.</param>
/// <returns>Immer 0, damit Diagnosefehler als UNKNOWN-Service statt als kaputte Agent-Sektion erscheinen.</returns>
/// <returns>Consumer immer 0; Provider 0 bei Erfolg und ungleich 0 bei einem Laufzeitfehler.</returns>
private static int Main(string[] args)
{
MonitoringOptions options = null;
@@ -34,38 +37,151 @@ namespace BizTalkCheckmkPulse
return 0;
}
var result = new ProbeResult();
var wmiProbe = new WmiBizTalkProbe(options);
wmiProbe.Query(result);
// Der SQL-Test verwendet bewusst dieselbe Windows-Identitaet wie der Checkmk-Agent.
var sqlProbe = new SqlConnectivityProbe(options);
sqlProbe.Query(result);
if (options.ProbeEventLog)
{
var eventLogProbe = new EventLogProbe(options);
eventLogProbe.Query(result);
}
foreach (var line in formatter.Format(result))
{
Console.WriteLine(line);
}
return 0;
return options.Collect
? RunProvider(options, formatter)
: RunConsumer(options, formatter);
}
catch (Exception ex)
{
var fallbackOptions = options ?? new MonitoringOptions();
var formatter = new CheckmkLocalFormatter(fallbackOptions);
foreach (var line in formatter.FormatFatal("BizTalk Checkmk Pulse failed: " + ex.GetType().Name + ": " + ex.Message))
foreach (var line in formatter.FormatSnapshotFailure(
"Programmstart fehlgeschlagen: " + ex.GetType().Name + ": " + ex.Message))
{
Console.WriteLine(line);
}
return options != null && options.Collect ? 1 : 0;
}
}
private static int RunProvider(MonitoringOptions options, CheckmkLocalFormatter formatter)
{
var logger = new FileLogger(options.LogDirectory, options.LogRetentionDays, "provider");
logger.Prune();
var stopwatch = Stopwatch.StartNew();
var lockPath = options.SnapshotPath + ".provider.lock";
try
{
EnsureProviderIdentity();
Directory.CreateDirectory(Path.GetDirectoryName(options.SnapshotPath));
using (new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
logger.Info("Collection started. snapshot=" + options.SnapshotPath);
var result = Collect(options);
var lines = formatter.Format(result).ToArray();
new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Write(lines, DateTime.UtcNow, CurrentIdentity());
stopwatch.Stop();
logger.Info(
"Collection completed. lines="
+ lines.Length
+ " diagnostics="
+ result.Diagnostics.Count
+ " elapsed_ms="
+ stopwatch.ElapsedMilliseconds);
return 0;
}
}
catch (IOException ex)
{
logger.Warning("Collection skipped or snapshot I/O failed: " + ex.Message);
return 2;
}
catch (Exception ex)
{
stopwatch.Stop();
logger.Error("Collection failed after " + stopwatch.ElapsedMilliseconds + " ms.", ex);
TryWriteFatalSnapshot(options, formatter, ex, logger);
return 1;
}
}
private static void EnsureProviderIdentity()
{
using (var identity = WindowsIdentity.GetCurrent())
{
if (identity != null
&& identity.User != null
&& identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid))
{
throw new InvalidOperationException(
"Provider mode must not run as LocalSystem. Configure the dedicated Scheduled Task account.");
}
}
}
private static int RunConsumer(MonitoringOptions options, CheckmkLocalFormatter formatter)
{
var snapshot = new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Read(DateTime.UtcNow, TimeSpan.FromSeconds(options.SnapshotMaxAgeSeconds));
if (snapshot.IsSuccess)
{
foreach (var line in snapshot.Lines)
{
Console.WriteLine(line);
}
return 0;
}
new FileLogger(options.LogDirectory, options.LogRetentionDays, "consumer")
.Warning("Snapshot rejected. reason=" + snapshot.Error + " path=" + options.SnapshotPath);
foreach (var line in formatter.FormatSnapshotFailure(snapshot.Error))
{
Console.WriteLine(line);
}
return 0;
}
private static ProbeResult Collect(MonitoringOptions options)
{
var result = new ProbeResult();
new WmiBizTalkProbe(options).Query(result);
new SqlConnectivityProbe(options).Query(result);
if (options.ProbeEventLog)
{
new EventLogProbe(options).Query(result);
}
return result;
}
private static void TryWriteFatalSnapshot(
MonitoringOptions options,
CheckmkLocalFormatter formatter,
Exception exception,
FileLogger logger)
{
try
{
var lines = formatter
.FormatFatal("Privilegierter Datenprovider fehlgeschlagen: " + exception.GetType().Name + ": " + exception.Message)
.ToArray();
new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Write(lines, DateTime.UtcNow, CurrentIdentity());
logger.Warning("A current UNKNOWN snapshot was written after the provider failure.");
}
catch (Exception snapshotException)
{
logger.Error("Fatal UNKNOWN snapshot could not be written.", snapshotException);
}
}
private static string CurrentIdentity()
{
try
{
var identity = WindowsIdentity.GetCurrent();
return identity == null ? "(unknown)" : identity.Name;
}
catch (Exception)
{
return "(unknown)";
}
}
}
}
@@ -1,3 +1,10 @@
using System.Runtime.CompilerServices;
using System.Reflection;
[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")]
[assembly: AssemblyTitle("BizTalk Checkmk Pulse")]
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
[assembly: AssemblyCompany("BEW")]
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
[assembly: AssemblyVersion("2.0.0.0")]
[assembly: AssemblyFileVersion("2.0.0.0")]
+353
View File
@@ -0,0 +1,353 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Diagnostics;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Schreibt und liest einen atomaren, integritaetsgeschuetzten Checkmk-Snapshot.
/// </summary>
internal sealed class SnapshotStore
{
internal const string Magic = "BIZTALK_CHECKMK_PULSE_SNAPSHOT_V1";
private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
private readonly string _path;
private readonly int _maxBytes;
public SnapshotStore(string path, int maxBytes)
{
_path = path;
_maxBytes = maxBytes;
}
public void Write(IReadOnlyCollection<string> lines, DateTime generatedUtc, string identity)
{
ValidatePayload(lines);
var payload = string.Join("\n", lines) + "\n";
var payloadBytes = StrictUtf8.GetBytes(payload);
var content = BuildHeader(generatedUtc, identity, lines.Count, Hash(payloadBytes)) + payload;
var contentBytes = StrictUtf8.GetBytes(content);
if (contentBytes.Length > _maxBytes)
{
throw new InvalidDataException("Snapshot exceeds configured SnapshotMaxBytes.");
}
var directory = Path.GetDirectoryName(_path);
if (string.IsNullOrWhiteSpace(directory))
{
throw new InvalidOperationException("Snapshot path has no parent directory.");
}
Directory.CreateDirectory(directory);
var temporaryPath = Path.Combine(
directory,
Path.GetFileName(_path) + "." + Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture) + "." + Guid.NewGuid().ToString("N") + ".tmp");
try
{
using (var stream = new FileStream(
temporaryPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
4096,
FileOptions.WriteThrough))
{
stream.Write(contentBytes, 0, contentBytes.Length);
stream.Flush(true);
}
if (File.Exists(_path))
{
File.Replace(temporaryPath, _path, null, true);
}
else
{
File.Move(temporaryPath, _path);
}
}
finally
{
try
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
catch (IOException)
{
}
}
}
public SnapshotReadResult Read(DateTime utcNow, TimeSpan maximumAge)
{
try
{
var bytes = ReadBytesWithRetry();
var text = StrictUtf8.GetString(bytes).Replace("\r\n", "\n");
var separator = text.IndexOf("\n\n", StringComparison.Ordinal);
if (separator < 0)
{
return SnapshotReadResult.Failed("Snapshot header separator is missing.");
}
var headerLines = text.Substring(0, separator).Split('\n');
if (headerLines.Length != 6 || !string.Equals(headerLines[0], Magic, StringComparison.Ordinal))
{
return SnapshotReadResult.Failed("Snapshot format or version is invalid.");
}
DateTime generatedUtc;
int expectedLineCount;
if (!TryReadDate(headerLines[1], "generatedUtc=", out generatedUtc)
|| !TryReadInt(headerLines[4], "payloadLines=", out expectedLineCount))
{
return SnapshotReadResult.Failed("Snapshot metadata is invalid.");
}
var expectedMachine = DecodeHeader(headerLines[2], "machineBase64=");
DecodeHeader(headerLines[3], "identityBase64=");
var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256=");
if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
{
return SnapshotReadResult.Failed("Snapshot was created for a different machine.");
}
var payload = text.Substring(separator + 2);
if (!payload.EndsWith("\n", StringComparison.Ordinal)
|| payload.IndexOf("\n\n", StringComparison.Ordinal) >= 0)
{
return SnapshotReadResult.Failed("Snapshot payload framing is invalid.");
}
var payloadBytes = StrictUtf8.GetBytes(payload);
if (!FixedTimeEquals(expectedHash, Hash(payloadBytes)))
{
return SnapshotReadResult.Failed("Snapshot SHA-256 validation failed.");
}
var lines = payload
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.ToArray();
if (lines.Length != expectedLineCount)
{
return SnapshotReadResult.Failed("Snapshot payload line count is invalid.");
}
ValidatePayload(lines);
var age = utcNow - generatedUtc;
if (age < TimeSpan.FromMinutes(-5))
{
return SnapshotReadResult.Failed("Snapshot timestamp is too far in the future.");
}
if (age > maximumAge)
{
return SnapshotReadResult.Failed(
"Snapshot is stale: age="
+ Math.Floor(age.TotalSeconds).ToString(CultureInfo.InvariantCulture)
+ "s, maximum="
+ Math.Floor(maximumAge.TotalSeconds).ToString(CultureInfo.InvariantCulture)
+ "s.");
}
return SnapshotReadResult.Success(lines, generatedUtc);
}
catch (FileNotFoundException)
{
return SnapshotReadResult.Failed("Snapshot file does not exist.");
}
catch (DirectoryNotFoundException)
{
return SnapshotReadResult.Failed("Snapshot directory does not exist.");
}
catch (UnauthorizedAccessException ex)
{
return SnapshotReadResult.Failed("Snapshot cannot be read: " + ex.Message);
}
catch (IOException ex)
{
return SnapshotReadResult.Failed("Snapshot I/O failed: " + ex.Message);
}
catch (Exception ex)
{
return SnapshotReadResult.Failed("Snapshot validation failed: " + ex.GetType().Name + ": " + ex.Message);
}
}
private byte[] ReadBytesWithRetry()
{
for (var attempt = 0; ; attempt++)
{
try
{
using (var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
{
if (stream.Length <= 0 || stream.Length > _maxBytes)
{
throw new InvalidDataException("Snapshot size is outside the allowed range.");
}
var bytes = new byte[(int)stream.Length];
var offset = 0;
while (offset < bytes.Length)
{
var read = stream.Read(bytes, offset, bytes.Length - offset);
if (read == 0)
{
throw new EndOfStreamException("Unexpected end of snapshot.");
}
offset += read;
}
return bytes;
}
}
catch (IOException)
{
if (attempt >= 2)
{
throw;
}
Thread.Sleep(25 * (attempt + 1));
}
}
}
private static string BuildHeader(DateTime generatedUtc, string identity, int lineCount, string hash)
{
return Magic + "\n"
+ "generatedUtc=" + generatedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture) + "\n"
+ "machineBase64=" + Convert.ToBase64String(StrictUtf8.GetBytes(Environment.MachineName)) + "\n"
+ "identityBase64=" + Convert.ToBase64String(StrictUtf8.GetBytes(identity ?? string.Empty)) + "\n"
+ "payloadLines=" + lineCount.ToString(CultureInfo.InvariantCulture) + "\n"
+ "payloadSha256=" + hash + "\n\n";
}
private static void ValidatePayload(IEnumerable<string> lines)
{
if (lines == null)
{
throw new ArgumentNullException("lines");
}
var count = 0;
foreach (var line in lines)
{
count++;
if (string.IsNullOrWhiteSpace(line)
|| line.IndexOf('\r') >= 0
|| line.IndexOf('\n') >= 0
|| line.Length < 5
|| line[1] != ' '
|| line[2] != '"'
|| line[0] < '0'
|| line[0] > '3')
{
throw new InvalidDataException("Snapshot contains an invalid Checkmk local-check line.");
}
}
if (count < 6)
{
throw new InvalidDataException("Snapshot must contain all six stable services.");
}
}
private static string Hash(byte[] bytes)
{
using (var sha = SHA256.Create())
{
return string.Concat(sha.ComputeHash(bytes).Select(x => x.ToString("x2", CultureInfo.InvariantCulture)));
}
}
private static bool FixedTimeEquals(string left, string right)
{
if (left == null || right == null || left.Length != right.Length)
{
return false;
}
var difference = 0;
for (var i = 0; i < left.Length; i++)
{
difference |= left[i] ^ right[i];
}
return difference == 0;
}
private static string ReadHeaderValue(string line, string prefix)
{
if (line == null || !line.StartsWith(prefix, StringComparison.Ordinal))
{
throw new InvalidDataException("Missing snapshot header " + prefix);
}
return line.Substring(prefix.Length);
}
private static string DecodeHeader(string line, string prefix)
{
return StrictUtf8.GetString(Convert.FromBase64String(ReadHeaderValue(line, prefix)));
}
private static bool TryReadDate(string line, string prefix, out DateTime result)
{
return DateTime.TryParseExact(
ReadHeaderValue(line, prefix),
"o",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out result);
}
private static bool TryReadInt(string line, string prefix, out int result)
{
return int.TryParse(ReadHeaderValue(line, prefix), NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
}
}
internal sealed class SnapshotReadResult
{
private SnapshotReadResult()
{
Lines = new string[0];
}
public bool IsSuccess { get; private set; }
public string Error { get; private set; }
public IReadOnlyList<string> Lines { get; private set; }
public DateTime GeneratedUtc { get; private set; }
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc)
{
return new SnapshotReadResult
{
IsSuccess = true,
Lines = lines,
GeneratedUtc = generatedUtc
};
}
public static SnapshotReadResult Failed(string error)
{
return new SnapshotReadResult
{
IsSuccess = false,
Error = error
};
}
}
}
@@ -56,8 +56,8 @@ namespace BizTalkCheckmkPulse
? "SQL-Zielermittlung ist unvollstaendig, weil der BizTalk-WMI-Provider beim SQL-Zugriff abgewiesen wurde."
: "SQL-Zielermittlung ist unvollstaendig, weil nicht alle erforderlichen BizTalk-Plattformklassen gelesen wurden.",
Action = wmiPermissionFailure
? "Zuerst die Wmi/Permission-Diagnose und die Mitgliedschaft des Computerkontos in der konfigurierten BizTalk-Operator-Gruppe beheben."
: "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting pruefen. Danach den Agent-Dump erneut ausfuehren."
? "Zuerst die Wmi/Permission-Diagnose sowie Provider-Konto, konfigurierte BizTalk-Read-Only-Gruppe und BTS_READONLY_USERS pruefen."
: "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting im Provider-Log pruefen. Danach den Scheduled Task erneut starten."
});
}
@@ -112,7 +112,7 @@ namespace BizTalkCheckmkPulse
Component = "Windows identity",
Required = false,
Summary = "Die Windows-Ausfuehrungsidentitaet konnte nicht bestimmt werden.",
Action = "Agent-Dienstkonto mit 'sc.exe qc CheckMKService' beziehungsweise in services.msc pruefen.",
Action = "Konto des Scheduled Tasks 'BizTalk Checkmk Pulse Provider' pruefen.",
TechnicalDetails = ex.GetType().Name + ": " + ex.Message
});
}
@@ -268,7 +268,7 @@ namespace BizTalkCheckmkPulse
case DiagnosticCategory.Timeout:
return "SQL-Verbindungsaufbau oder Testabfrage hat das konfigurierte Zeitlimit ueberschritten.";
case DiagnosticCategory.Connectivity:
return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Checkmk-Agent-Kontext nicht erreichbar.";
return "SQL Server oder die konfigurierte SQL-Instanz ist aus dem Provider-Kontext nicht erreichbar.";
case DiagnosticCategory.Configuration:
return "SQL-Verbindung scheitert an TLS-, Zertifikats-, SPN- oder SSPI-Konfiguration.";
default:
@@ -288,7 +288,7 @@ namespace BizTalkCheckmkPulse
switch (category)
{
case DiagnosticCategory.Permission:
return networkIdentity + " in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen.";
return "Provider-Konto " + networkIdentity + " und dessen Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe sowie BTS_READONLY_USERS pruefen; keine direkten BizTalk-DB-Rollen vergeben. Danach den Scheduled Task mit neuem Anmeldetoken starten.";
case DiagnosticCategory.Timeout:
return "Netzwerkpfad, DNS, SQL-Port, Firewall und Auslastung fuer " + target.Server + " pruefen; Timeout nur nach Ursachenanalyse erhoehen.";
case DiagnosticCategory.Connectivity:
+7 -6
View File
@@ -25,7 +25,7 @@ namespace BizTalkCheckmkPulse
private const int OrchestrationStarted = 4;
private const int EAccessDenied = unchecked((int)0x80070005);
internal const string GroupSettingQuery =
"SELECT Name, BizTalkOperatorGroup, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting";
"SELECT Name, BizTalkOperatorGroup, BizTalkReadOnlyUserGroup, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting";
private readonly MonitoringOptions _options;
/// <summary>
@@ -106,6 +106,7 @@ namespace BizTalkCheckmkPulse
groupFound = true;
result.Platform.GroupName = FirstNonEmpty(WmiHelpers.GetString(item, "Name"), WmiHelpers.GetString(item, "MgmtDbName"));
result.Platform.OperatorGroup = WmiHelpers.GetString(item, "BizTalkOperatorGroup");
result.Platform.ReadOnlyUserGroup = WmiHelpers.GetString(item, "BizTalkReadOnlyUserGroup");
result.Platform.ManagementDbServer = WmiHelpers.GetString(item, "MgmtDbServerName");
result.Platform.ManagementDbName = WmiHelpers.GetString(item, "MgmtDbName");
result.Platform.MessageBoxDbServer = WmiHelpers.GetString(item, "SubscriptionDBServerName");
@@ -672,7 +673,7 @@ namespace BizTalkCheckmkPulse
return "BizTalk-WMI-Provider konnte den SQL-Zugriff fuer " + sqlLoginPrincipal + " nicht anmelden.";
}
return "Zugriff auf " + component + " wurde im LocalSystem-Kontext verweigert.";
return "Zugriff des privilegierten Provider-Kontos auf " + component + " wurde verweigert.";
case DiagnosticCategory.Configuration:
return connectionFailure
? "BizTalk-WMI-Namespace ist nicht vorhanden oder nicht korrekt registriert."
@@ -704,13 +705,13 @@ namespace BizTalkCheckmkPulse
case DiagnosticCategory.Permission:
if (!string.IsNullOrWhiteSpace(sqlLoginPrincipal))
{
return "Computerkonto " + sqlLoginPrincipal
+ " der in der BizTalk-Gruppe konfigurierten BizTalk-Operator-Gruppe zuordnen; keine direkten SQL-Logins oder Datenbankrollen vergeben.";
return "Provider-Konto " + sqlLoginPrincipal
+ " und dessen Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe sowie BTS_READONLY_USERS pruefen; keine direkten SQL-Logins oder Datenbankrollen vergeben.";
}
return connectionFailure
? "Namespace-ACL fuer root\\MicrosoftBizTalkServer gezielt pruefen. LocalSystem benoetigt lokalen Lesezugriff; keine pauschalen WMI-Rechte vergeben."
: "Agent-Dump pruefen und Computerkonto <DOMAIN>\\" + Environment.MachineName + "$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen.";
? "Lokalen Namespace-Zugriff des Scheduled-Task-Kontos auf root\\MicrosoftBizTalkServer gezielt pruefen; keine pauschalen WMI-Rechte vergeben."
: "Provider-Identitaet und Mitgliedschaft in der konfigurierten BizTalk-Read-Only-Gruppe pruefen; Operator-Rechte nur nach klassenspezifischer Analyse erwaegen.";
case DiagnosticCategory.Configuration:
return "BizTalk-WMI-Provider/Namespace auf dem BizTalk-Server pruefen und gegebenenfalls mit dem BizTalk-Setup reparieren.";
case DiagnosticCategory.Schema: