Files

82 lines
4.1 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""One-time, scoped cutover from the loopback preview; preserve a recovery snapshot."""
import argparse
import hashlib
import json
from pathlib import Path
import subprocess
import tarfile
from start import CONTAINER, IMAGE, validate_runtime
OLD = 'adamwen-test-console-before-lan-20261013'
def run(args):
return subprocess.check_output(args, text=True, timeout=90).strip()
def inspect(name):
result = subprocess.run(['docker','inspect',name], capture_output=True, text=True)
return json.loads(result.stdout)[0] if result.returncode == 0 else None
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--project-root',type=Path,required=True)
args = parser.parse_args()
project = args.project_root.resolve(strict=True)
runtime = validate_runtime(project)
before = inspect(CONTAINER)
if before is None or before['Config'].get('Labels',{}).get('cn.aisuan.owner') != CONTAINER:
parser.error('Expected the existing project-owned preview container')
if before['HostConfig'].get('PortBindings',{}).get('4440/tcp') != [{'HostIp':'127.0.0.1','HostPort':'14440'}]:
parser.error('Expected the original loopback preview; do not repeat this migration')
if inspect(OLD) or inspect('adamwen-test-console-proxy'):
parser.error('Cutover containers already exist; inspect the saved outcome before retrying')
if inspect(IMAGE) is None:
parser.error('Build the new project image before cutover')
backup = runtime / 'backups/before-lan-20261013'
if not (backup/'source/deploy/start.py').is_file() or backup.is_symlink():
parser.error('The source snapshot must already exist inside this project')
archive = backup/'runtime-before-lan.tar.gz'
if archive.exists():
parser.error('A data snapshot already exists; refusing to overwrite it')
active = run(['docker','exec',CONTAINER,'python3','-c',
"from pathlib import Path;import os;print(len([p for p in Path('/proc').glob('[0-9]*/cmdline') if p.parent.name != str(os.getpid()) and any(x in p.read_bytes().split(b'\\x00') for x in (b'/opt/xas-test-library/scripts/run_smoke.py',b'/opt/xas-test-library/scripts/smoke_task.py'))]))"])
if active != '0':
parser.error('An acceptance task is still running; wait for it to finish')
for name in ('configure-login.sh','set_login.py'):
current, saved = project/'deploy'/name, backup/'source/deploy'/name
if current.exists() and hashlib.sha256(current.read_bytes()).digest() != hashlib.sha256(saved.read_bytes()).digest():
parser.error('A legacy source file has changed since backup; preserve the user change')
print(run(['docker','stop','--timeout','30',CONTAINER]),flush=True)
try:
with tarfile.open(archive,'x:gz') as tar:
for name in ('data','logs','projects','results','auth'):
source = runtime/name
if source.exists():
tar.add(source,arcname=name,recursive=True)
archive.chmod(0o600)
metadata = {'container_id':before['Id'],'image_id':before['Image'],
'started_at':before['State']['StartedAt'],'backup_container':OLD,
'database_snapshot':archive.name}
(backup/'metadata.json').write_text(json.dumps(metadata,indent=2),encoding='utf-8')
print(run(['docker','rename',CONTAINER,OLD]),flush=True)
except Exception:
run(['docker','start',CONTAINER])
raise
# The two removed files are this project's reviewed password provisioning implementation.
for name in ('configure-login.sh','set_login.py'):
path=project/'deploy'/name
if path.exists():
path.unlink()
result = subprocess.run(['python3',str(project/'deploy/start.py'),'--project-root',str(project)])
if result.returncode:
raise SystemExit('New stack was not fully started. Old container and data snapshot are preserved; inspect before rollback.')
print(json.dumps({'cutover':'started','backup':str(backup),'previous_container':OLD}))
if __name__ == '__main__':
main()