feat: add MAC address provisioning for secondary ethernet port

- Added step_set_eth1_mac() to generate and configure random locally administered MAC addresses
- Made --name argument optional and conditional based on selected provisioning steps
- Updated documentation to include MAC configuration in production deployment checklist
This commit is contained in:
2025-11-11 14:23:57 +01:00
parent da411e1ee6
commit d343ec2583
2 changed files with 65 additions and 8 deletions

View File

@@ -50,4 +50,5 @@ For production, the devices need to be provisoned uniquely
- set channel name etc. in bumble-auracast/src/auracast/.env - set channel name etc. in bumble-auracast/src/auracast/.env
- execute the update service scripts - execute the update service scripts
- start the application (script if custom device, server and frontend if ui version) - start the application (script if custom device, server and frontend if ui version)
- set mac add of secondary eth port in /etc/systemd/network/10-eth1-mac.link
- activate overlayfs (?) -probably not because we need persistent storage for stream states - activate overlayfs (?) -probably not because we need persistent storage for stream states

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import ipaddress, os, re, subprocess, tempfile, json, datetime, shlex import ipaddress, os, re, subprocess, tempfile, json, datetime, shlex
import secrets
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -101,6 +102,48 @@ def step_wireguard_provision(iot_host: str, client_name: str):
scp_and_enable(iot_host, cfg) scp_and_enable(iot_host, cfg)
return {"wg_name": name, "wg_iface": iface} return {"wg_name": name, "wg_iface": iface}
def step_set_eth1_mac(iot_host: str):
"""Generate a random locally administered MAC (02:xx:xx:xx:xx:xx) for eth1 and
configure it via systemd .link on the remote device. Prints that a reboot is required.
Returns a dict with the chosen mac.
"""
mac = "02:" + ":".join(f"{secrets.randbelow(256):02x}" for _ in range(5))
remote = (
"set -e\n"
"sudo mkdir -p /etc/systemd/network\n"
"sudo tee /etc/systemd/network/10-eth1-mac.link >/dev/null <<'EOF'\n"
"[Match]\n"
"Driver=smsc95xx\n"
"\n"
"[Link]\n"
f"MACAddress={mac}\n"
"EOF\n"
)
ssh_cmd = ["ssh", "-p", str(SSH_PORT)]
if SSH_KEY:
ssh_cmd += ["-i", SSH_KEY]
ssh_cmd += [f"{SSH_USER}@{iot_host}", remote]
proc = subprocess.run(ssh_cmd, check=False, capture_output=True, text=True)
stdout = (proc.stdout or "").strip()
stderr = (proc.stderr or "").strip()
if proc.returncode != 0:
print(f"❌ set eth1 mac: failed rc={proc.returncode}: {stderr}")
else:
print(f"✅ set eth1 mac: configured {mac} at /etc/systemd/network/10-eth1-mac.link")
print(" Reboot is required for the MAC change to take effect.")
return {
"rc": proc.returncode,
"mac": mac,
"out": stdout[-500:],
"err": stderr[-500:],
}
def step_set_hostname(iot_host: str, hostname: str | None): def step_set_hostname(iot_host: str, hostname: str | None):
"""Set hostname on the device by running the project's provision script over SSH. """Set hostname on the device by running the project's provision script over SSH.
@@ -326,11 +369,11 @@ def main():
) )
) )
ap.add_argument("iot_host", help="Local/LAN IP or hostname of the IoT device reachable via SSH") ap.add_argument("iot_host", help="Local/LAN IP or hostname of the IoT device reachable via SSH")
ap.add_argument("-n", "--name", required=True, help="Device name to use for both hostname and WireGuard client") ap.add_argument("-n", "--name", required=False, help="Device name to use for both hostname and WireGuard client")
ap.add_argument( ap.add_argument(
"--steps", "--steps",
nargs="+", nargs="+",
choices=["pull", "wg", "hostname", "update_app", "start_app", "finish", "all"], choices=["pull", "wg", "hostname", "mac", "update_app", "start_app", "finish", "all"],
default=["all"], default=["all"],
help="Which steps to run. Default: all", help="Which steps to run. Default: all",
) )
@@ -343,15 +386,20 @@ def main():
# Validate wg-easy env # Validate wg-easy env
get_env_auth() get_env_auth()
# Derive device name: if numeric, prefix with 'summitwave-beacon'
name = args.name
if re.fullmatch(r"\d+", name):
name = f"summitwave-beacon{name}"
# Normalize steps # Normalize steps
steps = args.steps steps = args.steps
if "all" in steps: if "all" in steps:
steps = ["pull", "hostname", "wg", "update_app", "start_app", "finish"] steps = ["pull", "hostname", "mac", "wg", "update_app", "start_app", "finish"]
# Validate required args per step
name = args.name
if any(s in steps for s in ("wg", "hostname")) and not name:
print("error: --name is required for steps: wg, hostname")
raise SystemExit(2)
# Derive device name: if numeric, prefix with 'summitwave-beacon'
if name and re.fullmatch(r"\d+", name):
name = f"summitwave-beacon{name}"
# Gather device facts once (may change after hostname step, but we at least log the initial state) # Gather device facts once (may change after hostname step, but we at least log the initial state)
facts = get_device_facts(args.iot_host) facts = get_device_facts(args.iot_host)
@@ -382,6 +430,14 @@ def main():
**wg_info, **wg_info,
}) })
if "mac" in steps:
mac_info = step_set_eth1_mac(args.iot_host)
write_provision_log({
"action": "mac",
**get_device_facts(args.iot_host),
**mac_info,
})
if "update_app" in steps: if "update_app" in steps:
upd_info = step_update_app(args.iot_host) upd_info = step_update_app(args.iot_host)
write_provision_log({ write_provision_log({