#!/bin/bash # # Donate-IT device provisioning script # - Installs standard software set # - Re-enables snap (Inkscape/GIMP etc kept via apt as before, snap store restored) # - Sets Firefox policy (Bitwarden auto-install) # - Creates desktop shortcuts # - Registers/updates the device in Snipe-IT (asset tag, model, serial, notes) # - Runs a basic hardware health check (WiFi, battery, disk, CPU, RAM) and # writes it to the Snipe-IT asset notes, in one run # # Run as: sudo ./donate-it-setup.sh # (the script calls sudo itself where needed, but needs to be run by a user # who can sudo without re-prompting failing mid-script) set -uo pipefail # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- TARGET_USER="user" TARGET_HOME="/home/${TARGET_USER}" DESKTOP_DIR="${TARGET_HOME}/Desktop" # Middleman server connection details. The device never talks to Snipe-IT # directly, or holds the Snipe-IT API token. It only needs the middleman's # URL and a shared API key. Defaults below point at the Donate-IT middleman # server; override with environment variables if you ever move it. MIDDLEMAN_URL="${MIDDLEMAN_URL:-https://dit-middleman.it.inclusivebytes.org}" MIDDLEMAN_API_KEY="${MIDDLEMAN_API_KEY:-}" LOG_FILE="/tmp/donate-it-setup.log" ( umask 077 && : > "${LOG_FILE}" ) exec > >(tee -a "${LOG_FILE}") 2>&1 echo "=== Jake's Amazingggg Installer (now with one-run Snipe-IT + diagnostics) ===" echo "Log: ${LOG_FILE}" echo # --------------------------------------------------------------------------- # Helper functions # --------------------------------------------------------------------------- require_root_actions() { if [ "$(id -u)" -eq 0 ]; then echo "Note: running as root directly; sudo calls below will still work." fi } press_continue() { read -rp "Press Enter to continue..." _ < /dev/tty } # --------------------------------------------------------------------------- # Step 1: Package installation (runs once) # --------------------------------------------------------------------------- install_packages() { echo "--- Updating system and installing packages ---" export DEBIAN_FRONTEND=noninteractive # needrestart pops up an interactive "which services to restart" prompt # during apt upgrade/install and reads from stdin, which leaves stdin at # EOF for any read commands later in the script. Force it to run # automatically instead of prompting. export NEEDRESTART_MODE=a sudo apt-get -y -qq update sudo apt-get -y -qq upgrade local cmd="sudo apt-get install -y -qq" $cmd gimp libreoffice inkscape vlc jq dmidecode upower smartmontools network-manager pciutils usbutils echo "--- Re-enabling snap ---" sudo apt-get remove -y -qq snapd || true if [ -f /etc/apt/preferences.d/no-snap.pref ]; then sudo rm -f /etc/apt/preferences.d/no-snap.pref fi $cmd snapd sudo snap install snap-store sudo snap install snapd-desktop-integration echo "--- Applying Firefox policy (Bitwarden) ---" sudo mkdir -p /etc/firefox/policies sudo tee /etc/firefox/policies/policies.json > /dev/null <<'EOF' { "policies": { "Extensions": { "Install": [ "https://addons.mozilla.org/firefox/downloads/latest/bitwarden-password-manager/latest.xpi" ] } } } EOF echo "--- Creating desktop shortcuts ---" sudo -u "${TARGET_USER}" mkdir -p "${DESKTOP_DIR}" declare -A shortcuts=( ["firefox.desktop"]="/usr/share/applications/firefox.desktop" ["inkscape.desktop"]="/usr/share/applications/org.inkscape.Inkscape.desktop" ["gimp.desktop"]="/usr/share/applications/gimp.desktop" ["libreoffice.desktop"]="/usr/share/applications/libreoffice-base.desktop" ["vlc.desktop"]="/usr/share/applications/vlc.desktop" ) for dest in "${!shortcuts[@]}"; do src="${shortcuts[$dest]}" if [ -f "${src}" ]; then cp "${src}" "${DESKTOP_DIR}/${dest}" chmod +x "${DESKTOP_DIR}/${dest}" chown "${TARGET_USER}:${TARGET_USER}" "${DESKTOP_DIR}/${dest}" else echo "Warning: ${src} not found, skipping shortcut for ${dest}" fi done echo "Package installation complete." echo } # --------------------------------------------------------------------------- # Step 2: Hardware facts # --------------------------------------------------------------------------- get_serial() { sudo dmidecode -s system-serial-number 2>/dev/null | head -n1 | tr -d '[:space:]' } get_model() { sudo dmidecode -s system-product-name 2>/dev/null | head -n1 | sed 's/^[ \t]*//;s/[ \t]*$//' } get_manufacturer() { sudo dmidecode -s system-manufacturer 2>/dev/null | head -n1 | sed 's/^[ \t]*//;s/[ \t]*$//' } # --------------------------------------------------------------------------- # Step 3: Middleman server connection # --------------------------------------------------------------------------- ensure_middleman_credentials() { if [ -z "${MIDDLEMAN_URL}" ]; then read -rp "Middleman server URL (e.g. https://dit-middleman.it.inclusivebytes.org): " MIDDLEMAN_URL < /dev/tty fi while [ -z "${MIDDLEMAN_API_KEY}" ]; do read -rsp "Middleman API key: " MIDDLEMAN_API_KEY < /dev/tty echo [ -z "${MIDDLEMAN_API_KEY}" ] && echo "API key cannot be empty, please try again." done MIDDLEMAN_URL="${MIDDLEMAN_URL%/}" if [[ "${MIDDLEMAN_URL}" != https://* ]]; then echo "Warning: MIDDLEMAN_URL (${MIDDLEMAN_URL}) is not HTTPS - the API key and asset data would be sent unencrypted." read -rp "Continue anyway? [y/N]: " confirm_insecure < /dev/tty case "${confirm_insecure}" in y|Y) ;; *) echo "Aborting."; exit 1 ;; esac fi } call_middleman_provision() { # $1 = asset_tag, $2 = serial, $3 = model_name, $4 = manufacturer, $5 = diagnostics text local asset_tag="$1" serial="$2" model_name="$3" manufacturer="$4" diagnostics="$5" local payload payload=$(jq -n \ --arg asset_tag "${asset_tag}" \ --arg serial "${serial}" \ --arg model_name "${model_name}" \ --arg manufacturer "${manufacturer}" \ --arg diagnostics "${diagnostics}" \ '{asset_tag: $asset_tag, serial: $serial, model_name: $model_name, manufacturer: $manufacturer, diagnostics: $diagnostics}') local max_attempts=3 attempt=1 delay=2 local raw_response curl_exit http_code while [ "${attempt}" -le "${max_attempts}" ]; do raw_response=$(curl -sS --connect-timeout 10 --max-time 30 -w "\n%{http_code}" \ -X POST "${MIDDLEMAN_URL}/api/provision" \ -H "X-API-Key: ${MIDDLEMAN_API_KEY}" \ -H "Content-Type: application/json" \ -d "${payload}") curl_exit=$? if [ "${curl_exit}" -eq 0 ]; then http_code=$(echo "${raw_response}" | tail -n1) if [ "${http_code}" -ge 200 ] 2>/dev/null && [ "${http_code}" -lt 300 ] 2>/dev/null; then echo "${raw_response}" return 0 elif [ "${http_code}" -lt 500 ] 2>/dev/null; then # 4xx (bad request/auth) won't fix itself on retry echo "${raw_response}" return 0 fi echo "Middleman returned HTTP ${http_code}, retrying (attempt ${attempt}/${max_attempts})..." >&2 else echo "curl failed to reach middleman (exit ${curl_exit}), retrying (attempt ${attempt}/${max_attempts})..." >&2 fi attempt=$((attempt + 1)) [ "${attempt}" -le "${max_attempts}" ] && sleep "${delay}" delay=$((delay * 2)) done printf '\n000' return 1 } # --------------------------------------------------------------------------- # Step 4: Hardware diagnostics # --------------------------------------------------------------------------- check_wifi() { local iface iface=$(nmcli -t -f DEVICE,TYPE device 2>/dev/null | awk -F: '$2=="wifi"{print $1; exit}') if [ -z "${iface}" ]; then echo "WiFi: no wireless interface detected" return fi echo "WiFi: interface ${iface} detected" if nmcli -t -f STATE g 2>/dev/null | grep -q "connected"; then local ssid ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | grep '^yes' | cut -d: -f2) echo "WiFi: connected, SSID=${ssid:-unknown}" else echo "WiFi: interface present but not associated to a network" fi if ping -c 2 -W 2 1.1.1.1 >/dev/null 2>&1; then echo "WiFi: internet reachability OK (ping 1.1.1.1 succeeded)" else echo "WiFi: internet reachability FAILED (ping 1.1.1.1 did not respond)" fi } check_battery() { local bat bat=$(upower -e 2>/dev/null | grep -i 'BAT') if [ -z "${bat}" ]; then echo "Battery: no battery detected (desktop, or battery not reporting)" return fi local info info=$(upower -i "${bat}" 2>/dev/null) local capacity health state percentage percentage=$(echo "${info}" | awk -F': *' '/percentage/{print $2}') state=$(echo "${info}" | awk -F': *' '/state/{print $2}') capacity=$(echo "${info}" | awk -F': *' '/capacity/{print $2}') echo "Battery: state=${state:-unknown}, charge=${percentage:-unknown}, health(capacity)=${capacity:-unknown}" if [ -n "${capacity}" ]; then local cap_int=${capacity%.*} if [ "${cap_int}" -lt 60 ] 2>/dev/null; then echo "Battery: WARNING - health below 60 percent, consider flagging for replacement" fi fi } check_disk() { local disk disk=$(lsblk -dno NAME,TYPE 2>/dev/null | awk '$2=="disk"{print $1; exit}') if [ -z "${disk}" ]; then echo "Disk: could not determine primary disk" return fi echo "Disk: primary disk /dev/${disk}" if command -v smartctl >/dev/null 2>&1; then local smart_health smart_health=$(sudo smartctl -H "/dev/${disk}" 2>/dev/null | grep -i "overall-health" | awk -F': *' '{print $2}') echo "Disk: SMART overall health = ${smart_health:-not reported}" else echo "Disk: smartctl not available, skipping SMART check" fi df -h / | awk 'NR==2{print "Disk: root filesystem usage " $5 " of " $2}' } check_cpu() { local model cores threads max_mhz if command -v lscpu >/dev/null 2>&1; then model=$(lscpu | awk -F': *' '/^Model name/{print $2; exit}') cores=$(lscpu | awk -F': *' '/^Core\(s\) per socket/{print $2; exit}') threads=$(lscpu | awk -F': *' '/^CPU\(s\):/{print $2; exit}') max_mhz=$(lscpu | awk -F': *' '/^CPU max MHz/{print $2; exit}') else model=$(awk -F': *' '/^model name/{print $2; exit}' /proc/cpuinfo) threads=$(grep -c '^processor' /proc/cpuinfo) fi echo "CPU: model=${model:-unknown}, cores=${cores:-unknown}, threads=${threads:-unknown}, max_mhz=${max_mhz:-unknown}" # Quick load check so a dead/throttled CPU shows up in the report too. local load1 load1=$(awk '{print $1}' /proc/loadavg 2>/dev/null) echo "CPU: 1-min load average = ${load1:-unknown}" } check_ram() { free -h | awk '/^Mem:/{print "RAM: total " $2 ", used " $3 ", free " $4}' } run_diagnostics() { echo "--- Running hardware diagnostics ---" { echo "Diagnostic report - $(date '+%Y-%m-%d %H:%M:%S')" check_wifi check_battery check_disk check_cpu check_ram } | tee /tmp/donate-it-diagnostics.txt echo } # --------------------------------------------------------------------------- # Step 5: Main flow # --------------------------------------------------------------------------- main() { require_root_actions install_packages echo "--- Asset registration ---" ASSET_TAG="" while [ -z "${ASSET_TAG}" ]; do read -rp "Enter Asset Tag for this device: " ASSET_TAG < /dev/tty if [ -z "${ASSET_TAG}" ]; then echo "Asset tag cannot be empty, please try again." fi done ensure_middleman_credentials SERIAL=$(get_serial) MODEL_NAME=$(get_model) MANUFACTURER=$(get_manufacturer) echo "Detected serial: ${SERIAL:-unknown}" echo "Detected model: ${MODEL_NAME:-unknown} (${MANUFACTURER:-unknown})" run_diagnostics DIAG_REPORT=$(cat /tmp/donate-it-diagnostics.txt) echo "Sending provisioning record to middleman server..." RAW_RESPONSE=$(call_middleman_provision "${ASSET_TAG}" "${SERIAL}" "${MODEL_NAME}" "${MANUFACTURER}" "${DIAG_REPORT}") HTTP_CODE=$(echo "${RAW_RESPONSE}" | tail -n1) BODY=$(echo "${RAW_RESPONSE}" | sed '$d') if [ "${HTTP_CODE}" -ge 200 ] 2>/dev/null && [ "${HTTP_CODE}" -lt 300 ] 2>/dev/null; then STATUS=$(echo "${BODY}" | jq -r '.status // "unknown"') ASSET_ID=$(echo "${BODY}" | jq -r '.snipeit_asset_id // "unknown"') echo "Snipe-IT update via middleman: status=${STATUS}, asset_id=${ASSET_ID}" else echo "Middleman call failed (HTTP ${HTTP_CODE:-unreachable}):" echo "${BODY}" fi echo "=== Setup complete ===" echo "Diagnostics saved to /tmp/donate-it-diagnostics.txt" echo "Full log saved to ${LOG_FILE}" } main "$@"