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.
|
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
|
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.
|
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.
|
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
|
Description=Viano BugAnalysis analysis worker
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=buganalysis
|
User=buganalysis
|
||||||
Group=buganalysis
|
Group=buganalysis
|
||||||
WorkingDirectory=/opt/viano-bug-analysis
|
WorkingDirectory=/var/lib/viano-bug-analysis
|
||||||
EnvironmentFile=/etc/viano-bug-analysis/worker.env
|
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
|
Environment=PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 GIT_CONFIG_GLOBAL=/etc/viano-bug-analysis/gitconfig GIT_TERMINAL_PROMPT=0
|
||||||
Restart=on-failure
|
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
|
RestartSec=10
|
||||||
KillSignal=SIGTERM
|
KillSignal=SIGTERM
|
||||||
# Finish the current checkpointed dispatch; operator can still force-stop.
|
# Finish the current checkpointed dispatch; operator can still force-stop.
|
||||||
TimeoutStopSec=infinity
|
TimeoutStopSec=infinity
|
||||||
|
TimeoutStartSec=300
|
||||||
UMask=0077
|
UMask=0077
|
||||||
NoNewPrivileges=true
|
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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
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
|
return 0
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from .app import create_app
|
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
|
return 0
|
||||||
except APIError as error:
|
except APIError as error:
|
||||||
print(json.dumps({'status': 'ERROR', 'code': error.code}))
|
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