Add Linux deployment installer, supervised services and readiness checks
This commit is contained in:
1 parent
203e765992
commit
ca4ab48fdc
15 files changed
+578
-4
No files matched your search
@@ -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())
|
||||
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,2 @@
|
||||
[safe]
|
||||
directory = /srv/viano-bug-analysis/viano-core
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user