From 878639147a0f5a7b9e84441f74d3bfa99dfe9126 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 10 May 2026 00:52:16 +0200 Subject: [PATCH] feat(deploy): left4me-apply-cake helper with apply/clear modes Co-Authored-By: Claude Opus 4.7 (1M context) --- .../local/libexec/left4me/left4me-apply-cake | 47 +++++++++++++++++++ deploy/tests/test_deploy_artifacts.py | 33 +++++++++++++ 2 files changed, 80 insertions(+) create mode 100755 deploy/files/usr/local/libexec/left4me/left4me-apply-cake diff --git a/deploy/files/usr/local/libexec/left4me/left4me-apply-cake b/deploy/files/usr/local/libexec/left4me/left4me-apply-cake new file mode 100755 index 0000000..880f021 --- /dev/null +++ b/deploy/files/usr/local/libexec/left4me/left4me-apply-cake @@ -0,0 +1,47 @@ +#!/bin/sh +# left4me — apply or clear CAKE egress shaper on the configured uplink. +# Driven by left4me-cake.service. See spec +# docs/superpowers/specs/2026-05-10-l4d2-network-shaping-design.md. +set -eu + +mode=${1:-apply} + +if [ -r /etc/left4me/cake.env ]; then + . /etc/left4me/cake.env +fi + +resolve_iface() { + if [ -n "${LEFT4ME_UPLINK_IFACE:-}" ]; then + printf '%s' "$LEFT4ME_UPLINK_IFACE" + return + fi + ip -4 route show default | awk '/default/ {print $5; exit}' +} + +case "$mode" in + apply) + if [ -z "${LEFT4ME_UPLINK_MBIT:-}" ]; then + echo "left4me-cake: LEFT4ME_UPLINK_MBIT unset; skipping shaper" >&2 + exit 0 + fi + iface=$(resolve_iface) + if [ -z "$iface" ]; then + echo "left4me-cake: cannot determine egress iface; skipping" >&2 + exit 0 + fi + exec tc qdisc replace dev "$iface" root cake \ + bandwidth "${LEFT4ME_UPLINK_MBIT}mbit" \ + internet diffserv4 dual-dsthost + ;; + clear) + iface=$(resolve_iface) + if [ -z "$iface" ]; then + exit 0 + fi + tc qdisc del dev "$iface" root 2>/dev/null || true + ;; + *) + echo "usage: $0 [apply|clear]" >&2 + exit 2 + ;; +esac diff --git a/deploy/tests/test_deploy_artifacts.py b/deploy/tests/test_deploy_artifacts.py index 6c47f3e..a72e1dd 100644 --- a/deploy/tests/test_deploy_artifacts.py +++ b/deploy/tests/test_deploy_artifacts.py @@ -20,6 +20,7 @@ SYSTEMCTL_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-systemctl" JOURNALCTL_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-journalctl" OVERLAY_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-overlay" SCRIPT_SANDBOX_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-script-sandbox" +APPLY_CAKE_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-apply-cake" SANDBOX_RESOLV_CONF = DEPLOY / "files/etc/left4me/sandbox-resolv.conf" CAKE_ENV = DEPLOY / "files/etc/left4me/cake.env" SUDOERS = DEPLOY / "files/etc/sudoers.d/left4me" @@ -770,3 +771,35 @@ def test_cake_env_template_documents_required_knobs(): # Empty defaults: shaper unit no-ops with a journal warning when unset. assert "LEFT4ME_UPLINK_MBIT=" in text assert "LEFT4ME_UPLINK_IFACE=" in text + + +def test_apply_cake_helper_supports_apply_and_clear_modes(): + assert APPLY_CAKE_HELPER.is_file() + text = APPLY_CAKE_HELPER.read_text() + + assert text.startswith("#!/bin/sh") + # Both knobs are read from the env file. + assert "LEFT4ME_UPLINK_MBIT" in text + assert "LEFT4ME_UPLINK_IFACE" in text + assert ". /etc/left4me/cake.env" in text + # Iface fallback to default route. + assert "ip -4 route show default" in text + # Two modes; default to apply. + assert "mode=${1:-apply}" in text + assert 'apply)' in text and 'clear)' in text + # Apply: idempotent `tc qdisc replace` with the documented flags. + assert "tc qdisc replace" in text + assert "cake" in text + assert "bandwidth" in text + assert "internet" in text + assert "diffserv4" in text + assert "dual-dsthost" in text + # Clear: tolerates a missing qdisc. + assert "tc qdisc del" in text + assert "|| true" in text + # Fail-soft on missing config. + assert "LEFT4ME_UPLINK_MBIT unset" in text + + +def test_apply_cake_helper_passes_shell_syntax_check(): + subprocess.run(["sh", "-n", str(APPLY_CAKE_HELPER)], check=True)