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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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}))
|
||||
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user