Compare commits

...

4 commits

Author SHA1 Message Date
9fbd84c3b5
left4me: tighten host.env to 0640 root:left4me
Both env files now follow the same pattern: root owns the config so the
service user can't overwrite its own config, group=left4me so the
sudo -u left4me alembic + seed-overlays actions can source the file
(they failed with 'permission denied' when group=root and mode=0640).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:57:21 +02:00
1039e23671
left4me: prefix steam_web_api_key vault value with !decrypt:
Without !decrypt: the encrypt$… string is rendered as a literal into
web.env, which then surfaces as 403 Forbidden from the Steam Web API
(because the URL key parameter contains "encrypt$gAAA…" instead of the
actual API key). Matches the existing pattern used by every other
encrypted secret in this repo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 22:57:21 +02:00
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
5 changed files with 41 additions and 2 deletions

View file

@ -5,3 +5,4 @@ JOB_WORKER_THREADS=${node.metadata.get('left4me/job_worker_threads')}
SESSION_COOKIE_SECURE=true
LEFT4ME_PORT_RANGE_START=${node.metadata.get('left4me/port_range_start')}
LEFT4ME_PORT_RANGE_END=${node.metadata.get('left4me/port_range_end')}
STEAM_WEB_API_KEY=${node.metadata.get('left4me/steam_web_api_key')}

View file

@ -111,9 +111,15 @@ files = {
'/etc/left4me/host.env': {
'source': 'etc/left4me/host.env.mako',
'content_type': 'mako',
'mode': '0644',
'mode': '0640',
'owner': 'root',
'group': 'root',
# group=left4me so the alembic + seed-overlays actions (which run as
# `sudo -u left4me sh -c '. /etc/left4me/host.env'`) can source it.
# Same pattern as web.env below.
'group': 'left4me',
'needs': [
'group:left4me',
],
},
'/etc/left4me/web.env': {
'source': 'etc/left4me/web.env.mako',

View file

@ -1,5 +1,6 @@
assert node.has_bundle('nftables')
assert node.has_bundle('systemd')
assert node.has_bundle('systemd-timers')
defaults = {
@ -11,6 +12,12 @@ defaults = {
'gunicorn_workers': 1,
'gunicorn_threads': 32,
'job_worker_threads': 4,
# Steam Web API key for the live-state panel's GetPlayerSummaries
# lookups (persona names + avatars). Empty default — nodes override
# in their own metadata with the actual key. If left empty in prod,
# the live-state panel still works but falls back to RCON in-game
# names and placeholder avatars.
'steam_web_api_key': '',
# Whole 27000-block: covers Steam's defaults (27015 game, 27005
# client/RCON) plus headroom for ad-hoc ports without further
# nftables changes. Mirrored into LEFT4ME_PORT_RANGE_{START,END}
@ -76,6 +83,28 @@ defaults = {
'/etc/left4me',
},
},
'systemd-timers': {
# Daily re-fetch of Steam Workshop metadata + .vpk downloads for any
# item whose author published an update. The CLI just inserts a
# `refresh_workshop_items` job; the web worker picks it up next.
# Idempotent — a re-fire while a refresh is already queued/running
# is a no-op (see l4d2web/cli.py:workshop_refresh).
'left4me-workshop-refresh': {
'command': '/opt/left4me/.venv/bin/flask --app l4d2web.app:create_app workshop-refresh',
'when': '*-*-* 04:00:00',
'persistent': True,
'user': 'left4me',
'working_dir': '/opt/left4me/src',
'environment_files': (
'/etc/left4me/host.env',
'/etc/left4me/web.env',
),
'after': {
'network-online.target',
'left4me-web.service',
},
},
},
}

View file

@ -44,6 +44,8 @@ def systemd(metadata):
units[f'{name}.service']['Service']['KillMode'] = config['kill_mode']
if config.get('RuntimeMaxSec'):
units[f'{name}.service']['Service']['RuntimeMaxSec'] = config['RuntimeMaxSec']
if config.get('environment_files'):
units[f'{name}.service']['Service']['EnvironmentFile'] = config['environment_files']
services[f'{name}.timer'] = {}

View file

@ -47,6 +47,7 @@
# /sys/devices/system/cpu/cpu0/topology/thread_siblings_list).
# Keeps system work off the physical cores running game ticks.
'system_cpus': {0, 4},
'steam_web_api_key': '!decrypt:encrypt$gAAAAABqA2whFHIw95XJcU9l8oWG-Lwe1ZQbYKDXa1iRI3Oopg3LZIgr--yksABXnKwfB2KIKM9y8o0hhIcUri7JEJjQvgh4IoG0J-IRPBEG56kiW5J4DKi8wW0ks-jeV7lZiW_j9o0z',
},
},
}