Add Linux deployment installer, supervised services and readiness checks

This commit is contained in:
Nico committed 2026-10-22 22:56:33 +02:00
1 parent 203e765992
commit ca4ab48fdc
15 files changed
+578 -4

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
"""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())
+3
View File
@@ -0,0 +1,3 @@
# Read by systemd as root, not by the web user. chmod 0600, owner root.
# Configure once; never commit a filled copy.
# VBA_INTAKE_TOKEN="replace-with-a-random-32-to-256-character-base64url-token"
+25
View File
@@ -0,0 +1,25 @@
[api]
workspace = "/var/lib/viano-bug-analysis"
host = "127.0.0.1"
port = 8080
[auth]
users_database = "/etc/viano-bug-analysis/auth/users.sqlite3"
session_seconds = 3600
[[projects]]
id = "viano-core"
type = "wow"
profile = "vcore"
adapter = "vcore"
planning_mode = "joint"
[projects.settings]
project_config = "project.toml"
llm_config = "llm.toml"
investigation_config = "investigation.bounded.toml"
budget_config = "budget.human-tests.toml"
[[clients]]
id = "wow-capture-worker"
token_env = "VBA_INTAKE_TOKEN"
projects = ["viano-core"]
+2
View File
@@ -0,0 +1,2 @@
[safe]
directory = /srv/viano-bug-analysis/viano-core
+29
View File
@@ -0,0 +1,29 @@
# Replace domain and certificate paths. Install manually after nginx -t.
# Root path on its own HTTPS hostname; upstream must remain loopback-only.
server {
listen 80;
server_name buganalysis.example.org;
return 308 https://buganalysis.example.org$request_uri;
}
server {
listen 443 ssl;
server_name buganalysis.example.org;
ssl_certificate /etc/letsencrypt/live/buganalysis.example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/buganalysis.example.org/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 768m;
client_body_timeout 120s;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_request_buffering off;
proxy_read_timeout 180s;
proxy_send_timeout 180s;
proxy_cache off;
proxy_no_cache 1;
}
}
+3
View File
@@ -0,0 +1,3 @@
# Read by systemd as root. chmod 0600, owner root. No shell export statements.
# GEMINI_API_KEY="your-cloud-key"
# VIANO_DB_PASSWORD="your-read-only-database-password"
+7
View File
@@ -0,0 +1,7 @@
[worker]
intake_config = "api.toml"
poll_seconds = 5
error_retry_seconds = 60
max_inbox_per_pass = 20
min_free_bytes = 536870912
telemetry_config = "telemetry.toml"
+36
View File
@@ -0,0 +1,36 @@
[Unit]
Description=Viano BugAnalysis web and intake API
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=buganalysis
Group=buganalysis
WorkingDirectory=/var/lib/viano-bug-analysis
EnvironmentFile=/etc/viano-bug-analysis/api.env
Environment=PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
ExecStartPre=/opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.deployment check --role api --config /etc/viano-bug-analysis/api.toml
ExecStart=/opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.api serve --config /etc/viano-bug-analysis/api.toml
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStartSec=60
TimeoutStopSec=180
UMask=0077
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/viano-bug-analysis
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
+18 -3
View File
@@ -2,21 +2,36 @@
Description=Viano BugAnalysis analysis worker
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=buganalysis
Group=buganalysis
WorkingDirectory=/opt/viano-bug-analysis
WorkingDirectory=/var/lib/viano-bug-analysis
EnvironmentFile=/etc/viano-bug-analysis/worker.env
ExecStart=/opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.worker run --config /opt/viano-bug-analysis/config/worker.local.toml
Restart=on-failure
Environment=PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 GIT_CONFIG_GLOBAL=/etc/viano-bug-analysis/gitconfig GIT_TERMINAL_PROMPT=0
ExecStartPre=/opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.deployment check --role worker --config /etc/viano-bug-analysis/worker.toml
ExecStart=/opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.worker run --config /etc/viano-bug-analysis/worker.toml
Restart=always
RestartSec=10
KillSignal=SIGTERM
# Finish the current checkpointed dispatch; operator can still force-stop.
TimeoutStopSec=infinity
TimeoutStartSec=300
UMask=0077
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/viano-bug-analysis
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target