Files

105 lines
5.2 KiB
Python

"""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())