#!/usr/bin/env python3 """One-time, guarded HN endpoint migration; no DPL/FUC mutations.""" from pathlib import Path import argparse,json,os,shutil,subprocess,sys,time sys.dont_write_bytecode=True sys.path.insert(0,str(Path(__file__).resolve().parent)) import install as deployment BASE=Path(__file__).resolve().parent BACKUP=Path('/root/codex-backups/hn-gateway/20261028-t34') STATE=deployment.STATE OLD='c2.hardnerd-nodes.com' NEW='t34.hardnerd-nodes.com' HOST=deployment.HOST PROXY=Path('/etc/nginx/hn-subscribe-proxy.conf') UNIT=Path('/etc/systemd/system/hn-gateway-cert.service') def run(args,**kw):return subprocess.run(args,check=True,**kw) def protected(): before=json.loads((BACKUP/'protected.json').read_text());now=deployment.original_state() assert now['containers']==before['containers'],'Original containers changed' assert all(now['nginx'].get(k)==v for k,v in before['nginx'].items()),'Original virtual hosts changed' print('DPL/FUC container and original Nginx baseline preserved.') def prepare(): assert not BACKUP.exists() and OLD in HOST.read_text() assert json.loads((STATE/'deployment.json').read_text())['host']==OLD BACKUP.mkdir(mode=0o700,parents=True) (BACKUP/'protected.json').write_text(json.dumps(deployment.original_state()));(BACKUP/'protected.json').chmod(0o600) for source,name in [(HOST,'host.conf'),(PROXY,'proxy.conf'),(UNIT,'cert.service'),(STATE/'deployment.json','deployment.json')]: shutil.copy2(source,BACKUP/name);(BACKUP/name).chmod(0o600) (BACKUP/'symlinks.json').write_text(json.dumps({name:os.readlink(STATE/'ssl'/name) for name in ['cert.pem','key.pem']}));(BACKUP/'symlinks.json').chmod(0o600) http='server_name '+OLD+' hardnerd-nodes.com www.hardnerd-nodes.com;' s=HOST.read_text();assert s.count(http)==1 HOST.write_text(s.replace(http,'server_name '+OLD+' '+NEW+' hardnerd-nodes.com www.hardnerd-nodes.com;',1)) try:run(['nginx','-t']);run(['systemctl','reload','nginx']) except Exception:shutil.copy2(BACKUP/'host.conf',HOST);raise protected();print('HN t34 ACME route ready; c2 subscription still active.') def certificate(): protected() args=['certbot','certonly','--webroot','-w',str(STATE/'acme'),'--config-dir',str(deployment.CERT),'--work-dir',str(STATE/'certbot'),'--logs-dir','/var/log/hn-gateway-certbot','--cert-name',NEW,'-d',NEW,'-d','hardnerd-nodes.com','-d','www.hardnerd-nodes.com','--non-interactive','--no-directory-hooks'] with (BACKUP/'certificate.log').open('w')as log:run(args,stdout=log,stderr=subprocess.STDOUT) run(['openssl','x509','-in',str(deployment.CERT/'live'/NEW/'fullchain.pem'),'-noout','-dates','-ext','subjectAltName']) def activate(): protected();assert not (BACKUP/'activated.json').exists() assert (deployment.CERT/'live'/NEW/'fullchain.pem').is_file() for name,file in [('cert.pem','fullchain.pem'),('key.pem','privkey.pem')]: temp=STATE/'ssl'/(name+'.new');assert not temp.exists();temp.symlink_to(deployment.CERT/'live'/NEW/file);temp.replace(STATE/'ssl'/name) PROXY.write_text((BASE/'proxy.conf').read_text());HOST.write_text((BASE/'host.conf').read_text());UNIT.write_text((BASE/'hn-gateway-cert.service').read_text()) # Recreate only HN so bind-mounted templates pick up their new inode, retaining volumes and credentials. deployment.compose('up','-d','--force-recreate','gateway','admin') deadline=time.monotonic()+55 for name in deployment.NEW: while subprocess.run(['docker','exec',name,'nginx','-t'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode: assert time.monotonic()