bundlewrap/bundles/left4me
CroneKorkN 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
..
files left4me: install steamcmd + drop importability gate on pip_install 2026-05-10 22:46:45 +02:00
items.py left4me: per-slice AllowedCPUs= driven by system_core_count 2026-05-11 00:04:35 +02:00
metadata.py left4me: per-slice AllowedCPUs= driven by system_core_count 2026-05-11 00:04:35 +02:00
README.md left4me: per-slice AllowedCPUs= driven by system_core_count 2026-05-11 00:04:35 +02:00

left4me

L4D2 game-server management platform: a Flask web UI on gunicorn that provisions per-instance srcds servers via templated systemd units, with kernel-overlayfs layering for shared installations + per-overlay maps, and uid-based DSCP/priority marking on the egress path so CAKE on the external interface prioritizes srcds UDP over bulk traffic.

Metadata

'metadata': {
    'left4me': {
        'domain': 'whatever.tld',  # required — the only per-node knob
        # Everything below is optional and has a sensible default in the
        # bundle. Override per-node only if the default is wrong:
        # 'git_url': 'git@git.sublimity.de:cronekorkn/left4me',
        # 'git_branch': 'master',
        # 'gunicorn_workers': 1,
        # 'gunicorn_threads': 32,
        # 'job_worker_threads': 4,
        # 'port_range_start': 27015,
        # 'port_range_end': 27115,
        # secret_key is auto-derived per node
        # (repo.vault.random_bytes_as_base64_for f'{node.name} left4me secret_key').
    },
},

The bundle's derived_from_domain reactor reads left4me/domain and emits the corresponding nginx/vhosts, letsencrypt/domains, monitoring/services/left4me-web (HTTPS health check), and the game- port nftables/input accept rules. Backup paths (/var/lib/left4me, /etc/left4me) are set-merged into backup/paths from defaults. None of these need to be declared per-node.

What this bundle does

  • Creates system users left4me (uid/gid 980, home /var/lib/left4me, mode 0711) and l4d2-sandbox (uid/gid 981, no home, used by bwrap script-overlay builds).
  • Drops privileged helpers under /usr/local/libexec/left4me/ (left4me-systemctl, left4me-journalctl, left4me-overlay, left4me-script-sandbox) plus a tight sudoers file (validated with visudo -cf before install).
  • git_deploys the left4me repo to /opt/left4me/src, builds a venv at /opt/left4me/.venv, pip install -es both l4d2host and l4d2web, runs alembic upgrade head and flask seed-script-overlays, then enables left4me-web.service.
  • Emits four systemd units via systemd/units metadata (consumed by bundles/systemd/):
    • left4me-web.service — gunicorn on 127.0.0.1:8000 (TLS terminates upstream).
    • left4me-server@.service — per-instance srcds template, started on demand by the web app via the left4me-systemctl helper.
    • l4d2-game.slice / l4d2-build.slice — cgroup slices for the perf-baseline (CPU/IO weights, memory caps).
  • Contributes uid-based DSCP/priority marks for srcds UDP egress to nftables/output (via defaults).

Gotchas

  • Requires bundles/nftables and bundles/systemd on the node. The bundle asserts membership at bw test time. On Debian-13 these ride in via the debian-13 group, so attaching the bundle to a Debian-13 node is enough.
  • left4me-web.service does not have NoNewPrivileges=true. This is intentional — workers sudo the privileged helpers; NoNewPrivileges would block setuid escalation. Per-instance server@.service units do have it.
  • CAKE shaping is configured separately, via network/<iface>/cake on the node (consumed by bundles/network/), not by this bundle.
  • First-run admin user is manual. After bw apply, ssh to the host and bootstrap the admin via the left4me wrapper (it sources the env files, drops to the left4me user, and runs the flask CLI): sudo left4me create-user <username> --admin (prompts for password via the flask CLI, or set LEFT4ME_ADMIN_PASSWORD first). The bundle deliberately doesn't seed an admin to keep credentials out of the metadata pipeline. The same left4me wrapper accepts any other flask subcommand: sudo left4me seed-script-overlays <dir>, sudo left4me routes, sudo left4me shell, etc.
  • CPU isolation is managed by this bundle, driven by one knob: left4me/system_core_count (default 1). The first N cores starting at 0 are pinned to system.slice / user.slice / l4d2-build.slice; the rest (up to vm/threads - 1) are pinned to l4d2-game.slice. l4d2-game.slice and l4d2-build.slice carry AllowedCPUs= inline on their unit definitions; system.slice and user.slice are pinned via drop-ins registered under systemd/units with the '<parent>.d/<basename>.conf' key convention (same shape nginx and autologin use), landing at /usr/local/lib/systemd/system/<slice>.d/99-left4me-cpuset.conf. The reactor raises if vm/threads < 2 or if system_core_count leaves no cores for games.
  • Kernel feature requirement: kernel-overlayfs (CONFIG_OVERLAY_FS). Standard on debian-13.
  • Game ports open by the web app on demand in the range 27015-27115 (UDP+TCP). Add corresponding accept rules to nftables/input per node if the host's policy is default-drop on input.
  • Pinned UIDs/GIDs (980/981). Chosen for deterministic ownership across rebuilds and backup restores. If you add another bundle that pins UIDs in this repo, make sure it doesn't collide.

Slice support requires bundles/systemd ≥ commit cc1c6a5

This bundle's l4d2-game.slice and l4d2-build.slice units rely on bundles/systemd/items.py accepting the .slice extension. Older revisions raised Exception(f'unknown type slice') at apply time. The repo-wide bw test will catch this if it regresses.