feat(enroll): opt-in IPvlan per-agent for Wi-Fi-bridged VMs
Wi-Fi APs bind one MAC per associated station, so VirtualBox/VMware guests bridged over Wi-Fi rotate the VM's DHCP lease when Docker's macvlan starts emitting container-MAC frames through the vNIC. Adds a `use_ipvlan` toggle on the Agent Enrollment tab (mirrors the updater daemon checkbox): flips the flag on SwarmHost, bakes `ipvlan=true` into the agent's decnet.ini, and `_worker_config` forces ipvlan=True on the per-host shard at dispatch. Safe no-op on wired/bare-metal agents.
This commit is contained in:
@@ -126,6 +126,11 @@ class SwarmHost(SQLModel, table=True):
|
||||
cert_bundle_path: str
|
||||
enrolled_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
notes: Optional[str] = Field(default=None, sa_column=Column("notes", Text, nullable=True))
|
||||
# Per-host driver preference. True => deckies on this host run over IPvlan
|
||||
# (L2) instead of macvlan — required when the host is a VirtualBox guest
|
||||
# bridged over Wi-Fi, because Wi-Fi APs only allow one MAC per station
|
||||
# and macvlan's per-container MACs rotate the VM's DHCP lease.
|
||||
use_ipvlan: bool = Field(default=False)
|
||||
|
||||
|
||||
class DeckyShard(SQLModel, table=True):
|
||||
@@ -322,6 +327,7 @@ class SwarmHostView(BaseModel):
|
||||
updater_cert_fingerprint: Optional[str] = None
|
||||
enrolled_at: datetime
|
||||
notes: Optional[str] = None
|
||||
use_ipvlan: bool = False
|
||||
|
||||
|
||||
class DeckyShardView(BaseModel):
|
||||
|
||||
@@ -43,8 +43,18 @@ def _shard_by_host(config: DecnetConfig) -> dict[str, list[DeckyConfig]]:
|
||||
return buckets
|
||||
|
||||
|
||||
def _worker_config(base: DecnetConfig, shard: list[DeckyConfig]) -> DecnetConfig:
|
||||
return base.model_copy(update={"deckies": shard})
|
||||
def _worker_config(
|
||||
base: DecnetConfig,
|
||||
shard: list[DeckyConfig],
|
||||
host: dict[str, Any],
|
||||
) -> DecnetConfig:
|
||||
updates: dict[str, Any] = {"deckies": shard}
|
||||
# Per-host driver opt-in (Wi-Fi-bridged VMs can't use macvlan — see
|
||||
# SwarmHost.use_ipvlan). Never downgrade: if the operator picked ipvlan
|
||||
# at the deploy level, keep it regardless of the per-host flag.
|
||||
if host.get("use_ipvlan"):
|
||||
updates["ipvlan"] = True
|
||||
return base.model_copy(update=updates)
|
||||
|
||||
|
||||
async def dispatch_decnet_config(
|
||||
@@ -69,7 +79,7 @@ async def dispatch_decnet_config(
|
||||
|
||||
async def _dispatch(host_uuid: str, shard: list[DeckyConfig]) -> SwarmHostResult:
|
||||
host = hosts[host_uuid]
|
||||
cfg = _worker_config(config, shard)
|
||||
cfg = _worker_config(config, shard, host)
|
||||
try:
|
||||
async with AgentClient(host=host) as agent:
|
||||
body = await agent.deploy(cfg, dry_run=dry_run, no_cache=no_cache)
|
||||
|
||||
@@ -87,6 +87,15 @@ class EnrollBundleRequest(BaseModel):
|
||||
default=True,
|
||||
description="Include updater cert bundle and auto-start decnet updater on the agent",
|
||||
)
|
||||
use_ipvlan: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Run deckies on this agent over IPvlan L2 instead of MACVLAN. "
|
||||
"Required when the agent is a VirtualBox/VMware guest bridged over Wi-Fi — "
|
||||
"Wi-Fi APs bind one MAC per station, so MACVLAN's extra container MACs "
|
||||
"rotate the VM's DHCP lease. Safe no-op on wired/bare-metal hosts."
|
||||
),
|
||||
)
|
||||
services_ini: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional INI text shipped to the agent as /etc/decnet/services.ini",
|
||||
@@ -166,13 +175,15 @@ def _is_excluded(rel: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _render_decnet_ini(master_host: str) -> bytes:
|
||||
def _render_decnet_ini(master_host: str, use_ipvlan: bool = False) -> bytes:
|
||||
ipvlan_line = f"ipvlan = {'true' if use_ipvlan else 'false'}\n"
|
||||
return (
|
||||
"; Generated by DECNET agent-enrollment bundle.\n"
|
||||
"[decnet]\n"
|
||||
"mode = agent\n"
|
||||
"disallow-master = true\n"
|
||||
"log-directory = /var/log/decnet\n"
|
||||
f"{ipvlan_line}"
|
||||
"\n"
|
||||
"[agent]\n"
|
||||
f"master-host = {master_host}\n"
|
||||
@@ -197,6 +208,7 @@ def _build_tarball(
|
||||
issued: pki.IssuedCert,
|
||||
services_ini: Optional[str],
|
||||
updater_issued: Optional[pki.IssuedCert] = None,
|
||||
use_ipvlan: bool = False,
|
||||
) -> bytes:
|
||||
"""Gzipped tarball with:
|
||||
- full repo source (minus excludes)
|
||||
@@ -216,7 +228,7 @@ def _build_tarball(
|
||||
continue
|
||||
tar.add(path, arcname=rel, recursive=False)
|
||||
|
||||
_add_bytes(tar, "etc/decnet/decnet.ini", _render_decnet_ini(master_host))
|
||||
_add_bytes(tar, "etc/decnet/decnet.ini", _render_decnet_ini(master_host, use_ipvlan))
|
||||
for unit in _SYSTEMD_UNITS:
|
||||
_add_bytes(
|
||||
tar,
|
||||
@@ -329,11 +341,15 @@ async def create_enroll_bundle(
|
||||
"cert_bundle_path": str(bundle_dir),
|
||||
"enrolled_at": datetime.now(timezone.utc),
|
||||
"notes": "enrolled via UI bundle",
|
||||
"use_ipvlan": req.use_ipvlan,
|
||||
}
|
||||
)
|
||||
|
||||
# 3. Render payload + bootstrap.
|
||||
tarball = _build_tarball(req.master_host, req.agent_name, issued, req.services_ini, updater_issued)
|
||||
tarball = _build_tarball(
|
||||
req.master_host, req.agent_name, issued, req.services_ini, updater_issued,
|
||||
use_ipvlan=req.use_ipvlan,
|
||||
)
|
||||
token = secrets.token_urlsafe(24)
|
||||
expires_at = datetime.now(timezone.utc) + BUNDLE_TTL
|
||||
|
||||
|
||||
Reference in New Issue
Block a user