Add isolated Classic data staging with integrity checks
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user