#!/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 # - Runs a basic hardware health check (WiFi, battery, disk, CPU, RAM) # - Reports back to the Provisioning Portal using the code shown on the asset's # page. The portal updates Snipe-IT (serial, manufacturer/model, CPU/RAM/ # storage/WiFi MAC fields, health check in the notes) and moves the asset from # Awaiting Config to Awaiting Grading, recorded against the team member who # generated the code. # # 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" # Provisioning Portal. The device never talks to Snipe-IT directly and holds no # API key: the one-time code from the portal is the only credential it needs. # Override with the PORTAL_URL environment variable if the portal ever moves. PORTAL_URL="${PORTAL_URL:-https://provisioning.inclusivebytes.org}" 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 Provisioning Portal + 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_model_version() { # On Lenovo (and many other OEMs) this field holds the human-friendly # model name (e.g. "ThinkPad L480"), while system-product-name only # gives the raw Machine Type-Model code (e.g. "20LSS0"). sudo dmidecode -s system-version 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]*$//' } # These feed the Snipe-IT custom fields (CPU, RAM, Storage, WiFi MAC). get_cpu() { grep -m1 'model name' /proc/cpuinfo 2>/dev/null | sed 's/^.*: //' } get_ram_gb() { awk '/^MemTotal:/{printf "%.1f", $2/1024/1024; exit}' /proc/meminfo 2>/dev/null } get_disk() { lsblk -dno NAME,SIZE,MODEL 2>/dev/null | grep -v '^loop' | sed 's/[ \t]*$//' | paste -sd ';' - } get_wifi_mac() { local iface for iface in /sys/class/net/*; do if [ -d "${iface}/wireless" ]; then cat "${iface}/address" 2>/dev/null return fi done } get_mac_addresses_json() { local iface addr for iface in /sys/class/net/*; do [ "$(basename "${iface}")" = "lo" ] && continue addr=$(cat "${iface}/address" 2>/dev/null) [ -n "${addr}" ] && echo "${addr}" done | jq -R . | jq -s -c . } # --------------------------------------------------------------------------- # Step 3: Provisioning Portal connection # --------------------------------------------------------------------------- ensure_portal_url() { if [ -z "${PORTAL_URL}" ]; then read -rp "Provisioning Portal URL (e.g. https://provisioning.inclusivebytes.org): " PORTAL_URL < /dev/tty fi PORTAL_URL="${PORTAL_URL%/}" if [[ "${PORTAL_URL}" != https://* ]]; then echo "Warning: PORTAL_URL (${PORTAL_URL}) is not HTTPS - the code 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 } prompt_for_code() { DEVICE_CODE="" while [ -z "${DEVICE_CODE}" ]; do read -rp "Please enter the code shown on the provisioning portal: " DEVICE_CODE < /dev/tty if [ -z "${DEVICE_CODE}" ]; then echo "The code cannot be empty, please try again." fi done } call_portal_complete() { # $1 = code, $2 = serial, $3 = model_name, $4 = manufacturer, $5 = diagnostics text # The hardware facts (CPU, RAM, disk, WiFi MAC, MACs) come from the globals set in main. local code="$1" serial="$2" model_name="$3" manufacturer="$4" diagnostics="$5" local payload payload=$(jq -n \ --arg code "${code}" \ --arg serial "${serial}" \ --arg model_name "${model_name}" \ --arg manufacturer "${manufacturer}" \ --arg cpu "${CPU}" \ --argjson ram_gb "${RAM_GB:-null}" \ --arg disk "${DISK}" \ --arg wifi_mac "${WIFI_MAC}" \ --argjson mac_addresses "${MAC_ADDRESSES:-[]}" \ --arg diagnostics "${diagnostics}" \ '{code: $code, serial: $serial, model_name: $model_name, manufacturer: $manufacturer, cpu: $cpu, ram_gb: $ram_gb, disk: $disk, wifi_mac: $wifi_mac, mac_addresses: $mac_addresses, 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 "${PORTAL_URL}/api/device/complete" \ -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 500 ] 2>/dev/null; then # Success, or a 4xx (bad code / wrong status) that won't fix itself on retry echo "${raw_response}" return 0 fi echo "Portal returned HTTP ${http_code}, retrying (attempt ${attempt}/${max_attempts})..." >&2 else echo "curl failed to reach the portal (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 ensure_portal_url SERIAL=$(get_serial) MODEL_NAME=$(get_model) MODEL_VERSION=$(get_model_version) MANUFACTURER=$(get_manufacturer) CPU=$(get_cpu) RAM_GB=$(get_ram_gb) DISK=$(get_disk) WIFI_MAC=$(get_wifi_mac) MAC_ADDRESSES=$(get_mac_addresses_json) case "${MODEL_VERSION}" in ""|"Not Available"|"None"|"System Version"|"To be filled by O.E.M."|"Default string") MODEL_VERSION="" ;; esac DISPLAY_MODEL="${MODEL_NAME:-unknown}" if [ -n "${MODEL_VERSION}" ]; then DISPLAY_MODEL="${MODEL_VERSION} (${MODEL_NAME:-unknown})" fi echo "Detected serial: ${SERIAL:-unknown}" echo "Detected model: ${DISPLAY_MODEL} (${MANUFACTURER:-unknown})" echo "Detected specs: CPU=${CPU:-unknown}, RAM=${RAM_GB:-unknown} GB, disk=${DISK:-unknown}, WiFi MAC=${WIFI_MAC:-none}" run_diagnostics DIAG_REPORT=$(cat /tmp/donate-it-diagnostics.txt) echo "--- Provisioning Portal ---" local attempt=1 max_attempts=3 done_ok=0 while [ "${attempt}" -le "${max_attempts}" ]; do prompt_for_code echo "Sending results to the provisioning portal..." RAW_RESPONSE=$(call_portal_complete "${DEVICE_CODE}" "${SERIAL}" "${DISPLAY_MODEL}" "${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_TAG=$(echo "${BODY}" | jq -r '.asset_tag // "unknown"') echo "Portal updated: asset ${ASSET_TAG} is now '${STATUS}'." SKIPPED=$(echo "${BODY}" | jq -r '(.skipped_fields // []) | join(", ")') if [ -n "${SKIPPED}" ]; then echo "Note: these details couldn't be saved as Snipe-IT fields and were added to the asset notes instead: ${SKIPPED}" fi if [ "$(echo "${BODY}" | jq -r '.serial_matches')" = "false" ]; then echo "WARNING: this device's serial (${SERIAL:-unknown}) did not match the serial on the portal asset," echo " so the asset's serial was updated. Check you entered the code for the right device." fi done_ok=1 break elif [ "${HTTP_CODE}" = "404" ]; then echo "That code wasn't recognised (or it expired / was already used). Check the portal page and try again." attempt=$((attempt + 1)) else echo "Portal call failed (HTTP ${HTTP_CODE:-unreachable}):" echo "${BODY}" break fi done if [ "${done_ok}" -ne 1 ]; then echo "The portal was NOT updated. Get a fresh code from the asset page and re-run, or use 'Mark configured manually' there." fi echo "=== Setup complete ===" echo "Diagnostics saved to /tmp/donate-it-diagnostics.txt" echo "Full log saved to ${LOG_FILE}" } main "$@"