diff --git a/.gitignore b/.gitignore index db879a2..c9470f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /config/ web/ux/ test* +example* .DS_Store .env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4db0f54 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.multiRootWorkspaceName": "numbus" +} \ No newline at end of file diff --git a/script/deploy.sh b/script/deploy.sh index 6e644c3..457fe94 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -1,5 +1,5 @@ #!/usr/bin/env nix-shell -#!nix-shell -i bash -p bash nano coreutils gnused gum fastfetch xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq yq python3 +#!nix-shell -i bash -p bash coreutils gnused gum xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq yq python3 @@ -14,7 +14,7 @@ echod() { ssh_to_host() { local COMMAND="${1}" - ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" + ssh -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" } get_valid_input() { @@ -32,12 +32,12 @@ get_valid_input() { fi while true; do - local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}") + local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}" --password="${SENSITIVE}") # Handle empty input if [[ -z "${INPUT}" ]]; then if [[ "${MANDATORY}" == true ]]; then - gum style --foreground "#ff0000" -- "✖ This field is mandatory." + gum style --foreground "#ff0000" -- " ❌ This field is mandatory." continue else INPUT="" @@ -51,7 +51,7 @@ get_valid_input() { export "${VAR_NAME}"="${INPUT}" break else - gum style --foreground "#ff0000" -- "✖ Invalid format. Please try again." + gum style --foreground "#ff0000" -- " ❌ Invalid format. Please try again." fi else export "${VAR_NAME}"="${INPUT}" @@ -65,196 +65,357 @@ get_valid_input() { # --- GLOBAL FUNCTIONS ---> cleanup() { - echo -e "\n ✅ Cleaning up..." + if [[ "${DEBUG}" -eq 1 ]]; then + echo -e "\n ✅ Exiting..." + echo -e "\n ✅ Debug mode is enabled. Clean up manually files located at ${INSTALL_DIR} or reboot." + else + echo -e "\n ✅ Cleaning up..." + rm -${DIR_RM_FLAGS} "${INSTALL_DIR}" + fi - rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/ - - if ps -p ${BRIDGE_PID:-} > /dev/null; then + if [[ -n "${BRIDGE_PID:-}" ]] && ps -p ${BRIDGE_PID} >> "${STDOUT}" 2>> "${STDERR}"; then kill ${BRIDGE_PID} fi + + echo -e "\n 🌟 Thanks for using Numbus, consider supporting the project !" } -compatibility_check() { - TEST_FAIL=0 +launch_gui() { + echo -e "\n 🚀 Launching Numbus Configurator..." + echo -e " ➡️ You will now proceed to the configuration of your device through your browser" - if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then - echo -e "\n ✅ NixOS system detected." - else - TEST_FAIL=$((TEST_FAIL + 1)) - echo -e "\n ❌ You are not on a NixOS based system. This is required to continue." - fi + python3 "${BRIDGE_SCRIPT}" >> "${STDOUT}" 2>> "${STDERR}" & + export BRIDGE_PID=$! + + local START_URL="http://localhost:${WEBSERVER_PORT}/pages/index.html" - if [[ "$(uname -m)" == "x86_64" ]]; then - echo -e "\n ✅ x86_64 system detected." - else - TEST_FAIL=$((TEST_FAIL + 1)) - echo -e "\n ❌ You are not on a x86_64 based system. This is required to continue." - fi + xdg-open "${START_URL}" >> "${STDOUT}" 2>> "${STDERR}" || open "${START_URL}" >> "${STDOUT}" 2>> "${STDERR}" || true - if [[ ${TEST_FAIL} -gt 0 ]]; then - COMPATIBILITY_OVERRIDE=$(gum choose --header "Some compatibility checks failed. The installation will very likely fail. Continue ?" \ - "No" \ - "Yes, I know what I am doing") - [[ "${COMPATIBILITY_OVERRIDE}" == "No" ]] && exit 1 - [[ "${COMPATIBILITY_OVERRIDE}" != "No" ]] && echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus." - fi + sleep 5 - return 0 + echo -e "\n ⚠️ If it doesn't automatically, open your browser at: $(gum style "${START_URL}")" } hierarchy_preparation() { - echod "\n 🔄 Preparing the folder hierarchy for the final configuration..." + echod "\n 🔄 Preparing the folder hierarchy for the final configuration...\n" - if [[ -e config/* ]]; then - echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up." - OLD_CONFIG_PATH="trash/$(date +"%Y-%m-%d-%Hh%M")/" - mkdir -${MKDIR_FLAGS} ${OLD_CONFIG_PATH} - mv -${MV_FLAGS} config/ ${OLD_CONFIG_PATH} - echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed." - fi - - # Script folders - mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config - mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs - mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp - [[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web - - # Secrets - mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ - mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/ - mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks - mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system - if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then - mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman - fi + # Extra files folders + mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/ + mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/var/lib/sops-nix/ + mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/etc/nixos/secrets/disks + mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/etc/nixos/secrets/system + mkdir -${MKDIR_FLAGS} ${TMP_EXTRA_PATH}/etc/nixos/secrets/podman echod "\n ✅ Folder hierarchy ready" } +setup_ssh() { + edit_var() { + echo -e "${1}" + echo -e " Please check the credentials provided in the configuration." + echo -e "\n ➡️ Here are the current settings : + Target IP address : $(gum style --italic "\"${LIVE_TARGET_IP}\"") + Target password : $(gum style --italic "\"${LIVE_TARGET_PASSWORD}\"")" + gum confirm "Are these correct ?" || { + get_valid_input "LIVE_TARGET_IP" "➡️ Provide the IP address of your machine in a NixOS live environment :" "192.168.1.100" "${IP_REGEX}"; + get_valid_input "LIVE_TARGET_PASSWORD" "➡️ Provide the password of your machine in a NixOS live environment :" "password" "" "true" "true"; + return 0; + } + gum confirm "Retry connection ?" || { + echo -e "\n ❌ Host unreachable or connection refused."; + exit 226; + } + } + + echod "\n ➡️ Generating new SSH key for numbus-admin..." + + chmod 700 "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/" + ssh-keygen -t "ed25519" -C "numbus-admin@numbus-${DEVICE_TYPE}" -f "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q + + echod "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..." + + while true; do + if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"; then + echod "\n ✅ SSH key copied successfully" + return 0 + else + local EXIT_CODE=$? + if [[ ${EXIT_CODE} -eq 5 ]]; then + edit_var "\n ❌ Invalid password for ${TARGET_USER}@${LIVE_TARGET_IP}." + elif ! ping -c 2 "${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"; then + edit_var "\n ❌ The IP address you specified cannot be reached." + elif ssh-keygen -F "${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}"; then + echo -e "\n ⚠️ The SSH fingerprint for the selected IP address $(gum style --italic "\"${LIVE_TARGET_IP}\"") is not the same as the one in $(gum style --italic "\".ssh/known_hosts\""). + + This could occur for multiple reasons : + - You ran this script multiple times + - Your live machine uses an IP address that was used by another devices you SSHed in + - You are under a Man-In-The-Middle attack + - Other + + The script $(gum style --bold "cannot continue") without the correct fingerprint installed. + If you are unsure, it is always better to check manually.\n" + + gum confirm "Remove the old fingerprint and accept the new one ?" || { + echo -e "\n ❌ SSH fingerprints don't match."; + exit 22; + } + ssh-keygen -R "${LIVE_TARGET_IP}" >> "${STDOUT}" 2>> "${STDERR}" + fi + fi + done +} + hardware_detection() { - local TMPFILE="/tmp/nixos-installation-hw-detection" + local TMPFILE="/run/user/1000/numbus-installer/hw_detection.json" - ssh_to_host 'bash -s' << SSHEND -TARGET_GRAPHICS_BRAND=() + ssh_to_host "nix-shell -p jq pciutils usbutils smartmontools iproute2 --run 'bash -s'" << SSHEND >> "${STDOUT}" 2>> "${STDERR}" +set -euo pipefail -for brand in Intel AMD NVIDIA; do - if lspci -nn 2>/dev/null | grep -i "vga" | grep -iq "\${brand}"; then - TARGET_GRAPHICS="true" - TARGET_GRAPHICS_BRAND+=("\${brand}") - else - TARGET_GRAPHICS="false" - fi -done +HW_REPORT=\$(jq -n '{}') -ls /dev/dri/ > /dev/null 2>&1 | grep -iq "renderD128" && TARGET_GRAPHICS_RENDERER="true" || TARGET_GRAPHICS_RENDERER="false" -lsusb > /dev/null 2>&1 | grep -iq "google" && TARGET_USB_CORAL="true" || TARGET_USB_CORAL="false" -lspci -nn > /dev/null 2>&1 | grep -iq "089a" && TARGET_PCIE_CORAL="true" || TARGET_PCIE_CORAL="false" -ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" && TARGET_ZIGBEE_DEVICE=\$(ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" | head -n 1) || TARGET_ZIGBEE_DEVICE="" +append_to_report() { + local key="\${1}" + local json_array="\${2}" + HW_REPORT=\$(echo "\${HW_REPORT}" | jq --argjson arr "\${json_array}" --arg k "\${key}" '.[\${k}] = \${arr}') +} -TARGET_INTERFACE=\$(ip -4 route show default | awk '{print \$5}' | head -n1) +detect_graphics() { + local gpus="[]" -if ls -l /sys/class/tpm/tpm0/ > /dev/null 2>&1; then - TARGET_TPM="true" - TARGET_TPM_VERSION=\$(cat /sys/class/tpm/tpm0/tpm_version_major) -else - TARGET_TPM="false" - TARGET_TPM_VERSION="N/A" -fi + while read -r line; do + [[ -z "\${line}" ]] && continue + local brand="unknown" + local renderer="none" + local product="unknown" + local integrated="false" + local pci_addr="none" -HDD=1 -DISK_DEVPATH=() -DISK_NAME=() -DISK_TYPE=() -DISK_HEALTH=() -DISK_ID=() + # Extract PCI address + pci_addr="\$(echo "\${line}" | cut -d' ' -f1)" -for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do + # Brand + for b in Intel AMD NVIDIA; do + if echo "\${line}" | grep -iq "\${b}"; then + brand="\${b}" + break + fi + done - # Disk name and simple path - DISK_DEVPATH+=("/dev/\$DISK") - DISK_NAME+=("\$DISK") - # Disk type - HDD=\$(cat /sys/block/\$DISK/queue/rotational) - TRANSPORT_PROTOCOL=\$(lsblk -x SIZE -d -n -e 7,11 -o TRAN /dev/\$DISK) - if [[ "\$DISK" == "nvme*" ]]; then DISK_TYPE+=("NVMe"); - elif [[ "\$TRANSPORT_PROTOCOL" == "usb" ]]; then DISK_TYPE+=("USB"); - elif [[ "\$HDD" -eq 1 ]]; then DISK_TYPE+=("HDD"); - elif [[ "\$HDD" -eq 0 ]]; then DISK_TYPE+=("SSD"); - else DISK_TYPE+=("Other") + # Renderer + if [[ -d "/dev/dri/by-path" ]]; then + local render_node=\$(ls /dev/dri/by-path | grep "\${pci_addr}" | grep "render" | head -n1 || true) + if [[ -n "\${render_node}" ]]; then + renderer=\$(basename "\$(readlink -f "/dev/dri/by-path/\${render_node}")") + fi + fi + + # Product name + product="\${line#*:}" + product="\${product#*: }" + + # Form factor + if [[ "\${brand}" == "NVIDIA" ]]; then + integrated="false" + fi + if [[ "\${brand}" == "Intel" ]]; then + if echo "\${line}" | grep -Ei "HD Graphics|Xe"; then + integrated="true" + else + integrated="false" + fi + fi + if [[ "\${brand}" == "AMD" ]]; then + if echo "\$line" | grep -i "Mobile"; then + integrated="true" + else + integrated="false" + fi + fi + + local obj=\$(jq -n \ + --arg b "\${brand}" \ + --arg r "\${renderer}" \ + --arg p "\${product}" \ + --argjson i "\${integrated}" \ + '{brand: \$b, renderer: \$r, product: \$p, integrated: \$i}') + + gpus=\$(echo "\$gpus" | jq --argjson obj "\$obj" '. += [\$obj]') + + done < <(lspci | grep -Ei "VGA|3D") + + append_to_report "graphics" "\$gpus" +} + +# --- 2. Detect Coral TPUs --- +detect_corals() { + local corals="[]" + + # Check PCIe Coral (Google ID 1ac1:089a) + if lspci -nn | grep -iq "1ac1:089a"; then + local pcie_count + pcie_count=\$(lspci -nn | grep -ic "1ac1:089a") + for ((i=1; i<=pcie_count; i++)); do + local obj=\$(jq -n --arg i "PCIe" '{interface: \$i, type: "Edge TPU"}') + corals=\$(echo "\$corals" | jq --argjson obj "\$obj" '. += [\$obj]') + done fi - # Disk health - if [[ \$(echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') == "PASSED" ]]; then - DISK_HEALTH+=("PASSED") - else - DISK_HEALTH+=("N/A") + # Check USB Coral (Google ID 18d1:9302) + if lsusb | grep -iq "18d1:9302"; then + local usb_count + usb_count=\$(lsusb | grep -ic "18d1:9302") + for ((i=1; i<=usb_count; i++)); do + local obj=\$(jq -n --arg i "USB" '{interface: \$i, type: "Edge TPU"}') + corals=\$(echo "\$corals" | jq --argjson obj "\$obj" '. += [\$obj]') + done fi - # Disk ID - DISK_ID+=("\$(ls -l /dev/disk/by-id | grep -m1 "../../\$DISK" | awk '{print "/dev/disk/by-id/" \$9}')") - DISK_SIZE+=("\$(lsblk -x SIZE -d -n -e 7,11 -o SIZE /dev/\$DISK)") -done -echo "# Hardware detection results on \$(date)" > "${TMPFILE}" -for var in \ - TARGET_GRAPHICS \ - TARGET_GRAPHICS_RENDERER \ - TARGET_USB_CORAL \ - TARGET_PCIE_CORAL \ - TARGET_ZIGBEE_DEVICE \ - TARGET_INTERFACE \ - TARGET_TPM \ - TARGET_TPM_VERSION; do - echo "export \${var}=\${!var}" >> "${TMPFILE}" -done + append_to_report "coral_devices" "\$corals" +} -for var in \ - TARGET_GRAPHICS_BRAND \ - DISK_DEVPATH \ - DISK_NAME \ - DISK_TYPE \ - DISK_HEALTH \ - DISK_ID \ - DISK_SIZE; do - declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}" -done -SSHEND +# --- 3. Detect Zigbee Coordinators --- +detect_zigbee() { + local zigbees="[]" + local serial_dir="/dev/serial/by-id" - scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null - source "${TMPFILE}" + if [[ -d "\$serial_dir" ]]; then + for dev in "\$serial_dir"/*; do + [[ -e "\$dev" ]] || continue # skip if empty directory pattern matched + # Match common Zigbee adapter names (Sonoff, ConBee, ITead, CC2531, etc.) + if echo "\$dev" | grep -iE 'zigbee|conbee|sonoff|cc2531|efr32|itead'; then + local obj=\$(jq -n --arg p "\$dev" '{device_path: \$p}') + zigbees=\$(echo "\$zigbees" | jq --argjson obj "\$obj" '. += [\$obj]') + fi + done + fi - local DISK_FLAT_ARRAY=() - for i in "${!DISK_NAME[@]}"; do - DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}") + append_to_report "zigbee_devices" "\$zigbees" +} + +# --- 4. Detect Network Interfaces --- +detect_network() { + local networks="[]" + local default_iface + default_iface=\$(ip -4 route show default | awk '{print \$5}' | head -n1) + + for iface_path in /sys/class/net/*; do + [[ -e "\$iface_path" ]] || continue + local iface + iface=\$(basename "\$iface_path") + + # Skip loopback and virtual interfaces + [[ "\$iface" == "lo" ]] && continue + [[ -L "\$iface_path" && \$(readlink "\$iface_path") == *"virtual"* ]] && continue + + local type="wired" + [[ -d "\$iface_path/wireless" ]] && type="wireless" + + local is_default="false" + [[ "\$iface" == "\$default_iface" ]] && is_default="true" + + local obj=\$(jq -n --arg n "\$iface" --arg t "\$type" --argjson d "\$is_default" \ + '{name: \$n, type: \$t, default: \$d}') + networks=\$(echo "\$networks" | jq --argjson obj "\$obj" '. += [\$obj]') done - jq -n \ - --argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \ - --argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \ - --argjson tpu_usb "${TARGET_USB_CORAL:-false}" \ - --argjson tpu_pcie "${TARGET_PCIE_CORAL:-false}" \ - --argjson tpm_enabled "${TARGET_TPM:-false}" \ - --arg tpm_version "${TARGET_TPM_VERSION:-N/A}" \ - --arg zigbee_device "${TARGET_ZIGBEE_DEVICE:-}" \ - --arg interface "${TARGET_INTERFACE:-}" \ - --argjson brands "$(jq -n '$ARGS.positional' --args ${TARGET_GRAPHICS_BRAND[@]:-})" \ - ' - { - graphics: { enabled: $graphics_enabled, brands: $brands, renderer: $graphics_renderer }, - tpu: { usb: $tpu_usb, pcie: $tpu_pcie }, - tpm: { enabled: $tpm_enabled, version: $tpm_version }, - zigbee: { device: $zigbee_device }, - network: { interface: $interface }, - disks: [ - $ARGS.positional | range(0; length; 6) as $i | { - name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5] - } - ] - }' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH} + append_to_report "network_interfaces" "\$networks" +} - if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then - echo -e "\n✅ Hardware configuration generated" +# --- 5. Detect TPM --- +detect_tpm() { + local tpms="[]" + + for tpm_dir in /sys/class/tpm/tpm*; do + [[ -e "\$tpm_dir" ]] || continue + + local name + name=\$(basename "\$tpm_dir") + local version="Unknown" + + if [[ -f "\$tpm_dir/tpm_version_major" ]]; then + version=\$(cat "\$tpm_dir/tpm_version_major") + fi + + local obj=\$(jq -n --arg n "\$name" --arg v "\$version" '{name: \$n, version: \$v}') + tpms=\$(echo "\$tpms" | jq --argjson obj "\$obj" '. += [\$obj]') + done + + append_to_report "tpm" "\$tpms" +} + +# --- 6. Detect Disks --- +detect_disks() { + local disks="[]" + + while read -r disk_name; do + # Disk Type Mapping + local disk_type="unknown" + local disk_transport=\$(lsblk -d -n -o TRAN "/dev/\$disk_name" || echo "unknown") + local rotational=\$(lsblk -d -n -o ROTA "/dev/\$disk_name" || echo "1") + + if [[ "\$disk_name" == nvme* ]]; then + disk_type="NVMe" + elif [[ "\$rotational" == "1" ]]; then + disk_type="HDD" + elif [[ "\$rotational" == "0" ]]; then + disk_type="SSD" + fi + + # Size in GB + local disk_size=\$(lsblk -d -n -o SIZE "/dev/\$disk_name" || echo "unknown") + + # ID via by-id + local disk_id="unknown" + if [[ -d "/dev/disk/by-id" ]]; then + local id_match=\$(find /dev/disk/by-id/ -type l -not -name "wwn-*" -not -name "nvme-eui*" -printf "%p %l\n" | grep -m1 "/\$disk_name\$" | awk '{print \$1}') + [[ -n "\$id_match" ]] && disk_id="\$id_match" + fi + + # Health + local health="unknown" + if echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$disk_name 2>/dev/null | grep -i "PASSED"; then + disk_health="Passed" + elif echo "${LIVE_TARGET_PASSWORD}" | sudo -S smartctl -H /dev/\$disk_name 2>/dev/null | grep -i "FAILED"; then + disk_health="Failed" + fi + + local obj=\$(jq -n \ + --arg n "\${disk_name}" \ + --arg t "\${disk_type}" \ + --arg tr "\${disk_transport}" \ + --arg h "\${disk_health}" \ + --arg s "\${disk_size}" \ + --arg i "\${disk_id}" \ + '{name: \$n, type: \$t, transport: \$tr, health: \$h, size: \$s, id: \$i}') + + disks=\$(echo "\$disks" | jq --argjson obj "\$obj" '. += [\$obj]') + + done < <(lsblk -d -n -o NAME -e 7,11,252) + + append_to_report "disks" "\$disks" +} + +# --- Execution --- +detect_graphics +detect_corals +detect_zigbee +detect_network +detect_tpm +detect_disks + +# --- Output --- +echo "\$HW_REPORT" | jq '.' > "$TMPFILE" +SSHEND + + scp -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${HW_DATA_FILE}" >> "${STDOUT}" 2>> "${STDERR}" + [[ ${DEBUG} -eq 1 ]] && scp -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${REMOTE_STDOUT}" "${INSTALL_DIR}/web/logs/hw_std.log" >> "${STDOUT}" 2>> "${STDERR}" + [[ ${DEBUG} -eq 1 ]] && scp -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${REMOTE_STDERR}" "${INSTALL_DIR}/web/logs/hw_err.log" >> "${STDOUT}" 2>> "${STDERR}" + + if ssh_to_host "echo \"${LIVE_TARGET_PASSWORD}\" | sudo -S nixos-generate-config --no-filesystems --show-hardware-config &> /dev/null" > ${TMP_EXTRA_PATH}/etc/nixos/hardware-configuration.nix; then + echo -e "\n ✅ Hardware configuration generated" else - echo -e "\n❌ Failed to generate hardware configuration" + echo -e "\n ❌ Failed to generate hardware configuration" exit 1 fi } @@ -262,269 +423,8 @@ SSHEND -# --- MAIN WEB FUNCTIONS ---> -launch_gui() { - echo -e "\n ➡️ You will now proceed to the configuration of your device through your browser" - echo -e "\n 🚀 Launching Numbus Configurator..." - - python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 & - export BRIDGE_PID=$! - xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true - - sleep 5 - - echo -e "\n ➡️ If it doesn't automatically, open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")" - -} -# --- MAIN WEB FUNCTIONS ---< - - - -# --- MAIN TUI FUNCTIONS ---> -preparation() { - echo -e "\n ➡️ This script will now guide you through the configuration and gather the necessary information." - - echo "" - RAW_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \ - "Numbus Server : Professional-grade hosting, strictly kept under your roof." \ - "Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \ - "Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \ - "Numbus TV : A premium cinematic experience free from trackers and forced subscriptions." \ - "Numbus Game Console : An unbreakable Steam bigscreen experience.") - - case "${RAW_DEVICE_TYPE}" in - "Numbus Server : "* ) DEVICE_TYPE="server" ;; - "Numbus Backup Server : "* ) DEVICE_TYPE="backup" ;; - "Numbus Computer : "* ) DEVICE_TYPE="computer" ;; - "Numbus TV : "* ) DEVICE_TYPE="tv" ;; - "Numbus Game Console : "* ) DEVICE_TYPE="console" ;; - esac - - RAW_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \ - "Interactive : You don't already have a configuration." \ - "Non-interactive : You have a valid configuration hosted on a Git platform.") - - case "${RAW_DEPLOYMENT_MODE}" in - "Interactive : "* ) DEPLOYMENT_MODE="interactive" ;; - "Non-interactive : "* ) DEPLOYMENT_MODE="non-interactive" ;; - esac - - if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then - git_url() { - IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :") - } - - git_url - - until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do - echo -e "\n ⚠️ This did not work correctly." - - echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}" - read URL - - if [[ "${URL^^}" == "N" ]]; then - git_url - fi - - echo -e "\n You will be prompted for your credentials again. Make sure that they are correct." - done - fi - - echo "" - gum format -- \ - "➡️ To continue, you need to start the target device in a NixOS live environment : - 1. Download the NixOS iso from the **[official website](https://nixos.org/download/)**. - 2. Flash it to a USB stick. (use a flashing tool like **[Rufus](https://rufus.ie/en/#download)**, **[BalenaEtcher](https://etcher.balena.io/#download-etcher)**, **[Impression](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression)**, ...) - 3. Make sure your computer allows booting from USB drives and is in UEFI mode. - 4. Boot into the NixOS live environment. - 5. Launch a terminal. Set a password using \`passwd\` and find the IP address using \`ip a\`" - - echo "" - gum confirm "Is the device ready ?" || { echo "❌ You need to prepare the device. The script cannot continue."; exit 1; } - - # LIVE TARGET SETTINGS - user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" - user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true" - - # INTERNATIONALIZATION SETTINGS - user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc" - user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc" - user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc" -} - -configuration() { - if [[ "${DEVICE_TYPE}" == "server" ]]; then - - # Users & Groups - user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" - user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}" - user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." - - echo -e "\n\n ➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains" - # TRAEFIK SETTINGS - user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" - user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true" - - echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)" - # SMTP SETTINGS - user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" - user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true" - user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format." - user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number." - - echo -e "\n\n ➡️ This server will connect to your local network and you will configure its IP address\n" - # NETWORK SETTINGS - user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)." - user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format." - user_input "HOME_SERVER_IP" " Please choose the ip address that your server will use (i.e. any address in the 192.168.1.1/24 range that is not in use.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format." - elif [[ "${DEVICE_TYPE}" == "backup" ]]; then - : - elif [[ "${DEVICE_TYPE}" == "computer" ]]; then - : - elif [[ "${DEVICE_TYPE}" == "tv" ]]; then - : - fi -} - -setup_ssh() { - echod "\n ✅ Generating new SSH key for numbus-admin..." - - chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ - ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q - - if [[ ${DEBUG} -eq 1 ]]; then - echo -e "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..." - fi - - if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then - if [[ ${DEBUG} -eq 1 ]]; then - echo -e "\n ✅ SSH key copied successfully" - fi - else - echo -e "\n ❌ Failed to copy SSH key. Please check the host IP and password." - exit 1 - fi -} - -services_selection() { - services_choice() { - local SERVICES_LIST=( "${1[@]}" ) - local SERVICES_DESCRIPTION=( "${2[@]}" ) - local FINAL_VARIABLE="${3}" - local HEADER="${4}" - local LIMIT="${5:---no-limit}" - - local SELECTED_SERVICES=() - local SELECTED_SERVICES_DESCRIPTION=() - - local SELECTED_SERVICES_DESCRIPTION=$(gum choose ${LIMIT} --header "${HEADER}" "${SERVICES_DESCRIPTION[@]}") - - for i in ${!SERVICES_LIST[@]}; do - if printf '%s' "${SELECTED_SERVICES_DESCRIPTION}" | grep -iq "${SERVICES_LIST[${i}]}"; then - SELECTED_SERVICES+=("${SERVICES_LIST[${i}]}") - fi - done - - export "${FINAL_VARIABLE}=(${SELECTED_SERVICES[@]})" - } - - echo -e "\n\n ➡️ You will now select the services you want installed on your server:" - - services_choice "${DNS_SERVICES_LIST[@]}" "${DNS_SERVICES_DESCRIPTION[@]}" "SELECTED_DNS_SERVICE" "Choose your preferred DNS service :" "--limit=1" - services_choice "${WEB_APPLICATIONS_LIST[@]}" "${WEB_APPLICATIONS_DESCRIPTION[@]}" "SELECTED_WEB_APPLICATIONS" "Choose the web applications you want to install :" - services_choice "${SYSTEM_SERVICES_LIST[@]}" "${SYSTEM_SERVICES_DESCRIPTION[@]}" "SELECTED_SYSTEM_SERVICES" "Choose the system services you want to install :" - - gum confirm "Do you want to edit the default subdomain of your services ?" || { echo -e "\n\n✅ Continuing..."; return 0; } - - for service in ${SELECTED_WEB_APPLICATIONS[@]} ${SELECTED_DNS_SERVICE[@]}; do - if gum confirm "Change the subdomain of ${service} ?"; then - SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" ) - fi - done - - return 0 -} - -disks_selection() { - gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " - ⚠️ $(gum style --foreground 212 'WARNING:') You will choose the disks to install NixOS on. - !! PLEASE MAKE SURE YOU BACKED UP ANY IMPORTANT DATA !! - !! ALL DATA WILL BE WIPED ON THE DISKS YOU CHOOSE !! - Please press CTRL+C to abort. - " - gum confirm "Do you understand and wish to proceed?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; } - - echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)" - - if [[ "${#DISK_NAME[@]}" -eq 0 ]]; then - echo -e "\n❌ No disks found on the target host. Aborting." - exit 1 - fi - - local HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "Path") - - for i in ${!DISK_NAME[@]}; do - local GUM_PRINTED_ELEMENT=$(printf "%-12s %-12s %-12s %-12s %s" \ - "${DISK_NAME[${i}]}" "${DISK_TYPE[${i}]}" "${DISK_SIZE[${i}]}" \ - "${DISK_HEALTH[${i}]}" "${DISK_DEVPATH[${i}]}") - local GUM_PRINTED_ELEMENTS+=("${GUM_PRINTED_ELEMENT}") - done - - echo "" - gum style --foreground 212 "➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation :" - - local SELECTED_BOOT_DISK=$(gum choose --limit 2 --header "${HEADER}" "${GUM_PRINTED_ELEMENTS[@]}") - - for i in ${!DISK_NAME[@]}; do - if printf '%s' "$SELECTED_BOOT_DISK" | grep -iqw "${DISK_NAME[${i}]}"; then - BOOT_DISKS_ID_LIST+=("\"${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}\"") - BOOT_DISKS_NAME+=("${DISK_NAME[${i}]}") - unset "GUM_PRINTED_ELEMENTS[${i}]" - fi - done - - echo "" - gum style --foreground 212 "➡️ Please choose data and parity disks (up to 9 total) :" - - local SELECTED_DATA_DISK=$(gum choose --limit 9 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}") - - for i in ${!DISK_NAME[@]}; do - if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[${i}]}"; then - DATA_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}") - DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}") - fi - done - - if [[ "${#DATA_DISKS_ID[@]}" -eq 1 ]]; then - export PARITY_DISK_NUMBER=0 - export CONTENT_DISK_NUMBER=1 - export PARITY_DISK_LIST=() - export CONTENT_DISK_LIST=("\"${DATA_DISKS_ID[0]}\"") - else - export PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3)) - export CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER)) - for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do - CONTENT_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"") - done - for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do - PARITY_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"") - done - fi - - if [[ "${#DATA_DISKS_ID[@]}" -gt 0 ]]; then - for i in ${!DATA_DISKS_ID[@]}; do - if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then - SPINDOWN_DISKS_LIST+=("\"${DATA_DISKS_ID[${i}]}\"") - fi - done - fi - - export SPINDOWN_DISKS_LIST - export BOOT_DISKS_ID_LIST - export PARITY_DISK_LIST - export CONTENT_DISK_LIST -} +# --- MAIN SCRIPT FUNCTIONS ---> server_config_generation() { echod "\n 📝 Generating structured settings.json..." @@ -558,13 +458,13 @@ server_config_generation() { enabledApps: $apps, managementConsole: $cockpit_enabled } - }' > "${EXTRA_FILES_PATH}/etc/nixos/settings.json" + }' > "${TMP_EXTRA_PATH}/etc/nixos/settings.json" echo -e "{\n numbus.settings = builtins.fromJSON (builtins.readFile ./settings.json);\n}" > "${CONFIGURATION_PATH}" # Ensure the settings file is writable by the management service # and that the directory is prepared for local git tracking - chmod 664 "${EXTRA_FILES_PATH}/etc/nixos/settings.json" + chmod 664 "${TMP_EXTRA_PATH}/etc/nixos/settings.json" } # The existing network_config_generation and services_config_generation functions @@ -600,8 +500,8 @@ disk_config_generation() { keys_generation() { for i in $(seq 1 "${#BOOT_DISKS_ID_LIST[@]}"); do PASS="$(xkcdpass)" - echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" - chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" + echo -n "$PASS" > "${TMP_EXTRA_PATH}/etc/secrets/disks/boot-${i}" + chmod 600 "${TMP_EXTRA_PATH}/etc/secrets/disks/boot-${i}" ssh_to_host 'bash -s' << EOF echo "$LIVE_TARGET_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/ echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}" @@ -610,8 +510,8 @@ EOF done for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do PASS="$(xkcdpass)" - echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" - chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" + echo -n "$PASS" > "${TMP_EXTRA_PATH}/etc/secrets/disks/content-${i}" + chmod 600 "${TMP_EXTRA_PATH}/etc/secrets/disks/content-${i}" ssh_to_host 'bash -s' << EOF echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}" echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/content-${i} @@ -619,8 +519,8 @@ EOF done for i in $(seq 1 "$PARITY_DISK_NUMBER"); do PASS="$(xkcdpass)" - echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" - chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" + echo -n "$PASS" > "${TMP_EXTRA_PATH}/etc/secrets/disks/parity-${i}" + chmod 600 "${TMP_EXTRA_PATH}/etc/secrets/disks/parity-${i}" ssh_to_host 'bash -s' << EOF echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}" echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i} @@ -628,7 +528,7 @@ EOF done local SSH_KEYS_FORMATTED="" - if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY 2>/dev/null)" =~ "declare -a" ]]; then + if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY)" =~ "declare -a" ]]; then for key in "${AUTHORIZED_SSH_PUBLIC_KEY[@]}"; do SSH_KEYS_FORMATTED+=" $key"$'\n' done @@ -638,99 +538,18 @@ EOF export SSH_KEYS_FORMATTED echo -e "\n ✅ Generating sops-nix keys..." - ssh-to-age -private-key -i ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt - export SOPS_PUBLIC_KEY=$(age-keygen -y ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt) + ssh-to-age -private-key -i ${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${TMP_EXTRA_PATH}/var/lib/sops-nix/key.txt + export SOPS_PUBLIC_KEY=$(age-keygen -y ${TMP_EXTRA_PATH}/var/lib/sops-nix/key.txt) echo -e "\n ✅ Generating sops-nix configuration files..." - envsubst < templates/nix-config/sops-nix/.sops.yaml > ${EXTRA_FILES_PATH}/etc/nixos/.sops.yaml + envsubst < templates/nix-config/sops-nix/.sops.yaml > ${TMP_EXTRA_PATH}/etc/nixos/.sops.yaml echo -e "\n ✅ Encrypting secrets in the correct file..." envsubst < "templates/nix-config/sops-nix/secrets.yaml" \ | sops encrypt --filename-override secrets.yaml \ --input-type yaml --output-type yaml \ --age $SOPS_PUBLIC_KEY \ - --output ${EXTRA_FILES_PATH}/etc/nixos/secrets/secrets.yaml -} - -sum_up() { - DISK_RECAP_CONTENT=$(cat << EOF -### Disk Configuration Summary - -Please review the selected disk layout before proceeding. - -**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :** - -* **Boot 1:** \`${BOOT_DISKS_ID_LIST[0]}\` -$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Boot 2:** \`${BOOT_DISKS_ID_LIST[1]}\`" ) - -**Data Disks ($CONTENT_DISK_NUMBER) :** - -$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Data ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done ) -$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) - -**Parity Disks ($PARITY_DISK_NUMBER) :** - -$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Parity ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done ) -$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) - -EOF -) - - gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")" - gum confirm "➡️ Proceed with this disk configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; } - - SERVICES_RECAP_CONTENT=$(cat << EOF -### Services Configuration Summary - -Please review the selected services before proceeding. - -**DNS Service (${#SELECTED_DNS_SERVICE[@]}) :** - -$(echo "* \`${SELECTED_DNS_SERVICE[0]^}\`") - -**Web Applications (${#SELECTED_WEB_APPLICATIONS[@]}) :** - -$(for app in "${SELECTED_WEB_APPLICATIONS[@]}"; do echo "* \`${app^}\`"; done) - -**System Services (${#SELECTED_SYSTEM_SERVICES[@]}) :** - -$(for service in "${SELECTED_SYSTEM_SERVICES[@]}"; do echo "* \`${service^}\`"; done) - -EOF -) - - gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${SERVICES_RECAP_CONTENT}")" - gum confirm "➡️ Proceed with this services configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; } - - DISK_RECAP_CONTENT=$(cat << EOF -### Secrets Summary - -Please save the following secrets to a secure place (i.e. your local password manager, or a hidden sheet of paper). - -**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :** - -* **Disk 1 Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-1 )\` -$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-2 )\`" ) - -**Data Disks ($CONTENT_DISK_NUMBER):** - -$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) -$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/content-${j} )\`" && j=$((j + 1)); done ) - -**Parity Disks ($PARITY_DISK_NUMBER):** - -$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) -$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat ${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${j} )\`" && j=$((j + 1)); done ) - -EOF -) - - gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")" - gum confirm "✅ I have stored these credentials in a safe place" || { echo -e "\n\n❌ Please store these credentials in a safe place as you will need them later."; exit 1; } - - gum confirm "➡️ Would you like to manually edit the configuration (⚠️ advanced users only)" || { echo -e "\n\n✅ continuing with the installation..."; return 0; } - - nano ${EXTRA_FILES_PATH}/etc/nixos/configuration.nix + --output ${TMP_EXTRA_PATH}/etc/nixos/secrets/secrets.yaml } cloudflare_dns_setup() { @@ -762,7 +581,7 @@ cloudflare_dns_setup() { local SUBDOMAIN="${1}" gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " - ⚠️ $(gum style --foreground 212 'WARNING:') One or more existing type A DNS records found for \`${SUBDOMAIN}\`. + ⚠️ $(gum style 'WARNING:') One or more existing type A DNS records found for \`${SUBDOMAIN}\`. This script can clear those DNS records for you and create the correct ones needed for the server. If you are unsure that these records are actually in use, please select \"no\"." gum confirm "Select \"yes\" to clear ALL EXISTING type A DNS records for this subdomain and automatically create the correct ones." \ @@ -775,7 +594,7 @@ cloudflare_dns_setup() { for id in ${RECORD_IDS}; do curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${id}" \ -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ - -H "Content-Type: application/json" > /dev/null 2>&1 + -H "Content-Type: application/json" >> "${STDOUT}" 2>> "${STDERR}" done create_records "${SUBDOMAIN}" @@ -835,24 +654,24 @@ cloudflare_dns_setup() { } deploy() { - git -C . add -f "${EXTRA_FILES_PATH}/" + git -C . add -f "${TMP_EXTRA_PATH}/" git -C . add -f "templates/" git -C . add -f "deploy.conf" # Initialize a git repo in the configuration to be deployed # This allows the Management UI on the appliance to commit changes # and provide a local history/rollback UI to the user. - if [ ! -d "${EXTRA_FILES_PATH}/etc/nixos/.git" ]; then - git -C "${EXTRA_FILES_PATH}/etc/nixos" init -q - git -C "${EXTRA_FILES_PATH}/etc/nixos" add . - git -C "${EXTRA_FILES_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q + if [ ! -d "${TMP_EXTRA_PATH}/etc/nixos/.git" ]; then + git -C "${TMP_EXTRA_PATH}/etc/nixos" init -q + git -C "${TMP_EXTRA_PATH}/etc/nixos" add . + git -C "${TMP_EXTRA_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q fi echo -e "\n\n🔄 Deploying to the remote server..." - nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos + nix flake update --flake ./${TMP_EXTRA_PATH}/etc/nixos nix run github:nix-community/nixos-anywhere -- \ - --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \ - --extra-files ${EXTRA_FILES_PATH} \ + --flake ${TMP_EXTRA_PATH}/etc/nixos#numbus-server \ + --extra-files ${TMP_EXTRA_PATH} \ --chown "/home/numbus-admin/" 1000:1000 \ --target-host ${TARGET_USER}@${LIVE_TARGET_IP} @@ -875,7 +694,7 @@ postrun_action() { FOUND="false" i="0" while [[ "${FOUND}" == "false" ]]; do - if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then + if ping -c1 -W1 $HOME_SERVER_IP >> "${STDOUT}" 2>> "${STDERR}"; then FOUND="true" echo -e "\n✅ Ping ${HOME_SERVER_IP} successful ! Continuing..." else @@ -900,7 +719,7 @@ postrun_action() { Do you want to enable automatic disk decryption on boot ?" if gum confirm "➡️ I understand, 'yes' to proceed."; then - sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF + sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${TMP_EXTRA_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF echo "Enrolling boot disk key to TPM..." BOOT_DISKS_NAME=(${BOOT_DISKS_NAME[@]}) @@ -934,7 +753,7 @@ EOF fi gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " - ⚠️ $(gum style --foreground 212 'WARNING:') You will now set the password of the numbus-admin user. + ⚠️ $(gum style 'WARNING:') You will now set the password of the numbus-admin user. You will almost never user it. Consider using a very strong password : you can write it down securely on a hidden sheet of paper or add it to your password manager (locally with Passbolt with any other online password manager provider)." @@ -943,29 +762,24 @@ securely on a hidden sheet of paper or add it to your password manager (locally echo $LIVE_TARGET_PASSWORD | sudo -S passwd numbus-admin } - -nix_update() { - echo -e "\n\n🔄 Updating NixOS on the remote server..." - - nixos-rebuild --target-host numbus-admin@${LIVE_TARGET_IP} \ - --use-remote-sudo switch --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server -} -# --- MAIN FUNCTIONS ---< +# --- MAIN SCRIPT FUNCTIONS ---< # --- DEFAULT VARIABLES ---> +INSTALL_DIR="/run/user/$(id -u)/numbus-installer" + WEBSERVER_PORT=${WEBSERVER_PORT:-8088} -LIVE_DATA_PATH="/run/user/$(id -u)/numbus/web/live_settings.json" -HARDWARE_DATA_PATH="/run/user/$(id -u)/numbus/web/hardware.json" +LIVE_DATA_FILE="web/config/live.yaml" +HW_DATA_FILE="web/config/hardware.yaml" +CONFIG_FILE="web/config/numbus.yaml" -CONFIG_FILE="../config/numbus.yaml" +BRIDGE_SCRIPT="web/logic/interactive.py" TARGET_USER="nixos" -TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")" -EXTRA_FILES_PATH="${TMP_FILES_PATH}/config" +TMP_EXTRA_PATH="${INSTALL_DIR}/extra" if [[ ${DEBUG-0} -eq 1 ]]; then FILES_CP_FLAGS="vau" @@ -973,6 +787,10 @@ if [[ ${DEBUG-0} -eq 1 ]]; then DIR_RM_FLAGS="rvf" MKDIR_FLAGS="pv" MV_FLAGS="vu" + STDOUT="${INSTALL_DIR}/web/logs/std.log" + STDERR="${INSTALL_DIR}/web/logs/err.log" + REMOTE_STDOUT="/run/user/1000/numbus-installer/std.log" + REMOTE_STDERR="/run/user/1000/numbus-installer/err.log" else DEBUG=0 FILES_CP_FLAGS="au" @@ -980,9 +798,12 @@ else DIR_RM_FLAGS="rf" MKDIR_FLAGS="p" MV_FLAGS="u" + STDOUT="/dev/null" + STDERR="/dev/null" fi -IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$' +IP_OCTET='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])' +IP_REGEX="^${IP_OCTET}\\.${IP_OCTET}\\.${IP_OCTET}\\.${IP_OCTET}$" SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$' EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' @@ -990,10 +811,11 @@ PORT_REGEX='^[0-9]{1,5}$' SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*' PHONE_REGEX='^\+[1-9][0-9]{7,14}$' -GUM_INPUT_PADDING="1 1" -GUM_INPUT_HEADER_FOREGROUND="212" -GUM_INPUT_CURSOR_FOREGROUND="212" -GUM_INPUT_TIMEOUT="3600" +export FOREGROUND="212" +export GUM_INPUT_PADDING="1 1" +export GUM_INPUT_HEADER_FOREGROUND="212" +export GUM_INPUT_CURSOR_FOREGROUND="212" +export GUM_INPUT_TIMEOUT="3600s" # --- DEFAULTS VARIABLES ---< @@ -1001,9 +823,7 @@ GUM_INPUT_TIMEOUT="3600" # --- PRE MAIN LOGIC ---> set -euo pipefail clear - trap cleanup EXIT -compatibility_check # --- PRE MAIN LOGIC ---< @@ -1016,21 +836,33 @@ echo """ /_/|_/\____/_/ /_/____/\____/___/ """ -DEPLOYMENT_STRATEGY=$(gum choose --header "Choose your preferred deployment strategy :" \ -"I don't have a configuration" \ -"I have a valid configuration hosted on a Git platform") +launch_gui -if [[ "${DEPLOYMENT_STRATEGY}" == "I don't have a configuration" ]]; then - BRIDGE_SCRIPT="../web/logic/interactive.py" - launch_gui -else - DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \ - "Through my web browser (Recommended for beginners)" \ - "Through my terminal (TUI)") - if [[ "${DEPLOYMENT_MODE}" == "Through my web browser (Recommended for beginners)" ]]; then - BRIDGE_SCRIPT="../web/logic/non-interactive.py" - launch_gui - else - launch_tui - fi -fi \ No newline at end of file +until [[ -e "${LIVE_DATA_FILE}" ]]; do + sleep 5 +done + +INTERNATIONALIZATION_LANGUAGE="$(yq -r '.internationalization.language' ${LIVE_DATA_FILE})" +INTERNATIONALIZATION_COUNTRY="$(yq -r '.internationalization.country' ${LIVE_DATA_FILE})" +INTERNATIONALIZATION_TIMEZONE="$(yq -r '.internationalization.timeZone' ${LIVE_DATA_FILE})" +DEVICE_TYPE="$(yq -r '.device.type' ${LIVE_DATA_FILE})" +DEPLOYMENT_MODE="$(yq -r '.deployment.mode' ${LIVE_DATA_FILE})" +DEPLOYMENT_GIT_URL="$(yq -r '.deployment.git_url' ${LIVE_DATA_FILE})" +DEPLOYMENT_GIT_USERNAME="$(yq -r '.deployment.git_username' ${LIVE_DATA_FILE})" +DEPLOYMENT_GIT_PASSWORD="$(yq -r '.deployment.git_password' ${LIVE_DATA_FILE})" +LIVE_TARGET_IP="$(yq -r '.live_target.ip' ${LIVE_DATA_FILE})" +LIVE_TARGET_PASSWORD="$(yq -r '.live_target.password' ${LIVE_DATA_FILE})" + +hierarchy_preparation +setup_ssh +hardware_detection + +until [[ -e web/signals/configuration_ready ]]; do + sleep 5 +done +until [[ -e web/signals/deployment_ready ]]; do + sleep 5 +done + + +# --- MAIN LOGIC ---< \ No newline at end of file diff --git a/script/error_codes.md b/script/error_codes.md new file mode 100644 index 0000000..f5a942f --- /dev/null +++ b/script/error_codes.md @@ -0,0 +1,5 @@ +0: successful. +1: error. +225: Bad SSH credentials. +226: Host unreachable or connection refused. +22: SSH fingerprint in `known_hosts` for the IP is different than the current one. \ No newline at end of file diff --git a/script/start.sh b/script/start.sh new file mode 100644 index 0000000..2cda3ba --- /dev/null +++ b/script/start.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Numbus Installer Bootstrap +# This script clones the repository and launches the NixOS deployment script. + +REPO_URL="https://gittea.dev/numbus/numbus.git" +INSTALL_DIR="/run/user/$(id -u)/numbus-installer" +BRANCH="${BRANCH:-production}" + +echo -e "\n ☁️ Initializing Numbus Installer..." + +# 1. Check for Nix +TEST_FAIL=0 + +if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then + echo -e "\n ✅ NixOS system detected." +else + TEST_FAIL=$((TEST_FAIL + 1)) + echo -e "\n ❌ You are not on a NixOS based system. This is required to continue." +fi + +if [[ "$(uname -m)" == "x86_64" ]]; then + echo -e "\n ✅ x86_64 system detected." +else + TEST_FAIL=$((TEST_FAIL + 1)) + echo -e "\n ❌ You are not on a x86_64 based system. This is required to continue." +fi + +if [[ ${TEST_FAIL} -gt 0 ]]; then + COMPATIBILITY_OVERRIDE=$(gum choose --header "Some compatibility checks failed. The installation will VERY LIKELY fail. Continue ?" \ + "No" \ + "Yes, I know what I am doing") + [[ "${COMPATIBILITY_OVERRIDE}" == "No" ]] && exit 1 + echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus." +fi + +# 2. Clone/Update the repository +if [[ -e ${INSTALL_DIR}/config/* ]]; then + echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up." + OLD_CONFIG_PATH="${INSTALL_DIR}/trash/$(date +"%Y-%m-%d-%Hh%M")" + mkdir -p ${OLD_CONFIG_PATH} + mv ${INSTALL_DIR} ${OLD_CONFIG_PATH} + echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed." +else + rm -rf "${INSTALL_DIR}" +fi + +git clone -b "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" -q + +# 3. Launch the deployment script +cd "${INSTALL_DIR}" +chmod +x script/deploy.sh +./script/deploy.sh \ No newline at end of file diff --git a/script/terminology.md b/script/terminology.md index b9fd977..9f2a851 100644 --- a/script/terminology.md +++ b/script/terminology.md @@ -1,6 +1,6 @@ # Terminology for the variables used -|Variable|Meaning|Possible values| -|-|-------|-| -|DEPLOYMENT_STRATEGY|Either deploy the machine with a config you already have or let the script guide you through the config options|**interactive** or **non-interactive**| -|DEPLOYMENT_MODE|Either configure the machine through your terminal (TUI) or through a slick web UI (GUI)|**TUI** (only available for **non-interactive** strategy) or **GUI**| \ No newline at end of file +|Variable|Meaning|Possible values|Deprecated| +|-|-------|-|-| +|DEPLOYMENT_STRATEGY|Either deploy the machine with a config you already have or let the script guide you through the config options|**interactive** or **non-interactive**|NO| +|DEPLOYMENT_MODE|Either configure the machine through your terminal (TUI) or through a slick web UI (GUI)|**TUI** (only available for **non-interactive** strategy) or **GUI**|YES| \ No newline at end of file diff --git a/web/config/.gitignore b/web/config/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/web/index.html b/web/index.html deleted file mode 100644 index e49f7c0..0000000 --- a/web/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - -
- - -
- Transform your device into a secure, reliable and private appliance
using the power of open-soure software.
You will be guided through the configuration process.
- -Privacy First: No data entered here ever leaves your device.
This configurator runs entirely locally in your browser and is fully private.
-