Add recovery preflight and ACC restart runbook

This commit is contained in:
2026-08-19 18:13:03 +02:00
parent 9ce7e8d45a
commit 1228f71765
16 changed files with 296 additions and 24 deletions
+3 -1
View File
@@ -2,7 +2,7 @@
## Projekt und Sicherheitsziel ## Projekt und Sicherheitsziel
Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.0. Das Repository enthält ein .NET-Framework-4.6.1-WinForms-Tool für kontrollierte BizTalk-2020-Wartungsoperationen. Änderungen müssen Dry-run, explizite Freigabe realer Aktionen, sichere Reihenfolgen und wiederherstellbare Installergrenzen erhalten. Die aktuelle Produktversion ist 2.2.1.
## Installerinvarianten ## Installerinvarianten
@@ -28,6 +28,8 @@ Die zentrale Implementierung liegt in `src/BizTalkPlatformManagementTool.Setup/I
- Restore-Kategorien gelten global über alle Anwendungen: Host Instances, Send Ports, Orchestrations, Receive Locations. - Restore-Kategorien gelten global über alle Anwendungen: Host Instances, Send Ports, Orchestrations, Receive Locations.
- Emergency Restore überschreibt niemals die Eingabe-`before.json`, erzeugt eine timestamp-basierte Kopie und stellt `ENTSSO` vor Host Instances sicher. - Emergency Restore überschreibt niemals die Eingabe-`before.json`, erzeugt eine timestamp-basierte Kopie und stellt `ENTSSO` vor Host Instances sicher.
- Emergency Restore muss mit genau einer validen `before.json` funktionieren; Dateien eines vorherigen fehlgeschlagenen Laufs dürfen keine Voraussetzung sein. - Emergency Restore muss mit genau einer validen `before.json` funktionieren; Dateien eines vorherigen fehlgeschlagenen Laufs dürfen keine Voraussetzung sein.
- Ein älterer kompatibler Snapshot, insbesondere aus 2.1.3, darf nicht allein anhand seines `ToolVersion`-Werts abgelehnt werden.
- Der echte Emergency Restore erzeugt nach Möglichkeit automatisch einen timestamp-basierten Soll/Ist-Diff aus Recovery-Quelle und Nachher-Snapshot.
- Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter. - Die zentrale best-effort Orchestrierung liegt in `OperationPlanExecutor`; die WMI-/Service-Zustandsprüfung bleibt im produktiven Runtime-Adapter.
## Versionierung ## Versionierung
+10
View File
@@ -1,6 +1,16 @@
# Changelog # Changelog
## [2.2.1] - 2026-08-19
### Added
- WMI-free **Validate State** preflight with snapshot metadata, server matching and artifact counts.
- Direct **State...** file selection for a preserved recovery snapshot.
- Automatic timestamped JSON/CSV/HTML target/actual diff after a real Emergency Restore.
- Regression coverage proving that a `before.json` created by version 2.1.3 remains a valid Emergency Restore source.
### Changed
- Emergency-recovery documentation now provides a dedicated operator runbook for the ACC restart and explicitly distinguishes dry-run evidence from real execution evidence.
## [2.2.0] - 2026-08-19 ## [2.2.0] - 2026-08-19
### Added ### Added
- State-aware **Emergency Restore** from an existing `before.json` without overwriting the recovery source. - State-aware **Emergency Restore** from an existing `before.json` without overwriting the recovery source.
+2 -1
View File
@@ -56,7 +56,7 @@ Selbsterklärende Zuweisungen und reine UI-Konstruktion werden nicht zeilenweise
9. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen. 9. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen.
10. Mit **Snapshot After** und **Compare** die Umgebung validieren. 10. Mit **Snapshot After** und **Compare** die Umgebung validieren.
Wenn ein echter Shutdown oder Restore nur teilweise ausgeführt wurde und lediglich die ursprüngliche `before.json` verfügbar ist, wird **Emergency Restore** verwendet. Der Modus kopiert die Quelle unter einem timestamp-basierten Namen, überschreibt `before.json` nicht, stellt zuerst `ENTSSO` sicher und gleicht danach den gespeicherten Sollzustand zustandsbewusst ab. Der erste Lauf muss als Dry-run erfolgen. Wenn ein echter Shutdown oder Restore nur teilweise ausgeführt wurde und lediglich die ursprüngliche `before.json` verfügbar ist, wird **Emergency Restore** verwendet. Die Quelle kann über **State...** ausgewählt und vorab über **Validate State** ohne WMI-Zugriff geprüft werden. Der Modus kopiert die Quelle unter einem timestamp-basierten Namen, überschreibt `before.json` nicht, stellt zuerst `ENTSSO` sicher und gleicht danach den gespeicherten Sollzustand zustandsbewusst ab. Der erste Lauf muss als Dry-run erfolgen.
Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. **Close** beendet die Anwendung. Die Statusanzeige rechts im Kopfbereich bewertet die Host-Instance-Zustaende des letzten Snapshots und zeigt `Started`, `Stopped`, `Partial` oder `Unknown`. **Clear** leert die sichtbaren Ergebnis- und Log-Grids, loescht aber keine Dateien. **Close** beendet die Anwendung.
@@ -109,6 +109,7 @@ Der Emergency Restore benötigt nur eine valide `before.json` und denselben Ziel
5. Bereits korrekte Zustände als `AlreadySatisfied` überspringen. 5. Bereits korrekte Zustände als `AlreadySatisfied` überspringen.
6. Isolierte WMI-/Adapterfehler als `Failed` erfassen und mit dem nächsten unabhängigen Schritt fortfahren. 6. Isolierte WMI-/Adapterfehler als `Failed` erfassen und mit dem nächsten unabhängigen Schritt fortfahren.
7. Nachher-Snapshot unabhängig versuchen und vollständigen Ergebnisreport schreiben. 7. Nachher-Snapshot unabhängig versuchen und vollständigen Ergebnisreport schreiben.
8. Bei erfolgreichem Nachher-Snapshot automatisch einen timestamp-basierten Soll/Ist-Diff als JSON, CSV und HTML erzeugen.
Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilfehler erzeugt einen roten/fehlgeschlagenen Abschluss und erfordert die Prüfung des Ergebnisreports. Er verhindert lediglich, dass beispielsweise eine einzelne nicht validierbare Receive Location alle späteren Host-, Port- oder Receive-Location-Schritte blockiert. Der Best-effort-Ansatz bedeutet nicht, dass Fehler ignoriert werden: Jeder Teilfehler erzeugt einen roten/fehlgeschlagenen Abschluss und erfordert die Prüfung des Ergebnisreports. Er verhindert lediglich, dass beispielsweise eine einzelne nicht validierbare Receive Location alle späteren Host-, Port- oder Receive-Location-Schritte blockiert.
+7 -6
View File
@@ -89,18 +89,19 @@ Ein erfolgreicher Installer-Self-Test bestätigt Paket, Programmstart und lokale
## Emergency Restore nach einem Teilabbruch ## Emergency Restore nach einem Teilabbruch
Version 2.2.0 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich. Version 2.2.1 kann einen Wiederanlauf allein aus einer erhaltenen `before.json` vorbereiten und ausführen. Eine mit Version 2.1.3 erzeugte Datei ist kompatibel; zusätzliche Plan- oder Nachher-Dateien des fehlgeschlagenen Laufs sind nicht erforderlich.
1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern. 1. Die erhaltene `before.json` außerhalb des Arbeitsverzeichnisses zusätzlich sichern.
2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist. 2. Anwendung als Administrator starten und denselben Zielserver wählen, der im Snapshot gespeichert ist.
3. Im Feld **State** die erhaltene Datei auswählen oder ihren vollständigen Pfad eintragen. 3. Über **State...** die erhaltene Datei auswählen oder im Feld **State** ihren vollständigen Pfad eintragen.
4. **Dry run** aktiviert lassen und **Emergency Restore** wählen. 4. **Validate State** ausführen. Diese Prüfung öffnet keine WMI-Verbindung und verändert keinen Laufzeitzustand.
5. Den timestamp-basierten `emergency-restore-plan-*` prüfen. Die bestehende `before.json` wird dabei nicht überschrieben. 5. **Dry run** aktiviert lassen und **Emergency Restore** wählen.
6. Dry run deaktivieren, **Emergency Restore** erneut wählen und den expliziten Dialog bestätigen. 6. Den timestamp-basierten `emergency-restore-plan-*` prüfen. Die bestehende `before.json` wird dabei nicht überschrieben.
7. Dry run deaktivieren, **Emergency Restore** erneut wählen und den expliziten Dialog bestätigen.
Der Emergency Restore stellt zuerst sicher, dass der Windows-Dienst `ENTSSO` läuft. Danach folgen Host Instances, Send Ports, Orchestrations und zuletzt Receive Locations. Vor jeder Mutation wird der aktuelle Zustand geprüft; bereits korrekte Zustände werden als `AlreadySatisfied` protokolliert. Ein isolierter Fehler wird als `Failed` festgehalten, während alle späteren unabhängigen Schritte weiter versucht werden. Der Emergency Restore stellt zuerst sicher, dass der Windows-Dienst `ENTSSO` läuft. Danach folgen Host Instances, Send Ports, Orchestrations und zuletzt Receive Locations. Vor jeder Mutation wird der aktuelle Zustand geprüft; bereits korrekte Zustände werden als `AlreadySatisfied` protokolliert. Ein isolierter Fehler wird als `Failed` festgehalten, während alle späteren unabhängigen Schritte weiter versucht werden.
Jeder Lauf schreibt eine unveränderte Snapshot-Kopie sowie timestamp-basierte Plan-, Ergebnis- und Nachher-Dateien. Ein Ergebnis mit mindestens einem fehlgeschlagenen Schritt bleibt im GUI ausdrücklich `Failed` und verlangt Operator-Review, auch wenn alle anderen Schritte erfolgreich waren. Jeder Lauf schreibt eine unveränderte Snapshot-Kopie sowie timestamp-basierte Plan- und Ergebnisdateien. Der echte Lauf versucht zusätzlich Nachher-Snapshot und `emergency-restore-diff-*` als JSON, CSV und HTML. Ein Ergebnis mit mindestens einem fehlgeschlagenen Schritt bleibt im GUI ausdrücklich `Failed` und verlangt Operator-Review, auch wenn alle anderen Schritte erfolgreich waren.
## Deinstallation ## Deinstallation
+3 -2
View File
@@ -18,6 +18,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
- Controlled shutdown from the current runtime state - Controlled shutdown from the current runtime state
- Controlled restore from `before.json` - Controlled restore from `before.json`
- State-aware emergency restore from a preserved `before.json`, including Enterprise SSO startup - State-aware emergency restore from a preserved `before.json`, including Enterprise SSO startup
- WMI-free validation and file selection for preserved recovery snapshots, including 2.1.3 snapshots
- Host instance handling for the selected BizTalk server - Host instance handling for the selected BizTalk server
- Dry-run mode enabled by default - Dry-run mode enabled by default
- WMI access through `root\MicrosoftBizTalkServer` - WMI access through `root\MicrosoftBizTalkServer`
@@ -45,7 +46,7 @@ WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations d
7. After maintenance, click **Restore** using the saved `before.json`. 7. After maintenance, click **Restore** using the saved `before.json`.
8. Click **Snapshot After** and **Compare**. 8. Click **Snapshot After** and **Compare**.
If a shutdown was interrupted and only the original `before.json` remains, select that file, keep **Dry run** enabled and click **Emergency Restore**. The recovery plan never overwrites the source snapshot, ensures the `ENTSSO` service is running first, skips already-correct runtime states and continues after isolated step failures. Disable Dry run only after reviewing the timestamped emergency plan. If a shutdown was interrupted and only the original `before.json` remains, select that file with **State...**, run **Validate State**, keep **Dry run** enabled and click **Emergency Restore**. The recovery plan never overwrites the source snapshot, ensures the `ENTSSO` service is running first, skips already-correct runtime states and continues after isolated step failures. Disable Dry run only after reviewing the timestamped emergency plan. A real run automatically writes a timestamped target/actual diff when the post-operation snapshot succeeds.
The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files. The environment indicator shows `Started`, `Stopped`, `Partial` or `Unknown` from the most recent snapshot. `Clear` removes the visible status and operation log grids; it does not delete files.
@@ -75,7 +76,7 @@ Orchestrations that were `Bound` are deliberately left unchanged during restore
- `shutdown-plan.json`, `restore-plan.json` - `shutdown-plan.json`, `restore-plan.json`
- `shutdown-after.json`, `restore-after.json` - `shutdown-after.json`, `restore-after.json`
- `shutdown-result.json`, `restore-result.json` - `shutdown-result.json`, `restore-result.json`
- Timestamped `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*` and `emergency-restore-after-*` files - Timestamped `emergency-source-before-*`, `emergency-restore-plan-*`, `emergency-restore-result-*`, `emergency-restore-after-*` and `emergency-restore-diff-*` files
- `diff.json`, `diff.csv`, `diff.html` - `diff.json`, `diff.csv`, `diff.html`
- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html` - Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html`
- Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs` - Runtime logs under `%ProgramData%\BizTalkPlatformManagementTool\Logs`
@@ -0,0 +1,119 @@
# ACC Emergency Restore aus `before.json`
Stand: 19.08.2026
Ausführung: Donnerstag, 20.08.2026, oder Montag, 24.08.2026
Zielserver: `AV23AGPWBI01`
Benötigte Toolversion: `2.2.1`
## Kurzantwort
Ja. Die vorhandene, mit Version 2.1.3 erzeugte `before.json` kann von Version 2.2.1 direkt als Recovery-Quelle verwendet werden. Zusätzliche Dateien des abgebrochenen Shutdowns sind nicht erforderlich. Die Kompatibilität wird durch einen Regressionstest abgedeckt; die echte BizTalk-/ACC-Ausführung muss noch vor Ort bestätigt werden.
Für diesen Teilzustand ausschließlich **Emergency Restore** verwenden. Dieser Modus startet zuerst `ENTSSO`, arbeitet danach den gespeicherten Sollzustand idempotent ab, setzt nach Einzelfehlern fort und überschreibt die Quell-`before.json` nicht.
## 1. Dateien vorbereiten
- [ ] Originale `before.json` an einem zweiten Ort unverändert sichern.
- [ ] Optional den Hash der Originaldatei protokollieren:
```bat
certutil -hashfile before.json SHA256
```
- [ ] Die Release-Datei `BizTalkPlatformManagementTool-Setup.zip.b64.txt` und die zugehörige SHA-256-Datei auf ACC übertragen.
- [ ] Die TXT-Datei rekonstruieren und das ZIP prüfen:
```bat
certutil -decode BizTalkPlatformManagementTool-Setup.zip.b64.txt BizTalkPlatformManagementTool-Setup.zip
certutil -hashfile BizTalkPlatformManagementTool-Setup.zip SHA256
type BizTalkPlatformManagementTool-Setup.zip.sha256.txt
```
- [ ] Nur fortfahren, wenn beide ZIP-Hashes exakt gleich sind.
- [ ] ZIP in einen neuen Ordner entpacken, die laufende Toolinstanz schließen und `Setup.exe` als Administrator starten.
- [ ] Installation/Update auf Version `2.2.1` vollständig abschließen.
- [ ] Ein neues, leeres Ausgabeverzeichnis anlegen, zum Beispiel `C:\BizTalk-Recovery\2026-08-20` oder `C:\BizTalk-Recovery\2026-08-24`.
Wichtig: Die Recovery-Quelle außerhalb dieses Ausgabeverzeichnisses lassen. Während der Recovery nicht **Snapshot Before** anklicken, damit kein neuer Teilzustand als vermeintlicher Sollzustand gespeichert wird.
## 2. `before.json` ohne WMI prüfen
- [ ] Anwendung als Administrator starten.
- [ ] Im Feld **Server** `AV23AGPWBI01` eintragen. Kurzname und FQDN desselben Hosts werden akzeptiert.
- [ ] Unter **Output** das neue Recovery-Ausgabeverzeichnis auswählen.
- [ ] Über **State...** die gesicherte `before.json` auswählen.
- [ ] **Dry run** aktiviert lassen.
- [ ] **Validate State** anklicken.
- [ ] Im Erfolgsdialog prüfen:
- Snapshotversion ist erwartungsgemäß `2.1.3-net461` oder `2.1.3`.
- Snapshotserver ist `AV23AGPWBI01` beziehungsweise dessen FQDN.
- Erstellzeit und Anzahl von Applications, Host Instances, Receive Locations, Send Ports und Orchestrations wirken plausibel.
**Validate State** öffnet keine WMI-Verbindung und verändert weder BizTalk noch Windows-Dienste. Bei einer roten Fehlermeldung nicht real ausführen, sondern zuerst Pfad, JSON-Datei und Serverauswahl korrigieren.
## 3. Emergency-Dry-run prüfen
- [ ] **Dry run** bleibt aktiviert.
- [ ] **Emergency Restore** anklicken.
- [ ] Den erzeugten `emergency-restore-plan-<Zeitstempel>.json` öffnen.
- [ ] Prüfen, dass der Plan zu `AV23AGPWBI01` gehört.
- [ ] Reihenfolge prüfen:
1. Windows-Dienst `ENTSSO`,
2. alle Host Instances,
3. alle Send Ports,
4. alle Orchestrations,
5. alle Receive Locations.
- [ ] Prüfen, dass der Dry-run-Report `emergency-restore-result-<Zeitstempel>.json` geschrieben wurde und keine Initialisierungsfehler enthält.
- [ ] Die timestamp-basierte Datei `emergency-source-before-<Zeitstempel>.json` als zusätzliche Recovery-Evidenz sichern.
Der Dry-run führt keine Zustandsänderung aus. Er bestätigt aber, dass genau die erhaltene Datei gelesen, validiert und in einen ausführbaren Emergency-Plan übersetzt werden kann.
## 4. Echten Wiederanlauf ausführen
- [ ] Sicherstellen, dass SQL Server, Netzwerk und die übrigen BizTalk-Abhängigkeiten erreichbar sind.
- [ ] Noch einmal kontrollieren: Server, Output-Pfad und State-Datei sind korrekt.
- [ ] **Dry run** deaktivieren.
- [ ] **Emergency Restore** anklicken.
- [ ] Im Bestätigungsdialog Quellpfad, Zielserver, Plandatei und Schrittzahl lesen.
- [ ] Erst danach mit **Yes** bestätigen.
- [ ] Anwendung bis zum Abschluss geöffnet lassen.
Die reale Reihenfolge ist:
1. `ENTSSO` auf `Running` bringen.
2. Host Instances gemäß `before.json` abgleichen.
3. Send Ports abgleichen.
4. Orchestrations abgleichen; im Snapshot nur `Bound` gebliebene Orchestrations werden bewusst zur manuellen Kontrolle markiert.
5. Receive Locations zuletzt abgleichen.
Vor jeder Mutation liest Version 2.2.1 den Istzustand. Ein bereits korrekter Zustand wird als `AlreadySatisfied` protokolliert und nicht erneut per WMI geändert. Ein einzelner Fehler wird als `Failed` gespeichert; alle späteren unabhängigen Schritte werden trotzdem versucht.
## 5. Ergebnis abnehmen
- [ ] `emergency-restore-result-<Zeitstempel>.json` prüfen.
- [ ] Idealfall: `FailedCount` ist `0`, `InitializationError` und `PostSnapshotError` sind leer.
- [ ] `AlreadySatisfied` ist normal und bedeutet, dass keine unnötige Mutation ausgeführt wurde.
- [ ] `emergency-restore-after-<Zeitstempel>.json` muss nach einem echten Lauf vorhanden sein, sofern der Nachher-Snapshot gelang.
- [ ] `emergency-restore-diff-<Zeitstempel>.json`, `.csv` und `.html` prüfen. Im vollständigen Sollzustand sind keine fachlichen Unterschiede enthalten.
- [ ] Danach **Diagnose** ausführen und den BizTalk-Zustand zusätzlich in der BizTalk Administration Console kontrollieren.
- [ ] Empfang, Verarbeitung und Versand mit dem für ACC vorgesehenen fachlichen Smoke-Test prüfen; Suspended Instances kontrollieren.
- [ ] Recovery-Quelle, Plan, Ergebnis, Nachher-Snapshot, Diff und Laufzeitlog gemeinsam sichern.
## Sonderfall `RV_PMP_Trigger_Schedule`
Wenn die Receive Location noch aktiviert ist und `before.json` ebenfalls `Enabled=true` vorgibt, wird sie als `AlreadySatisfied` übersprungen. Damit wird der fehlerhafte Scheduler-WMI-Aufruf nicht erneut ausgelöst.
Wenn sie deaktiviert ist, obwohl `before.json` sie aktiviert erwartet, versucht das Tool die Aktivierung. Tritt der bekannte Fehler zu `Microsoft.BizTalk.Scheduler, Version=3.13.0.0` erneut auf, wird genau dieser Schritt als `Failed` protokolliert; die übrigen Receive Locations werden weiterbearbeitet. Dann:
- [ ] Ergebnisreport und vollständige Exception sichern.
- [ ] Aktuellen Zustand der Receive Location in der BizTalk Administration Console prüfen.
- [ ] Scheduler-Assembly/Abhängigkeiten auf dem Administrations-/BizTalk-Host reparieren oder den Schritt nach fachlicher Freigabe manuell ausführen.
- [ ] PROD erst freigeben, wenn der verbleibende Einzelfehler behoben oder ausdrücklich akzeptiert und getestet ist.
## Wenn der Lauf erneut unterbrochen wird
Die ursprüngliche `before.json` bleibt unverändert. Version 2.2.1 kann mit derselben Recovery-Quelle erneut gestartet werden, weil bereits erreichte Zustände als `AlreadySatisfied` übersprungen werden. Vor dem Wiederholen immer den jüngsten Ergebnisreport sichern und prüfen, welcher Schritt tatsächlich fehlgeschlagen ist.
## Go/No-Go für PROD
ACC ist erst ein belastbarer PROD-Nachweis, wenn der echte Emergency Restore abgeschlossen wurde, der Ergebnisreport vollständig ist, der automatische Diff geprüft wurde und jeder verbliebene `Failed`-Schritt fachlich behandelt wurde. Die lokalen 24 Regressionstests und der Self-Test ersetzen diesen realen BizTalk-/Windows-Nachweis nicht.
@@ -48,6 +48,13 @@ Der neue Button **Emergency Restore** benötigt außer der erhaltenen `before.js
6. arbeitet nach isolierten Fehlern weiter, 6. arbeitet nach isolierten Fehlern weiter,
7. versucht einen Nachher-Snapshot und schreibt immer den vollständigen Ergebnisreport, soweit das Ausgabeverzeichnis verfügbar ist. 7. versucht einen Nachher-Snapshot und schreibt immer den vollständigen Ergebnisreport, soweit das Ausgabeverzeichnis verfügbar ist.
## Recovery-Ergänzungen in Version 2.2.1
- **State...** wählt die erhaltene `before.json` direkt aus.
- **Validate State** prüft JSON-Struktur und Zielserver ohne WMI-Verbindung oder Laufzeitänderung und zeigt Snapshotversion, Erstellzeit sowie Artefaktzahlen an.
- Ein 2.1.3-Snapshot wird ausdrücklich als kompatible Recovery-Quelle regressionsgetestet; `ToolVersion` ist keine Versionssperre.
- Nach einem echten Emergency Restore wird bei erfolgreichem Nachher-Snapshot automatisch `emergency-restore-diff-*` als JSON, CSV und HTML geschrieben.
### Globale Reihenfolge ### Globale Reihenfolge
Shutdown: Shutdown:
@@ -79,11 +86,11 @@ Die portable Regressionstestsuite enthält ab Version 2.2.0 unter anderem folgen
- globale Shutdown-/Restore-Reihenfolge mit mehreren Anwendungen, - globale Shutdown-/Restore-Reihenfolge mit mehreren Anwendungen,
- Kurzname/FQDN-Gleichheit für Host-Instance-Schritte. - Kurzname/FQDN-Gleichheit für Host-Instance-Schritte.
Die lokale Suite umfasst 23 bestandene Tests. Die abschließende Freigabe für PROD benötigt weiterhin einen Windows-/BizTalk-Test auf ACC: echter Scheduler-Fehlerpfad, vollständige Fortsetzung, Ergebnisreport, Emergency-Dry-run und realer Wiederanlauf aus einer Kopie der vorhandenen `before.json`. Die lokale Suite umfasst ab Version 2.2.1 insgesamt 24 Tests einschließlich der expliziten 2.1.3-Snapshot-Kompatibilität. Die abschließende Freigabe für PROD benötigt weiterhin einen Windows-/BizTalk-Test auf ACC: echter Scheduler-Fehlerpfad, vollständige Fortsetzung, Ergebnisreport, Emergency-Dry-run und realer Wiederanlauf aus einer Kopie der vorhandenen `before.json`.
## PROD-Freigabekriterien ## PROD-Freigabekriterien
- Version 2.2.0 installieren und Installer-Self-Test bestätigen. - Version 2.2.1 installieren und Installer-Self-Test bestätigen.
- `Diagnose` und einen vollständigen Shutdown-Dry-run ausführen. - `Diagnose` und einen vollständigen Shutdown-Dry-run ausführen.
- Kontrolliert nachweisen, dass eine isolierte Scheduler-Exception spätere Schritte nicht blockiert. - Kontrolliert nachweisen, dass eine isolierte Scheduler-Exception spätere Schritte nicht blockiert.
- `shutdown-result.json` und Nachher-Snapshot prüfen. - `shutdown-result.json` und Nachher-Snapshot prüfen.
@@ -23,7 +23,7 @@ namespace BizTalkPlatformManagementTool.Setup
private const string ProductName = "BizTalk Platform Management Tool"; private const string ProductName = "BizTalk Platform Management Tool";
/// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary> /// <summary>Aktuelle Produktversion des Installers und Uninstall-Eintrags.</summary>
private const string ProductVersion = "2.2.0"; private const string ProductVersion = "2.2.1";
/// <summary> /// <summary>
/// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen. /// Wartezeiten zwischen Wiederholungen atomarer Verzeichnisverschiebungen.
@@ -65,7 +65,7 @@ namespace BizTalkPlatformManagementTool.Setup
{ {
AutoSize = true, AutoSize = true,
Font = new Font(Font.FontFamily, 14, FontStyle.Bold), Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
Text = "BizTalk Platform Management Tool 2.2.0" Text = "BizTalk Platform Management Tool 2.2.1"
}); });
root.Controls.Add(new Label root.Controls.Add(new Label
{ {
@@ -8,6 +8,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyProduct("BizTalk Platform Management Tool")] [assembly: AssemblyProduct("BizTalk Platform Management Tool")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")] [assembly: Guid("675b68a9-bd80-46a5-b8c5-3b11b0b374e2")]
[assembly: AssemblyVersion("2.2.0.0")] [assembly: AssemblyVersion("2.2.1.0")]
[assembly: AssemblyFileVersion("2.2.0.0")] [assembly: AssemblyFileVersion("2.2.1.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")] [assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.2.0.0" name="BizTalkPlatformManagementTool.Setup" /> <assemblyIdentity version="2.2.1.0" name="BizTalkPlatformManagementTool.Setup" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security> <security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false" /></requestedPrivileges></security>
</trustInfo> </trustInfo>
@@ -9,6 +9,6 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")] [assembly: Guid("2c5b2c0a-f407-46c2-9e3b-1fa09fa8445a")]
[assembly: AssemblyVersion("2.2.0.0")] [assembly: AssemblyVersion("2.2.1.0")]
[assembly: AssemblyFileVersion("2.2.0.0")] [assembly: AssemblyFileVersion("2.2.1.0")]
[assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")] [assembly: InternalsVisibleTo("BizTalkPlatformManagementTool.Tests")]
@@ -16,7 +16,7 @@ namespace BizTalkPlatformManagementTool.Services
/// <summary> /// <summary>
/// Current tool version written into generated snapshots. /// Current tool version written into generated snapshots.
/// </summary> /// </summary>
public const string Version = "2.2.0-net461"; public const string Version = "2.2.1-net461";
/// <summary> /// <summary>
/// Fallback application name used when WMI does not expose an application property. /// Fallback application name used when WMI does not expose an application property.
@@ -110,6 +110,11 @@ namespace BizTalkPlatformManagementTool.Ui
/// </summary> /// </summary>
private Button _restoreButton; private Button _restoreButton;
/// <summary>
/// Button that validates the selected recovery snapshot without contacting WMI.
/// </summary>
private Button _validateStateButton;
/// <summary> /// <summary>
/// Button that performs a state-aware best-effort recovery from an existing snapshot. /// Button that performs a state-aware best-effort recovery from an existing snapshot.
/// </summary> /// </summary>
@@ -220,6 +225,8 @@ namespace BizTalkPlatformManagementTool.Ui
var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) }; var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) };
browseButton.Click += BrowseButtonClick; browseButton.Click += BrowseButtonClick;
var stateBrowseButton = new Button { Text = "State...", Dock = DockStyle.Fill, Margin = new Padding(8, 5, 8, 5) };
stateBrowseButton.Click += StateBrowseButtonClick;
_environmentStatusLabel.Dock = DockStyle.Fill; _environmentStatusLabel.Dock = DockStyle.Fill;
_environmentStatusLabel.Margin = new Padding(12, 5, 0, 5); _environmentStatusLabel.Margin = new Padding(12, 5, 0, 5);
@@ -239,6 +246,7 @@ namespace BizTalkPlatformManagementTool.Ui
panel.Controls.Add(_stateFileTextBox, 1, 1); panel.Controls.Add(_stateFileTextBox, 1, 1);
panel.SetColumnSpan(_stateFileTextBox, 2); panel.SetColumnSpan(_stateFileTextBox, 2);
panel.Controls.Add(_dryRunCheckBox, 3, 1); panel.Controls.Add(_dryRunCheckBox, 3, 1);
panel.Controls.Add(stateBrowseButton, 4, 1);
panel.Controls.Add(Label("Poll sec."), 7, 1); panel.Controls.Add(Label("Poll sec."), 7, 1);
panel.Controls.Add(_pollInput, 8, 1); panel.Controls.Add(_pollInput, 8, 1);
panel.Controls.Add(_environmentStatusLabel, 9, 0); panel.Controls.Add(_environmentStatusLabel, 9, 0);
@@ -260,6 +268,7 @@ namespace BizTalkPlatformManagementTool.Ui
_compareButton = ActionButton("Compare", CompareClick); _compareButton = ActionButton("Compare", CompareClick);
_shutdownButton = ActionButton("Shutdown", ShutdownClick); _shutdownButton = ActionButton("Shutdown", ShutdownClick);
_restoreButton = ActionButton("Restore", RestoreClick); _restoreButton = ActionButton("Restore", RestoreClick);
_validateStateButton = ActionButton("Validate State", ValidateStateClick);
_emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick); _emergencyRestoreButton = ActionButton("Emergency Restore", EmergencyRestoreClick);
_emergencyRestoreButton.Width = 142; _emergencyRestoreButton.Width = 142;
_clearButton = ActionButton("Clear", ClearClick); _clearButton = ActionButton("Clear", ClearClick);
@@ -271,6 +280,7 @@ namespace BizTalkPlatformManagementTool.Ui
panel.Controls.Add(_compareButton); panel.Controls.Add(_compareButton);
panel.Controls.Add(_shutdownButton); panel.Controls.Add(_shutdownButton);
panel.Controls.Add(_restoreButton); panel.Controls.Add(_restoreButton);
panel.Controls.Add(_validateStateButton);
panel.Controls.Add(_emergencyRestoreButton); panel.Controls.Add(_emergencyRestoreButton);
panel.Controls.Add(_clearButton); panel.Controls.Add(_clearButton);
panel.Controls.Add(_closeButton); panel.Controls.Add(_closeButton);
@@ -425,6 +435,39 @@ namespace BizTalkPlatformManagementTool.Ui
}); });
} }
/// <summary>
/// Validates the selected state file and target server without opening a WMI connection
/// or changing any runtime state.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">The event arguments.</param>
private void ValidateStateClick(object sender, EventArgs e)
{
var options = GetOptions();
RunAsync("Validating recovery state file...", () =>
{
var sourcePath = ResolveStateFile(options);
var snapshot = JsonFileStore.Load<BizTalkSnapshot>(sourcePath);
SnapshotValidator.EnsureServerMatches(snapshot, options.Server);
ShowSnapshot(snapshot);
var receiveLocationCount = snapshot.Applications.Sum(x => x.ReceiveLocations.Count);
var sendPortCount = snapshot.Applications.Sum(x => x.SendPorts.Count);
var orchestrationCount = snapshot.Applications.Sum(x => x.Orchestrations.Count);
var summary = "Recovery state is valid. Source=" + sourcePath
+ "; SnapshotVersion=" + (snapshot.ToolVersion ?? "(not recorded)")
+ "; CreatedAt=" + (snapshot.CreatedAt ?? "(not recorded)")
+ "; Server=" + snapshot.Server
+ "; Applications=" + snapshot.Applications.Count
+ "; HostInstances=" + snapshot.HostInstances.Count
+ "; ReceiveLocations=" + receiveLocationCount
+ "; SendPorts=" + sendPortCount
+ "; Orchestrations=" + orchestrationCount + ".";
_logger.Success(summary);
ShowStateValidationSummary(summary);
});
}
/// <summary> /// <summary>
/// Handles emergency recovery from the preserved state file without creating /// Handles emergency recovery from the preserved state file without creating
/// or overwriting <c>before.json</c>. /// or overwriting <c>before.json</c>.
@@ -454,7 +497,24 @@ namespace BizTalkPlatformManagementTool.Ui
} }
var report = _service.ExecutePlan(plan, options); var report = _service.ExecutePlan(plan, options);
CapturePostOperationSnapshot(options, report, "emergency-restore-after-" + stamp + ".json"); var after = CapturePostOperationSnapshot(options, report, "emergency-restore-after-" + stamp + ".json");
if (after != null)
{
try
{
var diff = SnapshotComparer.Compare(snapshot, after);
var diffPath = _service.SaveDiff(options.OutputDirectory, "emergency-restore-diff-" + stamp + ".json", diff);
_logger.Success("Saved emergency recovery target/actual comparison: " + diffPath);
}
catch (Exception ex)
{
var error = "Emergency recovery comparison failed: " + FormatException(ex);
report.PostSnapshotError = string.IsNullOrWhiteSpace(report.PostSnapshotError)
? error
: report.PostSnapshotError + " | " + error;
_logger.Error(error + " The execution report will still be saved.");
}
}
var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "emergency-restore-result-" + stamp + ".json", report); var reportPath = _service.SaveExecutionReport(options.OutputDirectory, "emergency-restore-result-" + stamp + ".json", report);
ShowExecutionReport(report); ShowExecutionReport(report);
ThrowWhenOperatorReviewIsRequired(report, reportPath); ThrowWhenOperatorReviewIsRequired(report, reportPath);
@@ -467,22 +527,24 @@ namespace BizTalkPlatformManagementTool.Ui
/// <param name="options">The operation options.</param> /// <param name="options">The operation options.</param>
/// <param name="report">The report that receives a snapshot error.</param> /// <param name="report">The report that receives a snapshot error.</param>
/// <param name="fileName">The snapshot file name.</param> /// <param name="fileName">The snapshot file name.</param>
private void CapturePostOperationSnapshot(OperationOptions options, OperationExecutionReport report, string fileName) private BizTalkSnapshot CapturePostOperationSnapshot(OperationOptions options, OperationExecutionReport report, string fileName)
{ {
if (options.DryRun) if (options.DryRun)
{ {
return; return null;
} }
try try
{ {
var after = _service.CreateSnapshot(options.Server); var after = _service.CreateSnapshot(options.Server);
_service.SaveSnapshot(options.OutputDirectory, fileName, after); _service.SaveSnapshot(options.OutputDirectory, fileName, after);
ShowSnapshot(after); ShowSnapshot(after);
return after;
} }
catch (Exception ex) catch (Exception ex)
{ {
report.PostSnapshotError = FormatException(ex); report.PostSnapshotError = FormatException(ex);
_logger.Error("Post-operation snapshot failed, but the execution report will still be saved. Error: " + report.PostSnapshotError); _logger.Error("Post-operation snapshot failed, but the execution report will still be saved. Error: " + report.PostSnapshotError);
return null;
} }
} }
@@ -788,6 +850,7 @@ namespace BizTalkPlatformManagementTool.Ui
_compareButton.Enabled = !busy; _compareButton.Enabled = !busy;
_shutdownButton.Enabled = !busy; _shutdownButton.Enabled = !busy;
_restoreButton.Enabled = !busy; _restoreButton.Enabled = !busy;
_validateStateButton.Enabled = !busy;
_emergencyRestoreButton.Enabled = !busy; _emergencyRestoreButton.Enabled = !busy;
_clearButton.Enabled = !busy; _clearButton.Enabled = !busy;
_closeButton.Enabled = !busy; _closeButton.Enabled = !busy;
@@ -812,6 +875,54 @@ namespace BizTalkPlatformManagementTool.Ui
} }
} }
/// <summary>
/// Handles selection of an existing JSON recovery snapshot.
/// </summary>
/// <param name="sender">The control that raised the event.</param>
/// <param name="e">The event arguments.</param>
private void StateBrowseButtonClick(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog())
{
dialog.Title = "Select preserved BizTalk state snapshot";
dialog.Filter = "BizTalk snapshot (*.json)|*.json|All files (*.*)|*.*";
dialog.CheckFileExists = true;
dialog.Multiselect = false;
var currentPath = ResolveStateFile(GetOptions());
if (File.Exists(currentPath))
{
dialog.InitialDirectory = Path.GetDirectoryName(currentPath);
dialog.FileName = Path.GetFileName(currentPath);
}
if (dialog.ShowDialog(this) == DialogResult.OK)
{
_stateFileTextBox.Text = dialog.FileName;
}
}
}
/// <summary>
/// Shows the successful WMI-free state-file validation result to the operator.
/// </summary>
/// <param name="summary">The complete validation summary.</param>
private void ShowStateValidationSummary(string summary)
{
Action show = () => MessageBox.Show(
this,
summary + "\n\nNo runtime state was changed and no WMI connection was opened.",
"Recovery State Valid",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
if (InvokeRequired)
{
Invoke(show);
}
else
{
show();
}
}
/// <summary> /// <summary>
/// Executes a UI update on the UI thread when required. /// Executes a UI update on the UI thread when required.
/// </summary> /// </summary>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="2.2.0.0" name="BizTalkPlatformManagementTool" /> <assemblyIdentity version="2.2.1.0" name="BizTalkPlatformManagementTool" />
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security> <security>
<requestedPrivileges> <requestedPrivileges>
@@ -24,6 +24,7 @@ namespace BizTalkPlatformManagementTool.Tests
Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic); Run("JsonRoundTripIsBomTolerantAndAtomic", JsonRoundTripIsBomTolerantAndAtomic);
Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity); Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity);
Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer); Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer);
Run("Legacy213SnapshotIsAcceptedForRecovery", Legacy213SnapshotIsAcceptedForRecovery);
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder); Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder); Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst); Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
@@ -98,6 +99,25 @@ namespace BizTalkPlatformManagementTool.Tests
Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B")); Expect<InvalidOperationException>(() => SnapshotValidator.EnsureServerMatches(snapshot, "BIZTALK-B"));
} }
/// <summary>Prüft, dass ein erhaltener 2.1.3-Snapshot ohne Versionssperre als Recovery-Quelle dient.</summary>
private static void Legacy213SnapshotIsAcceptedForRecovery()
{
InTemp(directory =>
{
var original = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
original.ToolVersion = "2.1.3-net461";
original.Server = "BIZTALK-A.example.local";
var path = Path.Combine(directory, "before.json");
JsonFileStore.Save(path, original);
var loaded = JsonFileStore.Load<BizTalkSnapshot>(path);
SnapshotValidator.EnsureServerMatches(loaded, "BIZTALK-A");
var plan = new BizTalkOperationService(null).CreateEmergencyRestorePlan(loaded, "BIZTALK-A");
Assert(plan.Steps.First().Kind == "WindowsService", "legacy snapshot did not create an emergency plan");
Assert(loaded.ToolVersion == "2.1.3-net461", "legacy tool version was not preserved");
});
}
/// <summary>Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen.</summary> /// <summary>Prüft die sichere Restore-Reihenfolge und den Schutz gebundener Orchestrierungen.</summary>
private static void RestorePlanUsesSafeOrder() private static void RestorePlanUsesSafeOrder()
{ {