"""Install the fixed Linux service layout. Never enables or starts services.""" import argparse import json import os from pathlib import Path import shutil import subprocess import sys import tomllib ROOT = Path(__file__).resolve().parents[1] CONFIG = Path('/etc/viano-bug-analysis') STATE = Path('/var/lib/viano-bug-analysis') SOURCE = Path('/srv/viano-bug-analysis') def project_template(root=ROOT): text=(root/'config/project.example.toml').read_text(encoding='utf-8') replacements={ 'path = "../service-data/research/viano-core"':'path = "/srv/viano-bug-analysis/viano-core"', 'port = 33079':'port = 3306', 'password_file = "../service-data/research/world-db/reader.local.txt"':'password_env = "VIANO_DB_PASSWORD"', } for old,new in replacements.items(): if text.count(old)!=1:raise ValueError('Deployment template changed; review replacements') text=text.replace(old,new) import re text=re.sub(r'^commit = "[0-9a-f]{40}"$', 'commit = "'+'0'*40+'"',text,flags=re.M) text=re.sub(r'^snapshot_id = .*$', 'snapshot_id = "CONFIGURE_ME"',text,flags=re.M) tomllib.loads(text) return text def templates(root=ROOT): files={name:(root/'deploy/linux'/name).read_text(encoding='utf-8') for name in ('api.toml','worker.toml','gitconfig','nginx.conf.example')} for target,source in { 'llm.toml':'llm.gemini-cache.example.toml', 'investigation.bounded.toml':'investigation.bounded.toml', 'budget.human-tests.toml':'budget.human-tests.toml', 'project-relations.vcore.toml':'project-relations.vcore.toml', 'telemetry.toml':'telemetry.example.toml', }.items():files[target]=(root/'config'/source).read_text(encoding='utf-8') files['project.toml']=project_template(root) for name in ('api','worker'): files[name+'.env']=(root/'deploy/linux'/(name+'.env.example')).read_text(encoding='utf-8') return files def main(argv=None): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--dry-run',action='store_true') args=parser.parse_args(argv) files=templates() if args.dry_run: print(json.dumps({'status':'PLAN','code':'/opt/viano-bug-analysis', 'config':str(CONFIG),'state':str(STATE),'source':str(SOURCE), 'config_files':sorted(files),'existing_files':'preserved','services_started':False})) return 0 if sys.platform!='linux' or os.geteuid()!=0: parser.error('Installation requires Linux and root; use --dry-run elsewhere') if ROOT!=Path('/opt/viano-bug-analysis') or ROOT.is_symlink(): parser.error('Place a root-owned checkout at /opt/viano-bug-analysis first') # Root executes packaging code: reject an untrusted checkout/venv. for base in (ROOT,ROOT/'src',ROOT/'deploy',ROOT/'config',ROOT/'pyproject.toml',ROOT/'.venv'): if not base.exists():continue paths=[base,*base.rglob('*')] if base.is_dir() and base!=ROOT else [base] for path in paths: stat=path.stat() if path.is_symlink() and base!=ROOT/'.venv':raise RuntimeError('Symlink in installation sources') if stat.st_uid!=0 or stat.st_mode&0o022:raise RuntimeError('Installation sources must be root-owned and not group/world writable') def run(*command):subprocess.run(command,check=True) import grp import pwd try:group=grp.getgrnam('buganalysis') except KeyError: run('groupadd','--system','buganalysis');group=grp.getgrnam('buganalysis') try:user=pwd.getpwnam('buganalysis') except KeyError: run('useradd','--system','--gid','buganalysis','--home-dir',str(STATE),'--no-create-home','--shell','/usr/sbin/nologin','buganalysis') user=pwd.getpwnam('buganalysis') if user.pw_uid==0 or user.pw_gid!=group.gr_gid:raise RuntimeError('Unexpected service account identity') for path,uid in ((CONFIG,0),(CONFIG/'auth',0),(STATE,user.pw_uid),(SOURCE,0)): if path.is_symlink():raise RuntimeError('Symlink in deployment directories') path.mkdir(parents=True,exist_ok=True) os.chown(path,uid,group.gr_gid);os.chmod(path,0o750) def install(path,text,mode): if path.is_symlink():raise RuntimeError('Symlink at deployment target') if path.exists():return with path.open('x',encoding='utf-8',newline='\n') as stream:stream.write(text) os.chown(path,0,group.gr_gid);os.chmod(path,mode) for name,text in files.items():install(CONFIG/name,text,0o600 if name.endswith('.env') else 0o640) for role in ('api','worker'): name=f'viano-bug-analysis-{role}.service' install(Path('/etc/systemd/system')/name,(ROOT/'deploy'/name).read_text(encoding='utf-8'),0o644) if not (ROOT/'.venv/bin/python').exists():run(sys.executable,'-m','venv',str(ROOT/'.venv')) run(str(ROOT/'.venv/bin/python'),'-m','pip','install',str(ROOT)+'[api,research]') run('systemctl','daemon-reload') print(json.dumps({'status':'INSTALLED','existing_files':'preserved','services_started':False, 'next':'Configure project, secrets, source checkout, web user and HTTPS; run readiness checks before enabling services'})) return 0 if __name__=='__main__':raise SystemExit(main())