149 lines
5 KiB
Python
149 lines
5 KiB
Python
"""
|
|
mod_networkd.py -- systemd-networkd configuration for VLAN interfaces.
|
|
|
|
Generates .netdev and .network files for each VLAN, removes legacy
|
|
conflicting files, and triggers a networkd reload when content changes.
|
|
"""
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import mod_shared as shared
|
|
import mod_validation as validation
|
|
|
|
NETWORKD_DIR = Path("/etc/systemd/network")
|
|
|
|
|
|
# ===================================================================
|
|
# Build
|
|
# ===================================================================
|
|
|
|
def build_netdev(vlan, vid, iface):
|
|
return "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"# Edit config.json and re-run: sudo python3 core.py --apply",
|
|
"",
|
|
"[NetDev]",
|
|
f"Name={iface}",
|
|
"Kind=vlan",
|
|
"",
|
|
"[VLAN]",
|
|
f"Id={vid}",
|
|
"",
|
|
])
|
|
|
|
def build_network(vlan, vid, iface, all_vlan_ids):
|
|
network = shared.network_for(vlan)
|
|
prefix = network.prefixlen
|
|
lines = [
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"# Edit config.json and re-run: sudo python3 core.py --apply",
|
|
"",
|
|
"[Match]",
|
|
f"Name={iface}",
|
|
"",
|
|
"[Network]",
|
|
"DHCP=no",
|
|
"LinkLocalAddressing=no",
|
|
]
|
|
for ident in vlan["server_identities"]:
|
|
lines.append(f"# {ident['description']}")
|
|
lines.append(f"Address={ident['ip']}/{prefix}")
|
|
|
|
if shared.is_physical(vlan):
|
|
lines.append("")
|
|
for v in all_vlan_ids:
|
|
if v != 1:
|
|
lines.append(f"VLAN={iface}.{v}")
|
|
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ===================================================================
|
|
# Apply
|
|
# ===================================================================
|
|
|
|
def find_legacy_files(managed_interfaces):
|
|
to_remove = []
|
|
for pattern in ("*.network", "*.netdev"):
|
|
for f in NETWORKD_DIR.glob(pattern):
|
|
if f.name.startswith(f"10-{shared.PRODUCT_NAME}-"):
|
|
continue
|
|
try:
|
|
content = f.read_text()
|
|
except OSError:
|
|
continue
|
|
for iface in managed_interfaces:
|
|
if f"Name={iface}" in content:
|
|
to_remove.append(f)
|
|
break
|
|
return to_remove
|
|
|
|
def apply_networkd(data, dry_run=False, only_if_changed=False):
|
|
"""Write systemd-networkd files and reload.
|
|
If only_if_changed=True, write files only when content differs from disk
|
|
and skip the networkd reload if nothing changed. Used by --apply mode.
|
|
"""
|
|
all_vlan_ids = [v.get('vlan_id') for v in data["vlans"] if not validation.is_wg(v)]
|
|
managed_ifaces = [validation.derive_interface(v, data) for v in data["vlans"]]
|
|
changed = False
|
|
|
|
legacy = find_legacy_files(managed_ifaces)
|
|
if legacy:
|
|
print("Removing legacy networkd files:")
|
|
for f in legacy:
|
|
if not dry_run:
|
|
f.unlink()
|
|
changed = True
|
|
print(f" {'[dry-run] would remove' if dry_run else 'Removed'}: {f}")
|
|
print()
|
|
|
|
for vlan in data["vlans"]:
|
|
if validation.is_wg(vlan):
|
|
continue
|
|
iface = validation.derive_interface(vlan, data)
|
|
vid = vlan.get('vlan_id')
|
|
stem = shared.networkd_stem(vlan)
|
|
|
|
if not shared.is_physical(vlan):
|
|
netdev_path = NETWORKD_DIR / f"{stem}.netdev"
|
|
netdev_content = build_netdev(vlan, vid, iface)
|
|
if dry_run:
|
|
print(f"# -- {netdev_path} (dry-run) --")
|
|
print(netdev_content)
|
|
else:
|
|
existing = netdev_path.read_text() if netdev_path.exists() else None
|
|
if existing != netdev_content:
|
|
netdev_path.write_text(netdev_content)
|
|
print(f"Written: {netdev_path}")
|
|
changed = True
|
|
elif not only_if_changed:
|
|
print(f"Unchanged: {netdev_path}")
|
|
|
|
network_path = NETWORKD_DIR / f"{stem}.network"
|
|
network_content = build_network(vlan, vid, iface, all_vlan_ids)
|
|
if dry_run:
|
|
print(f"# -- {network_path} (dry-run) --")
|
|
print(network_content)
|
|
else:
|
|
existing = network_path.read_text() if network_path.exists() else None
|
|
if existing != network_content:
|
|
network_path.write_text(network_content)
|
|
print(f"Written: {network_path}")
|
|
changed = True
|
|
elif not only_if_changed:
|
|
print(f"Unchanged: {network_path}")
|
|
|
|
if not dry_run:
|
|
if changed:
|
|
print("Reloading systemd-networkd...")
|
|
result = subprocess.run(
|
|
["networkctl", "reload"], capture_output=True, text=True
|
|
)
|
|
if result.returncode != 0:
|
|
print(f"WARNING: networkctl reload returned non-zero:\n{result.stderr.strip()}")
|
|
else:
|
|
print("systemd-networkd reloaded.")
|
|
elif only_if_changed:
|
|
print("systemd-networkd: no changes. Good.")
|