afegit setup-network
This commit is contained in:
Binary file not shown.
Executable
+27
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
[network]
|
||||
subnet_root = 10.0.1
|
||||
hostname_prefix = retro-alcoi-
|
||||
@@ -0,0 +1 @@
|
||||
pyinstaller
|
||||
Executable
+242
@@ -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()
|
||||
Reference in New Issue
Block a user