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

+2
View File
@@ -17,6 +17,8 @@ The [continuous analysis worker](docs/analysis-worker.md) consumes that inbox,
routes cases to configured projects and resumes checkpointed analysis after restart.
The [web console](docs/web-console.md) shows active workers, case progress and token
charts, with corrections accounted for separately, and accepts new/return submissions.
The [Linux deployment guide](docs/linux-deployment.md) covers systemd services,
protected credentials, HTTPS and non-billable readiness checks.
Requires Python 3.11 or newer.
+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
+154
View File
@@ -0,0 +1,154 @@
# Linux-Betrieb
API und Analyseworker laufen als getrennte systemd-Dienste. Ein Auftragseingang
weckt keinen neuen Prozess: Der dauerhaft laufende Worker übernimmt Inbox-Aufträge.
Die Weboberfläche ist eine alternative Bedienung desselben Arbeitsablaufs.
Die Installation startet keine Dienste und führt keine Modellanfrage aus.
## Voraussetzungen und Verzeichnisse
Linux mit systemd, Python 3.11 oder neuer inklusive venv/pip, Git und Nginx.
Diese Programme über die Paketverwaltung des Servers installieren. Die aktuelle
Projektvorlage ist für WoW/Vcore eingerichtet; sie ist kein allgemeiner Adapter.
| Pfad | Zweck und Rechte |
| --- | --- |
| `/opt/viano-bug-analysis` | Root-eigener Checkout und virtuelle Python-Umgebung; Dienst darf nur lesen |
| `/etc/viano-bug-analysis` | Root-eigene Konfiguration, Gruppe `buganalysis`, Verzeichnis 0750, Dateien 0640 |
| `/etc/viano-bug-analysis/*.env` | Zugangsdaten, root:buganalysis 0600; systemd liest sie vor dem Benutzerwechsel |
| `/etc/viano-bug-analysis/auth/users.sqlite3` | Root-verwaltete Nutzer, Gruppe `buganalysis` darf lesen |
| `/var/lib/viano-bug-analysis` | Dienst-eigene Fallakten, Laufzeitdaten und Websessions |
| `/srv/viano-bug-analysis/viano-core` | Root-eigener Source-Checkout, Dienst hat Leserechte |
Die env-Dateien sind durch Betriebssystemrechte geschützte Klartextdateien, kein
verschlüsselter Tresor. Sie gehören nicht ins Repository oder in Diagnosepakete.
API und Worker erhalten unterschiedliche Dateien: Nur der Worker erhält Cloud-
und DB-Zugangsdaten. Die Diensthärtung und EnvironmentFile-Nutzung folgen der
[systemd-Dokumentation](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html).
## Installation
Den vertrauenswürdigen BugAnalysis-Checkout als root unter
`/opt/viano-bug-analysis` ablegen. Code und Unterverzeichnisse dürfen nicht für
andere Benutzer schreibbar sein. Dann:
```sh
cd /opt/viano-bug-analysis
python3 deploy/install.py --dry-run
sudo python3 deploy/install.py
```
Der Installer legt den Dienstbenutzer `buganalysis`, Konfigurationen, venv und
Units an. Er installiert die Python-Abhängigkeiten und lädt systemd neu.
Bestehende Zielkonfigurationen und Units werden erhalten. Updates an bereits
installierten Units daher bewusst mit den versionierten Vorlagen abgleichen.
Bei Installationsfehlern bleiben vorhandene Dateien erhalten; erneutes Ausführen
setzt die Einrichtung fort. Der Installer verändert keine bestehende Datenbank.
## Einmalige Konfiguration
Mit `sudoedit` die Dateien unter `/etc/viano-bug-analysis` bearbeiten:
1. `project.toml`: DB-Adresse/Port, Datenbank und nur lesenden Benutzer einstellen,
tatsächliche Snapshotkennung und vollständigen Source-Commit eintragen.
Die ausgelieferten Null-Commit- und `CONFIGURE_ME`-Werte sind Platzhalter.
2. Den Vcore-Checkout als root unter `/srv/viano-bug-analysis/viano-core` anlegen;
der konfigurierte Commit muss lokal vorhanden und für `buganalysis` lesbar sein.
Der Dienst lädt keine Commits nach. `gitconfig` vertraut ausschließlich diesem
konkreten Root-Checkout, nicht beliebigen Repositories.
3. `worker.env`: `GEMINI_API_KEY=...` und `VIANO_DB_PASSWORD=...` eintragen.
Keine `export`-Anweisungen verwenden. Für Sonderzeichen gelten die
EnvironmentFile-Quotingregeln von systemd. Werte nicht als Shellargumente übergeben.
4. `api.env`: `VBA_INTAKE_TOKEN=...` für den Erfassungsworker setzen
(32–256 Zeichen aus Buchstaben, Ziffern, `_` und `-`). Dies ist ein separater
Integrationsschlüssel, kein Gemini-Key. Wird keine externe API-Integration
benötigt, den zugehörigen `[[clients]]`-Block aus `api.toml` entfernen.
5. `llm.toml`: gewünschtes Modell, Anbieter und Cacheeinstellungen kontrollieren.
Investigation-, Budget- und Telemetrie-Dateien sind separat konfigurierbar.
Die DB-Verbindung benötigt einen Benutzer mit ausschließlich passenden
SELECT-Rechten auf die freigegebenen World-Tabellen. Diese Rechte legt der
DB-Administrator an; BugAnalysis vergibt keine Grants. Der Readiness-Test belegt
Lesbarkeit, nicht die Abwesenheit zusätzlicher Schreibrechte. Für entfernte DBs
einen geschützten Netzwerkweg verwenden: Der aktuelle DB-Provider konfiguriert
selbst kein TLS. Zugang zu Produktionsdaten ist für die lokale Entwicklung
weiterhin nicht erforderlich.
Webnutzer anlegen; das Passwort wird verdeckt abgefragt:
```sh
sudo /opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.api create_user NAME --config /etc/viano-bug-analysis/api.toml --service-group buganalysis
```
Nutzerverwaltung bleibt root vorbehalten. Siehe [Nutzerverwaltung](user-accounts.md).
Keine Nutzer werden automatisch angelegt. Nach der Installation besitzen die
env-Dateien Modus 0600; diese Rechte nach manuellen Änderungen erhalten.
## Kostenfreie Bereitschaftsprüfung
Die Prüfung unter der tatsächlichen Dienstidentität samt env-Datei ausführen:
```sh
sudo systemd-run --wait --pipe --collect --property=User=buganalysis --property=Group=buganalysis --property=EnvironmentFile=/etc/viano-bug-analysis/api.env /opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.deployment check --role api --config /etc/viano-bug-analysis/api.toml
sudo systemd-run --wait --pipe --collect --property=User=buganalysis --property=Group=buganalysis --property=EnvironmentFile=/etc/viano-bug-analysis/worker.env --setenv=GIT_CONFIG_GLOBAL=/etc/viano-bug-analysis/gitconfig /opt/viano-bug-analysis/.venv/bin/python -m viano_bug_analysis.deployment check --role worker --config /etc/viano-bug-analysis/worker.toml
```
Erwartet: `READY`, `model_requests: 0`. Geprüft werden Konfiguration,
Zugangsdaten-Präsenz, Speicherreserve, Schreibbarkeit der Laufzeitablage,
Webnutzer/Sessions beziehungsweise Git-Objekte und die freigegebenen DB-Schemata.
Cloud-Key-Gültigkeit und Modellverfügbarkeit sind damit noch nicht getestet.
`--offline` überspringt Git-/DB-Zugriffe und meldet nur `CONFIG_READY`.
Fehlerausgaben enthalten kontrollierte Fehlercodes statt Zugangsdaten.
## HTTPS und Start
Die API bleibt auf `127.0.0.1:8080`. `nginx.conf.example` enthält eine
Reverse-Proxy-Vorlage: Domain und Zertifikatpfade anpassen, gültiges Zertifikat
bereitstellen und als Nginx-Site installieren. Die Vorlage übernimmt die
Forwarded-Header kontrolliert; die API vertraut ihnen ausschließlich vom
Loopback-Proxy. So verwenden Anmeldung und Cookies das öffentliche HTTPS-Schema.
Die Proxyparameter sind in der
[Nginx-Dokumentation](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) beschrieben.
Vor dem ersten Start auf dem Linux-Zielsystem prüfen:
```sh
sudo systemd-analyze verify /etc/systemd/system/viano-bug-analysis-api.service /etc/systemd/system/viano-bug-analysis-worker.service
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl enable --now viano-bug-analysis-api.service viano-bug-analysis-worker.service
sudo systemctl status viano-bug-analysis-api.service viano-bug-analysis-worker.service
```
Sobald der Worker aktiviert ist, kann er vorhandene Aufträge verarbeiten und
kostenpflichtige Modellaufrufe senden. Erst mit leerem Eingang starten, dann einen
kontrollierten Feldtest über die Übergabe-API oder Weboberfläche übergeben.
Anmeldung, Auftragsannahme, Fortschritt, menschliche Testanfrage und Outbox dabei
gemeinsam prüfen. Die automatisierte Einrichtung ersetzt diesen Feldtest nicht.
## Laufender Betrieb
Die Dienste starten beim Booten und nach unerwartetem Prozessende erneut.
Fehlende Startvoraussetzungen werden nach zehn Sekunden erneut geprüft.
Externe DB und Sourcebereitstellung werden nicht von diesen Units installiert.
API und Worker können unabhängig voneinander laufen.
```sh
sudo journalctl -u viano-bug-analysis-api -u viano-bug-analysis-worker -f
sudo systemctl stop viano-bug-analysis-worker
```
Der Worker beendet beim Stoppen den aktuellen Dispatch kontrolliert; seine Unit
setzt dafür kein hartes Stop-Zeitlimit. Ein Stop kann daher während einer
langlaufenden Anfrage dauern. Für Code-/Profiländerungen den Worker zuerst
anhalten. Profilbindungen laufender Fälle nicht beiläufig ändern: Unterschiede
werden als Aufmerksamkeit erfordernder Zustand behandelt, Budgets nicht erneuert.
Codeänderungen danach in die root-eigene venv installieren und Dienste neu starten.
Backups umfassen die Fallablage, Konfiguration einschließlich geschützter Secrets
und Nutzer-SQLite. Für eine einfache konsistente Dateisicherung beide Dienste
anhalten. Sessions sind keine dauerhaft nötigen Fachdaten. Produktive Fallaufbewahrung
separat festlegen; das 20-GB-Entwicklungslimit ist kein Produktionslimit.
Unter Windows bleibt die vorhandene PowerShell-Umgebungsvariable für Entwicklung
unterstützt. Dort installiert dieser Installer keine Dienste; `--dry-run` ist möglich.
@@ -0,0 +1,31 @@
# Linux-Betriebspaket – lokale Prüfung 2026-10-22
Ziel: getrennte API-/Worker-Dienste, persistente geschützte Zugangsdaten,
Installation ohne automatische Aktivierung und Bereitschaftsprüfung ohne LLM.
Änderungen: `deploy/install.py`, Linux-Konfigurationsvorlagen, beide systemd-Units,
`viano_bug_analysis.deployment` sowie explizite Loopback-Proxy-Vertrauensgrenze
beim API-Start. Anleitung: `docs/linux-deployment.md`.
Validierung auf Windows mit Python aus `.venv`:
```text
python -m pytest tests/test_deployment.py tests/test_web_auth.py tests/test_worker.py tests/profiles/vcore/test_worker_intake.py -q
31 passed, 1 dependency deprecation warning
git diff --check: keine Whitespacefehler
```
Prüfumfang: Installer-Dry-Run ohne Seiteneffekte, TOML-Vorlagen und Dateiverweise,
getrennte env-Dateien und Unit-Härtung, API-Start mit/ohne erforderliche
Zugangsdaten, Worker-Leseprüfung mit kontrollierten Git-/DB-Testdoubles,
Offline-Modus, keine Modellanfrage, sichere Fehlerausgabe, HTTPS-Login-Cookie
über Uvicorns Proxy-Middleware, bestehende Nutzer-/Worker-/Intake-Tests.
Vor den Tests: Trial-Workspaces 16.939.129.428 Bytes, kuratiertes Fehlerkorpus
2.471.041 Bytes. Nur kleine synthetische Tempdateien; keine Paketkopien oder neuen
LLM-Trials. Das Entwicklungsbudget von 20 GB bleibt eingehalten.
Keine Cloud-/DB-Zugangsdaten im Bericht. Keine realen LLM-Aufrufe oder neuen
Abrechnungskosten. Keine Installation, Dienstaktivierung oder Serveränderung.
Native systemd-/Nginx-Prüfung und produktiver End-to-End-Test stehen aus:
Auf dieser Windows-Umgebung ist kein nutzbares Linux/systemd vorhanden.
+2 -1
View File
@@ -47,7 +47,8 @@ def main():
return 0
import uvicorn
from .app import create_app
uvicorn.run(create_app(config), host=config.host, port=config.port, access_log=False)
uvicorn.run(create_app(config), host=config.host, port=config.port, access_log=False,
proxy_headers=True, forwarded_allow_ips='127.0.0.1')
return 0
except APIError as error:
print(json.dumps({'status': 'ERROR', 'code': error.code}))
+68
View File
@@ -0,0 +1,68 @@
"""Non-billable readiness checks for supervised Linux services."""
import argparse
import json
import re
import shutil
import tempfile
from .api.config import APIConfig,APIError,directory
from .api.sessions import Sessions
from .api.users import database
from .worker.config import WorkerConfig,load_project
def check(role,path,*,offline=False,loader=load_project):
checks=[]
try:
if role not in {'api','worker'}:raise APIError('DEPLOY_ROLE')
worker=WorkerConfig.load(path) if role=='worker' else None
config=worker.intake if worker else APIConfig.load(path)
if config.host not in {'127.0.0.1','::1','localhost'}:
raise APIError('DEPLOY_LOOPBACK_REQUIRED')
workspace=directory(config.workspace)
reserve=worker.min_free_bytes if worker else config.limits.min_free_bytes
if shutil.disk_usage(workspace).free<reserve:raise APIError('DEPLOY_STORAGE_LOW')
runtime=directory(workspace/'runtime')
with tempfile.TemporaryFile(dir=runtime) as probe:probe.write(b'readiness')
checks.append('workspace_writable_and_space_available')
if role=='api':
tokens=config.tokens()
if len(tokens)!=len(config.clients):raise APIError('DEPLOY_MACHINE_CREDENTIALS')
sessions=Sessions(config)
if not sessions.users.path.is_file():raise APIError('DEPLOY_USERS_MISSING')
with database(sessions.users.path,readonly=True) as db:
if not db.execute('SELECT 1 FROM users LIMIT 1').fetchone():raise APIError('DEPLOY_USERS_EMPTY')
checks+=['user_database_readable','session_database_ready','machine_credentials_configured']
else:
for project in config.projects:
gateway,policies,_=loader(project) # preflight only; never generate()
research=policies.get('project_research')
if not research or not research.source or not research.database:
raise APIError('DEPLOY_RESEARCH_REQUIRED')
checks.append('profile_and_cloud_credentials_configured')
if not offline:
research.source.inventory() # pinned Git objects, no fetch or checkout
for table in research.database.tables:
research.database.query({'kind':'database.schema','table':table})
checks+=['pinned_source_readable','allowed_database_schemas_readable']
return {'status':'CONFIG_READY' if offline else 'READY','role':role,'checks':checks,
'research_tested':role=='worker' and not offline,'cloud_api_tested':False,'model_requests':0}
except Exception as error:
code=getattr(error,'code','DEPLOY_CHECK_FAILED')
if not isinstance(code,str) or not re.fullmatch('[A-Z][A-Z0-9_]{0,79}',code):code='DEPLOY_CHECK_FAILED'
return {'status':'NOT_READY','role':role,'checks':checks,'code':code,'model_requests':0}
def main(argv=None):
parser=argparse.ArgumentParser(description='Check service dependencies; never invokes an LLM')
parser.add_argument('command',choices=['check'])
parser.add_argument('--role',choices=['api','worker'],required=True)
parser.add_argument('--config',required=True)
parser.add_argument('--offline',action='store_true',help='skip Git object and database access')
args=parser.parse_args(argv)
result=check(args.role,args.config,offline=args.offline)
print(json.dumps(result))
return 0 if result['status'] in {'READY','CONFIG_READY'} else 1
if __name__=='__main__':raise SystemExit(main())
+94
View File
@@ -0,0 +1,94 @@
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import tomllib
from types import SimpleNamespace
from viano_bug_analysis import deployment
from viano_bug_analysis.api.config import APIConfig, Project, Client
from viano_bug_analysis.api.sessions import Sessions
from viano_bug_analysis.worker.config import WorkerConfig
ROOT=Path(__file__).resolve().parents[1]
def installer():
spec=importlib.util.spec_from_file_location('linux_install',ROOT/'deploy/install.py')
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
return module
def test_install_plan_and_templates():
result=subprocess.run([sys.executable,str(ROOT/'deploy/install.py'),'--dry-run'],capture_output=True,text=True,check=True)
assert json.loads(result.stdout)['services_started'] is False
files=installer().templates()
for name,text in files.items():
if name.endswith('.toml'):tomllib.loads(text)
project=tomllib.loads(files['project.toml'])
assert project['database']['password_env']=='VIANO_DB_PASSWORD'
assert 'password_file' not in project['database']
assert project['source']['commit']=='0'*40
assert tomllib.loads(files['api.toml'])['auth']['users_database'].startswith('/etc/')
def test_api_ready_and_missing_machine_credentials(tmp_path,monkeypatch):
config=APIConfig(tmp_path,(Project('p','wow','vcore'),),())
monkeypatch.setattr(deployment.APIConfig,'load',lambda path:config)
assert deployment.check('api','unused')['status']=='NOT_READY'
Sessions(config).users.manage('create_user','alice','example password')
assert deployment.check('api','unused')['status']=='READY'
config=APIConfig(tmp_path,config.projects,(Client('m','VBA_TEST_MISSING',('p',)),))
monkeypatch.delenv('VBA_TEST_MISSING',raising=False)
assert deployment.check('api','unused')['code']=='DEPLOY_MACHINE_CREDENTIALS'
def test_worker_checks_only_read_dependencies(tmp_path,monkeypatch):
config=APIConfig(tmp_path,(Project('p','wow','vcore'),),())
monkeypatch.setattr(deployment.WorkerConfig,'load',lambda path:SimpleNamespace(intake=config,min_free_bytes=1))
calls=[]
research=SimpleNamespace(source=SimpleNamespace(inventory=lambda:calls.append('git')),
database=SimpleNamespace(tables=['creature'],query=lambda query:calls.append(query)))
def loader(project):return object(),{'project_research':research},'hash'
result=deployment.check('worker','unused',loader=loader)
assert result['status']=='READY' and result['model_requests']==0 and not result['cloud_api_tested']
assert calls==['git',{'kind':'database.schema','table':'creature'}]
calls.clear()
assert deployment.check('worker','unused',offline=True,loader=loader)['status']=='CONFIG_READY'
assert calls==[]
def failing(project):raise RuntimeError('secret-value')
assert 'secret-value' not in json.dumps(deployment.check('worker','unused',loader=failing))
def test_units_keep_secrets_separate_and_code_readonly():
for role in ('api','worker'):
unit=(ROOT/'deploy'/f'viano-bug-analysis-{role}.service').read_text()
assert f'EnvironmentFile=/etc/viano-bug-analysis/{role}.env' in unit
assert f'deployment check --role {role}' in unit
assert 'ProtectSystem=strict' in unit
assert 'ReadWritePaths=/var/lib/viano-bug-analysis' in unit
assert 'Restart=always' in unit and '--once' not in unit
def test_installed_config_references_resolve(tmp_path):
for name,text in installer().templates().items():
(tmp_path/name).write_text(text,encoding='utf-8')
config=WorkerConfig.load(tmp_path/'worker.toml')
assert config.intake.projects[0].planning_mode=='joint'
assert all(path.is_file() for path in config.intake.projects[0].settings.values())
def test_https_proxy_preserves_secure_login_cookie(tmp_path):
from starlette.testclient import TestClient
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from viano_bug_analysis.api.app import create_app
config=APIConfig(tmp_path,(Project('p','wow','vcore'),),())
app=create_app(config)
app.state.sessions.users.manage('create_user','alice','example password')
proxied=ProxyHeadersMiddleware(app,trusted_hosts=['127.0.0.1'])
with TestClient(proxied,client=('127.0.0.1',1234)) as client:
result=client.post('/v1/auth/login',headers={'X-Forwarded-Proto':'https','Origin':'https://testserver'},
json={'username':'alice','password':'example password'})
assert result.status_code==200
assert 'secure' in result.headers['set-cookie'].lower()