From 05469332e88c3b10c525a0a09fb1744a076b3732 Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Sat, 4 Jul 2026 21:26:56 +0300 Subject: [PATCH] feat: port configure actions (base, ufw, crowdsec, cron, systemd, routing) --- actions/configure.py | 34 ++++++++++++++++++++++++++++++++++ actions/configure/base.py | 31 +++++++++++++++++++++++++++++++ actions/configure/cron.py | 14 ++++++++++++++ actions/configure/crowdsec.py | 24 ++++++++++++++++++++++++ actions/configure/routing.py | 21 +++++++++++++++++++++ actions/configure/systemd.py | 15 +++++++++++++++ actions/configure/ufw.py | 19 +++++++++++++++++++ 7 files changed, 158 insertions(+) create mode 100644 actions/configure.py create mode 100644 actions/configure/base.py create mode 100644 actions/configure/cron.py create mode 100644 actions/configure/crowdsec.py create mode 100644 actions/configure/routing.py create mode 100644 actions/configure/systemd.py create mode 100644 actions/configure/ufw.py diff --git a/actions/configure.py b/actions/configure.py new file mode 100644 index 0000000..768fc5f --- /dev/null +++ b/actions/configure.py @@ -0,0 +1,34 @@ +# actions/configure.py +"""Umbrella: run every configure/* action in a stable order (SP2 §3.1).""" +import os +from engine.descriptor import Meta, Flag, build_parser +from engine import overlay + +METADATA = Meta(summary="Run every configure/* action in stable order.", + args=[Flag("--dry-run", "Pass --dry-run to sub-actions that support it.")]) +ORDER = ["configure/base", "configure/ufw", "configure/crowdsec", + "configure/cron", "configure/systemd", "configure/routing"] + +def run(ctx, args): + commons_actions = os.path.join(ctx.paths.commons, "actions") + prop = os.path.join(ctx.paths.proprietaries, "actions") + prop = prop if os.path.isdir(prop) else None + names = [i.name for i in overlay.discover(commons_actions, prop) if i.name.startswith("configure/")] + ordered = [n for n in ORDER if n in names] + sorted(n for n in names if n not in ORDER) + rc = 0 + for name in ordered: + r = overlay.resolve(name, commons_actions, prop) + mod = overlay.load_module(r.path, "apex_umbrella_" + name.replace("/", "_")) + if getattr(mod, "DISABLED", None): + ctx.log.warn(f"Skipping disabled {name}"); continue + meta = getattr(mod, "METADATA") + has_dry = any(getattr(a, "name", None) == "--dry-run" for a in meta.args) + argv = ["--dry-run"] if (args.dry_run and has_dry) else [] + sub_args = build_parser(meta, f"apex {name}").parse_args(argv) + ctx.log.info(f"=== {name} ===") + try: + mod.run(ctx, sub_args) + except SystemExit as e: + if e.code: + ctx.log.error(f"{name} exited {e.code}"); rc = rc or e.code + return rc diff --git a/actions/configure/base.py b/actions/configure/base.py new file mode 100644 index 0000000..a0e680e --- /dev/null +++ b/actions/configure/base.py @@ -0,0 +1,31 @@ +# actions/configure/base.py +"""Port of configure/base — base packages + ufw + ufw-docker helper.""" +import os, shutil, stat, urllib.request +from engine.descriptor import Meta + +METADATA = Meta(summary="Configure base system packages (curl, wget, git, rsyslog, ufw, ufw-docker).") +UFW_DOCKER_URL = "https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker" + +def run(ctx, args): + log, s = ctx.log, ctx.sys + log.info("Starting base system initialization...") + if not os.path.isfile("/etc/debian_version"): + log.error("Requires a Debian-based system (Debian/Ubuntu)."); raise SystemExit(1) + log.info("Updating package lists..."); s.sudo(["apt-get", "update", "-y"]) + log.info("Installing essential packages...") + s.sudo(["apt-get", "install", "-y", "curl", "wget", "git", "rsyslog"]) + s.ensure_running("rsyslog") + if not shutil.which("ufw"): + log.info("Installing UFW..."); s.sudo(["apt-get", "install", "-y", "ufw"]) + else: + log.info("UFW already installed.") + binp = os.path.expanduser("~/.local/bin/ufw-docker") + if not (os.path.isfile(binp) and os.access(binp, os.X_OK)): + log.info("Downloading ufw-docker helper...") + os.makedirs(os.path.dirname(binp), exist_ok=True) + urllib.request.urlretrieve(UFW_DOCKER_URL, binp) # stdlib; replaces wget/curl + os.chmod(binp, os.stat(binp).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + log.success(f"ufw-docker downloaded to {binp}.") + else: + log.info(f"ufw-docker already exists at {binp}.") + log.success("Base system initialization completed.") diff --git a/actions/configure/cron.py b/actions/configure/cron.py new file mode 100644 index 0000000..00cc301 --- /dev/null +++ b/actions/configure/cron.py @@ -0,0 +1,14 @@ +# actions/configure/cron.py +"""Port of configure/cron — install crontab from configs/cron/crontab.""" +import os +from engine.descriptor import Meta, Flag + +METADATA = Meta(summary="Install the node crontab from configs/cron/crontab.", + args=[Flag("--dry-run", "Render + preview; do not install.")]) + +def run(ctx, args): + crontab = os.path.join(ctx.paths.configs, "cron", "crontab") + if not os.path.isfile(crontab): + ctx.log.error(f"Crontab file not found: {crontab}"); raise SystemExit(1) + text = ctx.tpl.render(crontab, ctx.vars()) + ctx.host.install_crontab(text, dry_run=args.dry_run) diff --git a/actions/configure/crowdsec.py b/actions/configure/crowdsec.py new file mode 100644 index 0000000..eda34eb --- /dev/null +++ b/actions/configure/crowdsec.py @@ -0,0 +1,24 @@ +# actions/configure/crowdsec.py +"""Port of configure/crowdsec — install, deploy /etc/crowdsec, collections, bouncer.""" +import os +from engine.descriptor import Meta, Flag + +METADATA = Meta(summary="Install + configure CrowdSec from configs/crowdsec.", + args=[Flag("--dry-run", "Render only; do not write/reload.")]) + +def run(ctx, args): + cfg = os.path.join(ctx.paths.configs, "crowdsec") + if not os.path.isdir(cfg): + ctx.log.error(f"Config directory not found: {cfg}"); raise SystemExit(1) + ctx.log.info("Configuring CrowdSec...") + if not args.dry_run: + ctx.crowdsec.ensure_installed() + ctx.crowdsec.deploy("crowdsec", ctx.vars(), dry_run=args.dry_run) + if not args.dry_run: + ctx.crowdsec.install_collections(["crowdsecurity/linux", "crowdsecurity/traefik"]) + bouncer = os.path.join(cfg, "bouncers", "crowdsec-firewall-bouncer.yaml.local") + if os.path.isfile(bouncer) and not ctx.sys.ok( + ["bash", "-c", "dpkg -l | grep -q crowdsec-firewall-bouncer-iptables"]): + ctx.log.info("Installing crowdsec-firewall-bouncer-iptables...") + ctx.sys.sudo(["apt-get", "install", "-y", "crowdsec-firewall-bouncer-iptables"]) + ctx.log.success("Configured crowdsec.") diff --git a/actions/configure/routing.py b/actions/configure/routing.py new file mode 100644 index 0000000..c832850 --- /dev/null +++ b/actions/configure/routing.py @@ -0,0 +1,21 @@ +# actions/configure/routing.py +"""Port of configure/routing — idempotent node routing (ip_forward, fwmark rule, table 100).""" +from engine.descriptor import Meta + +METADATA = Meta(summary="Idempotent node-level routing rules and routes.") + +def run(ctx, args): + log, s = ctx.log, ctx.sys + log.info("Applying persistent node-level routing configuration...") + s.sudo(["sysctl", "-w", "net.ipv4.ip_forward=1"]) + if s.ok(["bash", "-c", "sudo ip rule show | grep -q 'fwmark 0x1 lookup 100'"]): + log.info("IP rule for mark 0x1 already exists.") + else: + s.run(["bash", "-c", "sudo ip rule add fwmark 0x1 lookup 100 priority 100 || true"]) + log.info("IP rule added for mark 0x1.") + if s.ok(["bash", "-c", "sudo ip route show table 100 2>/dev/null | grep -q 'local default dev lo'"]): + log.info("Local route in table 100 already exists.") + else: + s.run(["bash", "-c", "sudo ip route add local 0.0.0.0/0 dev lo table 100 || true"]) + log.info("Local route added to table 100.") + log.success("Routing configuration applied.") diff --git a/actions/configure/systemd.py b/actions/configure/systemd.py new file mode 100644 index 0000000..7ca62d4 --- /dev/null +++ b/actions/configure/systemd.py @@ -0,0 +1,15 @@ +# actions/configure/systemd.py +"""Port of configure/systemd — deploy + enable units from configs/systemd.""" +import os +from engine.descriptor import Meta, Flag + +METADATA = Meta(summary="Deploy + enable systemd units from configs/systemd.", + args=[Flag("--dry-run", "Render only; do not write/enable.")]) + +def run(ctx, args): + cfg = os.path.join(ctx.paths.configs, "systemd") + if not os.path.isdir(cfg): + ctx.log.error(f"Config directory not found: {cfg}"); raise SystemExit(1) + ctx.log.info("Deploying systemd units...") + ctx.systemd.deploy_units("systemd", ctx.vars(), dry_run=args.dry_run) + ctx.log.success("Configured systemd.") diff --git a/actions/configure/ufw.py b/actions/configure/ufw.py new file mode 100644 index 0000000..f357bf4 --- /dev/null +++ b/actions/configure/ufw.py @@ -0,0 +1,19 @@ +# actions/configure/ufw.py +"""Port of configure/ufw — apply docker + host ufw rules from configs/ufw.""" +import os +from engine.descriptor import Meta, Flag + +METADATA = Meta(summary="Render + apply this node's UFW rules (configs/ufw).", + args=[Flag("--dry-run", "Preview; do not apply.")]) + +def run(ctx, args): + cfg = os.path.join(ctx.paths.configs, "ufw") + if not os.path.isdir(cfg): + ctx.log.error(f"Config directory not found: {cfg}"); raise SystemExit(1) + docker_dir = os.path.join(cfg, "docker") + host_dir = os.path.join(cfg, "host") + ctx.log.info("Configuring UFW rules...") + if not os.path.isdir(docker_dir): + ctx.log.warn(f"Docker rules dir not found at {docker_dir}. Skipping ufw-docker rules.") + ctx.ufw.apply(docker_dir, host_dir if os.path.isdir(host_dir) else None, dry_run=args.dry_run) + ctx.log.success("Configured ufw.")