Add isolated Classic data staging with integrity checks

This commit is contained in:
Codex
2026-09-25 22:05:17 +02:00
parent 1f9ad52d07
commit 58b2019015
2 changed files with 97 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# Classic migration staging
Target: CMaNGOS Classic, client 1.12.1 build 5875, with CMaNGOS Playerbots.
This directory does not implement the runtime connector or authorize a cutover.
`stage-data.py --root /path/to/isolated/migration` observes an existing
`azerothai-classic-build` container, verifies the transferred MPQs against
`client-manifest.json`, and runs the upstream extractor with two worker threads.
The manifest maps relative MPQ paths to `size` and `sha256`. Checksums verify
transfer integrity against the supplied client manifest, not publisher authenticity.
The Docker builder image must be named `azerothai-classic-builder:local`; the
installed extractor must be under `run/bin/tools`. The staging directory and
container files belong to UID/GID 1000. Containers have a two-core/four-GiB limit.
Progress and failures are written to `prerequisites-status.json`. A successful
extraction is only a prerequisite: database import, the Classic control adapter,
the 50-character roster, account protection, and gameplay validation remain.
The script never starts the new realm, changes the old realm, or deletes bots.
Migration safeguards:
- Preserve the verified Wrath backup and the active-50 identity export.
- Maintain 25 Alliance and 25 Horde. Use valid Vanilla race/class combinations.
- Start new Vanilla characters at level 1; do not directly import Wrath inventory,
talent, spell, or quest tables. Blood Elves, Draenei, Death Knights, Alliance
Shamans, and Horde Paladins need explicit compatible replacements.
- Retain personalities and historical memories; reset live observations, GUIDs,
action queues, and progress baselines when binding the new characters.
- Protect human/GM accounts. Inactive managed profiles are cleanup candidates,
not proof that corresponding game characters can safely be deleted.
- Test native bot login, progress, resurrection, safety rules, read-only visitor
access, and current observations before switching the live dashboard or client.
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Finish isolated Classic prerequisites. Never starts a realm or edits live data."""
import argparse
import hashlib
import json
import pathlib
import shutil
import subprocess
import time
parser = argparse.ArgumentParser()
parser.add_argument('--root', required=True)
args = parser.parse_args()
root = pathlib.Path(args.root).resolve()
def status(stage, **extra):
value = {'at': time.time(), 'stage': stage, 'cutover': False, **extra}
temporary = root / 'prerequisites-status.tmp'
temporary.write_text(json.dumps(value, indent=2))
temporary.replace(root / 'prerequisites-status.json')
print(stage, flush=True)
def run(*command):
return subprocess.check_output(command, text=True).strip()
try:
status('waiting for Classic compilation and client transfer')
deadline = time.monotonic() + 6 * 3600
while True:
state = json.loads(run('docker', 'inspect', 'azerothai-classic-build'))[0]['State']
if not state['Running']:
if state['ExitCode'] != 0:
raise RuntimeError('Classic build failed; inspect docker logs azerothai-classic-build')
break
if time.monotonic() > deadline:
raise RuntimeError('Classic build exceeded six-hour observation window')
time.sleep(30)
manifest = json.loads((root / 'client-manifest.json').read_text())
while not all((root / 'client' / f).exists() and (root / 'client' / f).stat().st_size == v['size'] for f, v in manifest.items()):
if time.monotonic() > deadline:
raise RuntimeError('Client transfer incomplete after six hours')
time.sleep(30)
status('verifying transferred client archives')
for filename, expected in manifest.items():
with (root / 'client' / filename).open('rb') as stream:
actual = hashlib.file_digest(stream, 'sha256').hexdigest()
if actual != expected['sha256']:
raise RuntimeError('Client archive checksum mismatch: ' + filename)
if shutil.disk_usage(root).free < 8 * 1024**3:
raise RuntimeError('Less than 8 GiB free for extracted Vanilla data')
output = root / 'extracted'; output.mkdir(exist_ok=True)
status('extracting Vanilla maps and navigation data; realm not switched')
with (root / 'data-extraction.log').open('w') as log:
subprocess.run([
'docker', 'run', '--name', 'azerothai-classic-extract', '--cpus', '2', '--memory', '4g',
'--user', '1000:1000', '-i', '-v', str(root) + ':/work', '-w', '/work/run/bin/tools',
'azerothai-classic-builder:local', 'bash', './ExtractResources.sh', '/work/client', '/work/extracted'
], input='y\n2\n\nn\nn\n\n', text=True, stdout=log, stderr=subprocess.STDOUT, check=True)
counts = {name: sum(1 for f in (output / name).rglob('*') if f.is_file()) for name in ('dbc', 'maps', 'vmaps', 'mmaps')}
if not all(counts.values()):
raise RuntimeError('An extracted data directory is empty: ' + str(counts))
status('prerequisites extracted; database, connector, roster and gameplay validation remain', files=counts)
except Exception as error:
status('blocked', error=str(error))
raise