243 lines
6.9 KiB
Python
Executable File
243 lines
6.9 KiB
Python
Executable File
#!/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 <machine_num>
|
|
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 <subnet_root>.<machine_num>/24 "
|
|
"and hostname to <hostname_prefix><machine_num>.",
|
|
)
|
|
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 <iface>' 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()
|