Separate BizTalk collection from Checkmk agent
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user