Compare commits

...

4 Commits

14 changed files with 1395 additions and 0 deletions

0
config/installer.conf Normal file
View File

View File

@ -82,6 +82,24 @@ verify_workdir() {
echo "GRUB verification passed." echo "GRUB verification passed."
} }
create_iso() {
echo "Creating Tuxflotte ISO..."
local output_iso="$OUTPUT_DIR/tuxflotte-provisioning-0.1.iso"
rm -f "$output_iso"
xorriso \
-indev "$SOURCE_ISO" \
-outdev "$output_iso" \
-compliance no_emul_toc \
-map "$REPO_DIR/grub/EFI-BOOT-grub.cfg" /EFI/BOOT/grub.cfg \
-map "$REPO_DIR/grub/boot-grub2-grub.cfg" /boot/grub2/grub.cfg \
-boot_image any replay
echo "ISO created: $output_iso"
}
main() { main() {
check_input check_input
check_dependencies check_dependencies
@ -89,6 +107,7 @@ main() {
extract_iso extract_iso
patch_grub patch_grub
verify_workdir verify_workdir
create_iso
echo echo
echo "Build preparation complete." echo "Build preparation complete."

33
scripts/installer.sh Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALLER_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$SCRIPT_DIR/lib/logging.sh"
source "$SCRIPT_DIR/lib/errors.sh"
source "$SCRIPT_DIR/lib/utils.sh"
source "$SCRIPT_DIR/lib/checks.sh"
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
*) error_exit "Unbekannter Parameter: $arg" ;;
esac
done
load_config "$INSTALLER_ROOT/config/installer.conf"
log_info "Tuxflotte Installer gestartet"
log_info "Installer Root: $INSTALLER_ROOT"
[[ "$DRY_RUN" == true ]] && log_warn "Dry-Run aktiv"
run_module "$SCRIPT_DIR/modules/00_preflight.sh" "always"
run_module "$SCRIPT_DIR/modules/10_hardware.sh" "always"
run_module "$SCRIPT_DIR/modules/20_storage.sh" "dry-run-safe"
run_module "$SCRIPT_DIR/modules/99_finish.sh" "always"
log_success "Tuxflotte Installer abgeschlossen"

17
scripts/lib/checks.sh Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
detect_uefi() {
if [[ -d /sys/firmware/efi ]]; then
echo "uefi"
else
echo "bios"
fi
}
check_network() {
ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1
}
list_install_disks() {
lsblk -dpno NAME,SIZE,MODEL,TRAN,TYPE | awk '$5 == "disk" && $4 != "usb" {print}'
}

8
scripts/lib/errors.sh Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
error_exit() {
log_error "$1"
exit "${2:-1}"
}
trap 'error_exit "Unerwarteter Fehler in Zeile $LINENO."' ERR

View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
get_cpu_model() {
awk -F: '
$1 ~ /^model name[[:space:]]*$/ {
value = $2
sub(/^[[:space:]]*/, "", value)
print value
exit
}
' /proc/cpuinfo
}
get_cpu_count() {
getconf _NPROCESSORS_ONLN
}
get_memory_bytes() {
awk '
$1 == "MemTotal:" {
print $2 * 1024
exit
}
' /proc/meminfo
}
build_storage_devices_json() {
lsblk \
--bytes \
--json \
--nodeps \
--output NAME,TYPE,MODEL,SERIAL,SIZE,TRAN |
jq '
[
.blockdevices[]
| select(.type == "disk")
| {
name: .name,
model: (
if .model == null or .model == ""
then null
else (.model | gsub("^[[:space:]]+|[[:space:]]+$"; ""))
end
),
serial: (
if .serial == null or .serial == ""
then null
else (.serial | gsub("^[[:space:]]+|[[:space:]]+$"; ""))
end
),
size_bytes: .size,
transport: (
if .tran == null or .tran == ""
then null
else .tran
end
)
}
]
'
}

17
scripts/lib/logging.sh Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
log_info() {
echo "[INFO ] $*"
}
log_warn() {
echo "[WARN ] $*" >&2
}
log_error() {
echo "[ERROR] $*" >&2
}
log_success() {
echo "[ OK ] $*"
}

40
scripts/lib/utils.sh Normal file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env bash
load_config() {
local config_file="$1"
if [[ -f "$config_file" ]]; then
# shellcheck disable=SC1090
source "$config_file"
log_info "Konfiguration geladen: $config_file"
else
log_warn "Keine Konfiguration gefunden: $config_file"
fi
}
run_module() {
local module="$1"
local mode="${2:-normal}"
[[ -f "$module" ]] || error_exit "Modul nicht gefunden: $module"
log_info "Starte Modul: $(basename "$module")"
if [[ "${DRY_RUN:-false}" == true && "$mode" != "always" ]]; then
log_warn "Dry-Run: Modul übersprungen: $module"
return 0
fi
# shellcheck disable=SC1090
source "$module"
log_success "Modul abgeschlossen: $(basename "$module")"
}
require_root() {
[[ "$EUID" -eq 0 ]] || error_exit "Installer muss als root ausgeführt werden."
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}

View File

497
scripts/modules/05_network.sh Executable file
View File

@ -0,0 +1,497 @@
#!/usr/bin/env bash
# Tuxflotte Installer
# Phase 2 Netzwerkinitialisierung
#
# Unterstützt zunächst:
# - Ethernet über DHCP
# - bereits aktive NetworkManager-Verbindungen
# - WPA2/WPA3 Personal
# - interaktive WLAN-Auswahl
# - Prüfung des Tuxflotte-Servers
# - geschützte Runtime-Ablage des aktiven Netzwerkprofils
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly RUNTIME_DIR="/run/tuxflotte/network"
readonly STATE_FILE="${RUNTIME_DIR}/state.env"
readonly CONNECTION_EXPORT="${RUNTIME_DIR}/connection.nmconnection"
readonly SERVER_URL="${TUXFLOTTE_SERVER_URL:-https://anode.tuxflotte.de/health}"
readonly SERVER_TIMEOUT="${TUXFLOTTE_SERVER_TIMEOUT:-10}"
readonly NMCLI="${NMCLI:-nmcli}"
readonly CURL="${CURL:-curl}"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
warn() {
printf '[%s] WARNUNG: %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Netzwerkmodul muss als root ausgeführt werden."
fi
}
require_command() {
local command_name="$1"
command -v "${command_name}" >/dev/null 2>&1 ||
fatal "Benötigtes Programm nicht gefunden: ${command_name}"
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${STATE_FILE}" "${CONNECTION_EXPORT}"
}
networkmanager_is_running() {
"${NMCLI}" -t -f RUNNING general 2>/dev/null |
grep -qx 'running'
}
start_networkmanager_if_possible() {
if networkmanager_is_running; then
return 0
fi
log "NetworkManager ist noch nicht aktiv."
if command -v systemctl >/dev/null 2>&1; then
log "Versuche NetworkManager zu starten."
systemctl start NetworkManager.service 2>/dev/null || true
fi
networkmanager_is_running ||
fatal "NetworkManager konnte nicht verwendet werden."
}
enable_networking() {
"${NMCLI}" networking on >/dev/null 2>&1 || true
"${NMCLI}" radio wifi on >/dev/null 2>&1 || true
}
device_has_ipv4() {
local device="$1"
"${NMCLI}" -g IP4.ADDRESS device show "${device}" 2>/dev/null |
grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/'
}
get_active_device() {
local device
local type
while IFS=: read -r device type _; do
case "${type}" in
ethernet|wifi)
;;
*)
continue
;;
esac
[[ -e "/sys/class/net/${device}/device" ]] || continue
if device_has_ipv4 "${device}"; then
printf '%s\n' "${device}"
return 0
fi
done < <(
"${NMCLI}" \
--terse \
--fields DEVICE,TYPE,STATE \
device status
)
return 1
}
get_active_connection_name() {
local device="$1"
"${NMCLI}" \
--get-values GENERAL.CONNECTION \
device show "${device}" 2>/dev/null |
head -n 1
}
get_device_type() {
local device="$1"
"${NMCLI}" \
--get-values GENERAL.TYPE \
device show "${device}" 2>/dev/null |
head -n 1
}
server_is_reachable() {
"${CURL}" \
--silent \
--show-error \
--fail \
--location \
--connect-timeout "${SERVER_TIMEOUT}" \
--max-time "${SERVER_TIMEOUT}" \
--output /dev/null \
"${SERVER_URL}"
}
check_existing_connection() {
local device
device="$(get_active_device || true)"
if [[ -z "${device}" ]]; then
return 1
fi
if ! device_has_ipv4 "${device}"; then
warn "Interface ${device} ist verbunden, besitzt aber keine IPv4-Adresse."
return 1
fi
log "Aktive Netzwerkverbindung über ${device} gefunden."
if server_is_reachable; then
log "Tuxflotte-Server ist erreichbar."
return 0
fi
warn "Netzwerk ist aktiv, aber der Tuxflotte-Server ist nicht erreichbar."
return 1
}
get_ethernet_devices() {
local device
while IFS=: read -r device type state; do
[[ "${type}" == "ethernet" ]] || continue
[[ "${state}" != "unavailable" ]] || continue
[[ -e "/sys/class/net/${device}/device" ]] || continue
printf '%s\n' "${device}"
done < <(
"${NMCLI}" \
--terse \
--fields DEVICE,TYPE,STATE \
device status
)
}
try_ethernet() {
local device
while IFS= read -r device; do
[[ -n "${device}" ]] || continue
log "Prüfe Ethernet-Interface ${device}."
"${NMCLI}" device connect "${device}" >/dev/null 2>&1 || true
if device_has_ipv4 "${device}"; then
log "Ethernet-Verbindung über ${device} hergestellt."
if server_is_reachable; then
log "Tuxflotte-Server ist über Ethernet erreichbar."
return 0
fi
warn "Ethernet besitzt eine IP-Adresse, aber der Server ist nicht erreichbar."
fi
done < <(get_ethernet_devices)
return 1
}
get_wifi_device() {
"${NMCLI}" \
--terse \
--fields DEVICE,TYPE,STATE \
device status |
awk -F: '$2 == "wifi" && $3 != "unavailable" { print $1; exit }'
}
scan_wifi_networks() {
local wifi_device="$1"
"${NMCLI}" device wifi rescan ifname "${wifi_device}" >/dev/null 2>&1 ||
true
sleep 2
"${NMCLI}" \
--terse \
--escape yes \
--fields SSID,SIGNAL,SECURITY \
device wifi list \
ifname "${wifi_device}" |
awk -F: '
$1 != "" && !seen[$1]++ {
printf "%s\t%s\t%s\n", $1, $2, $3
}
' |
sort -t $'\t' -k2,2nr
}
choose_wifi_ssid() {
local wifi_device="$1"
local -a networks=()
local entry
local choice
local index=1
while IFS= read -r entry; do
[[ -n "${entry}" ]] && networks+=("${entry}")
done < <(scan_wifi_networks "${wifi_device}")
if [[ "${#networks[@]}" -eq 0 ]]; then
warn "Keine sichtbaren WLAN-Netze gefunden."
read -r -p "Versteckte SSID manuell eingeben oder leer abbrechen: " WIFI_SSID
[[ -n "${WIFI_SSID}" ]]
return
fi
printf '\nVerfügbare WLAN-Netze:\n\n' >&2
for entry in "${networks[@]}"; do
IFS=$'\t' read -r ssid signal security <<<"${entry}"
printf ' %2d) %-32s Signal: %-3s Sicherheit: %s\n' \
"${index}" \
"${ssid}" \
"${signal}" \
"${security:-offen}" >&2
((index += 1))
done
printf '\n' >&2
read -r -p "WLAN auswählen [1-${#networks[@]}], m = manuell, q = abbrechen: " choice
case "${choice}" in
q|Q)
return 1
;;
m|M)
read -r -p "SSID: " WIFI_SSID
[[ -n "${WIFI_SSID}" ]]
;;
*)
if [[ ! "${choice}" =~ ^[0-9]+$ ]] ||
(( choice < 1 || choice > ${#networks[@]} )); then
warn "Ungültige Auswahl."
return 1
fi
IFS=$'\t' read -r WIFI_SSID _ _ <<<"${networks[choice - 1]}"
;;
esac
}
wifi_network_is_open() {
local wifi_device="$1"
local ssid="$2"
local security
security="$(
"${NMCLI}" \
--terse \
--escape no \
--fields SSID,SECURITY \
device wifi list \
ifname "${wifi_device}" |
awk -F: -v wanted="${ssid}" '
$1 == wanted {
print $2
exit
}
'
)"
[[ -z "${security}" || "${security}" == "--" ]]
}
connect_wifi() {
local wifi_device="$1"
choose_wifi_ssid "${wifi_device}" ||
return 1
log "Verbinde mit WLAN '${WIFI_SSID}'."
if wifi_network_is_open "${wifi_device}" "${WIFI_SSID}"; then
if ! "${NMCLI}" \
device wifi connect "${WIFI_SSID}" \
ifname "${wifi_device}" \
>/dev/null; then
warn "Verbindung mit dem offenen WLAN konnte nicht hergestellt werden."
return 1
fi
else
log "NetworkManager fragt die WLAN-Zugangsdaten geschützt ab."
if ! "${NMCLI}" \
--ask \
device wifi connect "${WIFI_SSID}" \
ifname "${wifi_device}" \
>/dev/null; then
warn "WLAN-Anmeldung ist fehlgeschlagen."
return 1
fi
fi
if ! device_has_ipv4 "${wifi_device}"; then
warn "WLAN-Verbindung besitzt keine IPv4-Adresse."
return 1
fi
log "WLAN-Verbindung wurde hergestellt."
if ! server_is_reachable; then
warn "WLAN ist verbunden, aber der Tuxflotte-Server ist nicht erreichbar."
return 1
fi
log "Tuxflotte-Server ist über WLAN erreichbar."
}
store_network_state() {
local device
local connection_name
local device_type
local connection_uuid=""
device="$(get_active_device)" ||
fatal "Kein aktives Provisionierungsinterface gefunden."
connection_name="$(
get_active_connection_name "${device}" || true
)"
device_type="$(get_device_type "${device}")"
if [[ -n "${connection_name}" && "${connection_name}" != "--" ]]; then
connection_uuid="$(
"${NMCLI}" \
--get-values connection.uuid \
connection show "${connection_name}" 2>/dev/null |
head -n 1
)"
else
connection_name=""
warn "Interface ${device} wird nicht durch ein aktives NetworkManager-Profil verwaltet."
fi
umask 077
{
printf 'TUXFLOTTE_NETWORK_DEVICE=%q\n' "${device}"
printf 'TUXFLOTTE_NETWORK_TYPE=%q\n' "${device_type}"
printf 'TUXFLOTTE_CONNECTION_NAME=%q\n' "${connection_name}"
printf 'TUXFLOTTE_CONNECTION_UUID=%q\n' "${connection_uuid}"
printf 'TUXFLOTTE_SERVER_URL=%q\n' "${SERVER_URL}"
} >"${STATE_FILE}"
chmod 0600 "${STATE_FILE}"
log "Netzwerkstatus wurde unter ${STATE_FILE} gespeichert."
}
export_connection_profile() {
local device
local connection_name
local source_file
device="$(get_active_device)" ||
fatal "Kein aktives Provisionierungsinterface gefunden."
connection_name="$(
get_active_connection_name "${device}" || true
)"
if [[ -z "${connection_name}" || "${connection_name}" == "--" ]]; then
warn "Für Interface ${device} existiert kein aktives NetworkManager-Profil."
return 0
fi
source_file="$(
"${NMCLI}" \
--get-values connection.filename \
connection show "${connection_name}" 2>/dev/null |
head -n 1
)"
if [[ -z "${source_file}" || ! -f "${source_file}" ]]; then
warn "NetworkManager-Profil konnte nicht exportiert werden."
return 0
fi
install \
--mode=0600 \
--owner=root \
--group=root \
"${source_file}" \
"${CONNECTION_EXPORT}"
log "Aktives Verbindungsprofil wurde geschützt vorgemerkt."
}
main() {
require_root
require_command "${NMCLI}"
require_command "${CURL}"
prepare_runtime_directory
start_networkmanager_if_possible
enable_networking
log "Prüfe vorhandene Netzwerkverbindungen."
if check_existing_connection; then
:
elif try_ethernet; then
:
else
local wifi_device
wifi_device="$(get_wifi_device || true)"
if [[ -z "${wifi_device}" ]]; then
fatal "Keine funktionierende Ethernet-Verbindung und keine WLAN-Hardware gefunden."
fi
log "Ethernet ist nicht verfügbar. WLAN-Initialisierung wird gestartet."
connect_wifi "${wifi_device}" ||
fatal "Es konnte keine Verbindung zum Tuxflotte-Server hergestellt werden."
fi
store_network_state
export_connection_profile
log "Netzwerkinitialisierung erfolgreich abgeschlossen."
}
main "$@"

550
scripts/modules/10_hardware.sh Executable file
View File

@ -0,0 +1,550 @@
#!/usr/bin/env bash
# Tuxflotte Installer
# Phase 2 Hardware- und Geräteidentität
#
# Ermittelt:
# - DMI-/SMBIOS-Daten
# - System-UUID und Seriennummer
# - CPU-Architektur
# - physische Netzwerkinterfaces und MAC-Adressen
# - TPM-Verfügbarkeit
# - UEFI- und Secure-Boot-Status
#
# Ausgabe:
# /run/tuxflotte/hardware/hardware.json
set -Eeuo pipefail
SCRIPT_DIR="$(
cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &&
pwd
)"
readonly SCRIPT_DIR
readonly COLLECTORS_FILE="${SCRIPT_DIR}/../lib/hardware_collectors.sh"
if [[ ! -r "${COLLECTORS_FILE}" ]]; then
printf '[%s] FEHLER: Collector-Library nicht gefunden: %s\n' \
"${0##*/}" \
"${COLLECTORS_FILE}" >&2
exit 1
fi
# shellcheck source=../lib/hardware_collectors.sh
source "${COLLECTORS_FILE}"
readonly SCRIPT_NAME="${0##*/}"
readonly RUNTIME_DIR="/run/tuxflotte/hardware"
readonly HARDWARE_FILE="${RUNTIME_DIR}/hardware.json"
readonly SYS_DMI_DIR="/sys/class/dmi/id"
readonly SYS_NET_DIR="/sys/class/net"
readonly EFI_VARS_DIR="/sys/firmware/efi/efivars"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
warn() {
printf '[%s] WARNUNG: %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Hardwaremodul muss als root ausgeführt werden."
fi
}
require_command() {
local command_name="$1"
command -v "${command_name}" >/dev/null 2>&1 ||
fatal "Benötigtes Programm nicht gefunden: ${command_name}"
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${HARDWARE_FILE}"
}
read_trimmed_file() {
local file="$1"
local value
if [[ ! -r "${file}" ]]; then
return 0
fi
value="$(tr -d '\000' <"${file}")"
value="$(
printf '%s' "${value}" |
sed \
-e 's/^[[:space:]]*//' \
-e 's/[[:space:]]*$//'
)"
printf '%s' "${value}"
}
read_dmi_value() {
local name="$1"
read_trimmed_file "${SYS_DMI_DIR}/${name}"
}
normalize_uuid() {
local value="$1"
value="${value,,}"
case "${value}" in
""|\
"none"|\
"not specified"|\
"to be filled by o.e.m."|\
"00000000-0000-0000-0000-000000000000"|\
"ffffffff-ffff-ffff-ffff-ffffffffffff")
return 0
;;
esac
printf '%s' "${value}"
}
normalize_serial() {
local value="$1"
local normalized
normalized="${value,,}"
case "${normalized}" in
""|\
"none"|\
"unknown"|\
"not specified"|\
"default string"|\
"system serial number"|\
"to be filled by o.e.m.")
return 0
;;
esac
printf '%s' "${value}"
}
get_machine_id() {
local candidate
for candidate in \
/etc/machine-id \
/var/lib/dbus/machine-id
do
if [[ -r "${candidate}" ]]; then
read_trimmed_file "${candidate}"
return 0
fi
done
}
get_architecture() {
uname -m
}
get_boot_mode() {
if [[ -d /sys/firmware/efi ]]; then
printf 'uefi'
else
printf 'bios'
fi
}
get_secure_boot_state() {
local secure_boot_file
local value
if [[ ! -d /sys/firmware/efi ]]; then
printf 'unsupported'
return 0
fi
secure_boot_file="$(
find "${EFI_VARS_DIR}" \
-maxdepth 1 \
-type f \
-name 'SecureBoot-*' \
-print \
-quit 2>/dev/null || true
)"
if [[ -z "${secure_boot_file}" || ! -r "${secure_boot_file}" ]]; then
printf 'unknown'
return 0
fi
value="$(
od \
--address-radix=n \
--format=u1 \
--skip-bytes=4 \
--read-bytes=1 \
"${secure_boot_file}" 2>/dev/null |
tr -d '[:space:]'
)"
case "${value}" in
1)
printf 'enabled'
;;
0)
printf 'disabled'
;;
*)
printf 'unknown'
;;
esac
}
get_tpm_version() {
if [[ ! -e /dev/tpm0 && ! -e /dev/tpmrm0 ]]; then
printf 'none'
return 0
fi
if [[ -r /sys/class/tpm/tpm0/tpm_version_major ]]; then
read_trimmed_file /sys/class/tpm/tpm0/tpm_version_major
return 0
fi
if [[ -r /sys/class/tpm/tpm0/device/description ]]; then
local description
description="$(
read_trimmed_file /sys/class/tpm/tpm0/device/description
)"
case "${description}" in
*2.0*)
printf '2'
;;
*1.2*)
printf '1.2'
;;
*)
printf 'unknown'
;;
esac
return 0
fi
printf 'unknown'
}
interface_is_physical() {
local interface="$1"
[[ "${interface}" != "lo" ]] || return 1
[[ -e "${SYS_NET_DIR}/${interface}/device" ]] || return 1
[[ -r "${SYS_NET_DIR}/${interface}/address" ]] || return 1
}
get_interface_type() {
local interface="$1"
if [[ -d "${SYS_NET_DIR}/${interface}/wireless" ]]; then
printf 'wifi'
else
printf 'ethernet'
fi
}
build_network_interfaces_json() {
local interface
local mac
local type
local -a interfaces=()
for interface_path in "${SYS_NET_DIR}"/*; do
[[ -e "${interface_path}" ]] || continue
interface="${interface_path##*/}"
interface_is_physical "${interface}" || continue
mac="$(read_trimmed_file "${interface_path}/address")"
type="$(get_interface_type "${interface}")"
[[ -n "${mac}" ]] || continue
interfaces+=("$(
jq \
--null-input \
--arg name "${interface}" \
--arg type "${type}" \
--arg mac "${mac,,}" \
'{
name: $name,
type: $type,
mac: $mac
}'
)")
done
if [[ "${#interfaces[@]}" -eq 0 ]]; then
printf '[]'
return 0
fi
printf '%s\n' "${interfaces[@]}" |
jq --slurp 'sort_by(.type, .name)'
}
build_device_fingerprint() {
local system_uuid="$1"
local system_serial="$2"
local board_serial="$3"
local interfaces_json="$4"
local identity_material
local mac_addresses
mac_addresses="$(
jq \
--raw-output \
'.[].mac // empty' \
<<<"${interfaces_json}" |
tr '[:upper:]' '[:lower:]' |
sort -u |
paste -sd ',' -
)"
identity_material="$(
printf 'system_uuid=%s\n' "${system_uuid,,}"
printf 'system_serial=%s\n' "${system_serial,,}"
printf 'board_serial=%s\n' "${board_serial,,}"
printf 'mac_addresses=%s\n' "${mac_addresses}"
)"
printf '%s' "${identity_material}" |
sha256sum |
awk '{ print $1 }'
}
build_hardware_json() {
local system_uuid
local system_serial
local machine_id
local manufacturer
local product_name
local product_version
local board_vendor
local board_name
local board_serial
local bios_vendor
local bios_version
local architecture
local boot_mode
local secure_boot
local tpm_version
local interfaces_json
local device_fingerprint
local cpu_model
local cpu_count
local memory_bytes
local storage_devices_json
system_uuid="$(normalize_uuid "$(read_dmi_value product_uuid)")"
system_serial="$(normalize_serial "$(read_dmi_value product_serial)")"
machine_id="$(get_machine_id)"
manufacturer="$(read_dmi_value sys_vendor)"
product_name="$(read_dmi_value product_name)"
product_version="$(read_dmi_value product_version)"
board_vendor="$(read_dmi_value board_vendor)"
board_name="$(read_dmi_value board_name)"
board_serial="$(normalize_serial "$(read_dmi_value board_serial)")"
bios_vendor="$(read_dmi_value bios_vendor)"
bios_version="$(read_dmi_value bios_version)"
architecture="$(get_architecture)"
boot_mode="$(get_boot_mode)"
secure_boot="$(get_secure_boot_state)"
tpm_version="$(get_tpm_version)"
cpu_model="$(get_cpu_model)"
cpu_count="$(get_cpu_count)"
memory_bytes="$(get_memory_bytes)"
interfaces_json="$(build_network_interfaces_json)"
storage_devices_json="$(build_storage_devices_json)"
device_fingerprint="$(
build_device_fingerprint \
"${system_uuid}" \
"${system_serial}" \
"${board_serial}" \
"${interfaces_json}"
)"
jq \
--null-input \
--arg schema_version "1" \
--arg device_fingerprint "${device_fingerprint}" \
--arg system_uuid "${system_uuid}" \
--arg system_serial "${system_serial}" \
--arg machine_id "${machine_id}" \
--arg manufacturer "${manufacturer}" \
--arg product_name "${product_name}" \
--arg product_version "${product_version}" \
--arg board_vendor "${board_vendor}" \
--arg board_name "${board_name}" \
--arg board_serial "${board_serial}" \
--arg bios_vendor "${bios_vendor}" \
--arg bios_version "${bios_version}" \
--arg architecture "${architecture}" \
--arg boot_mode "${boot_mode}" \
--arg secure_boot "${secure_boot}" \
--arg tpm_version "${tpm_version}" \
--arg cpu_model "${cpu_model}" \
--argjson cpu_count "${cpu_count}" \
--argjson memory_bytes "${memory_bytes}" \
--argjson network_interfaces "${interfaces_json}" \
--argjson storage_devices "${storage_devices_json}" \
'{
schema_version: ($schema_version | tonumber),
identity: {
device_fingerprint: $device_fingerprint,
system_uuid: (
if $system_uuid == "" then null else $system_uuid end
),
system_serial: (
if $system_serial == "" then null else $system_serial end
),
board_serial: (
if $board_serial == "" then null else $board_serial end
),
machine_id: (
if $machine_id == "" then null else $machine_id end
)
},
system: {
manufacturer: (
if $manufacturer == "" then null else $manufacturer end
),
product_name: (
if $product_name == "" then null else $product_name end
),
product_version: (
if $product_version == "" then null else $product_version end
),
architecture: $architecture,
cpu: {
model: (
if $cpu_model == "" then null else $cpu_model end
),
logical_count: $cpu_count
},
memory_bytes: $memory_bytes,
},
mainboard: {
vendor: (
if $board_vendor == "" then null else $board_vendor end
),
name: (
if $board_name == "" then null else $board_name end
)
},
firmware: {
bios_vendor: (
if $bios_vendor == "" then null else $bios_vendor end
),
bios_version: (
if $bios_version == "" then null else $bios_version end
),
boot_mode: $boot_mode,
secure_boot: $secure_boot
},
security: {
tpm_version: $tpm_version
},
network_interfaces: $network_interfaces,
storage_devices: $storage_devices
}'
}
validate_hardware_identity() {
local uuid
local serial
local board_serial
local mac_count
uuid="$(jq -r '.identity.system_uuid // empty' "${HARDWARE_FILE}")"
serial="$(jq -r '.identity.system_serial // empty' "${HARDWARE_FILE}")"
board_serial="$(jq -r '.identity.board_serial // empty' "${HARDWARE_FILE}")"
mac_count="$(jq '.network_interfaces | length' "${HARDWARE_FILE}")"
if [[ -z "${uuid}" &&
-z "${serial}" &&
-z "${board_serial}" &&
"${mac_count}" -eq 0 ]]; then
fatal "Es konnte kein stabiles Hardwaremerkmal ermittelt werden."
fi
if [[ -z "${uuid}" ]]; then
warn "Das Gerät stellt keine verwertbare System-UUID bereit."
fi
if [[ -z "${serial}" ]]; then
warn "Das Gerät stellt keine verwertbare Systemseriennummer bereit."
fi
}
main() {
require_root
require_command jq
require_command uname
require_command sed
require_command find
require_command od
require_command sha256sum
require_command sort
require_command paste
require_command tr
require_command awk
prepare_runtime_directory
log "Ermittle Hardware- und Geräteidentität."
umask 077
build_hardware_json >"${HARDWARE_FILE}"
chmod 0600 "${HARDWARE_FILE}"
jq --exit-status . "${HARDWARE_FILE}" >/dev/null ||
fatal "Die erzeugte Hardwaredatei enthält kein gültiges JSON."
validate_hardware_identity
log "Hardwareinformationen wurden unter ${HARDWARE_FILE} gespeichert."
}
main "$@"

View File

@ -0,0 +1,139 @@
#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly NETWORK_STATE="/run/tuxflotte/network/state.env"
readonly HARDWARE_FILE="/run/tuxflotte/hardware/hardware.json"
readonly RUNTIME_DIR="/run/tuxflotte/server"
readonly ACTIVATION_FILE="${RUNTIME_DIR}/activation.json"
readonly RESPONSE_FILE="${RUNTIME_DIR}/response.json"
log() {
printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" >&2
}
fatal() {
printf '[%s] FEHLER: %s\n' "${SCRIPT_NAME}" "$*" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
fatal "Das Server-Handshake-Modul muss als root ausgeführt werden."
fi
}
prepare_runtime_directory() {
install -d \
--mode=0700 \
--owner=root \
--group=root \
"${RUNTIME_DIR}"
rm -f -- "${ACTIVATION_FILE}" "${RESPONSE_FILE}"
}
validate_inputs() {
[[ -r "${NETWORK_STATE}" ]] ||
fatal "Netzwerkstatus nicht gefunden: ${NETWORK_STATE}"
[[ -r "${HARDWARE_FILE}" ]] ||
fatal "Hardwareinformationen nicht gefunden: ${HARDWARE_FILE}"
jq --exit-status . "${HARDWARE_FILE}" >/dev/null ||
fatal "Hardwaredatei enthält kein gültiges JSON."
}
build_activation_request() {
local activation_code="$1"
local hostname
local machine_id
hostname="$(hostname)"
machine_id="$(
jq --raw-output \
'.identity.machine_id // empty' \
"${HARDWARE_FILE}"
)"
jq \
--null-input \
--arg activation_code "${activation_code}" \
--arg hostname "${hostname}" \
--arg machine_id "${machine_id}" \
--arg client_version "0.1.0" \
--slurpfile hardware "${HARDWARE_FILE}" \
'{
activation_code: $activation_code,
device_fingerprint: $hardware[0].identity.device_fingerprint,
hostname: $hostname,
machine_id: (
if $machine_id == ""
then null
else $machine_id
end
),
client_version: $client_version,
hardware: $hardware[0]
}'
}
send_activation_request() {
local server_url
# shellcheck disable=SC1090
source "${NETWORK_STATE}"
server_url="${TUXFLOTTE_SERVER_URL%/health}"
curl \
--silent \
--show-error \
--fail \
--location \
--header 'Content-Type: application/json' \
--data-binary "@${ACTIVATION_FILE}" \
--output "${RESPONSE_FILE}" \
"${server_url}/api/v1/activate" ||
fatal "Provisioning-Server konnte nicht erfolgreich kontaktiert werden."
chmod 0600 "${RESPONSE_FILE}"
jq --exit-status . "${RESPONSE_FILE}" >/dev/null ||
fatal "Serverantwort enthält kein gültiges JSON."
jq --exit-status '.success == true' "${RESPONSE_FILE}" >/dev/null ||
fatal "Provisioning-Server hat die Aktivierung abgelehnt."
log "Provisioning-Handshake erfolgreich abgeschlossen."
}
main() {
require_root
prepare_runtime_directory
validate_inputs
log "Eingabedaten für den Provisioning-Handshake sind gültig."
local activation_code
activation_code="${TUXFLOTTE_ACTIVATION_CODE:-LAB-2026-START}"
build_activation_request "${activation_code}" >"${ACTIVATION_FILE}"
chmod 0600 "${ACTIVATION_FILE}"
jq --exit-status . "${ACTIVATION_FILE}" >/dev/null ||
fatal "Aktivierungsrequest enthält kein gültiges JSON."
log "Aktivierungsrequest wurde unter ${ACTIVATION_FILE} gespeichert."
send_activation_request
}
main "$@"

14
scripts/modules/20_storage.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
log_info "Datenträger werden erkannt..."
DISKS="$(list_install_disks || true)"
if [[ -z "$DISKS" ]]; then
error_exit "Keine geeigneten Datenträger erkannt."
fi
echo "$DISKS"
log_warn "Phase 1: Datenträger werden nur angezeigt, nicht verändert."
log_warn "Partitionierung ist noch deaktiviert."

0
scripts/modules/99_finish.sh Executable file
View File