#!/usr/bin/env python3 """ setup-network: Assign a static IP and hostname to a Debian 13 machine based on a machine number (1-254) for use at a retro gaming fair. Usage: sudo ./setup-network Example: sudo ./setup-network 4 → IP: 10.0.1.4/24, hostname: retro-alcoi-4 """ import argparse import configparser import os import re import shutil import subprocess import sys def get_script_dir(): """Return the directory containing the script or binary.""" if getattr(sys, "frozen", False): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) def parse_args(): parser = argparse.ArgumentParser( description="Assign a static IP and hostname based on a machine number." ) parser.add_argument( "machine_num", type=int, help="Machine number (1-254). Sets IP to ./24 " "and hostname to .", ) args = parser.parse_args() if not 1 <= args.machine_num <= 254: parser.error("machine_num must be between 1 and 254") return args def check_root(): if os.geteuid() != 0: print("Error: this script must be run as root (use sudo).", file=sys.stderr) sys.exit(1) def load_config(): config_path = os.path.join(get_script_dir(), "config.ini") cfg = configparser.ConfigParser() cfg.read(config_path) subnet_root = cfg.get("network", "subnet_root", fallback="10.0.1") hostname_prefix = cfg.get("network", "hostname_prefix", fallback="retro-alcoi-") return subnet_root, hostname_prefix def detect_wired_iface(): """Detect the first wired (non-virtual, non-bridge, non-WiFi) Ethernet interface.""" net_dir = "/sys/class/net" candidates = [] preferred = [] for name in os.listdir(net_dir): iface_dir = os.path.join(net_dir, name) # Skip virtual/loopback if name.startswith("veth") or name == "lo": continue # Must be Ethernet (type == 1) type_file = os.path.join(iface_dir, "type") try: with open(type_file) as f: if f.read().strip() != "1": continue except OSError: continue # Skip WiFi interfaces if os.path.isdir(os.path.join(iface_dir, "wireless")): continue # Skip bridge interfaces (docker0, br-*) if os.path.isdir(os.path.join(iface_dir, "bridge")): continue candidates.append(name) # Prefer PCI-named Ethernet interfaces if name.startswith("eth") or re.match(r"en.*s", name): preferred.append(name) pool = preferred if preferred else candidates if not pool: print("Error: no wired Ethernet interface detected.", file=sys.stderr) sys.exit(1) pool.sort() return pool[0] def set_hostname(hostname): print(f" Setting hostname to '{hostname}'...") subprocess.run(["hostnamectl", "set-hostname", hostname], check=True) hosts_path = "/etc/hosts" with open(hosts_path) as f: content = f.read() new_line = f"127.0.1.1\t{hostname}" pattern = re.compile(r"^127\.0\.1\.1\s+.*$", re.MULTILINE) if pattern.search(content): content = pattern.sub(new_line, content) else: if not content.endswith("\n"): content += "\n" content += new_line + "\n" with open(hosts_path, "w") as f: f.write(content) print(f" /etc/hosts updated with: {new_line}") def configure_via_nmcli(iface, ip_cidr): print(f" Configuring via nmcli (interface: {iface}, address: {ip_cidr})...") # Delete existing connection if present (idempotent) subprocess.run( ["nmcli", "connection", "delete", "retro-static"], capture_output=True, ) subprocess.run( [ "nmcli", "connection", "add", "type", "ethernet", "con-name", "retro-static", "ifname", iface, "ipv4.method", "manual", "ipv4.addresses", ip_cidr, "ipv4.gateway", "", "ipv4.dns", "", "ipv4.dns-search", "", "ipv6.method", "disabled", "connection.autoconnect", "yes", ], check=True, ) subprocess.run(["nmcli", "connection", "up", "retro-static"], check=True) print(" nmcli connection 'retro-static' is up.") def configure_via_interfaces(iface, ip_cidr): ip, prefix = ip_cidr.split("/") prefix = int(prefix) # Calculate netmask from prefix length mask_int = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF netmask = ".".join(str((mask_int >> (8 * i)) & 0xFF) for i in reversed(range(4))) print(f" Configuring via /etc/network/interfaces (interface: {iface}, IP: {ip}, netmask: {netmask})...") interfaces_path = "/etc/network/interfaces" with open(interfaces_path) as f: content = f.read() # Remove existing stanza for this interface stanza_pattern = re.compile( r"(?:^auto\s+" + re.escape(iface) + r"\s*\n)?" r"^(?:allow-hotplug\s+" + re.escape(iface) + r"\s*\n)?" r"^iface\s+" + re.escape(iface) + r"\s+.*?(?=\n(?:auto|allow-hotplug|iface|source|\Z))", re.MULTILINE | re.DOTALL, ) content = stanza_pattern.sub("", content) # Remove any remaining bare 'auto ' line content = re.sub(r"^auto\s+" + re.escape(iface) + r"\s*\n", "", content, flags=re.MULTILINE) # Append new static stanza stanza = ( f"\nauto {iface}\n" f"iface {iface} inet static\n" f" address {ip}\n" f" netmask {netmask}\n" ) content = content.rstrip("\n") + "\n" + stanza with open(interfaces_path, "w") as f: f.write(content) print(f" /etc/network/interfaces updated.") subprocess.run(["ifdown", iface], capture_output=True) subprocess.run(["ifup", iface], check=True) print(f" Interface {iface} brought up.") def configure_network(iface, ip_cidr): if shutil.which("nmcli"): configure_via_nmcli(iface, ip_cidr) else: configure_via_interfaces(iface, ip_cidr) def main(): args = parse_args() check_root() subnet_root, hostname_prefix = load_config() machine_num = args.machine_num ip = f"{subnet_root}.{machine_num}" ip_cidr = f"{ip}/24" hostname = f"{hostname_prefix}{machine_num}" print(f"=== setup-network ===") print(f" Machine number : {machine_num}") print(f" IP address : {ip_cidr}") print(f" Hostname : {hostname}") print() iface = detect_wired_iface() print(f" Detected wired interface: {iface}") print() print("[1/2] Setting hostname...") set_hostname(hostname) print() print("[2/2] Configuring network...") configure_network(iface, ip_cidr) print() print("Done. Verify with:") print(f" ip addr show {iface}") print(f" hostname") if __name__ == "__main__": main()