69 lines
5.2 KiB
Python
69 lines
5.2 KiB
Python
"""Install receipt-driven cleanup; restart only private HTTP bridges, never workers."""
|
|
import datetime,hashlib,json,pathlib,shutil,sqlite3,subprocess,time,urllib.request,urllib.error
|
|
root=pathlib.Path('/home/ps/huobao-video-cleanup')
|
|
expected='646600a3c34354689b8d5aec0927b7481583da5478619e4e172f42583e5f4ffc'
|
|
bridges=[{'name':'huobao-model-bridge','folder':'/home/ps/huobao-model-bridge','database':'/home/ps/huobao-model-bridge/state/jobs.sqlite3','normalizer':'canvas-h3-prompt-normalizer-healthy-20261026','port':18792},
|
|
{'name':'huobao-vdn-bridge','folder':'/home/ps/huobao-vdn/bridge','database':'/home/ps/huobao-vdn/bridge-state/jobs.sqlite3','normalizer':'huobao-vdn-normalizer','port':18794}]
|
|
workers={'default':'comfyui','gpu1_8189':'comfyui-manju','gpu3':'h3-caption-gpu3-canary-8a5f','gpu1':'huobao-comfy-gpu1'}
|
|
names=list(workers.values())+[b['normalizer'] for b in bridges]
|
|
def identities():
|
|
return {i['Name']:{'id':i['Id'],'started':i['State']['StartedAt']} for i in json.loads(subprocess.check_output(['docker','inspect',*names],text=True))}
|
|
before=identities()
|
|
for filename in ['comfy_video_cleanup.candidate.py','video_cleanup_api.candidate.py','bridge-cleanup.candidate.py']:
|
|
compile((root/filename).read_text(),filename,'exec')
|
|
for entry in bridges:
|
|
path=pathlib.Path(entry['folder'])/'bridge.py'
|
|
assert hashlib.sha256(path.read_bytes()).hexdigest()==expected,'Bridge changed since review'
|
|
assert not (path.parent/'video_cleanup_api.py').exists(),'Unexpected pre-existing cleanup module'
|
|
unit=pathlib.Path.home()/'.config/systemd/user/huobao-video-cleanup.service'
|
|
assert not unit.exists(),'Unexpected existing service; review first'
|
|
backup=root/('backup-'+datetime.datetime.now().strftime('%Y%m%d-%H%M%S'));backup.mkdir(mode=0o700)
|
|
for entry in bridges:
|
|
folder=pathlib.Path(entry['folder']);shutil.copy2(folder/'bridge.py',backup/(entry['name']+'.py'))
|
|
with sqlite3.connect('file:'+entry['database']+'?mode=ro',uri=True) as source,sqlite3.connect(backup/(entry['name']+'.sqlite3')) as target:source.backup(target)
|
|
shutil.copy2(root/'comfy_video_cleanup.candidate.py',root/'comfy_video_cleanup.py')
|
|
config={'bridges':[{k:b[k] for k in ['name','database','normalizer']}|{'workers':workers} for b in bridges]}
|
|
(root/'config.json').write_text(json.dumps(config,indent=2));(root/'config.json').chmod(0o600)
|
|
unit.parent.mkdir(parents=True,exist_ok=True)
|
|
unit.write_text('[Unit]\nDescription=Huobao verified video cleanup\n\n[Service]\nType=simple\nWorkingDirectory=/home/ps/huobao-video-cleanup\nExecStart=/usr/bin/python3 /home/ps/huobao-video-cleanup/comfy_video_cleanup.py /home/ps/huobao-video-cleanup/config.json\nRestart=on-failure\nRestartSec=10\nTimeoutStopSec=250\nNoNewPrivileges=true\nUMask=0077\n\n[Install]\nWantedBy=default.target\n')
|
|
def request(entry,path,body=None,auth=True):
|
|
cfg=json.loads((pathlib.Path(entry['folder'])/'config.json').read_text())
|
|
headers={'Content-Type':'application/json'}
|
|
if auth:headers['Authorization']='Bearer '+cfg['token']
|
|
req=urllib.request.Request(f"http://192.168.110.111:{entry['port']}"+path,data=None if body is None else json.dumps(body).encode(),headers=headers)
|
|
try:
|
|
with urllib.request.urlopen(req,timeout=15) as response:return response.status
|
|
except urllib.error.HTTPError as error:return error.code
|
|
changed=[]
|
|
try:
|
|
for entry in bridges:
|
|
folder=pathlib.Path(entry['folder']);shutil.copy2(root/'video_cleanup_api.candidate.py',folder/'video_cleanup_api.py')
|
|
shutil.copy2(root/'bridge-cleanup.candidate.py',folder/'bridge.py');changed.append(entry)
|
|
subprocess.run(['docker','restart','--timeout','60',entry['name']],check=True,capture_output=True)
|
|
for attempt in range(15):
|
|
try:
|
|
if request(entry,'/v1/models')==200:break
|
|
except OSError:pass
|
|
time.sleep(1)
|
|
else:raise RuntimeError('Bridge health check failed')
|
|
receipt={'version':1,'index':0,'sha256':'a'*64,'bytes':1}
|
|
endpoint='/v2/video_generation/unknown-cleanup-selftest/cleanup'
|
|
assert request(entry,endpoint,receipt)==404
|
|
assert request(entry,endpoint,receipt,auth=False)==401
|
|
with sqlite3.connect(entry['database']) as db:assert db.execute('select count(*) from video_cleanup_acks').fetchone()[0]==0
|
|
print(entry['name'],'receipt endpoint ready',flush=True)
|
|
subprocess.run(['systemctl','--user','daemon-reload'],check=True,capture_output=True)
|
|
subprocess.run(['systemctl','--user','enable','--now','huobao-video-cleanup.service'],check=True,capture_output=True)
|
|
time.sleep(1)
|
|
assert subprocess.check_output(['systemctl','--user','is-active','huobao-video-cleanup.service'],text=True).strip()=='active'
|
|
assert identities()==before,'Unexpected model/normalizer restart'
|
|
report={'installed':True,'backup':str(backup),'service':'huobao-video-cleanup.service','model_services_not_restarted':True,'existing_jobs_not_cancelled':True,'pending_cleanup_requests':0}
|
|
(root/'deployment.json').write_text(json.dumps(report,indent=2));print(json.dumps(report))
|
|
except Exception:
|
|
subprocess.run(['systemctl','--user','disable','--now','huobao-video-cleanup.service'],capture_output=True)
|
|
for entry in changed:
|
|
folder=pathlib.Path(entry['folder']);shutil.copy2(backup/(entry['name']+'.py'),folder/'bridge.py')
|
|
(folder/'video_cleanup_api.py').unlink(missing_ok=True)
|
|
subprocess.run(['docker','restart','--timeout','60',entry['name']],capture_output=True)
|
|
raise
|