dpl/scripts/hn/gateway/migrate-t34.py

64 lines
4.5 KiB
Python

#!/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()<deadline,'HN readiness timed out; inspect and restore backup'
time.sleep(2)
run(['docker','exec',name,'nginx','-t'])
run(['nginx','-t']);run(['systemctl','reload','nginx']);run(['systemctl','daemon-reload'])
data=json.loads((STATE/'deployment.json').read_text());data['host']=NEW;data['commit']=subprocess.check_output(['git','-C',str(BASE),'rev-parse','HEAD'],text=True).strip()
(STATE/'deployment.json').write_text(json.dumps(data,indent=2));(STATE/'deployment.json').chmod(0o600)
protected();(BACKUP/'activated.json').write_text(json.dumps({'host':NEW,'commit':data['commit']}));(BACKUP/'activated.json').chmod(0o600)
print('HN t34 gateway activated. Application URL and DNS retirement require final acceptance.')
if __name__=='__main__':
assert os.geteuid()==0
parser=argparse.ArgumentParser();parser.add_argument('phase',choices=['prepare','certificate','activate','verify-original']);args=parser.parse_args()
{'prepare':prepare,'certificate':certificate,'activate':activate,'verify-original':protected}[args.phase]()