commit 7714cdb2d7b0d45d62381405199bbaf92a95fa3d Author: jonnybravo Date: Wed Sep 17 12:26:13 2025 +0200 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..afed073 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/README.md b/README.md new file mode 100644 index 0000000..477bb1f --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Anleitung: Automatisierte Erstellung mehrerer LXC-Container + +## Übersicht + +Dieses Projekt automatisiert die Erstellung und Konfiguration von LXC-Containern mit verschiedenen Linux-Distributionen. + +Es besteht aus zwei Python-Skripten: + +1. `setup_host.py`: Das **Haupt-Skript**, das Sie ausführen. Es orchestriert die Erstellung und Konfiguration von einem oder mehreren Containern. +2. `setup_container.py`: Ein **Hilfs-Skript**, das vom Haupt-Skript automatisch in jeden neuen Container kopiert und dort ausgeführt wird, um die interne Konfiguration (Benutzer, SSH, etc.) vorzunehmen. + +Sie interagieren nur mit `setup_host.py`. + +## Voraussetzungen + +- **LXD installiert:** Sie müssen LXD auf Ihrem Host-System installiert und konfiguriert haben. Führen Sie bei Bedarf `lxd init` aus, um LXD zu initialisieren. +- **Benutzerrechte:** Der Benutzer, der das Skript ausführt, muss Mitglied der `lxd`-Gruppe sein, um Container erstellen und verwalten zu dürfen. + +## Benutzung + +### Schritt 1: Skripte ausführbar machen + +```bash +chmod +x setup_host.py setup_container.py +``` + +### Schritt 2: Haupt-Skript ausführen + +Führen Sie `setup_host.py` aus. Das Skript akzeptiert verschiedene Argumente, um den Erstellungsprozess anzupassen. + +#### Wichtige Argumente + +* `-c`, `--count`: Die Gesamtanzahl der Container. (Standard: `1`) +* `-p`, `--root-password`: **(Erforderlich)** Das Passwort für `root` und `jonnybravo`. +* `--image`: Das zu verwendende Image. (Standard: `images:archlinux`) +* `--hostname-template`: Vorlage für die Namen der Container. Unterstützt `{basename}` und `{i}`. **Wichtig:** Muss `{i}` enthalten, wenn `--count > 1`. +* `-b`, `--basename`: Der Basisname für die Container, falls die Vorlage `{basename}` enthält. (Standard: `vm`) +* `--no-port-forward`: Deaktiviert die SSH-Port-Weiterleitung. +* `--port-start`: Der Start-Port für die SSH-Weiterleitung, falls diese aktiv ist. (Standard: `2201`) + +--- + +### Anwendungsbeispiele + +#### Beispiel 1: Standard-Container + +Erstellt einen einzelnen Arch-Linux-Container, der nur über den Host per Port-Weiterleitung erreichbar ist. + +```bash +python3 setup_host.py -p "IhrSicheresPasswort" +``` +*Ergebnis: Container ist über `ssh root@localhost -p 2201` erreichbar. * + +#### Beispiel 2: Mehrere Container ohne Port-Weiterleitung + +Erstellt zwei Ubuntu-Container. Der Zugriff erfolgt hier nur über `lxc exec` oder durch manuelle Konfiguration im Nachhinein, da keine Ports weitergeleitet werden. + +```bash +python3 setup_host.py \ + -c 2 \ + -p "EinAnderesSicheresPasswort" \ + --image ubuntu:22.04 \ + --hostname-template "backend-{i}" \ + --no-port-forward +``` +*Ergebnis: Container sind nur über die vom Host zugewiesene interne IP oder `lxc exec` erreichbar.* + +--- + +### Ausgabe & Verwaltung + +- **CSV-Datei:** Alle erstellten Container werden mit Name und IP in `out/clients.csv` geloggt. +- **Container-Verwaltung:** Nutzen Sie Standard-Befehle wie `lxc list`, `lxc stop `, `lxc delete `. +- **Massen-Löschung:** `lxc list projekt-x --format csv -c n | xargs -I {} lxc delete {} --force` löscht alle Container, die mit `projekt-x` beginnen. +- **Image-Verwaltung:** `lxc image list` und `lxc image delete `. + +### Port-Verwaltung (falls genutzt) + +Das Skript richtet eine Port-Weiterleitung namens `ssh-proxy` ein. + +* **Anzeigen:** `lxc config show ` +* **Entfernen:** `lxc config device remove ssh-proxy` + +``` \ No newline at end of file diff --git a/setup_container.py b/setup_container.py new file mode 100755 index 0000000..cda7cb1 --- /dev/null +++ b/setup_container.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import subprocess +import os +import sys +import argparse + +# HINWEIS: Dieses Skript ist für die Ausführung INNERHALB des Containers gedacht. + +def run_command(command_str, description, input_str=None): + """Führt einen Shell-Befehl als String aus und prüft auf Fehler.""" + print(f"\n--- {description} ---") + try: + process = subprocess.run( + command_str, + check=True, + shell=True, + text=True, + input=input_str, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + print(f"--- ERFOLG: {description} ---") + if process.stdout: + print(f"STDOUT:\n{process.stdout}") + if process.stderr: + print(f"STDERR:\n{process.stderr}", file=sys.stderr) + except subprocess.CalledProcessError as e: + print(f"FEHLER bei '{description}': Befehl '{command_str}' gab den Exit-Code {e.returncode} zurück", file=sys.stderr) + if e.stdout: + print(f"STDOUT:\n{e.stdout}", file=sys.stderr) + if e.stderr: + print(f"STDERR:\n{e.stderr}", file=sys.stderr) + sys.exit(1) + +def main(): + """Hauptfunktion des Skripts.""" + if os.geteuid() != 0: + print("FEHLER: Dieses Skript muss als root im Container ausgeführt werden.", file=sys.stderr) + sys.exit(1) + + parser = argparse.ArgumentParser(description="Konfiguriert einen neuen Container.") + parser.add_argument("password", help="Das gewünschte root-Passwort.") + parser.add_argument("hostname", help="Der Hostname für diesen Container.") + parser.add_argument("distro", help="Die Distribution des Containers (z.B. 'arch', 'ubuntu').") + args = parser.parse_args() + + root_password = args.password + hostname = args.hostname + distro = args.distro + password_input = f"{root_password}\n{root_password}\n" + + # --- Distributionsspezifische Konfiguration --- + distro_configs = { + "arch": { + "update": "pacman -Syu --noconfirm", + "install": "pacman -S --noconfirm", + "ssh_package": "openssh", + "ssh_service": "sshd.service", + "pre_install_cmds": [ + ("Pacman-Schlüsselbund initialisieren", "pacman-key --init"), + ("Pacman-Schlüsselbund füllen", "pacman-key --populate archlinux") + ], + "user_groups": "users,wheel" + }, + "ubuntu": { + "update": "apt-get update && apt-get upgrade -y", + "install": "apt-get install -y", + "ssh_package": "openssh-server", + "ssh_service": "ssh.service", + "pre_install_cmds": [ + ("Locales-Paket installieren", "apt-get install -y locales"), + ("Locales generieren", "echo 'de_DE.UTF-8 UTF-8' > /etc/locale.gen && echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && locale-gen") + ], + "user_groups": "users,sudo" + }, + "debian": { + "update": "apt-get update && apt-get upgrade -y", + "install": "apt-get install -y", + "ssh_package": "openssh-server", + "ssh_service": "ssh.service", + "pre_install_cmds": [ + ("Locales-Paket installieren", "apt-get install -y locales"), + ("Locales generieren", "echo 'de_DE.UTF-8 UTF-8' > /etc/locale.gen && echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen && locale-gen") + ], + "user_groups": "users,sudo" + }, + "fedora": { + "update": "dnf upgrade -y", + "install": "dnf install -y", + "ssh_package": "openssh-server", + "ssh_service": "sshd.service", + "pre_install_cmds": [], + "user_groups": "users,wheel" + } + } + + if distro not in distro_configs: + print(f"FEHLER: Nicht unterstützte Distribution: {distro}", file=sys.stderr) + sys.exit(1) + + config = distro_configs[distro] + print(f"Starte die Konfiguration für Container '{hostname}' mit Distribution '{distro}'...") + + # --- Allgemeine Konfiguration --- + run_command("passwd", "Root-Passwort setzen", input_str=password_input) + run_command(f"echo '{hostname}' > /etc/hostname", "Hostname setzen") + run_command("ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime", "Zeitzone setzen") + run_command("echo 'LANG=de_DE.UTF-8' > /etc/locale.conf", "Standardsprache setzen") + if distro != "fedora": # Fedora hat andere Keyboard-Settings + run_command("echo 'KEYMAP=de-latin1' > /etc/vconsole.conf", "Tastaturlayout setzen") + + # --- Distributionsspezifische Befehle ausführen --- + for description, command in config["pre_install_cmds"]: + run_command(command, description) + + run_command(config["update"], "System aktualisieren") + run_command(f"{config['install']} {config['ssh_package']}", "OpenSSH installieren") + + # Störende Cloud-Init-Konfiguration entfernen, die Passwort-Auth blockiert + run_command("rm -f /etc/ssh/sshd_config.d/60-cloudimg-settings.conf", "Entferne Cloud-Image-SSH-Einstellungen", input_str=None) + + # --- SSH und Benutzerkonfiguration --- + run_command(f"systemctl enable {config['ssh_service']}", "SSH-Dienst aktivieren") + run_command("sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config", "Root-Login in sshd_config erlauben") + run_command("sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config", "Passwort-Authentifizierung in sshd_config erlauben") + run_command("sed -i 's/^#*LogLevel.*/LogLevel DEBUG3/' /etc/ssh/sshd_config", "SSH LogLevel DEBUG3 setzen") + run_command(f"systemctl restart {config['ssh_service']}", "SSH-Dienst neustarten") + + # Benutzer 'jonnybravo' erstellen und sudo-Rechte geben + wheel_group_equivalent = "wheel" if distro in ["arch", "fedora"] else "sudo" + run_command(f"useradd -m -G {config['user_groups']} -s /bin/bash jonnybravo", "Benutzer 'jonnybravo' hinzufügen") + run_command("passwd jonnybravo", "Passwort für jonnybravo setzen", input_str=password_input) + + if wheel_group_equivalent == "wheel": + run_command("echo '%wheel ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/wheel_nopasswd", "Passwortloses sudo für Gruppe 'wheel' aktivieren") + else: # sudo group + run_command("echo '%sudo ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/sudo_nopasswd", "Passwortloses sudo für Gruppe 'sudo' aktivieren") + + + print(f"\n=== Container '{hostname}' erfolgreich konfiguriert! ===") + +if __name__ == "__main__": + main() diff --git a/setup_host.py b/setup_host.py new file mode 100755 index 0000000..de4f3b3 --- /dev/null +++ b/setup_host.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import subprocess +import os +import sys +import argparse +import time +import json + +def run_command(command_str, description, ignore_errors=False): + print(f"\n--- {description} ---") + try: + process = subprocess.run( + command_str, shell=True, text=True, check=not ignore_errors, capture_output=True) + if process.returncode == 0: + print(f"--- ERFOLG: {description} ---") + return process + else: + raise subprocess.CalledProcessError( + process.returncode, command_str, output=process.stdout, stderr=process.stderr) + except subprocess.CalledProcessError as e: + print(f"FEHLER bei '{description}': Befehl '{ + e.cmd}' gab den Exit-Code {e.returncode} zurück", file=sys.stderr) + if e.stdout: + print(f"STDOUT:\n{e.stdout}", file=sys.stderr) + if e.stderr: + print(f"STDERR:\n{e.stderr}", file=sys.stderr) + if not ignore_errors: + sys.exit(1) + return None + +def wait_for_network(container_name): + print(f"\n--- Warte auf Netzwerk für Container '{container_name}' ---") + for i in range(30): + print(f"Versuch {i+1}/30...") + try: + result = run_command(f"lxc list {container_name} --format json", "Frage Container-Status ab", ignore_errors=True) + if result and result.returncode == 0: + data = json.loads(result.stdout) + if data and data[0]["state"] and data[0]["state"]["network"] and "eth0" in data[0]["state"]["network"]: + addresses = data[0]["state"]["network"]["eth0"]["addresses"] + for addr in addresses: + if addr["family"] == "inet": + print(f"Container '{container_name}' hat IP-Adresse {addr['address']} erhalten.") + return + except (json.JSONDecodeError, IndexError, KeyError): + pass + time.sleep(2) + print(f"FEHLER: Container '{container_name}' hat nach 60 Sekunden keine IP-Adresse erhalten.", file=sys.stderr) + sys.exit(1) + +def get_container_ip(container_name): + for _ in range(15): + try: + result = run_command(f"lxc list {container_name} --format json", "Frage Container-IP ab", ignore_errors=True) + if result and result.returncode == 0: + data = json.loads(result.stdout) + if data and data[0]["state"] and data[0]["state"]["network"] and "eth0" in data[0]["state"]["network"]: + addresses = data[0]["state"]["network"]["eth0"]["addresses"] + for addr in addresses: + if addr["family"] == "inet": + return addr['address'] + except (json.JSONDecodeError, IndexError, KeyError): + pass + time.sleep(2) + return None + +def main(): + parser = argparse.ArgumentParser(description="Erstellt und konfiguriert mehrere LXD-Container.") + parser.add_argument("-c", "--count", type=int, default=1, help="Anzahl der zu erstellenden Container.") + parser.add_argument("-b", "--basename", default="vm", help="Basisname für die Container.") + parser.add_argument("--hostname-template", default="{basename}-{i:02d}", help="Vorlage für den Hostnamen, z.B. 'webserver-{i}'. Unterstützt {basename} und {i}.") + parser.add_argument("-p", "--root-password", required=True, help="Root-Passwort für alle Container.") + parser.add_argument("--port-start", type=int, default=2201, help="Start-Port für die SSH-Weiterleitung.") + parser.add_argument("--image", default="images:archlinux", help="Zu verwendendes Image (z.B. images:archlinux, ubuntu:22.04).") + parser.add_argument("--no-port-forward", action="store_true", help="Überspringt die Einrichtung der SSH-Port-Weiterleitung.") + args = parser.parse_args() + + if args.count > 1 and '{i}' not in args.hostname_template: + print("FEHLER: Bei --count > 1 muss die --hostname-template den Platzhalter '{i}' enthalten, um eindeutige Namen zu gewährleisten.", file=sys.stderr) + sys.exit(1) + + run_command("command -v lxc >/dev/null", "Prüfe, ob LXD installiert ist") + print("LXD ist verfügbar. WICHTIG: Es wird davon ausgegangen, dass 'lxd init' bereits ausgeführt wurde.") + + if not os.path.exists("out"): + os.makedirs("out") + elif not os.path.isdir("out"): + os.remove("out") + os.makedirs("out") + + clients = [] + + for i in range(1, args.count + 1): + vm_name = args.hostname_template.format(basename=args.basename, i=i) + ssh_port = args.port_start + i - 1 + + print(f"{ '=' * 20} ERSTELLE CONTAINER: {vm_name} {'=' * 20}") + + run_command(f"lxc delete {vm_name} --force", f"{vm_name} vorsorglich löschen", ignore_errors=True) + run_command(f"lxc launch {args.image} {vm_name}", f"Starte {vm_name} von Image") + wait_for_network(vm_name) + + distro_id_command = "lxc exec {} -- cat /etc/os-release | grep '^ID=' | cut -d'=' -f2" + distro_id_result = run_command(distro_id_command.format(vm_name), f"Ermittle Distributions-ID für {vm_name}") + distro_id = distro_id_result.stdout.strip().replace('"', '') + + install_cmd = "" + if distro_id == "arch": + install_cmd = "pacman -Sy --noconfirm python" + elif distro_id in ["ubuntu", "debian"]: + install_cmd = "sh -c 'export DEBIAN_FRONTEND=noninteractive; apt-get update && apt-get install -y python3'" + elif distro_id == "fedora": + install_cmd = "dnf install -y python3" + else: + print(f"FEHLER: Nicht unterstützte Distribution: {distro_id}", file=sys.stderr) + sys.exit(1) + + run_command(f"lxc exec {vm_name} -- {install_cmd}", f"Installiere Python in {vm_name}") + + run_command(f"lxc file push setup_container.py {vm_name}/root/", f"Kopiere Setup-Skript nach {vm_name}") + run_command(f"lxc exec {vm_name} -- python3 /root/setup_container.py '{args.root_password}' '{vm_name}' '{distro_id}'", f"Führe Konfiguration in {vm_name} aus") + + ssh_info = "" + if not args.no_port_forward: + run_command(f"lxc config device add {vm_name} ssh-proxy proxy listen=tcp:0.0.0.0:{ssh_port} connect=tcp:127.0.0.1:22", f"Richte Port-Weiterleitung für {vm_name} ein") + ssh_info = f"SSH-Zugang: ssh root@localhost -p {ssh_port}" + else: + print("\n--- Überspringe Port-Weiterleitung ---") + + ip_address = get_container_ip(vm_name) + if ip_address: + clients.append({"name": vm_name, "ip": ip_address}) + print(f"\n>>> Container {vm_name} erfolgreich erstellt und konfiguriert! <<<") + if ssh_info: + print(ssh_info) + print(f"IP-Adresse: {ip_address}") + else: + print(f"\nFEHLER: IP-Adresse für Container {vm_name} konnte nicht abgerufen werden.", file=sys.stderr) + + if clients: + csv_path = "out/clients.csv" + with open(csv_path, "w") as f: + f.write("name,ip\n") + for client in clients: + f.write(f"{client['name']},{client['ip']}\n") + print(f"\nAlle Container wurden erstellt und in {csv_path} gespeichert.") + else: + print("\nKeine Container wurden erstellt.") + +if __name__ == "__main__": + main() \ No newline at end of file