diff --git a/.gitignore b/.gitignore index ec9f101..3798f23 100644 --- a/.gitignore +++ b/.gitignore @@ -22,11 +22,14 @@ _ReSharper*/ [Bb]in/ [Oo]bj/ [TestResult*/ -artifacts/ -publish/ - -logs/ -state/ +artifacts/ +publish/ + +HIP-VMs.txt +hostcheck.ps + +logs/ +state/ packages/ .nuget/ diff --git a/Documentation.en.md b/Documentation.en.md index f6d4744..4ab167a 100644 --- a/Documentation.en.md +++ b/Documentation.en.md @@ -12,6 +12,7 @@ The monitor checks technical reachability and writes: - Windows Application Event Log - email notifications for failures, recoveries, and a daily status summary - a state file for transition detection, cooldown handling, and once-per-day summary tracking +- optional static HIP VM checks from `hip-vms.json` for network/ping and RDP/TCP 3389 ### 2. BizTalkMonitorConfigGenerator @@ -60,7 +61,8 @@ The goal is to avoid manually maintaining the target list and instead derive it ├── App.config ├── HostAvailabilityMonitor.csproj ├── Program.cs - └── appsettings.json + ├── appsettings.json + └── hip-vms.json ``` This layout is intentionally simple so it can be committed to **Gitea**, built on Windows, and deployed to BizTalk or application servers with minimal friction. @@ -95,6 +97,7 @@ The generator tries to transform BizTalk addresses into monitor-compatible endpo - `sftp://...` - `tcp://...` - `icmp://...` +- `rdp://...` - plain host / host:port values on a best-effort basis ### Special handling @@ -139,6 +142,7 @@ File: `src\HostAvailabilityMonitor\appsettings.json` - `Logging`: file logging configuration - `EventLog`: Windows Event Log configuration - `Email`: SMTP and notification configuration +- `HipVirtualMachines`: optional static VM checks - `Targets`: endpoints to probe ### Email section @@ -233,6 +237,7 @@ The daily status email contains: - status - detail - host/port/HTTP status when available +- a HIP VM snapshot with network and RDP status per VM when `HipVirtualMachines.Enabled=true` Note: @@ -240,6 +245,39 @@ The daily counters are derived from the **current day's rolling log file**. If t --- +## HIP VM Monitoring + +The HIP VMs are kept in a dedicated file: + +```text +src\HostAvailabilityMonitor\hip-vms.json +``` + +This keeps them independent from the `Targets` regenerated by the BizTalk generator. Changes to BizTalk send ports or receive locations do not overwrite the static VM list. + +Enable the feature in `appsettings.json`: + +```json +"HipVirtualMachines": { + "Enabled": true, + "ConfigPath": "hip-vms.json", + "ApplicationName": "HIP Virtual Machines", + "TimeoutSeconds": 5, + "RdpPort": 3389, + "CheckNetwork": true, + "CheckRdp": true +} +``` + +The `hip-vms.json` file contains group, name, address, description, and enabled state for each VM. At startup, the monitor turns every enabled VM into normal monitor targets: + +- `Network`: `icmp://
` for ping/network connectivity +- `RDP`: `rdp://
:3389` for TCP connectivity to the RDP service + +These targets use the same state file as all other checks. Lost network or RDP reachability and later recoveries are therefore reported through the normal failure/recovery emails. File logs and Windows Event Log entries include source, group, machine, and check type. + +--- + ## Generator configuration details File: `generator-settings.json` @@ -297,7 +335,7 @@ It contains: Important: -When the generator writes the monitor JSON, the daily summary options are also copied into the generated target file. That means `GeneratedMonitorConfig.Email.SendDailySummary`, `DailySummaryHourLocal`, and `DailySummaryMinuteLocal` flow directly into the output monitor configuration. +When the generator writes the monitor JSON, the daily summary options and static HIP VM settings are also copied into the generated target file. That means `GeneratedMonitorConfig.Email.SendDailySummary`, `DailySummaryHourLocal`, `DailySummaryMinuteLocal`, and `GeneratedMonitorConfig.HipVirtualMachines` flow directly into the output monitor configuration. --- diff --git a/Dokumentation.md b/Dokumentation.md index 044c621..5f78240 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -12,6 +12,7 @@ Der Monitor prüft technische Erreichbarkeit und schreibt: - Windows Application Event Log - E-Mail-Benachrichtigungen bei Ausfall, Recovery und täglichem Status - Statusdatei für Zustandswechsel, Cooldown-Logik und die tägliche Summary-Mail +- optional statische HIP-VM-Prüfungen aus `hip-vms.json` für Netzwerk/Ping und RDP/TCP 3389 ### 2. BizTalkMonitorConfigGenerator @@ -60,7 +61,8 @@ Ziel ist, dass die Ziel-Liste nicht manuell gepflegt werden muss, sondern direkt ├── App.config ├── HostAvailabilityMonitor.csproj ├── Program.cs - └── appsettings.json + ├── appsettings.json + └── hip-vms.json ``` Diese Struktur ist für ein normales Gitea-Repository ausgelegt. Build und Deployment können aus der Solution oder projektweise erfolgen. @@ -95,6 +97,7 @@ Der Generator versucht, BizTalk-Adressen in Monitor-kompatible Endpunkte zu übe - `sftp://...` - `tcp://...` - `icmp://...` +- `rdp://...` - Plain Host / Host:Port (best effort) ### Spezielle Behandlung @@ -139,6 +142,7 @@ Datei: `src\HostAvailabilityMonitor\appsettings.json` - `Logging`: Dateilog-Konfiguration - `EventLog`: Windows Event Log Konfiguration - `Email`: SMTP- und Benachrichtigungskonfiguration +- `HipVirtualMachines`: optionale statische VM-Prüfungen - `Targets`: die zu prüfenden Endpunkte ### Email @@ -233,6 +237,7 @@ Die Tagesstatus-Mail enthält: - Status - Detail - Host/Port/HTTP-Status, soweit vorhanden +- HIP-VM-Snapshot mit Netzwerk- und RDP-Status pro VM, wenn `HipVirtualMachines.Enabled=true` Hinweis: @@ -240,6 +245,39 @@ Die Tageszähler werden aus dem **heutigen Rolling-Log** ausgewertet. Sollte die --- +## HIP-VM-Monitoring + +Die HIP-VMs werden bewusst in einer eigenen Datei gepflegt: + +```text +src\HostAvailabilityMonitor\hip-vms.json +``` + +Damit bleiben sie unabhängig von den durch den BizTalk-Generator neu erzeugten `Targets`. Änderungen an BizTalk-Sendports oder Receive Locations überschreiben diese statische VM-Liste nicht. + +Aktiviert wird die Erweiterung in `appsettings.json`: + +```json +"HipVirtualMachines": { + "Enabled": true, + "ConfigPath": "hip-vms.json", + "ApplicationName": "HIP Virtual Machines", + "TimeoutSeconds": 5, + "RdpPort": 3389, + "CheckNetwork": true, + "CheckRdp": true +} +``` + +Die Datei `hip-vms.json` enthält pro VM Gruppe, Namen, Adresse, Beschreibung und Aktivstatus. Beim Start erzeugt der Monitor daraus normale Targets: + +- `Network`: `icmp://` für Ping/Netzwerk-Konnektivität +- `RDP`: `rdp://:3389` für TCP-Konnektivität zum RDP-Dienst + +Diese Targets nutzen dieselbe State-Datei wie alle anderen Checks. Dadurch werden verlorene Netzwerk- oder RDP-Erreichbarkeit und spätere Recoveries automatisch in den normalen Failure-/Recovery-Mails gemeldet. Im Dateilog und Event Log werden zusätzlich Quelle, Gruppe, Maschine und Check-Typ geschrieben. + +--- + ## Generator-Konfiguration im Detail Datei: `generator-settings.json` @@ -297,7 +335,7 @@ Hier werden gepflegt: Wichtig: -Wenn der Generator die Monitor-JSON erzeugt, werden auch die Felder für die Tagesstatus-Mail mit erzeugt. Das heißt: `GeneratedMonitorConfig.Email.SendDailySummary`, `DailySummaryHourLocal` und `DailySummaryMinuteLocal` werden direkt in die Zielkonfiguration übernommen. +Wenn der Generator die Monitor-JSON erzeugt, werden auch die Felder für die Tagesstatus-Mail und die statische HIP-VM-Prüfung mit erzeugt. Das heißt: `GeneratedMonitorConfig.Email.SendDailySummary`, `DailySummaryHourLocal`, `DailySummaryMinuteLocal` und `GeneratedMonitorConfig.HipVirtualMachines` werden direkt in die Zielkonfiguration übernommen. --- diff --git a/README.md b/README.md index 55b95fb..6017b42 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Dieses Repository ist für eine **Gitea-kompatible Repo-Struktur** aufgebaut und enthält zwei Konsolenanwendungen für **Windows Server 2019 / BizTalk Server 2019** auf **.NET Framework 4.7.2**: 1. **HostAvailabilityMonitor** - - prüft Endpunkte wie UNC/SMB, `file://`, HTTP/HTTPS, SFTP, FTP, TCP und ICMP + - prüft Endpunkte wie UNC/SMB, `file://`, HTTP/HTTPS, SFTP, FTP, TCP, ICMP und RDP + - lädt optional eine statische HIP-VM-Liste aus `hip-vms.json` und prüft pro VM Netzwerk/Ping sowie RDP/TCP 3389 - schreibt Rolling-Logs, Windows Application Event Log und E-Mails bei DOWN, Recovery und täglichem Status 2. **BizTalkMonitorConfigGenerator** @@ -61,7 +62,8 @@ Der typische Betrieb auf einer BizTalk-Maschine ist: ├── App.config ├── HostAvailabilityMonitor.csproj ├── Program.cs - └── appsettings.json + ├── appsettings.json + └── hip-vms.json ``` Diese Struktur ist bewusst einfach gehalten, damit das Repository sauber in **Gitea** eingecheckt, gebaut und auf Windows-Servern ausgerollt werden kann. @@ -88,6 +90,8 @@ Diese Struktur ist bewusst einfach gehalten, damit das Repository sauber in **Gi - Windows Application Event Log - SMTP-Mails bei DOWN, Recovery (`DOWN -> UP`) und täglichem Status - SMTP-Sendports bzw. reine E-Mail-Empfaengerlisten werden vom Generator uebersprungen und vom Monitor defensiv nur als `Skipped` protokolliert, nicht alarmiert +- statische HIP-VM-Prüfung über separate `hip-vms.json`, damit durch BizTalk neu erzeugte Endpunktlisten nicht mit der VM-Liste kollidieren +- pro HIP-VM werden zwei normale Monitor-Targets erzeugt: `icmp://` für Netzwerk-Konnektivität und `rdp://:3389` für RDP - tägliche Status-Mail auf Basis des **ersten Laufs ab der konfigurierten Uhrzeit** - State-Datei gegen Mail-Spam und für die Einmal-pro-Tag-Logik der Status-Mail - `ApplicationName` pro Target @@ -257,6 +261,36 @@ Inhalt der Tagesstatus-Mail: - Anzahl heutiger erfolgreicher und fehlgeschlagener Checks - aktueller Snapshot des letzten Laufs - Liste der aktuell DOWN befindlichen Endpunkte mit Anwendung und Umgebung +- HIP-VM-Snapshot mit Netzwerk- und RDP-Status pro VM + +## HIP-VM-Monitoring + +Die statischen HIP-VMs liegen getrennt von den BizTalk-Endpunkten in: + +```text +src\HostAvailabilityMonitor\hip-vms.json +``` + +Aktiviert wird die Erweiterung in `appsettings.json`: + +```json +"HipVirtualMachines": { + "Enabled": true, + "ConfigPath": "hip-vms.json", + "ApplicationName": "HIP Virtual Machines", + "TimeoutSeconds": 5, + "RdpPort": 3389, + "CheckNetwork": true, + "CheckRdp": true +} +``` + +Pro VM erzeugt der Monitor beim Start zwei normale Checks: + +- `Network`: Ping/ICMP gegen die konfigurierte Adresse +- `RDP`: TCP-Verbindung auf den konfigurierten RDP-Port, standardmäßig `3389` + +Ausfälle und Recoveries laufen über dieselbe State-Datei und dieselbe Mail-Logik wie alle anderen Targets. Die tägliche Status-Mail enthält zusätzlich eine kompakte HIP-VM-Übersicht je Gruppe, Maschine und Beschreibung. ## Mehrere Mail-Empfaenger konfigurieren @@ -307,6 +341,7 @@ Wichtige Felder: - `BizTalk.ApplicationMappings`: Mapping von BizTalk-Anwendungsnamen auf fachliche App-Namen - `Output.Path`: Pfad der zu erzeugenden Monitor-JSON - `GeneratedMonitorConfig`: Vorlage für Logging, Runtime, Event Log und Mail des Monitors +- `GeneratedMonitorConfig.HipVirtualMachines`: statische HIP-VM-Prüfung in der generierten Monitor-JSON einschalten - `GeneratedMonitorConfig.Email.SendDailySummary`: tägliche Status-Mail in der generierten Monitor-JSON einschalten - `GeneratedMonitorConfig.Email.DailySummaryHourLocal`: lokale Uhrzeit der Tageszusammenfassung - `GeneratedMonitorConfig.Email.DailySummaryMinuteLocal`: lokale Minute der Tageszusammenfassung diff --git a/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs b/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs index 3bc9982..9c55b5c 100644 --- a/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs +++ b/src/BizTalkMonitorConfigGenerator/Configuration/GeneratorConfiguration.cs @@ -41,6 +41,7 @@ namespace BizTalkMonitorConfigGenerator.Configuration config.GeneratedMonitorConfig.Logging = config.GeneratedMonitorConfig.Logging ?? new LoggingOptions(); config.GeneratedMonitorConfig.EventLog = config.GeneratedMonitorConfig.EventLog ?? new EventLogOptions(); config.GeneratedMonitorConfig.Email = config.GeneratedMonitorConfig.Email ?? new EmailOptions(); + config.GeneratedMonitorConfig.HipVirtualMachines = config.GeneratedMonitorConfig.HipVirtualMachines ?? new HipVirtualMachineOptions(); config.GeneratedMonitorConfig.Email.To = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.To); config.GeneratedMonitorConfig.Email.Cc = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.Cc); config.GeneratedMonitorConfig.Email.Bcc = RecipientListHelper.SanitizeRecipientList(config.GeneratedMonitorConfig.Email.Bcc); @@ -213,6 +214,7 @@ namespace BizTalkMonitorConfigGenerator.Configuration configuration.Logging = configuration.Logging ?? new LoggingOptions(); configuration.EventLog = configuration.EventLog ?? new EventLogOptions(); configuration.Email = configuration.Email ?? new EmailOptions(); + configuration.HipVirtualMachines = configuration.HipVirtualMachines ?? new HipVirtualMachineOptions(); configuration.Targets = configuration.Targets ?? new List(); if (configuration.Runtime.MaxConcurrency <= 0) @@ -225,6 +227,24 @@ namespace BizTalkMonitorConfigGenerator.Configuration throw new InvalidOperationException("GeneratedMonitorConfig.Logging.RetentionDays darf nicht negativ sein."); } + if (configuration.HipVirtualMachines.Enabled) + { + if (string.IsNullOrWhiteSpace(configuration.HipVirtualMachines.ConfigPath)) + { + throw new InvalidOperationException("GeneratedMonitorConfig.HipVirtualMachines.Enabled=true, aber ConfigPath ist leer."); + } + + if (configuration.HipVirtualMachines.TimeoutSeconds <= 0) + { + configuration.HipVirtualMachines.TimeoutSeconds = 5; + } + + if (configuration.HipVirtualMachines.RdpPort <= 0 || configuration.HipVirtualMachines.RdpPort > 65535) + { + throw new InvalidOperationException("GeneratedMonitorConfig.HipVirtualMachines.RdpPort muss zwischen 1 und 65535 liegen."); + } + } + if (configuration.Email.Enabled) { if (string.IsNullOrWhiteSpace(configuration.Email.SmtpHost)) @@ -306,6 +326,16 @@ namespace BizTalkMonitorConfigGenerator.Configuration CooldownMinutes = configuration.Email.CooldownMinutes, TimeoutSeconds = configuration.Email.TimeoutSeconds }, + HipVirtualMachines = new HipVirtualMachineOptions + { + Enabled = configuration.HipVirtualMachines.Enabled, + ConfigPath = configuration.HipVirtualMachines.ConfigPath, + ApplicationName = configuration.HipVirtualMachines.ApplicationName, + TimeoutSeconds = configuration.HipVirtualMachines.TimeoutSeconds, + RdpPort = configuration.HipVirtualMachines.RdpPort, + CheckNetwork = configuration.HipVirtualMachines.CheckNetwork, + CheckRdp = configuration.HipVirtualMachines.CheckRdp + }, Targets = new List() }; diff --git a/src/BizTalkMonitorConfigGenerator/generator-settings.json b/src/BizTalkMonitorConfigGenerator/generator-settings.json index c014519..0d1e383 100644 --- a/src/BizTalkMonitorConfigGenerator/generator-settings.json +++ b/src/BizTalkMonitorConfigGenerator/generator-settings.json @@ -94,6 +94,15 @@ "CooldownMinutes": 0, "TimeoutSeconds": 30 }, + "HipVirtualMachines": { + "Enabled": true, + "ConfigPath": "hip-vms.json", + "ApplicationName": "HIP Virtual Machines", + "TimeoutSeconds": 5, + "RdpPort": 3389, + "CheckNetwork": true, + "CheckRdp": true + }, "Targets": [] } } diff --git a/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs b/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs new file mode 100644 index 0000000..458c92c --- /dev/null +++ b/src/HostAvailabilityMonitor/Configuration/HipVirtualMachineConfiguration.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Web.Script.Serialization; + +namespace HostAvailabilityMonitor.Configuration +{ + public sealed class HipVirtualMachineConfiguration + { + public List VirtualMachines { get; set; } = new List(); + + public static HipVirtualMachineConfiguration Load(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Der Pfad zur HIP-VM-Konfiguration ist leer.", nameof(path)); + } + + if (!File.Exists(path)) + { + throw new FileNotFoundException("HIP-VM-Konfiguration wurde nicht gefunden: " + path, path); + } + + var json = File.ReadAllText(path); + var serializer = new JavaScriptSerializer(); + var config = serializer.Deserialize(json); + if (config == null) + { + throw new InvalidOperationException("HIP-VM-Konfiguration konnte nicht deserialisiert werden."); + } + + config.VirtualMachines = config.VirtualMachines ?? new List(); + return config; + } + } + + public sealed class HipVirtualMachine + { + public string Group { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Address { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public bool Enabled { get; set; } = true; + } +} diff --git a/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs b/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs index 680ef2d..37e87fc 100644 --- a/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs +++ b/src/HostAvailabilityMonitor/Configuration/MonitorConfiguration.cs @@ -14,6 +14,7 @@ namespace HostAvailabilityMonitor.Configuration public LoggingOptions Logging { get; set; } = new LoggingOptions(); public EventLogOptions EventLog { get; set; } = new EventLogOptions(); public EmailOptions Email { get; set; } = new EmailOptions(); + public HipVirtualMachineOptions HipVirtualMachines { get; set; } = new HipVirtualMachineOptions(); public List Targets { get; set; } = new List(); public static MonitorConfiguration Load(string path) @@ -36,6 +37,7 @@ namespace HostAvailabilityMonitor.Configuration config.Logging = config.Logging ?? new LoggingOptions(); config.EventLog = config.EventLog ?? new EventLogOptions(); config.Email = config.Email ?? new EmailOptions(); + config.HipVirtualMachines = config.HipVirtualMachines ?? new HipVirtualMachineOptions(); config.Email.To = SanitizeRecipientList(config.Email.To); config.Email.Cc = SanitizeRecipientList(config.Email.Cc); config.Email.Bcc = SanitizeRecipientList(config.Email.Bcc); @@ -84,7 +86,9 @@ namespace HostAvailabilityMonitor.Configuration public void Validate() { - if (Targets == null || Targets.Count == 0) + Targets = Targets ?? new List(); + + if ((Targets == null || Targets.Count == 0) && (HipVirtualMachines == null || !HipVirtualMachines.Enabled)) { throw new InvalidOperationException("Es wurde kein Ziel in der Konfiguration definiert."); } @@ -112,6 +116,29 @@ namespace HostAvailabilityMonitor.Configuration Runtime.MaxConcurrency = 1; } + if (HipVirtualMachines == null) + { + HipVirtualMachines = new HipVirtualMachineOptions(); + } + + if (HipVirtualMachines.Enabled) + { + if (string.IsNullOrWhiteSpace(HipVirtualMachines.ConfigPath)) + { + throw new InvalidOperationException("HipVirtualMachines.Enabled=true, aber ConfigPath ist leer."); + } + + if (HipVirtualMachines.TimeoutSeconds <= 0) + { + HipVirtualMachines.TimeoutSeconds = 5; + } + + if (HipVirtualMachines.RdpPort <= 0 || HipVirtualMachines.RdpPort > 65535) + { + throw new InvalidOperationException("HipVirtualMachines.RdpPort muss zwischen 1 und 65535 liegen."); + } + } + if (Logging.RetentionDays < 0) { throw new InvalidOperationException("Logging.RetentionDays darf nicht negativ sein."); @@ -227,6 +254,17 @@ namespace HostAvailabilityMonitor.Configuration public int TimeoutSeconds { get; set; } = 30; } + public sealed class HipVirtualMachineOptions + { + public bool Enabled { get; set; } = false; + public string ConfigPath { get; set; } = "hip-vms.json"; + public string ApplicationName { get; set; } = "HIP Virtual Machines"; + public int TimeoutSeconds { get; set; } = 5; + public int RdpPort { get; set; } = 3389; + public bool CheckNetwork { get; set; } = true; + public bool CheckRdp { get; set; } = true; + } + public sealed class TargetOptions { public string Name { get; set; } = string.Empty; @@ -236,6 +274,11 @@ namespace HostAvailabilityMonitor.Configuration public int? Port { get; set; } public bool? ValidateUncPathAccess { get; set; } public string HttpMethod { get; set; } + public string Source { get; set; } + public string Group { get; set; } + public string Description { get; set; } + public string MachineName { get; set; } + public string CheckName { get; set; } public bool Enabled { get; set; } = true; public string DisplayName diff --git a/src/HostAvailabilityMonitor/HostAvailabilityMonitor.csproj b/src/HostAvailabilityMonitor/HostAvailabilityMonitor.csproj index fac127b..1c74ffc 100644 --- a/src/HostAvailabilityMonitor/HostAvailabilityMonitor.csproj +++ b/src/HostAvailabilityMonitor/HostAvailabilityMonitor.csproj @@ -41,6 +41,7 @@ + @@ -56,6 +57,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/HostAvailabilityMonitor/Logging/FileLogger.cs b/src/HostAvailabilityMonitor/Logging/FileLogger.cs index 31123bd..a78df20 100644 --- a/src/HostAvailabilityMonitor/Logging/FileLogger.cs +++ b/src/HostAvailabilityMonitor/Logging/FileLogger.cs @@ -104,6 +104,26 @@ namespace HostAvailabilityMonitor.Logging parts.Add("Port=" + result.Port.Value); } + if (!string.IsNullOrWhiteSpace(result.Source)) + { + parts.Add("Source=" + Escape(result.Source)); + } + + if (!string.IsNullOrWhiteSpace(result.Group)) + { + parts.Add("Group=" + Escape(result.Group)); + } + + if (!string.IsNullOrWhiteSpace(result.MachineName)) + { + parts.Add("Machine=" + Escape(result.MachineName)); + } + + if (!string.IsNullOrWhiteSpace(result.CheckName)) + { + parts.Add("Check=" + Escape(result.CheckName)); + } + if (result.HttpStatusCode.HasValue) { parts.Add("HttpStatus=" + result.HttpStatusCode.Value); diff --git a/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs b/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs index 8c11157..8d2d9b1 100644 --- a/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs +++ b/src/HostAvailabilityMonitor/Monitoring/EndpointProbeService.cs @@ -65,12 +65,14 @@ namespace HostAvailabilityMonitor.Monitoring return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, null), startedAt, TargetKind.Tcp, endpoint, cancellationToken).ConfigureAwait(false); case "icmp": return await CheckIcmpAsync(target, uri.Host, startedAt, endpoint, cancellationToken).ConfigureAwait(false); + case "rdp": + return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 3389), startedAt, TargetKind.Rdp, endpoint, cancellationToken).ConfigureAwait(false); case "ftp": return await CheckTcpAsync(target, uri.Host, target.Port ?? ResolvePort(uri, 21), startedAt, TargetKind.Ftp, endpoint, cancellationToken).ConfigureAwait(false); case "file": return await CheckFileUriAsync(target, uri, startedAt, cancellationToken).ConfigureAwait(false); default: - return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Nicht unterstütztes Schema '" + uri.Scheme + "'. Unterstützt: UNC, file, http, https, sftp, ftp, tcp, icmp.", null, uri.Host, uri.IsDefaultPort ? (int?)null : uri.Port); + return Fail(target, TargetKind.Unknown, endpoint, startedAt, "Nicht unterstütztes Schema '" + uri.Scheme + "'. Unterstützt: UNC, file, http, https, sftp, ftp, tcp, icmp, rdp.", null, uri.Host, uri.IsDefaultPort ? (int?)null : uri.Port); } } @@ -395,6 +397,11 @@ namespace HostAvailabilityMonitor.Monitoring Detail = detail, Host = host, Port = port, + Source = target.Source, + Group = target.Group, + Description = target.Description, + MachineName = target.MachineName, + CheckName = target.CheckName, HttpStatusCode = httpStatusCode, ErrorType = errorType, StartedAtLocal = startedAt, diff --git a/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs b/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs index 48ccc60..c5fc745 100644 --- a/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs +++ b/src/HostAvailabilityMonitor/Monitoring/ProbeResult.cs @@ -13,7 +13,8 @@ namespace HostAvailabilityMonitor.Monitoring Icmp = 6, Host = 7, Ftp = 8, - File = 9 + File = 9, + Rdp = 10 } public sealed class ProbeResult @@ -29,6 +30,11 @@ namespace HostAvailabilityMonitor.Monitoring public string Detail { get; set; } public string Host { get; set; } public int? Port { get; set; } + public string Source { get; set; } + public string Group { get; set; } + public string Description { get; set; } + public string MachineName { get; set; } + public string CheckName { get; set; } public int? HttpStatusCode { get; set; } public string ErrorType { get; set; } public DateTimeOffset StartedAtLocal { get; set; } diff --git a/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs b/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs index f1e7e12..a249601 100644 --- a/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs +++ b/src/HostAvailabilityMonitor/Notifications/EmailNotifier.cs @@ -276,6 +276,8 @@ namespace HostAvailabilityMonitor.Notifications sb.AppendLine("Aktuell sind keine Endpunkte down / There are currently no unavailable endpoints."); } + AppendHipVirtualMachineSnapshot(sb, summary.CurrentResults); + return sb.ToString(); } @@ -296,6 +298,22 @@ namespace HostAvailabilityMonitor.Notifications { sb.AppendLine(" Port: " + result.Port.Value); } + if (!string.IsNullOrWhiteSpace(result.Group)) + { + sb.AppendLine(" Gruppe / Group: " + result.Group); + } + if (!string.IsNullOrWhiteSpace(result.MachineName)) + { + sb.AppendLine(" Maschine / Machine: " + result.MachineName); + } + if (!string.IsNullOrWhiteSpace(result.CheckName)) + { + sb.AppendLine(" Check: " + result.CheckName); + } + if (!string.IsNullOrWhiteSpace(result.Description)) + { + sb.AppendLine(" Beschreibung / Description: " + result.Description); + } if (result.HttpStatusCode.HasValue) { sb.AppendLine(" HTTP Status: " + result.HttpStatusCode.Value); @@ -310,6 +328,84 @@ namespace HostAvailabilityMonitor.Notifications } } + private static void AppendHipVirtualMachineSnapshot(StringBuilder sb, IReadOnlyCollection results) + { + if (results == null) + { + return; + } + + var vmResults = results + .Where(r => r != null && string.Equals(r.Source, "HipVirtualMachines", StringComparison.OrdinalIgnoreCase)) + .ToList(); + if (vmResults.Count == 0) + { + return; + } + + sb.AppendLine(); + sb.AppendLine("HIP-VM Snapshot / HIP VM snapshot:"); + + var networkUp = vmResults.Count(r => string.Equals(r.CheckName, "Network", StringComparison.OrdinalIgnoreCase) && r.IsSuccess); + var networkDown = vmResults.Count(r => string.Equals(r.CheckName, "Network", StringComparison.OrdinalIgnoreCase) && !r.IsSkipped && !r.IsSuccess); + var rdpUp = vmResults.Count(r => string.Equals(r.CheckName, "RDP", StringComparison.OrdinalIgnoreCase) && r.IsSuccess); + var rdpDown = vmResults.Count(r => string.Equals(r.CheckName, "RDP", StringComparison.OrdinalIgnoreCase) && !r.IsSkipped && !r.IsSuccess); + sb.AppendLine("- Netzwerk / Network UP: " + networkUp + ", DOWN: " + networkDown); + sb.AppendLine("- RDP UP: " + rdpUp + ", DOWN: " + rdpDown); + + var groups = vmResults + .GroupBy(r => BuildVmSnapshotKey(r), StringComparer.OrdinalIgnoreCase) + .OrderBy(g => FirstNonEmpty(g.Select(r => r.Group)), StringComparer.OrdinalIgnoreCase) + .ThenBy(g => FirstNonEmpty(g.Select(r => r.MachineName)), StringComparer.OrdinalIgnoreCase); + + foreach (var group in groups) + { + var entries = group.ToList(); + var first = entries[0]; + var network = entries.FirstOrDefault(r => string.Equals(r.CheckName, "Network", StringComparison.OrdinalIgnoreCase)); + var rdp = entries.FirstOrDefault(r => string.Equals(r.CheckName, "RDP", StringComparison.OrdinalIgnoreCase)); + + sb.AppendLine("- " + Safe(first.Group) + " | " + Safe(first.MachineName) + " | " + Safe(first.Description)); + sb.AppendLine(" Netzwerk / Network: " + FormatCompactStatus(network)); + sb.AppendLine(" RDP: " + FormatCompactStatus(rdp)); + } + } + + private static string BuildVmSnapshotKey(ProbeResult result) + { + return string.Join("|", new[] + { + Safe(result.Group), + Safe(result.MachineName), + Safe(result.Description) + }); + } + + private static string FormatCompactStatus(ProbeResult result) + { + if (result == null) + { + return "n/a"; + } + + if (result.IsSkipped) + { + return "SKIPPED - " + Safe(result.Detail); + } + + return (result.IsSuccess ? "UP" : "DOWN") + " - " + Safe(result.Detail); + } + + private static string FirstNonEmpty(IEnumerable values) + { + if (values == null) + { + return string.Empty; + } + + return values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)) ?? string.Empty; + } + private static string Safe(string value) { return string.IsNullOrWhiteSpace(value) ? "n/a" : value; @@ -329,6 +425,7 @@ namespace HostAvailabilityMonitor.Notifications public int CurrentTargetsDown { get; set; } public int CurrentTargetsSkipped { get; set; } public List CurrentFailures { get; set; } = new List(); + public List CurrentResults { get; set; } = new List(); public IDictionary CurrentState { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); } } diff --git a/src/HostAvailabilityMonitor/Program.cs b/src/HostAvailabilityMonitor/Program.cs index 0b35041..e7b70aa 100644 --- a/src/HostAvailabilityMonitor/Program.cs +++ b/src/HostAvailabilityMonitor/Program.cs @@ -31,6 +31,7 @@ namespace HostAvailabilityMonitor var configPath = args != null && args.Length > 0 && !string.IsNullOrWhiteSpace(args[0]) ? args[0] : Path.Combine(baseDirectory, "appsettings.json"); + var fullConfigPath = Path.GetFullPath(configPath); var config = MonitorConfiguration.Load(configPath); config.Validate(); @@ -43,7 +44,7 @@ namespace HostAvailabilityMonitor logger = new FileLogger(logDirectory, config.Logging.FilePrefix, config.Logging.RetentionDays, config.ApplicationName, config.EnvironmentName); logger.CleanupOldFiles(); logger.Info(config.ApplicationName + " gestartet. Umgebung=" + config.EnvironmentName + ", Server=" + Environment.MachineName + "."); - logger.Info("Konfiguration geladen: " + Path.GetFullPath(configPath)); + logger.Info("Konfiguration geladen: " + fullConfigPath); eventLogger = new WindowsEventLogger(config.EventLog, logger); eventLogger.TryInitialize(); @@ -67,7 +68,7 @@ namespace HostAvailabilityMonitor var currentState = new Dictionary(previousState, StringComparer.OrdinalIgnoreCase); var probeService = new EndpointProbeService(config.Runtime); - var enabledTargets = config.Targets.Where(t => t != null && t.Enabled).ToList(); + var enabledTargets = BuildEnabledTargets(config, fullConfigPath, baseDirectory, logger); if (enabledTargets.Count == 0) { @@ -219,6 +220,126 @@ namespace HostAvailabilityMonitor return results; } + private static List BuildEnabledTargets(MonitorConfiguration config, string configPath, string baseDirectory, FileLogger logger) + { + var targets = config.Targets == null + ? new List() + : config.Targets.Where(t => t != null && t.Enabled).ToList(); + + if (config.HipVirtualMachines == null || !config.HipVirtualMachines.Enabled) + { + return targets; + } + + var vmConfigPath = ResolveConfigurationPath(configPath, baseDirectory, config.HipVirtualMachines.ConfigPath); + logger.Info("HIP-VM-Konfiguration wird geladen: " + vmConfigPath); + + var vmConfig = HipVirtualMachineConfiguration.Load(vmConfigPath); + var vmTargets = CreateHipVirtualMachineTargets(vmConfig, config.HipVirtualMachines, logger); + targets.AddRange(vmTargets); + + logger.Info("HIP-VM-Konfiguration geladen. VMs=" + vmConfig.VirtualMachines.Count + ", generierte Targets=" + vmTargets.Count + "."); + return targets; + } + + private static List CreateHipVirtualMachineTargets(HipVirtualMachineConfiguration vmConfig, HipVirtualMachineOptions options, FileLogger logger) + { + var targets = new List(); + var machines = vmConfig == null || vmConfig.VirtualMachines == null + ? new List() + : vmConfig.VirtualMachines; + + foreach (var vm in machines) + { + if (vm == null || !vm.Enabled) + { + continue; + } + + var address = (vm.Address ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(address)) + { + logger.Warn("HIP-VM ohne Adresse wurde uebersprungen. Name=" + (vm.Name ?? string.Empty) + "."); + continue; + } + + var machineName = string.IsNullOrWhiteSpace(vm.Name) ? address : vm.Name.Trim(); + var applicationName = string.IsNullOrWhiteSpace(options.ApplicationName) ? "HIP Virtual Machines" : options.ApplicationName.Trim(); + var timeoutSeconds = Math.Max(1, options.TimeoutSeconds); + + if (options.CheckNetwork) + { + targets.Add(CreateHipVirtualMachineTarget( + vm, + machineName, + applicationName, + "Network", + "Netzwerk", + "icmp://" + address, + null, + timeoutSeconds)); + } + + if (options.CheckRdp) + { + targets.Add(CreateHipVirtualMachineTarget( + vm, + machineName, + applicationName, + "RDP", + "RDP", + "rdp://" + address + ":" + Math.Max(1, options.RdpPort), + Math.Max(1, options.RdpPort), + timeoutSeconds)); + } + } + + return targets; + } + + private static TargetOptions CreateHipVirtualMachineTarget( + HipVirtualMachine vm, + string machineName, + string applicationName, + string checkName, + string displayCheckName, + string endpoint, + int? port, + int timeoutSeconds) + { + return new TargetOptions + { + Name = machineName + " - " + displayCheckName, + ApplicationName = applicationName, + Endpoint = endpoint, + Port = port, + TimeoutSeconds = timeoutSeconds, + Enabled = true, + Source = "HipVirtualMachines", + Group = vm.Group, + Description = vm.Description, + MachineName = machineName, + CheckName = checkName + }; + } + + private static string ResolveConfigurationPath(string configPath, string baseDirectory, string configuredPath) + { + if (Path.IsPathRooted(configuredPath)) + { + return configuredPath; + } + + var configDirectory = Path.GetDirectoryName(Path.GetFullPath(configPath)); + var configRelativePath = Path.GetFullPath(Path.Combine(configDirectory, configuredPath)); + if (File.Exists(configRelativePath)) + { + return configRelativePath; + } + + return Path.GetFullPath(Path.Combine(baseDirectory, configuredPath)); + } + private static async Task TrySendDailySummaryAsync( MonitorConfiguration config, EmailNotifier notifier, @@ -258,6 +379,7 @@ namespace HostAvailabilityMonitor CurrentTargetsDown = latestResults.Count(r => !r.IsSkipped && !r.IsSuccess), CurrentTargetsSkipped = latestResults.Count(r => r.IsSkipped), CurrentFailures = currentFailures, + CurrentResults = latestResults, CurrentState = currentState }; @@ -368,11 +490,16 @@ namespace HostAvailabilityMonitor private static string BuildEventMessage(MonitorConfiguration config, ProbeResult result) { return string.Format( - "Umgebung={0} | Anwendung={1} | Name={2} | Endpoint={3} | Status={4} | Detail={5}", + "Umgebung={0} | Anwendung={1} | Name={2} | Endpoint={3} | Typ={4} | Quelle={5} | Gruppe={6} | Maschine={7} | Check={8} | Status={9} | Detail={10}", config.EnvironmentName, result.ApplicationName, result.Name, result.Endpoint, + result.Kind, + result.Source, + result.Group, + result.MachineName, + result.CheckName, result.StatusText, result.Detail); } diff --git a/src/HostAvailabilityMonitor/appsettings.json b/src/HostAvailabilityMonitor/appsettings.json index 4f70f53..1340d8d 100644 --- a/src/HostAvailabilityMonitor/appsettings.json +++ b/src/HostAvailabilityMonitor/appsettings.json @@ -48,6 +48,15 @@ "CooldownMinutes": 0, "TimeoutSeconds": 30 }, + "HipVirtualMachines": { + "Enabled": true, + "ConfigPath": "hip-vms.json", + "ApplicationName": "HIP Virtual Machines", + "TimeoutSeconds": 5, + "RdpPort": 3389, + "CheckNetwork": true, + "CheckRdp": true + }, "Targets": [ { "Name": "Internes Fileshare 1", diff --git a/src/HostAvailabilityMonitor/hip-vms.json b/src/HostAvailabilityMonitor/hip-vms.json new file mode 100644 index 0000000..ea9df17 --- /dev/null +++ b/src/HostAvailabilityMonitor/hip-vms.json @@ -0,0 +1,179 @@ +{ + "VirtualMachines": [ + { + "Group": "Entwickermaschinen", + "Name": "dv23aasw0001", + "Address": "10.121.157.21", + "Description": "Developer Machine (Steffen R.)", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23aasw0006", + "Address": "10.121.157.27", + "Description": "Developer Machine (Johannes Rest)", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23aasw0007", + "Address": "10.121.157.28", + "Description": "Developer Machine (Reza Takhsha)", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23aasw0002", + "Address": "10.121.157.22", + "Description": "Developer Machine (Chris Schild)", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23agpwbi01", + "Address": "10.121.157.8", + "Description": "Developer Machine (Ronny)", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23agpwbi02", + "Address": "10.121.157.9", + "Description": "Developer Machine (Lars)", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23agpwbi03", + "Address": "10.121.157.10", + "Description": "BizTalk Server", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23agpwbi04", + "Address": "10.121.157.7", + "Description": "Monitoring/Administration Server", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23adbwbi01", + "Address": "10.121.157.6", + "Description": "BizTalk DB Server", + "Enabled": true + }, + { + "Group": "Entwickermaschinen", + "Name": "dv23wgpwbi01", + "Address": "10.123.49.100", + "Description": "IIS Server", + "Enabled": true + }, + { + "Group": "TST", + "Name": "tv23agpwbi01", + "Address": "10.121.157.11", + "Description": "BizTalk Server", + "Enabled": true + }, + { + "Group": "TST", + "Name": "tv23agpwbi02", + "Address": "10.121.157.12", + "Description": "Monitoring/Administration Server", + "Enabled": true + }, + { + "Group": "TST", + "Name": "tv23adbwbi01", + "Address": "10.121.157.13", + "Description": "BizTalk DB Server", + "Enabled": true + }, + { + "Group": "TST", + "Name": "tv23wgpwbi01", + "Address": "10.123.49.228", + "Description": "IIS Server", + "Enabled": true + }, + { + "Group": "ACC", + "Name": "av23agpwbi01", + "Address": "10.121.145.6", + "Description": "BizTalk Server", + "Enabled": true + }, + { + "Group": "ACC", + "Name": "av23agpwbi02", + "Address": "10.121.145.7", + "Description": "Monitoring/Administration Server", + "Enabled": true + }, + { + "Group": "ACC", + "Name": "av23adbwbi01", + "Address": "10.121.145.8", + "Description": "BizTalk DB Server", + "Enabled": true + }, + { + "Group": "ACC", + "Name": "acc-lbi-23-agw-bewintegration0001", + "Address": "10.123.47.198", + "Description": "Load Balancer", + "Enabled": true + }, + { + "Group": "ACC", + "Name": "av23wgpwbi01", + "Address": "10.123.47.196", + "Description": "IIS Server", + "Enabled": true + }, + { + "Group": "ACC", + "Name": "av23wgpwbi02", + "Address": "10.123.47.197", + "Description": "IIS Server", + "Enabled": true + }, + { + "Group": "Produktion", + "Name": "pv23agpwbi01", + "Address": "10.121.145.9", + "Description": "BizTalk Server", + "Enabled": true + }, + { + "Group": "Produktion", + "Name": "pv23agpwbi02", + "Address": "10.121.145.10", + "Description": "Monitoring/Administration Server", + "Enabled": true + }, + { + "Group": "Produktion", + "Name": "pv23adbwbi01", + "Address": "10.121.145.11", + "Description": "BizTalk DB Server", + "Enabled": true + }, + { + "Group": "Produktion", + "Name": "pv23wgpwbi01", + "Address": "10.123.48.196", + "Description": "IIS Server", + "Enabled": true + }, + { + "Group": "Produktion", + "Name": "pv23wgpwbi02", + "Address": "10.123.48.197", + "Description": "IIS Server", + "Enabled": true + } + ] +}