Adds operations/services.py: a fixed ManagedService allow-list (act-runner, act-runner-rust, vmware-host, ci-web) with find_service/service_states/control_service. control_service builds an argument list only (never shell=True), validates the action against ALLOWED_ACTIONS (ValueError->400), refuses stop on restart-only services (ci-web, allow_stop=False) without building a command, and defers ci-web's own restart via 'sudo -n systemd-run --on-active=2 systemctl restart ci-web.service' so the HTTP response returns first.
Route POST /services/{service_id}/{action} in web/routes.py mirrors vms_remove: require_user + require_csrf + two-step confirm gate + one audit entry per stage; unknown id / bad action / disallowed-stop short-circuit to 400 before any subprocess or audit; the vmware-host confirm step warns with the active-VM count. New /services read partial with state-aware Start/Stop/Restart buttons, polled every 15s.
deploy/sudoers.d/ci-web grants ci-runner exact-command NOPASSWD for exactly these nine systemctl lines plus the systemd-run deferral — no wildcards, no bare systemctl, no ci-web stop; setup-web-linux.sh installs it mode 0440 and validates with 'visudo -cf', rolling back on failure. Config gains [web].managed_services override; docs/WEB-UI.md documents the panel + security model.
Gates: 1044 tests pass (46 new security/unit tests), coverage 94.20%, ruff + mypy --strict clean. Not yet deployed: sudoers install + prod-venv reinstall + ci-web restart are a human privilege-boundary step.
132 lines
5.7 KiB
TOML
132 lines
5.7 KiB
TOML
# ci-orchestrator config example.
|
||
#
|
||
# Copy to $CI_ROOT/config.toml or set $CI_CONFIG to the file path.
|
||
# Environment variables (CI_ROOT, CI_TEMPLATES, CI_BUILD_VMS, CI_ARTIFACTS,
|
||
# CI_KEYS, CI_VMRUN_PATH, CI_SSH_KEY_PATH, CI_GUEST_CRED_TARGET) override
|
||
# anything set here.
|
||
#
|
||
# IMPORTANT (TOML rule): top-level keys must appear BEFORE any [section] header.
|
||
# Keys placed after a [section] belong to that section, not to the root table.
|
||
|
||
# Top-level orchestrator settings.
|
||
vmrun_path = "C:/Program Files (x86)/VMware/VMware Workstation/vmrun.exe"
|
||
ssh_key_path = "F:/CI/keys/ci_linux"
|
||
guest_cred_target = "BuildVMGuest"
|
||
|
||
# --------------------------------------------------------------- Windows host
|
||
[paths]
|
||
root = "F:/CI"
|
||
templates = "F:/CI/Templates"
|
||
build_vms = "F:/CI/BuildVMs"
|
||
artifacts = "F:/CI/Artifacts"
|
||
keys = "F:/CI/keys"
|
||
|
||
# Phase B (Linux host) — uncomment after migration.
|
||
# [paths]
|
||
# root = "/var/lib/ci"
|
||
# templates = "/var/lib/ci/templates"
|
||
# build_vms = "/var/lib/ci/build-vms"
|
||
# artifacts = "/var/lib/ci/artifacts"
|
||
# keys = "/etc/ci/keys"
|
||
|
||
# Phase C hook: backend selector. Only "workstation" is implemented in Phase A.
|
||
[backend]
|
||
type = "workstation"
|
||
|
||
# Optional: pre-assign static IPs to build VMs via VMX guestinfo injection.
|
||
# Eliminates VMware-Tools IP-detect polling (20–180 s variance) — the host
|
||
# injects guestinfo.ip-assignment into each cloned VMX before vmrun start,
|
||
# and the guest boot script applies the static IP immediately.
|
||
#
|
||
# Prerequisite: install the guest-side startup script from
|
||
# template/guest-setup/linux/ci-report-ip.sh (Linux guests)
|
||
# template/guest-setup/windows/ci-static-ip.ps1 (Windows guests)
|
||
# and re-take the BaseClean / BaseClean-Linux snapshot.
|
||
#
|
||
# Pool size should equal runner capacity (default 4).
|
||
# Addresses must be outside the VMnet8 DHCP range (check
|
||
# /etc/vmware/vmnet8/dhcpd/dhcpd.conf — typically .128–.254).
|
||
#
|
||
# [ip_pool]
|
||
# addresses = ["192.168.79.201", "192.168.79.202",
|
||
# "192.168.79.203", "192.168.79.204"]
|
||
# netmask = "255.255.255.0"
|
||
# gateway = "192.168.79.2" # VMnet8 gateway; leave empty to skip default route
|
||
# lease_file = "/var/lib/ci/ip-pool.json"
|
||
|
||
# ------------------------------------------------- cargo dependency cache
|
||
# Compiled Rust dependency artifacts, reused between jobs. Engages only for
|
||
# Linux guests whose build workdir contains a Cargo.lock, so non-Rust jobs
|
||
# need no flag. Workspace crates are stripped before an entry is stored and
|
||
# the job refuses to publish if any survive, so a restored entry cannot carry
|
||
# the code under test.
|
||
#
|
||
# One immutable file per key under `root`; a key is
|
||
# sha256(Cargo.lock) + rustc version + host triple. Deleting entries by hand
|
||
# is always safe -- each one costs exactly one cold build to regenerate.
|
||
#
|
||
# `root` defaults to <paths.root>/cache/cargo and follows CI_ROOT unless set
|
||
# here explicitly. Measured on LinuxRust2404: an entry is ~645 MB and a warm
|
||
# job pays ~8s to skip ~120s of dependency compilation.
|
||
[cargo_cache]
|
||
enabled = true
|
||
# root = "/var/lib/ci/cache/cargo"
|
||
retention_days = 30
|
||
|
||
# --------------------------------------------------------------------- web UI
|
||
# Optional management web UI (`ci web serve`). Disabled unless `enabled = true`.
|
||
# Server-rendered dashboard for VM/host management, auth'd via Gitea OIDC.
|
||
#
|
||
# SECURITY:
|
||
# * `bind_host` defaults to 127.0.0.1 (localhost only). Only set it to a
|
||
# LAN IP explicitly and deliberately -- never "0.0.0.0" (that exposes the
|
||
# dashboard, which can kill VMs and purge caches, to every network the
|
||
# host is reachable from).
|
||
# * `session_secret_cred` and `[web.oidc] client_secret_cred` below are
|
||
# KEYRING KEY NAMES, not the secrets themselves. The actual secret
|
||
# values are never written to this file -- set them with:
|
||
# ci creds set WebSessionSecret
|
||
# ci creds set WebOidcClientSecret
|
||
# (the `_cred` values here just tell the app which keyring entry to read
|
||
# at request time).
|
||
#
|
||
# [web]
|
||
# enabled = true
|
||
# bind_host = "192.168.1.50" # explicit LAN IP; never 0.0.0.0
|
||
# bind_port = 8800
|
||
# audit_log = "/var/lib/ci/logs/web-audit.jsonl"
|
||
# allowed_users = ["simone"] # Gitea usernames allowed to log in
|
||
# session_secret_cred = "WebSessionSecret" # keyring KEY NAME, not the secret
|
||
#
|
||
# Gitea OIDC client registration: Gitea -> Settings -> Applications -> OAuth2,
|
||
# redirect URI must exactly match `redirect_url` below (host+port+path).
|
||
#
|
||
# [web.oidc]
|
||
# issuer = "https://gitea.lan"
|
||
# client_id = "ci-web"
|
||
# redirect_url = "http://192.168.1.50:8800/auth/callback"
|
||
# client_secret_cred = "WebOidcClientSecret" # keyring KEY NAME, not the secret
|
||
# scopes = ["openid", "profile"]
|
||
#
|
||
# Optional: override the default systemd-unit allow-list the dashboard's
|
||
# service-management panel (Stage D) is allowed to start/stop/restart. Leave
|
||
# unset to use the built-in default (act-runner, act-runner-rust,
|
||
# vmware-host, ci-web -- see operations/services.py::MANAGED_SERVICES).
|
||
# A malformed row (missing/wrong-typed key) is skipped, never crashes
|
||
# startup. Every command reaching sudo is an exact-match systemctl/systemd-run
|
||
# invocation approved in deploy/sudoers.d/ci-web -- adding a unit here without
|
||
# also adding its NOPASSWD entries there just means every action for it fails
|
||
# (sudo prompts for a password that is never supplied, exit non-zero).
|
||
#
|
||
# [[web.managed_services]]
|
||
# id = "act-runner"
|
||
# unit = "act-runner.service"
|
||
# label = "Runner"
|
||
# allow_stop = true
|
||
#
|
||
# [[web.managed_services]]
|
||
# id = "ci-web"
|
||
# unit = "ci-web.service"
|
||
# label = "Web UI"
|
||
# allow_stop = false # restart-only -- stopping it would lock the operator out
|