105 lines
5.2 KiB
Python
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())
|