Thomas Stallinger 3739da37dc mint-image/hardware: Boot-Medium-Platte in gemeinsame lib ausgelagert
_mint_image_boot_medium_disk() aus backend.sh nach scripts/lib/boot_medium.sh
verschoben (tuxflotte_boot_medium_disk()) - build_storage_devices_json()
(hardware_collectors.sh) braucht dieselbe Ausschluss-Logik jetzt ebenfalls.

Nutzer-Feedback (01.09.2026, echter Hardware-Test): der Installations-
USB-Stick selbst tauchte in der Kundenplattform unter Flotte -> Aktionen ->
Hardware-Info -> Datenträger auf - fuer den Kunden irrefuehrend, da dieser
Datentraeger nach der Installation gar nicht mehr existiert. Gleiche
Ursache wie der parted-Bug (Commit fa54461): lsblk liefert das Boot-Medium
auf echter USB-Stick-Hardware als normalen TYPE="disk" zurueck.
2026-09-01 15:45:25 +02:00

561 lines
14 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
readonly BOOT_MEDIUM_FILE="${SCRIPT_DIR}/../lib/boot_medium.sh"
if [[ ! -r "${COLLECTORS_FILE}" ]]; then
printf '[%s] FEHLER: Collector-Library nicht gefunden: %s\n' \
"${0##*/}" \
"${COLLECTORS_FILE}" >&2
exit 1
fi
if [[ ! -r "${BOOT_MEDIUM_FILE}" ]]; then
printf '[%s] FEHLER: Boot-Medium-Library nicht gefunden: %s\n' \
"${0##*/}" \
"${BOOT_MEDIUM_FILE}" >&2
exit 1
fi
# shellcheck source=../lib/hardware_collectors.sh
source "${COLLECTORS_FILE}"
# shellcheck source=../lib/boot_medium.sh
source "${BOOT_MEDIUM_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 "$@"