Public Access
ci: accept QEMU paused prelaunch state and retire one-time cleanup
This commit is contained in:
@@ -19,15 +19,15 @@ static class ExistingKvmDiagnostic
|
||||
{
|
||||
if (args.SequenceEqual(new[] { "--help" }))
|
||||
{
|
||||
Console.WriteLine("ExistingKvmDiagnostic.cs --run|--cleanup|--cleanup-run4165-volume --output DIRECTORY\nRequires .NET 10 and the existing Docker CLI/daemon. --help never calls Docker.\n--run tests only a paused, diskless QEMU with existing /dev/kvm; retains evidence and cleans up its own container. --cleanup retries that saved cleanup.\n--cleanup-run4165-volume removes only run 4165's frozen unused anonymous volume after daemon, old-container absence and reference checks.");
|
||||
Console.WriteLine("ExistingKvmDiagnostic.cs --run|--cleanup --output DIRECTORY\nExistingKvmDiagnostic.cs --validate-evidence DIRECTORY\nRequires .NET 10; --run/--cleanup also require the existing Docker CLI/daemon. --help never calls Docker.\n--run tests only a paused, diskless QEMU with existing /dev/kvm; retains evidence and cleans up its own container. --cleanup retries that saved cleanup.\n--validate-evidence reads saved monitor/result/container evidence through the run's success parser; no Docker commands or file writes.");
|
||||
return 0;
|
||||
}
|
||||
if (args.Length != 3 || args[0] is not ("--run" or "--cleanup" or "--cleanup-run4165-volume") || args[1] != "--output")
|
||||
throw new ArgumentException("Use --help or --run|--cleanup|--cleanup-run4165-volume --output DIRECTORY.");
|
||||
if (args.Length == 2 && args[0] == "--validate-evidence") return ValidateEvidence(Path.GetFullPath(args[1]));
|
||||
if (args.Length != 3 || args[0] is not ("--run" or "--cleanup") || args[1] != "--output")
|
||||
throw new ArgumentException("Use --help, --validate-evidence DIRECTORY or --run|--cleanup --output DIRECTORY.");
|
||||
var output = Path.GetFullPath(args[2]);
|
||||
Directory.CreateDirectory(output);
|
||||
if (args[0] == "--cleanup") return await Cleanup(output) ? 0 : 1;
|
||||
if (args[0] == "--cleanup-run4165-volume") return await CleanupRun4165Volume(output) ? 0 : 1;
|
||||
if (Directory.EnumerateFileSystemEntries(output).Any()) throw new InvalidOperationException("Run output must be empty; existing receipts cannot be reused.");
|
||||
|
||||
var started = DateTimeOffset.UtcNow;
|
||||
@@ -69,11 +69,7 @@ static class ExistingKvmDiagnostic
|
||||
var exited = await InspectOwned(output, owner, "container-exited", budget.Token);
|
||||
var state = exited.GetProperty("State");
|
||||
var text = Read(output, "qemu-monitor", "stdout");
|
||||
var cleanExit = monitor.ExitCode == 0 && !monitor.TimedOut && !state.GetProperty("Running").GetBoolean()
|
||||
&& state.GetProperty("ExitCode").GetInt32() == 0 && !state.GetProperty("OOMKilled").GetBoolean();
|
||||
var enabled = Regex.IsMatch(text, @"(?m)^kvm support: enabled\r?$", RegexOptions.CultureInvariant);
|
||||
var paused = Regex.IsMatch(text, @"(?m)^VM status: paused\r?$", RegexOptions.CultureInvariant);
|
||||
status = cleanExit && enabled && paused ? "kvm_usable" : ClassifyFailure(Read(output, "qemu-monitor", "stderr") + "\n" + state.GetProperty("Error").GetString());
|
||||
status = KvmUsable(text, monitor, state) ? "kvm_usable" : ClassifyFailure(Read(output, "qemu-monitor", "stderr") + "\n" + state.GetProperty("Error").GetString());
|
||||
detail = status == "kvm_usable" ? "QEMU initialized KVM, reported enabled and paused, and exited successfully after quit. No guest CPU or OS was run."
|
||||
: "KVM initialization or its enabled/paused/clean-exit proof did not pass; inspect raw monitor output and container state.";
|
||||
}
|
||||
@@ -85,6 +81,31 @@ static class ExistingKvmDiagnostic
|
||||
return status == "kvm_usable" && cleaned ? 0 : 1;
|
||||
}
|
||||
|
||||
static bool KvmUsable(string text, CommandResult monitor, JsonElement state)
|
||||
{
|
||||
var cleanExit = monitor.ExitCode == 0 && !monitor.TimedOut && monitor.Error is null && !state.GetProperty("Running").GetBoolean()
|
||||
&& state.GetProperty("ExitCode").GetInt32() == 0 && !state.GetProperty("OOMKilled").GetBoolean();
|
||||
var enabled = Regex.IsMatch(text, @"(?m)^kvm support: enabled\r?$", RegexOptions.CultureInvariant);
|
||||
var paused = Regex.IsMatch(text, @"(?m)^VM status: paused(?: \(prelaunch\))?\r?$", RegexOptions.CultureInvariant);
|
||||
return cleanExit && enabled && paused;
|
||||
}
|
||||
|
||||
static int ValidateEvidence(string evidence)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var monitorDocument = JsonDocument.Parse(File.ReadAllText(Path.Combine(evidence, "qemu-monitor.result.json")));
|
||||
var result = monitorDocument.RootElement;
|
||||
var monitor = new CommandResult(result.GetProperty("ExitCode").GetInt32(), result.GetProperty("TimedOut").GetBoolean(), result.GetProperty("Error").GetString());
|
||||
using var exited = JsonDocument.Parse(Read(evidence, "container-exited", "stdout"));
|
||||
if (exited.RootElement.GetArrayLength() != 1) throw new InvalidOperationException("Expected exactly one saved exited container.");
|
||||
var usable = KvmUsable(Read(evidence, "qemu-monitor", "stdout"), monitor, exited.RootElement[0].GetProperty("State"));
|
||||
Console.WriteLine(JsonSerializer.Serialize(new { usable, scope = "Saved HMP protocol and clean exit interpretation only; source, ownership and container boundary are not requalified." }));
|
||||
return usable ? 0 : 1;
|
||||
}
|
||||
catch (Exception error) { Console.Error.WriteLine("Evidence interpretation failed: " + error.Message); return 1; }
|
||||
}
|
||||
|
||||
static string ClassifyFailure(string text)
|
||||
{
|
||||
if (text.Contains("/dev/kvm", StringComparison.Ordinal) && text.Contains("error gathering device information", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -177,72 +198,6 @@ static class ExistingKvmDiagnostic
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<bool> CleanupRun4165Volume(string output)
|
||||
{
|
||||
// Frozen from actual run 4165: the exact owner-labelled container never started (created/PID 0/zero StartedAt).
|
||||
// Its image's VOLUME instruction created this anonymous /storage volume before the mount guard failed.
|
||||
const string daemonId = "528941c8-73ac-49ff-8eb7-69113eb4a2a1";
|
||||
const string containerId = "1753f95ef334244e7a1b393a839f218ea885363de7d5335eec53132d64627010";
|
||||
const string token = "43b7f4676c514f2a95c63c02577ac36e";
|
||||
const string volumeName = "ef7daa62ef89a2ffb8aae50a9b7803f1d9b3075ee509aa3183f3e170f69ce595";
|
||||
const string containerName = "meeting-assistant-kvm-" + token;
|
||||
var prefix = "run4165-volume-" + Guid.NewGuid().ToString("N");
|
||||
Save(output, prefix + ".target.json", new { runId = 4165, daemonId, containerId, containerName, token, volumeName,
|
||||
archiveSha256 = "6745d90e8b81c867740405c99b4364cc165c47ebb165455052314459d5cd547b",
|
||||
createdInspectSha256 = "7afdfc6c30c933bee2ef1d6c18ed011c8b2f709d1a5e88531928f9f40471c055",
|
||||
ownerReceiptSha256 = "d96f15588412a5928ebe8a64b115764f113adaaaf6d164209fd87de8a4505572",
|
||||
priorState = "created", priorPid = 0, priorStartedAt = "0001-01-01T00:00:00Z" });
|
||||
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
var success = false;
|
||||
var outcome = "refused";
|
||||
string? detail = null;
|
||||
try
|
||||
{
|
||||
await Require(Command("docker", ["info", "--format", "{{.ID}}"], output, prefix + "-daemon", budget.Token));
|
||||
if (Read(output, prefix + "-daemon", "stdout").Trim() != daemonId) throw new InvalidOperationException("Frozen run 4165 daemon ID differs; refusing volume cleanup.");
|
||||
foreach (var selector in new[] { containerId, containerName })
|
||||
{
|
||||
var step = prefix + (selector == containerId ? "-old-id" : "-old-name");
|
||||
var inspection = await Command("docker", ["inspect", "--type", "container", selector], output, step, budget.Token);
|
||||
if (inspection.TimedOut || inspection.Error != null || inspection.ExitCode == 0 || !Read(output, step, "stderr").Contains("No such container", StringComparison.OrdinalIgnoreCase)
|
||||
|| !Read(output, step, "stderr").Contains(selector, StringComparison.Ordinal)) throw new InvalidOperationException("Old run 4165 container absence was not proved for " + selector + ".");
|
||||
}
|
||||
var inspectStep = prefix + "-inspect";
|
||||
var inspect = await Command("docker", ["volume", "inspect", volumeName], output, inspectStep, budget.Token);
|
||||
if (VolumeAbsent(inspect, output, inspectStep, volumeName)) { success = true; outcome = "already-absent"; }
|
||||
else
|
||||
{
|
||||
if (inspect.ExitCode != 0 || inspect.TimedOut || inspect.Error != null) throw new InvalidOperationException("Cannot inspect the exact run 4165 volume.");
|
||||
using var document = JsonDocument.Parse(Read(output, inspectStep, "stdout"));
|
||||
var values = document.RootElement;
|
||||
if (values.GetArrayLength() != 1 || values[0].GetProperty("Name").GetString() != volumeName
|
||||
|| values[0].GetProperty("Driver").GetString() != "local" || values[0].GetProperty("Scope").GetString() != "local"
|
||||
|| !Empty(values[0].GetProperty("Options"))) throw new InvalidOperationException("Frozen volume name/local driver/scope/options boundary failed.");
|
||||
await Require(Command("docker", ["ps", "--all", "--no-trunc", "--filter", "volume=" + volumeName, "--format", "{{.ID}}"], output, prefix + "-references", budget.Token));
|
||||
if (Read(output, prefix + "-references", "stdout").Trim().Length != 0) throw new InvalidOperationException("A container references the frozen run 4165 volume; refusing removal.");
|
||||
// No --force: Docker rejects an attachment made after the reference check, too.
|
||||
await Require(Command("docker", ["volume", "rm", volumeName], output, prefix + "-remove", budget.Token));
|
||||
var afterStep = prefix + "-after";
|
||||
var after = await Command("docker", ["volume", "inspect", volumeName], output, afterStep, budget.Token);
|
||||
if (!VolumeAbsent(after, output, afterStep, volumeName)) throw new InvalidOperationException("Volume absence after removal was not proved.");
|
||||
success = true;
|
||||
outcome = "removed";
|
||||
}
|
||||
}
|
||||
catch (Exception error) { detail = error.Message; }
|
||||
var receipt = new { runId = 4165, daemonId, volumeName, success, outcome, detail, finished = DateTimeOffset.UtcNow };
|
||||
Save(output, prefix + ".receipt.json", receipt);
|
||||
Console.WriteLine(JsonSerializer.Serialize(receipt));
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool VolumeAbsent(CommandResult result, string output, string step, string name)
|
||||
{
|
||||
var error = Read(output, step, "stderr");
|
||||
return result.ExitCode is not (null or 0) && !result.TimedOut && result.Error == null
|
||||
&& error.Contains("no such volume", StringComparison.OrdinalIgnoreCase) && error.Contains(name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
static async Task<CommandResult> Command(string program, string[] arguments, string output, string step, CancellationToken cancellation, string? input = null)
|
||||
{
|
||||
var started = DateTimeOffset.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user