Add resilient emergency restore for partial BizTalk operations
This commit is contained in:
@@ -25,6 +25,12 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
Run("DiffUsesApplicationAndNameIdentity", DiffUsesApplicationAndNameIdentity);
|
||||
Run("RestoreRejectsDifferentServer", RestoreRejectsDifferentServer);
|
||||
Run("RestorePlanUsesSafeOrder", RestorePlanUsesSafeOrder);
|
||||
Run("ShutdownPlanUsesGlobalSafeOrder", ShutdownPlanUsesGlobalSafeOrder);
|
||||
Run("EmergencyRestorePlanStartsSsoFirst", EmergencyRestorePlanStartsSsoFirst);
|
||||
Run("HostInstancePlanAcceptsShortAndFqdnServer", HostInstancePlanAcceptsShortAndFqdnServer);
|
||||
Run("PlanExecutionContinuesAfterSchedulerFailure", PlanExecutionContinuesAfterSchedulerFailure);
|
||||
Run("PlanExecutionSkipsAlreadySatisfiedState", PlanExecutionSkipsAlreadySatisfiedState);
|
||||
Run("ExecutionReportRoundTripPreservesFailure", ExecutionReportRoundTripPreservesFailure);
|
||||
Run("CsvNeutralizesFormulaValues", CsvNeutralizesFormulaValues);
|
||||
Run("PackageManifestRejectsTampering", PackageManifestRejectsTampering);
|
||||
Run("PackageManifestRejectsUndeclaredAndTraversalFiles", PackageManifestRejectsUndeclaredAndTraversalFiles);
|
||||
@@ -99,13 +105,108 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted });
|
||||
snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP", Name = "ORCH", OrchestrationStatus = ArtifactStates.OrchestrationBound });
|
||||
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP", Name = "RL", Enabled = true });
|
||||
var second = Snapshot("APP-B", "PORT-B", ArtifactStates.SendPortStarted).Applications[0];
|
||||
second.Orchestrations.Add(new OrchestrationState { Application = "APP-B", Name = "ORCH-B", OrchestrationStatus = ArtifactStates.OrchestrationStarted });
|
||||
second.ReceiveLocations.Add(new ReceiveLocationState { Application = "APP-B", Name = "RL-B", Enabled = true });
|
||||
snapshot.Applications.Add(second);
|
||||
var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, snapshot.Server);
|
||||
Assert(plan.Steps.First().Kind == "HostInstance", "host instance must start first");
|
||||
Assert(plan.Steps.Last().Kind == "ReceiveLocation", "receive location must be restored last");
|
||||
var firstReceiveLocation = plan.Steps.FindIndex(x => x.Kind == "ReceiveLocation");
|
||||
var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort");
|
||||
var lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration" || x.Kind == "Note");
|
||||
Assert(firstReceiveLocation > lastSendPort && firstReceiveLocation > lastOrchestration, "all receive locations must be restored after all send ports and orchestrations");
|
||||
var bound = plan.Steps.Single(x => x.Name == "ORCH");
|
||||
Assert(!bound.Execute && bound.Kind == "Note", "bound orchestration must remain unchanged");
|
||||
}
|
||||
|
||||
/// <summary>Prüft die globale Shutdown-Reihenfolge auch über mehrere Anwendungen hinweg.</summary>
|
||||
private static void ShutdownPlanUsesGlobalSafeOrder()
|
||||
{
|
||||
var snapshot = Snapshot("APP-A", "SP-A", ArtifactStates.SendPortStarted);
|
||||
snapshot.Applications[0].ReceiveLocations.Add(new ReceiveLocationState { Application = "APP-A", Name = "RL-A", Enabled = true });
|
||||
snapshot.Applications[0].Orchestrations.Add(new OrchestrationState { Application = "APP-A", Name = "ORCH-A", OrchestrationStatus = ArtifactStates.OrchestrationStarted });
|
||||
var appB = Snapshot("APP-B", "SP-B", ArtifactStates.SendPortStarted).Applications[0];
|
||||
appB.ReceiveLocations.Add(new ReceiveLocationState { Application = "APP-B", Name = "RL-B", Enabled = true });
|
||||
appB.Orchestrations.Add(new OrchestrationState { Application = "APP-B", Name = "ORCH-B", OrchestrationStatus = ArtifactStates.OrchestrationStarted });
|
||||
snapshot.Applications.Add(appB);
|
||||
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted });
|
||||
|
||||
var plan = new BizTalkOperationService(null).CreateShutdownPlan(snapshot, snapshot.Server);
|
||||
var lastReceiveLocation = plan.Steps.FindLastIndex(x => x.Kind == "ReceiveLocation");
|
||||
var firstOrchestration = plan.Steps.FindIndex(x => x.Kind == "Orchestration");
|
||||
var lastOrchestration = plan.Steps.FindLastIndex(x => x.Kind == "Orchestration");
|
||||
var firstSendPort = plan.Steps.FindIndex(x => x.Kind == "SendPort");
|
||||
var lastSendPort = plan.Steps.FindLastIndex(x => x.Kind == "SendPort");
|
||||
var firstHost = plan.Steps.FindIndex(x => x.Kind == "HostInstance");
|
||||
Assert(lastReceiveLocation < firstOrchestration, "receive locations were not globally first");
|
||||
Assert(lastOrchestration < firstSendPort, "orchestrations were not globally before send ports");
|
||||
Assert(lastSendPort < firstHost, "host instances were not globally last");
|
||||
}
|
||||
|
||||
/// <summary>Prüft ENTSSO als erste Voraussetzung des Emergency Restore.</summary>
|
||||
private static void EmergencyRestorePlanStartsSsoFirst()
|
||||
{
|
||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
||||
snapshot.HostInstances.Add(new HostInstanceState { InstanceName = "HOST:SERVER", HostName = "HOST", Server = snapshot.Server, RawState = ArtifactStates.HostStarted });
|
||||
var plan = new BizTalkOperationService(null).CreateEmergencyRestorePlan(snapshot, snapshot.Server);
|
||||
Assert(plan.Mode == OperationMode.EmergencyRestore.ToString(), "emergency restore mode missing");
|
||||
Assert(plan.Steps.First().Kind == "WindowsService" && plan.Steps.First().Name == "ENTSSO", "ENTSSO must be the first emergency step");
|
||||
Assert(plan.Steps[1].Kind == "HostInstance", "host instance must follow ENTSSO");
|
||||
}
|
||||
|
||||
/// <summary>Prüft, dass Kurzname und FQDN dieselbe Host Instance nicht fälschlich überspringen.</summary>
|
||||
private static void HostInstancePlanAcceptsShortAndFqdnServer()
|
||||
{
|
||||
var snapshot = Snapshot("APP", "PORT", ArtifactStates.SendPortStarted);
|
||||
snapshot.Server = "BIZTALK-A.example.local";
|
||||
snapshot.HostInstances.Add(new HostInstanceState
|
||||
{
|
||||
InstanceName = "HOST:BIZTALK-A",
|
||||
HostName = "HOST",
|
||||
Server = "BIZTALK-A.example.local",
|
||||
RawState = ArtifactStates.HostStarted
|
||||
});
|
||||
var plan = new BizTalkOperationService(null).CreateRestorePlan(snapshot, "BIZTALK-A");
|
||||
Assert(plan.Steps.First(x => x.Kind == "HostInstance").Execute, "short name/FQDN match incorrectly skipped host instance");
|
||||
}
|
||||
|
||||
/// <summary>Prüft, dass eine Scheduler-artige Exception spätere Schritte nicht mehr verhindert.</summary>
|
||||
private static void PlanExecutionContinuesAfterSchedulerFailure()
|
||||
{
|
||||
var plan = TestPlan("RL-A", "RV_PMP_Trigger_Schedule", "RL-C");
|
||||
var runtime = new FakeOperationStepRuntime { FailingName = "RV_PMP_Trigger_Schedule" };
|
||||
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime);
|
||||
Assert(runtime.Calls.SequenceEqual(new[] { "RL-A", "RV_PMP_Trigger_Schedule", "RL-C" }), "executor stopped after isolated failure");
|
||||
Assert(report.SucceededCount == 2 && report.FailedCount == 1, "unexpected continuation summary");
|
||||
Assert(report.Steps[1].Outcome == OperationStepOutcomes.Failed, "failed step outcome missing");
|
||||
Assert(report.Steps[1].Error.Contains("Microsoft.BizTalk.Scheduler"), "nested Scheduler exception missing from report");
|
||||
}
|
||||
|
||||
/// <summary>Prüft idempotentes Überspringen eines bereits erreichten Sollzustands.</summary>
|
||||
private static void PlanExecutionSkipsAlreadySatisfiedState()
|
||||
{
|
||||
var plan = TestPlan("ALREADY", "CHANGE");
|
||||
var runtime = new FakeOperationStepRuntime();
|
||||
runtime.AlreadySatisfiedNames.Add("ALREADY");
|
||||
var report = new OperationPlanExecutor(null).Execute(plan, TestOptions(false), runtime);
|
||||
Assert(report.AlreadySatisfiedCount == 1 && report.SucceededCount == 1 && report.FailedCount == 0, "idempotent outcome counts are wrong");
|
||||
Assert(runtime.MutatedNames.SequenceEqual(new[] { "CHANGE" }), "already-satisfied step was mutated");
|
||||
}
|
||||
|
||||
/// <summary>Prüft die dauerhafte Serialisierung eines Teilfehler-Reports.</summary>
|
||||
private static void ExecutionReportRoundTripPreservesFailure()
|
||||
{
|
||||
InTemp(directory =>
|
||||
{
|
||||
var report = new OperationPlanExecutor(null).Execute(TestPlan("FAIL", "CONTINUE"), TestOptions(false), new FakeOperationStepRuntime { FailingName = "FAIL" });
|
||||
var path = Path.Combine(directory, "restore-result.json");
|
||||
JsonFileStore.Save(path, report);
|
||||
var loaded = JsonFileStore.Load<OperationExecutionReport>(path);
|
||||
Assert(loaded.HasFailures && loaded.FailedCount == 1, "serialized report lost failure summary");
|
||||
Assert(loaded.Steps.Count == 2 && loaded.Steps[1].Outcome == OperationStepOutcomes.Succeeded, "serialized report lost continued step");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Prüft die Neutralisierung formelartiger CSV-Feldwerte.</summary>
|
||||
private static void CsvNeutralizesFormulaValues()
|
||||
{
|
||||
@@ -399,6 +500,96 @@ namespace BizTalkPlatformManagementTool.Tests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Erstellt einen minimalen ausführbaren Testplan.</summary>
|
||||
/// <param name="names">Die Namen der Testschritte in Ausführungsreihenfolge.</param>
|
||||
/// <returns>Ein Restore-Testplan.</returns>
|
||||
private static OperationPlan TestPlan(params string[] names)
|
||||
{
|
||||
var plan = new OperationPlan
|
||||
{
|
||||
Mode = OperationMode.Restore.ToString(),
|
||||
CreatedAt = DateTimeOffset.Now.ToString("o"),
|
||||
Server = Environment.MachineName
|
||||
};
|
||||
foreach (var name in names)
|
||||
{
|
||||
plan.Steps.Add(new OperationStep
|
||||
{
|
||||
Kind = "ReceiveLocation",
|
||||
Application = "APP",
|
||||
Name = name,
|
||||
Action = "Restore " + name,
|
||||
WmiClass = "MSBTS_ReceiveLocation",
|
||||
KeyProperty = "Name",
|
||||
KeyValue = name,
|
||||
MethodName = "Enable",
|
||||
Execute = true
|
||||
});
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>Erstellt konsistente Optionen für portable Executor-Tests.</summary>
|
||||
/// <param name="dryRun">True für einen simulierenden Lauf.</param>
|
||||
/// <returns>Testoptionen.</returns>
|
||||
private static OperationOptions TestOptions(bool dryRun)
|
||||
{
|
||||
return new OperationOptions
|
||||
{
|
||||
Server = Environment.MachineName,
|
||||
OutputDirectory = Path.GetTempPath(),
|
||||
StateFile = "before.json",
|
||||
DryRun = dryRun,
|
||||
WaitTimeoutSeconds = 30,
|
||||
PollIntervalSeconds = 1
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Simuliert bereits erreichte Zustände, Mutationen und isolierte Laufzeitfehler.</summary>
|
||||
private sealed class FakeOperationStepRuntime : IOperationStepRuntime
|
||||
{
|
||||
/// <summary>Initialisiert die Aufruf- und Mutationslisten.</summary>
|
||||
public FakeOperationStepRuntime()
|
||||
{
|
||||
Calls = new List<string>();
|
||||
MutatedNames = new List<string>();
|
||||
AlreadySatisfiedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the step name that throws the simulated Scheduler error.</summary>
|
||||
public string FailingName { get; set; }
|
||||
|
||||
/// <summary>Gets every runtime call in order.</summary>
|
||||
public List<string> Calls { get; private set; }
|
||||
|
||||
/// <summary>Gets only steps that required a simulated mutation.</summary>
|
||||
public List<string> MutatedNames { get; private set; }
|
||||
|
||||
/// <summary>Gets the names reported as already satisfied.</summary>
|
||||
public HashSet<string> AlreadySatisfiedNames { get; private set; }
|
||||
|
||||
/// <summary>Simuliert einen zustandsbewussten Laufzeitschritt.</summary>
|
||||
/// <param name="step">Der Testschritt.</param>
|
||||
/// <param name="options">Die ungenutzten Testoptionen.</param>
|
||||
/// <returns>Das simulierte Ergebnis.</returns>
|
||||
public RuntimeStepOutcome Execute(OperationStep step, OperationOptions options)
|
||||
{
|
||||
Calls.Add(step.Name);
|
||||
if (string.Equals(step.Name, FailingName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Could not validate TransportTypeData.",
|
||||
new FileNotFoundException("Could not load file or assembly Microsoft.BizTalk.Scheduler, Version=3.13.0.0."));
|
||||
}
|
||||
if (AlreadySatisfiedNames.Contains(step.Name))
|
||||
{
|
||||
return RuntimeStepOutcome.AlreadySatisfied;
|
||||
}
|
||||
MutatedNames.Add(step.Name);
|
||||
return RuntimeStepOutcome.Succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Erstellt einen minimalen Snapshot für Vergleiche und Planprüfungen.</summary>
|
||||
/// <param name="application">Der Name der Testanwendung.</param>
|
||||
/// <param name="port">Der Name des Test-Send-Ports.</param>
|
||||
|
||||
Reference in New Issue
Block a user