Handle Miniflare metadata during Classic cutover and preserve daily backups
Core tests / core (push) Successful in 28s
Core tests / core (push) Successful in 28s
This commit is contained in:
@@ -61,11 +61,10 @@ registration and enables activity even when no human player is present.
|
|||||||
Automatic population creation remains disabled; the native identity allowlist
|
Automatic population creation remains disabled; the native identity allowlist
|
||||||
still limits managed login to the planned 50.
|
still limits managed login to the planned 50.
|
||||||
|
|
||||||
Remaining cutover gates include live quest/training/survival observations,
|
Before cutover, verify fresh 50-character observations, quest and spell outcomes,
|
||||||
event telemetry and human-player conversation parity, dashboard/operator actions,
|
normal resurrection, actual party receipts, native GM commands, authentication,
|
||||||
normal resurrection, actual party acceptance and speech, authentication/client
|
and rollback. Human player replies require an actual in-game session for an
|
||||||
configuration, and a verified rollback route. Human-player chat delivery is
|
end-to-end acceptance check. Lua/ALE reflection remains a Wrath-only capability.
|
||||||
explicitly disabled in the staged Classic adapter until implemented and tested.
|
|
||||||
|
|
||||||
Migration safeguards:
|
Migration safeguards:
|
||||||
|
|
||||||
@@ -94,3 +93,8 @@ copies only ENDAMI's existing SRP credentials into the isolated authentication
|
|||||||
database, and disables upstream sample logins. `check-auth.py --root ROOT`
|
database, and disables upstream sample logins. `check-auth.py --root ROOT`
|
||||||
verifies a complete SRP handshake and realm listing without entering the game.
|
verifies a complete SRP handshake and realm listing without entering the game.
|
||||||
`health.py --root ROOT --url URL` reports host health without Wrath SQL.
|
`health.py --root ROOT --url URL` reports host health without Wrath SQL.
|
||||||
|
|
||||||
|
`cutover.py --root ROOT` requires a passing build and all 50 fresh staging bots,
|
||||||
|
backs up stopped Wrath accounts/characters, switches the existing endpoints, and
|
||||||
|
automatically restores Wrath endpoints if login or population verification fails.
|
||||||
|
Miniflare metadata databases are excluded from game-state backup selection.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bounded daily backup of Classic persistent accounts, characters and dashboard."""
|
||||||
|
import argparse,gzip,json,os,pathlib,shutil,sqlite3,subprocess,time
|
||||||
|
p=argparse.ArgumentParser();p.add_argument('--root',type=pathlib.Path,required=True);a=p.parse_args();root=a.root
|
||||||
|
base=root/'backups';base.mkdir(mode=0o700,exist_ok=True)
|
||||||
|
target=base/time.strftime('%Y%m%dT%H%M%SZ',time.gmtime());target.mkdir(mode=0o700)
|
||||||
|
c=json.loads((root/'database-credentials.json').read_text())
|
||||||
|
try:
|
||||||
|
with gzip.open(target/'classic-auth-characters.sql.gz','wb') as out:
|
||||||
|
p=subprocess.Popen(['docker','exec','-e','MYSQL_PWD','azerothai-classic-db','mysqldump','-uroot','--single-transaction','--set-gtid-purged=OFF','--databases','classicrealmd','classiccharacters'],stdout=subprocess.PIPE,stderr=subprocess.PIPE,env=dict(os.environ,MYSQL_PWD=c['root']))
|
||||||
|
shutil.copyfileobj(p.stdout,out)
|
||||||
|
if p.wait():raise RuntimeError('Classic backup failed')
|
||||||
|
dbs=[p for p in (root/'app-staging/.wrangler/state/v3/d1').rglob('*.sqlite') if p.name!='metadata.sqlite'];assert len(dbs)==1
|
||||||
|
source=sqlite3.connect(dbs[0]);dest=sqlite3.connect(target/'dashboard.sqlite');source.backup(dest);dest.close();source.close()
|
||||||
|
shutil.copytree(root/'run/etc',target/'config')
|
||||||
|
(target/'complete.json').write_text(json.dumps({'at':time.time(),'backend':'cmangos-classic'}))
|
||||||
|
for old in sorted(base.iterdir(),reverse=True)[3:]:
|
||||||
|
if old.is_dir() and (old/'complete.json').exists():shutil.rmtree(old)
|
||||||
|
print('Classic backup complete: '+str(target))
|
||||||
|
except Exception:
|
||||||
|
(target/'failed').touch();raise
|
||||||
@@ -8,7 +8,7 @@ import argparse,gzip,json,os,pathlib,shutil,sqlite3,subprocess,time,urllib.reque
|
|||||||
|
|
||||||
def main(root):
|
def main(root):
|
||||||
home=pathlib.Path.home();units=home/'.config/systemd/user';app=root/'app-staging'
|
home=pathlib.Path.home();units=home/'.config/systemd/user';app=root/'app-staging'
|
||||||
archive=root/'cutover-backup';archive.mkdir(mode=0o700,exist_ok=False)
|
archive=root/('cutover-backup-'+str(int(time.time())));archive.mkdir(mode=0o700,exist_ok=False)
|
||||||
def run(args,check=True):
|
def run(args,check=True):
|
||||||
p=subprocess.run(args,capture_output=True,text=True)
|
p=subprocess.run(args,capture_output=True,text=True)
|
||||||
if check and p.returncode:raise RuntimeError('Command failed: '+' '.join(args[:4])+' '+p.stderr[-500:])
|
if check and p.returncode:raise RuntimeError('Command failed: '+' '.join(args[:4])+' '+p.stderr[-500:])
|
||||||
@@ -46,7 +46,9 @@ def main(root):
|
|||||||
assert (archive/'wrath-auth-characters.sql.gz').stat().st_size>10000
|
assert (archive/'wrath-auth-characters.sql.gz').stat().st_size>10000
|
||||||
status('Publishing Classic game and authentication ports')
|
status('Publishing Classic game and authentication ports')
|
||||||
run(['docker','stop','-t','30','azerothai-classic-world','azerothai-classic-auth'])
|
run(['docker','stop','-t','30','azerothai-classic-world','azerothai-classic-auth'])
|
||||||
for name in ['azerothai-classic-world','azerothai-classic-auth']:run(['docker','rename',name,name+'-staging'])
|
for name in ['azerothai-classic-world','azerothai-classic-auth']:
|
||||||
|
run(['docker','rm','-f',name+'-staging'],False)
|
||||||
|
run(['docker','rename',name,name+'-staging'])
|
||||||
common=['--network','azerothai-classic-access','--restart','unless-stopped','--user','1000:1000','-v',str(root)+':/work','-w','/work/run/bin']
|
common=['--network','azerothai-classic-access','--restart','unless-stopped','--user','1000:1000','-v',str(root)+':/work','-w','/work/run/bin']
|
||||||
run(['docker','create','--name','azerothai-classic-world',*common,'--cpus','2','--memory','3g','-p','8086:8086','azerothai-classic-builder:local','bash','-c','exec 3<>/work/run/console; exec ./mangosd -c /work/run/etc/mangosd.conf <&3'])
|
run(['docker','create','--name','azerothai-classic-world',*common,'--cpus','2','--memory','3g','-p','8086:8086','azerothai-classic-builder:local','bash','-c','exec 3<>/work/run/console; exec ./mangosd -c /work/run/etc/mangosd.conf <&3'])
|
||||||
run(['docker','create','--name','azerothai-classic-auth',*common,'--cpus','0.5','--memory','128m','-p','3724:3724','azerothai-classic-builder:local','./realmd','-c','/work/run/etc/realmd.conf'])
|
run(['docker','create','--name','azerothai-classic-auth',*common,'--cpus','0.5','--memory','128m','-p','3724:3724','azerothai-classic-builder:local','./realmd','-c','/work/run/etc/realmd.conf'])
|
||||||
@@ -57,7 +59,7 @@ def main(root):
|
|||||||
p=app/'.dev.vars';text=p.read_text();import re
|
p=app/'.dev.vars';text=p.read_text();import re
|
||||||
text=re.sub(r'^SOAP_URL=.*$','SOAP_URL=http://'+ip+':7879',text,flags=re.M);p.write_text(text)
|
text=re.sub(r'^SOAP_URL=.*$','SOAP_URL=http://'+ip+':7879',text,flags=re.M);p.write_text(text)
|
||||||
# Discard copied Wrath metrics; native Classic counters remain intact.
|
# Discard copied Wrath metrics; native Classic counters remain intact.
|
||||||
databases=list((app/'.wrangler/state/v3/d1').rglob('*.sqlite'))
|
databases=[p for p in (app/'.wrangler/state/v3/d1').rglob('*.sqlite') if p.name!='metadata.sqlite']
|
||||||
assert len(databases)==1
|
assert len(databases)==1
|
||||||
db=sqlite3.connect(databases[0]);backup=sqlite3.connect(archive/'classic-before-cutover.sqlite');db.backup(backup);backup.close()
|
db=sqlite3.connect(databases[0]);backup=sqlite3.connect(archive/'classic-before-cutover.sqlite');db.backup(backup);backup.close()
|
||||||
names=[x[0] for x in db.execute("SELECT name FROM sqlite_master WHERE type='table'")]
|
names=[x[0] for x in db.execute("SELECT name FROM sqlite_master WHERE type='table'")]
|
||||||
|
|||||||
Reference in New Issue
Block a user