diff --git a/decnet/cli/init.py b/decnet/cli/init.py index bb52cb65..54be1f62 100644 --- a/decnet/cli/init.py +++ b/decnet/cli/init.py @@ -74,6 +74,7 @@ _CONFIG_PLACEHOLDER = """\ # master-host = 10.0.0.1 # syslog-port = 6514 # swarmctl-port = 8770 +# swarmctl-host = 127.0.0.1 # [logging] # system-log = /var/log/decnet/decnet.system.log diff --git a/decnet/cli/swarmctl.py b/decnet/cli/swarmctl.py index 687823c9..78fb9c00 100644 --- a/decnet/cli/swarmctl.py +++ b/decnet/cli/swarmctl.py @@ -16,8 +16,16 @@ from .utils import console, log def register(app: typer.Typer) -> None: @app.command() def swarmctl( - port: int = typer.Option(8770, "--port", help="Port for the swarm controller"), - host: str = typer.Option("127.0.0.1", "--host", help="Bind address for the swarm controller"), + port: int = typer.Option( + 8770, "--port", + envvar="DECNET_SWARMCTL_PORT", + help="Port for the swarm controller. Defaults to [swarm] swarmctl-port from /etc/decnet/decnet.ini, else 8770.", + ), + host: str = typer.Option( + "127.0.0.1", "--host", + envvar="DECNET_SWARMCTL_HOST", + help="Bind address for the swarm controller. Defaults to [swarm] swarmctl-host from /etc/decnet/decnet.ini, else 127.0.0.1.", + ), daemon: bool = typer.Option(False, "--daemon", "-d", help="Detach to background as a daemon process"), no_listener: bool = typer.Option(False, "--no-listener", help="Do not auto-spawn the syslog-TLS listener alongside swarmctl"), tls: bool = typer.Option(False, "--tls", help="Serve over HTTPS with mTLS (required for cross-host worker heartbeats)"), diff --git a/decnet/config_ini.py b/decnet/config_ini.py index a3747003..7db44abd 100644 --- a/decnet/config_ini.py +++ b/decnet/config_ini.py @@ -39,6 +39,7 @@ Shape:: master-host = 10.0.0.1 # required on agents syslog-port = 6514 swarmctl-port = 8770 + swarmctl-host = 127.0.0.1 # bind address for `decnet swarmctl` [logging] system-log = /var/log/decnet/decnet.system.log @@ -120,6 +121,7 @@ _DOMAIN_MAP: dict[str, dict[str, str]] = { "master-host": "DECNET_SWARM_MASTER_HOST", "syslog-port": "DECNET_SWARM_SYSLOG_PORT", "swarmctl-port": "DECNET_SWARMCTL_PORT", + "swarmctl-host": "DECNET_SWARMCTL_HOST", }, "logging": { "system-log": "DECNET_SYSTEM_LOGS", diff --git a/decnet/env.py b/decnet/env.py index e6029999..0040580d 100644 --- a/decnet/env.py +++ b/decnet/env.py @@ -114,6 +114,11 @@ DECNET_SWARM_MASTER_HOST: str | None = os.environ.get("DECNET_SWARM_MASTER_HOST" DECNET_HOST_UUID: str | None = os.environ.get("DECNET_HOST_UUID") DECNET_MASTER_HOST: str | None = os.environ.get("DECNET_MASTER_HOST") DECNET_SWARMCTL_PORT: int = _port("DECNET_SWARMCTL_PORT", 8770) +# Bind address for the master-side swarm controller. Loopback by default — +# operators flip to 0.0.0.0 (or a specific NIC) on production masters where +# workers heartbeat in over mTLS from other hosts. Seeded by [swarm] +# swarmctl-host in /etc/decnet/decnet.ini. +DECNET_SWARMCTL_HOST: str = os.environ.get("DECNET_SWARMCTL_HOST", "127.0.0.1") # Ingester batching: how many log rows to accumulate per commit, and the # max wait (ms) before flushing a partial batch. Larger batches reduce diff --git a/deploy/decnet-swarmctl.service.j2 b/deploy/decnet-swarmctl.service.j2 index 9dcdbd20..7c25988d 100644 --- a/deploy/decnet-swarmctl.service.j2 +++ b/deploy/decnet-swarmctl.service.j2 @@ -10,10 +10,12 @@ User={{ user }} Group={{ group }} WorkingDirectory={{ install_dir }} EnvironmentFile=-{{ install_dir }}/.env.local -# Default bind is loopback — the controller is a master-local orchestrator -# reached by the CLI and the web dashboard, not by workers. +# Bind/port resolved from /etc/decnet/decnet.ini ([swarm] swarmctl-host / +# swarmctl-port) — falls back to 127.0.0.1:8770 when the keys are absent. +# Pass --host/--port on the ExecStart line only if you want to pin them +# regardless of what the INI says. Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.swarmctl.log -ExecStart={{ venv_dir }}/bin/decnet swarmctl --host 127.0.0.1 --port 8770 +ExecStart={{ venv_dir }}/bin/decnet swarmctl StandardOutput=append:/var/log/decnet/decnet.swarmctl.log StandardError=append:/var/log/decnet/decnet.swarmctl.log diff --git a/tests/config/test_config_ini.py b/tests/config/test_config_ini.py index 5355a62a..ff71b1d7 100644 --- a/tests/config/test_config_ini.py +++ b/tests/config/test_config_ini.py @@ -302,3 +302,18 @@ swarmctl-port = 9999 load_ini_config(ini) # [master] loaded first, [swarm] lost via setdefault assert os.environ["DECNET_SWARMCTL_PORT"] == "9001" + + +def test_swarm_section_seeds_swarmctl_host(monkeypatch, tmp_path): + """[swarm] swarmctl-host → DECNET_SWARMCTL_HOST so the systemd unit and + `decnet swarmctl` CLI both pick up the operator's bind choice from the + INI without anyone passing --host on ExecStart.""" + _scrub(monkeypatch, "DECNET_MODE", "DECNET_SWARMCTL_HOST", "DECNET_SWARMCTL_PORT") + ini = _write_ini(tmp_path, """ +[swarm] +swarmctl-host = 0.0.0.0 +swarmctl-port = 9000 +""") + load_ini_config(ini) + assert os.environ["DECNET_SWARMCTL_HOST"] == "0.0.0.0" + assert os.environ["DECNET_SWARMCTL_PORT"] == "9000"