Commit graph

20 commits

Author SHA1 Message Date
1445aaff0a
left4me: wire STEAM_WEB_API_KEY through to web.env
Adds the metadata key default (None — node must override) and pipes it
into web.env.mako so the live-state poller can resolve Steam IDs to
persona names + avatars via GetPlayerSummaries.

ovh.left4me gets the actual key as an encrypted vault secret.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:42:51 +02:00
7ad9bbcec3
left4me: schedule daily workshop-refresh via systemd-timers
Adds a left4me-workshop-refresh entry to the systemd-timers bundle,
firing nightly at 04:00 and invoking the new flask workshop-refresh
CLI that enqueues a refresh_workshop_items job. Owner of the job is
NULL (system-enqueued). The bw worker picks it up under existing
scheduler rules; idempotent against an already-queued/running refresh.

Also extends bundles/systemd-timers to accept an optional
environment_files key so the new unit can pull DATABASE_URL etc.
from /etc/left4me/{host,web}.env.
2026-05-12 10:29:45 +02:00
c6caf2a1cf
left4me: per-node system_cpus set; pin HT siblings on ovh.left4me
Replaces bundle-default system_core_count int with a per-node set of
CPU ids; reactor takes set complement for game cores. ovh.left4me sets
{0, 4} to keep both HT siblings of physical core 0 in system.slice
so games don't share L1/L2 with system work. systemd_units reactor
return inlined.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:20:28 +02:00
1b3f3ecf97
left4me: per-slice AllowedCPUs= driven by system_core_count
First N cores pin system/user/build (inline on owned slices, drop-ins
on upstream system.slice and user.slice via the systemd/units
'<parent>.d/<basename>.conf' convention). Remainder pins
l4d2-game.slice. Reactor raises on hosts with <2 threads or
system_core_count that leaves no cores for games.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:04:35 +02:00
1d30830824
left4me: install steamcmd + drop importability gate on pip_install
Two changes from the same debug session, both prerequisites for
`l4d2ctl install` to work end-to-end on a fresh node:

1) Install steamcmd via tarball under /opt/left4me/steam.
   - dpkg --add-architecture i386 + libc6:i386 + lib32z1 (32-bit deps;
     bw pkg_apt translates _ to : at install time, hence libc6_i386)
   - curl|tar one-shot, guarded by `test -x steamcmd.sh`
   - LEFT4ME_STEAMCMD in host.env so l4d2host invokes by absolute path
     (mirrors the old bundles/left4dead2/files/setup approach; avoids
     the dirname-$0 trap that bites when steamcmd is reached via a
     PATH symlink)

2) Drop the `unless` on left4me_pip_install. The gate checked
   importability of l4d2host/l4d2web, which is too weak a proxy for
   install state: adding [project.scripts] to pyproject.toml later
   wouldn't be picked up if the package was already importable from a
   prior `pip install -e`. Cost is ~2s/apply for a no-op pip
   resolution — not enough to keep the gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:46:45 +02:00
149ce6c870
left4me: use https git URL so bw clones locally per-apply
bw's git_deploy.py:103 falls into a per-apply temp clone path when the
repo URL contains '://' (HTTPS, ssh://, …). Without that, it requires
a static git_deploy_repos map file pointing at a long-lived local
clone — which is the wrong shape for left4me, where the source of
truth is git.sublimity.de.

Switching the default to the HTTPS URL means anyone with the bundle
gets a working clone-from-source on `bw apply`, no operator-side
mirror map required.

Note: the host will pull whatever is pushed to git.sublimity.de
master. Push local commits before applying.
2026-05-10 18:49:10 +02:00
5d69180466
left4me: terse bundle-membership asserts 2026-05-10 18:34:09 +02:00
7d3554f8a5
left4me: split derived_from_domain into one reactor per consumer
Each reactor now scopes to a single downstream bundle:
  nginx_vhosts    -> nginx/vhosts
  nftables_input  -> nftables/input

Easier to grep "what writes nginx/vhosts" and harder to accidentally
couple unrelated keys together. Same merged metadata.
2026-05-10 18:33:11 +02:00
fc66267656
left4me: reuse nginx bundle's auto-monitoring via check_path
bundles/nginx/metadata.py:91-104 already creates a monitoring/services
entry per nginx/vhost using the vhost's check_protocol/check_path. Set
check_path: '/health' on the left4me vhost so the auto-check hits the
Flask health endpoint, drop the explicit monitoring/services/left4me-web
block from this reactor.

Net effect: same curl command lands in monitoring as before, but the
service name is now 'left4.me' (the hostname, per the nginx reactor's
naming convention) instead of 'left4me-web'.
2026-05-10 18:31:52 +02:00
758660b131
left4me: drop redundant letsencrypt/domains from reactor
bundles/nginx/metadata.py auto-populates letsencrypt/domains from
nginx/vhosts.keys(). Declaring it again in the left4me reactor was a
no-op duplication. Removed; bw metadata still shows the same merged
state (left4.me with reload: [nginx]).
2026-05-10 18:29:15 +02:00
90f14b69e4
left4me: pull node-agnostic metadata into the bundle
Nodes should only carry node-specific metadata. Previously each node
running left4me had to declare git_url, git_branch, secret_key, plus
nginx vhost / letsencrypt / monitoring / nftables-input blocks for
every game port. All of those are derivable from one truly node-
specific value: the domain.

Move into the bundle:
  - git_url + git_branch as defaults (override per-node only if needed).
  - secret_key as a per-node vault-derived value
    (random_bytes_as_base64_for f'{node.name} left4me secret_key',
    same convention as postgresql/mosquitto/etc.).
  - backup/paths defaults (set-merged with backup group / node paths).

Add a `derived_from_domain` reactor that reads left4me/domain and
emits:
  - nginx/vhosts/<domain> proxying 127.0.0.1:8000
  - letsencrypt/domains/<domain>
  - monitoring/services/left4me-web (curl /health)
  - nftables/input rules for the configured port range
    (defaults 27015-27115, derived from left4me/port_range_*).

Net effect: a node opting into left4me declares only
  metadata.left4me.domain = 'whatever.tld'
plus the universal node-level stuff (id, vm/cores, network, …).
2026-05-10 18:23:34 +02:00
f9bf289ef0
left4me: assert nftables + systemd bundle membership
Catches misconfiguration at bw test time if a node attaches left4me
without those two bundles. Both contribute load-bearing metadata
materializers (nftables/output rules; systemd/units → unit files).
2026-05-10 18:06:35 +02:00
a8fc3f2298
left4me: fix bundle defects surfaced by real-node validation
Three issues caught once `bw test ovh.left4me` ran with the bundle
actually attached (vs. the earlier `bw test` with no node opting in,
which only checks parsing):

1. systemd_services + nftables_output reactors didn't read any metadata.
   bw rejects this with "did not request any metadata, you might want
   to use defaults instead". Both contributions are static, so they
   belong in `defaults` — moved.

2. git_deploy:/opt/left4me/src triggered action:left4me_create_venv,
   but create_venv lacked `triggered: True`. bw enforces that any
   action in a triggers list must be `triggered: True`. Removed
   create_venv from the trigger list — it's gated by `unless` for
   idempotency and doesn't need to refire on git updates anyway
   (the venv persists). pip_install stays in triggers so editable
   installs pick up new code.
2026-05-10 18:05:38 +02:00
c82737b162
left4me: contribute uid-based DSCP/priority marks to nftables/output
Replaces the per-app inet left4me_mark table from
deploy/files/usr/local/lib/left4me/nft/left4me-mark.nft with two rules
in the central bundles/nftables/ inet filter table's output chain.
Same selectors (skuid left4me + l4proto udp), same actions (DSCP EF +
priority 6) for both v4 and v6.
2026-05-10 17:53:17 +02:00
b1edcac3c7
left4me: enable+start left4me-web.service via systemd/services
The server@ template intentionally has no svc_systemd entry — instances
are started on-demand by the web app through the left4me-systemctl
helper. Slices are activated implicitly when units use Slice=.
2026-05-10 17:49:50 +02:00
72da6c0a8d
left4me: pin EnvironmentFile order via tuples (was sets)
Sets in libs/systemd.py:18 are sorted alphabetically. The current
output is correct by accident — host.env < web.env, host.env < /var.
Adding a third path later would silently reorder. Tuples preserve
insertion order; generate_unitfile() iterates them the same way.

Environment (HOME=, PATH=) stays a set: each line is an independent
KEY=VALUE assignment, order is irrelevant.
2026-05-10 17:48:03 +02:00
6965441e9a
left4me: emit server@ template + game/build slice units
Translates the remaining three unit files from left4me/deploy/files/.
Server template carries the full hardening + cgroup/IO/Mem keys
verbatim. Slices need the bundles/systemd .slice support added in
prior commit.
2026-05-10 17:43:25 +02:00
6bf46ce9a4
left4me: emit left4me-web.service via systemd/units reactor
Translates left4me/deploy/files/usr/local/lib/systemd/system/left4me-web.service
into a Python dict consumed by bundles/systemd/. Two changes vs. the
shell-deploy unit:
  - --bind 0.0.0.0:8000 -> 127.0.0.1:8000 (nginx terminates TLS in front)
  - workers/threads are templated from left4me/gunicorn_{workers,threads}
    (defaults: 1 worker + 32 threads — same as the static unit)
2026-05-10 17:38:15 +02:00
def010c976
left4me: git_deploy + venv/pip/alembic/seed action chain
Mirrors deploy-test-server.sh:233-242 + :329-333. Single pip command
installs both editable packages (l4d2host + l4d2web) from the same
checkout. Alembic and seed-overlays run as the left4me user with
JOB_WORKER_ENABLED=false sourced from web.env.
2026-05-10 17:32:19 +02:00
7547d041a2
left4me: scaffold bundle (items/metadata/README stubs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:05:13 +02:00