afegit setup-network

This commit is contained in:
2026-03-16 09:56:32 +01:00
parent b705ad1b63
commit 773fb30ee6
7 changed files with 337 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
# Navigate to setup-network/ regardless of where we're called from
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Activate virtualenv if present, otherwise require pyinstaller in PATH
if [[ -f .venv/bin/activate ]]; then
source .venv/bin/activate
echo "Activated .venv"
elif ! command -v pyinstaller &>/dev/null; then
echo "Error: pyinstaller not found. Either create a .venv or install pyinstaller." >&2
echo " python3 -m venv .venv && .venv/bin/pip install -r requirements.txt" >&2
exit 1
fi
echo "Building setup-network binary..."
pyinstaller --onefile --clean --name setup-network setup_network.py
echo "Copying config.ini to dist/..."
cp config.ini dist/
echo ""
echo "Build complete. Output:"
echo " ${SCRIPT_DIR}/dist/setup-network"
echo " ${SCRIPT_DIR}/dist/config.ini"
+3
View File
@@ -0,0 +1,3 @@
[network]
subnet_root = 10.0.1
hostname_prefix = retro-alcoi-
+1
View File
@@ -0,0 +1 @@
pyinstaller
+242
View File
@@ -0,0 +1,242 @@
#!/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()