feat: collect hardware and device identity

This commit is contained in:
Thomas Stallinger 2026-07-13 09:15:00 +02:00
parent b1f35a5ef4
commit 74a6e181ce
2 changed files with 611 additions and 0 deletions

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
)
}
]
'
}

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 "$@"