82 lines
4.1 KiB
Python
82 lines
4.1 KiB
Python
#!/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()
|