blancco-progress: take the final state from the report file
First normal (non-debug) run stuck at 6/7 "Report sent to cloud": outside Blancco's debug mode Albus does not log erasureFinishedForEntity, Report saving success or "Auto process reached the final step", which the parser had been taken from a debug-mode journal. A watcher thread now reads each new /reports/*.xml (auto mode always saves there) once it is stable and posts 7: "Wipe complete" (succeeded) or "Erasure FAILED: <model serial, ...>" (failed). Judged per drive, so a failed Purge with a successful Clear fallback on the same drive is a wipe. --outcome FILE prints the verdict for a report.
This commit is contained in:
@@ -11,14 +11,24 @@ blancco-init.sh; this script posts 3 waiting for licence, 4 erasing,
|
|||||||
5 report saved, 6 report sent / on share, 7 done. A failed erasure step
|
5 report saved, 6 report sent / on share, 7 done. A failed erasure step
|
||||||
marks the session failed.
|
marks the session failed.
|
||||||
|
|
||||||
|
The final state comes from the report itself, not the journal: outside
|
||||||
|
Blancco's debug mode Albus does not log "erasureFinishedForEntity",
|
||||||
|
"Report saving success" or "Auto process reached the final step", so a
|
||||||
|
watcher on /reports (where auto mode always saves the report) reads each
|
||||||
|
new report and posts 7 "Wipe complete" or "Erasure FAILED".
|
||||||
|
|
||||||
Usage: blancco-progress.py [--replay FILE] [--dry-run]
|
Usage: blancco-progress.py [--replay FILE] [--dry-run]
|
||||||
"""
|
"""
|
||||||
|
import glob
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
STATUS_URL = "http://172.16.9.1:9009/imaging/status"
|
STATUS_URL = "http://172.16.9.1:9009/imaging/status"
|
||||||
CLOUD_URL = "http://172.16.9.1:9009/reports/cloud-status"
|
CLOUD_URL = "http://172.16.9.1:9009/reports/cloud-status"
|
||||||
@@ -50,6 +60,41 @@ def pxe_iface():
|
|||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
|
|
||||||
|
REPORTS_DIR = "/reports"
|
||||||
|
|
||||||
|
|
||||||
|
def report_outcome(path):
|
||||||
|
"""(wiped: bool, failed_drives: list[str]) from a Blancco report XML.
|
||||||
|
|
||||||
|
Per drive: wiped if any of its erasures succeeded, so a failed NIST
|
||||||
|
Purge followed by a successful Clear fallback still counts as wiped.
|
||||||
|
None if the report has no erasures (hardware-only report).
|
||||||
|
"""
|
||||||
|
root = ET.parse(path).getroot()
|
||||||
|
drives = {}
|
||||||
|
for i, er in enumerate(root.iter("entries")):
|
||||||
|
if er.get("name") != "erasure":
|
||||||
|
continue
|
||||||
|
state, serial, model = "", "", ""
|
||||||
|
for e in er:
|
||||||
|
if e.tag == "entry" and e.get("name") == "state":
|
||||||
|
state = (e.text or "").strip()
|
||||||
|
if e.tag == "entries" and e.get("name") == "target":
|
||||||
|
for t in e:
|
||||||
|
if t.get("name") == "serial":
|
||||||
|
serial = (t.text or "").strip()
|
||||||
|
elif t.get("name") == "model":
|
||||||
|
model = (t.text or "").strip()
|
||||||
|
key = serial or f"#{i}"
|
||||||
|
ok = state.lower() == "successful"
|
||||||
|
prev = drives.get(key, (False, ""))
|
||||||
|
drives[key] = (prev[0] or ok, f"{model} {serial}".strip())
|
||||||
|
if not drives:
|
||||||
|
return None
|
||||||
|
failed = [label for ok, label in drives.values() if not ok]
|
||||||
|
return (not failed, failed)
|
||||||
|
|
||||||
|
|
||||||
SERIAL = read("/sys/class/dmi/id/product_serial") or read("/etc/machine-id")
|
SERIAL = read("/sys/class/dmi/id/product_serial") or read("/etc/machine-id")
|
||||||
MODEL = " ".join(x for x in (read("/sys/class/dmi/id/sys_vendor"),
|
MODEL = " ".join(x for x in (read("/sys/class/dmi/id/sys_vendor"),
|
||||||
read("/sys/class/dmi/id/product_name")) if x)
|
read("/sys/class/dmi/id/product_name")) if x)
|
||||||
@@ -175,9 +220,50 @@ class Reporter:
|
|||||||
self.post()
|
self.post()
|
||||||
|
|
||||||
|
|
||||||
|
def finish(self, path):
|
||||||
|
"""Final stage from a finished report on disk."""
|
||||||
|
try:
|
||||||
|
outcome = report_outcome(path)
|
||||||
|
except (OSError, ET.ParseError):
|
||||||
|
return False
|
||||||
|
if outcome is None:
|
||||||
|
return True # nothing erased in this report; keep watching
|
||||||
|
wiped, failed = outcome
|
||||||
|
name = os.path.basename(path)
|
||||||
|
self.stage_index = 7
|
||||||
|
if wiped:
|
||||||
|
self.stage, self.status = "Wipe complete", "succeeded"
|
||||||
|
else:
|
||||||
|
self.stage, self.status = "Erasure FAILED: " + ", ".join(failed)[:120], "failed"
|
||||||
|
self.pending.append(f"Report {name}: {'all drives wiped' if wiped else 'failed: ' + ', '.join(failed)}")
|
||||||
|
self.post(force=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def watch_reports(rep, interval=5, stable=5):
|
||||||
|
"""Post the final stage for each new report Albus saves in /reports."""
|
||||||
|
seen = set(glob.glob(os.path.join(REPORTS_DIR, "*.xml")))
|
||||||
|
while True:
|
||||||
|
time.sleep(interval)
|
||||||
|
for path in sorted(glob.glob(os.path.join(REPORTS_DIR, "*.xml"))):
|
||||||
|
if path in seen:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
st = os.stat(path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if st.st_size == 0 or time.time() - st.st_mtime < stable:
|
||||||
|
continue # Albus may still be writing it
|
||||||
|
if rep.finish(path):
|
||||||
|
seen.add(path)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
dry_run = "--dry-run" in sys.argv
|
dry_run = "--dry-run" in sys.argv
|
||||||
rep = Reporter(dry_run=dry_run)
|
rep = Reporter(dry_run=dry_run)
|
||||||
|
if "--outcome" in sys.argv:
|
||||||
|
print(report_outcome(sys.argv[sys.argv.index("--outcome") + 1]))
|
||||||
|
return
|
||||||
if "--replay" in sys.argv:
|
if "--replay" in sys.argv:
|
||||||
with open(sys.argv[sys.argv.index("--replay") + 1]) as f:
|
with open(sys.argv[sys.argv.index("--replay") + 1]) as f:
|
||||||
for text in f:
|
for text in f:
|
||||||
@@ -185,6 +271,7 @@ def main():
|
|||||||
rep.post(force=True)
|
rep.post(force=True)
|
||||||
return
|
return
|
||||||
rep.post(force=True)
|
rep.post(force=True)
|
||||||
|
threading.Thread(target=watch_reports, args=(rep,), daemon=True).start()
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
["journalctl", "-f", "-b", "-o", "cat", "-t", "AlbusSeven", "-n", "all"],
|
["journalctl", "-f", "-b", "-o", "cat", "-t", "AlbusSeven", "-n", "all"],
|
||||||
stdout=subprocess.PIPE, text=True, bufsize=1)
|
stdout=subprocess.PIPE, text=True, bufsize=1)
|
||||||
|
|||||||
Reference in New Issue
Block a user