blob: f959ebf152b787196e36e73151e56e9f861db977 [file] [edit]
#!/bin/bash
# Copyright 2025 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# This script configures a Satlab drone environment.
# It includes robust error handling to ensure a safe and reliable setup.
# Exit immediately if a command exits with a non-zero status.
# Treat unset variables as an error.
# The return value of a pipeline is the status of the last command to exit with a non-zero status.
set -euo pipefail
# --- UI Colors ---
C_YELLOW='\033[1;33m'
C_GREEN='\033[1;32m'
C_RED='\033[0;31m'
B_WHITE='\033[1;37m'
C_NC='\033[0m' # No Color
# --- Default channel ---
CHANNEL="release"
# --- Argument Parsing ---
for arg in "$@"; do
case $arg in
--beta)
CHANNEL="beta"
shift
;;
*)
# Ignore unknown arguments or handle them here
;;
esac
done
##
# Centralized error handler. It's triggered by the 'trap' command below.
##
error_handler() {
local line_number=$1
local failed_command=$2
echo -e "\n${C_RED}Error on line ${line_number}: Command failed: '${failed_command}'${C_NC}"
echo -e "${C_RED}Aborting script execution.${C_NC}"
}
# Trap errors and call the error_handler function
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR
# --- Configuration and Function Sourcing ---
# Define the CONFIG path early so it's available to sourced files
CONFIG="/etc/satlab/satlab.conf"
# Attempt to source the config file to load the set_config function and variables.
if [[ -f "$CONFIG" ]]; then
source "$CONFIG"
fi
# Verify set_config was loaded from the file; if not, define it as a fallback.
# This ensures the script works on a fresh system.
if ! declare -F "set_config" > /dev/null; then
echo -e "${C_YELLOW}Info: 'set_config' function not found in config file. Using fallback definition.${C_NC}"
function set_config(){
local key=$1
local value=$2
# Use the global CONFIG variable
mkdir -p "$(dirname "$CONFIG")"
touch "$CONFIG"
if grep -q "^${key}=" "$CONFIG"; then
# Use | as a delimiter for sed to avoid issues with paths
sed -i "s|^\(${key}\s*=\s*\).*$|\1${value}|" "$CONFIG"
else
echo "${key}=${value}" >> "$CONFIG"
fi
}
fi
# --- Pre-flight Checks ---
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${C_RED}This script must be run as root. Please use sudo.${C_NC}"
exit 1
fi
# Check if running in SSH (optional)
if [[ -z ${SSH_TTY:-} ]]; then
echo -e "${C_YELLOW}It's recommended to run this script within an SSH session.${C_NC}"
while true; do
read -p "Do you want to continue (y/n)?" yn
case $yn in
[Yy]* ) break;;
[Nn]* ) exit;;
* ) echo -e "${C_YELLOW}Please answer yes or no.${C_NC}";;
esac
done
fi
# --- Kernel Version Check ---
KERNEL_FULL=$(uname -r)
KERNEL_MAJOR=$(echo "$KERNEL_FULL" | cut -d. -f1)
KERNEL_MINOR=$(echo "$KERNEL_FULL" | cut -d. -f2)
echo -e "${C_YELLOW}Checking kernel compatibility...${C_NC}"
if [[ "$KERNEL_MAJOR" -gt 6 ]] || [[ "$KERNEL_MAJOR" -eq 6 && "$KERNEL_MINOR" -gt 13 ]]; then
echo -e "${C_RED}Error: Incompatible kernel version found: ($KERNEL_FULL).${C_NC}"
echo -e "${C_RED}Satlab requires kernel version 6.13 or lower.${C_NC}"
echo "Aborting installation."
exit 1
else
echo -e "${C_GREEN}Kernel version $KERNEL_FULL is compatible.${C_NC}"
fi
# --- Install Docker ---
TARGET_DOCKER_VERSION="28.3.3"
echo -e "\n${C_YELLOW}Checking Docker installation...${C_NC}"
INSTALL_DOCKER=false
# Safely check if docker exists and is executable
DOCKER_BIN=$(command -v docker 2>/dev/null || true)
if [[ -n "$DOCKER_BIN" && -x "$DOCKER_BIN" ]]; then
CURRENT_DOCKER_VERSION=$("$DOCKER_BIN" --version 2>/dev/null | awk '{print $3}' | tr -d ',' || true)
# Check if the current version matches the target version
if [[ -n "$CURRENT_DOCKER_VERSION" && "$CURRENT_DOCKER_VERSION" == "$TARGET_DOCKER_VERSION"* ]]; then
echo -e "${C_GREEN}Docker version $TARGET_DOCKER_VERSION is already installed. Skipping installation.${C_NC}"
else
echo -e "${C_RED}Error: Found Docker version ${CURRENT_DOCKER_VERSION:-Unknown}, but target version is $TARGET_DOCKER_VERSION.${C_NC}"
echo -e "${C_YELLOW}Please uninstall the current Docker version manually before continuing with this installation.${C_NC}"
exit 1
fi
else
echo -e "${C_YELLOW}Docker is not installed. Installing version $TARGET_DOCKER_VERSION...${C_NC}"
INSTALL_DOCKER=true
fi
if [[ "$INSTALL_DOCKER" == true ]]; then
curl -fsSL https://get.docker.com -o install-docker.sh
sh install-docker.sh --version "$TARGET_DOCKER_VERSION"
rm -f install-docker.sh
echo -e "${C_GREEN}Docker $TARGET_DOCKER_VERSION installed successfully.${C_NC}"
fi
# --- Configure swap memory ---
SWAP_FILE="/swap.img"
DESIRED_GB=32
# Calculate KiB for assertion (32 * 1024 * 1024).
# We'll check if the reported size is "close enough" (within 1% of this value).
TARGET_KIB=$(( DESIRED_GB * 1024 * 1024 ))
echo -e "\n${C_YELLOW}Setting swap memory size to ${DESIRED_GB}GB...${C_NC}"
# Disable current swap if it's active (ignore errors if it isn't)
swapoff "$SWAP_FILE" 2>/dev/null || true
# Resize the file. Using 'fallocate' is fast; if it fails, 'dd' is a fallback.
if ! fallocate -l "${DESIRED_GB}G" "$SWAP_FILE"; then
echo -e "${C_YELLOW}fallocate failed, attempting dd...${C_NC}"
dd if=/dev/zero of="$SWAP_FILE" bs=1M count=$(( DESIRED_GB * 1024 ))
fi
# Secure the file permissions (critical for swap security)
chmod 600 "$SWAP_FILE"
# Format and enable
mkswap "$SWAP_FILE"
swapon "$SWAP_FILE"
# Assertion Logic
ACTUAL_KIB=$(awk -v f="$SWAP_FILE" '$1==f {print $3}' /proc/swaps)
if [[ -n "$ACTUAL_KIB" ]]; then
# Calculate a 1% margin of error for filesystem overhead
MARGIN=$(( TARGET_KIB / 100 ))
DIFF=$(( TARGET_KIB - ACTUAL_KIB ))
# Check if the absolute difference is within the margin
if [ "${DIFF#-}" -lt "$MARGIN" ]; then
echo -e "${C_GREEN}Swap resized successfully to ~${DESIRED_GB}GB (${ACTUAL_KIB} KiB).${C_NC}"
else
echo -e "${C_RED}Assertion Failed: Swap size is ${ACTUAL_KIB} KiB, expected ~${TARGET_KIB} KiB.${C_NC}"
exit 1
fi
else
echo -e "${C_RED}Error: Swap file $SWAP_FILE is not active.${C_NC}"
exit 1
fi
echo -e "Using software channel: ${C_YELLOW}${CHANNEL}${C_NC}"
# --- Configure packages ---
echo -e "\n${C_YELLOW}Configuring packages...${C_NC}"
apt-get update -y
apt-get install -y bridge-utils net-tools python3-grpcio python3-netifaces python3-protobuf
systemctl stop unattended-upgrades || true
systemctl disable unattended-upgrades || true
apt-get remove --purge -y unattended-upgrades
apt-mark hold linux-image-generic \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
# --- Disable auto-mounting partitions (to prevent blockdev busy errors while writing to a USB drive) ---
systemctl stop udisks2.service
systemctl mask udisks2.service
# --- Optional: Install Graphical Environment (Xfce) ---
echo -e "\n${B_WHITE}Optional: Install desktop environment${C_NC}"
read -p "Do you want to install the Xfce desktop environment? (y/N): " install_gui
if [[ "${install_gui,,}" =~ ^(y|yes)$ ]]; then
echo -e "${C_YELLOW}Installing Xfce and LightDM (this may take a few minutes)...${C_NC}"
# Pre-configure LightDM to avoid interactive debconf prompts during install
echo "lightdm shared/default-x-display-manager select lightdm" | debconf-set-selections
# Install Xfce, the X server, and the display manager
# --no-install-recommends keeps the installation lightweight
apt-get install -y xfce4 xfce4-goodies xfce4-session xorg lightdm lightdm-gtk-greeter dbus-x11 chromium-browser --no-install-recommends
# Force Xfce in AccountsService so it doesn't look for 'ubuntu'
if [ -f /var/lib/AccountsService/users/satlab ]; then
sed -i 's/Session=.*/Session=xfce/' /var/lib/AccountsService/users/satlab
else
# If the file doesn't exist, create it with xfce as default
mkdir -p /var/lib/AccountsService/users/
echo -e "[User]\nSession=xfce" > /var/lib/AccountsService/users/satlab
fi
# Create the .dmrc for the user
echo -e "[Desktop]\nSession=xfce" > /home/satlab/.dmrc
chown satlab:satlab /home/satlab/.dmrc
# System-wide override
mkdir -p /etc/lightdm/lightdm.conf.d/
echo -e "[Seat:*]\nuser-session=xfce" > /etc/lightdm/lightdm.conf.d/90-xfce.conf
# Set the system to boot into the graphical target
systemctl set-default graphical.target
echo -e "${C_GREEN}Xfce Desktop Environment installed successfully.${C_NC}"
else
echo -e "Skipping desktop environment installation."
fi
# --- Configure the host ---
echo -e "\n${C_YELLOW}Sourcing local configuration scripts...${C_NC}"
nic_config_script="$(dirname "$0")/satlab_config_nics"
if [[ ! -f "$nic_config_script" ]]; then
echo -e "${C_RED}Error: Required scripts (satlab_config_nics, satlab_get_keys) not found in the same directory.${C_NC}"
exit 1
fi
source "$nic_config_script"
echo -e "\n${C_YELLOW}Configuring SSH server...${C_NC}"
sed -i -E 's/^#?Port\s+22\b/Port 2233/' /etc/ssh/sshd_config
echo -e "\n${C_YELLOW}Configuring udev rules for persistent peripheral access...${C_NC}"
cat > /etc/udev/rules.d/99-satlab-peripherals.rules <<'EOF'
# Persistent access for serial devices
KERNEL=="ttyACM*", MODE="0666"
KERNEL=="ttyUSB*", MODE="0666"
# Persistent access for video peripheral devices
KERNEL=="video*", MODE="0666"
EOF
if command -v udevadm >/dev/null 2>&1; then
udevadm control --reload-rules || echo "Warning: Failed to reload udev rules"
udevadm trigger || echo "Warning: Failed to trigger udev events"
else
echo "Warning: udevadm not found, skipping rule reloading"
fi
echo -e "\n${C_YELLOW}Configuring the Docker socket...${C_NC}"
mkdir -p /etc/systemd/system/docker.service.d/
cat > /etc/systemd/system/docker.service.d/override.conf <<'EOT'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375
EOT
echo -e "\n${C_YELLOW}Configuring the Satlab hostname...${C_NC}"
if [[ ! -f /etc/machine-id ]]; then
echo -e "${C_RED}Error: /etc/machine-id not found. Cannot generate hostname.${C_NC}"
exit 1
fi
host_identifier="5w$(cat /etc/machine-id | sha256sum | head -c 10)"
echo "Setting Satlab hostname: ${host_identifier}"
set_config "HOST_IDENTIFIER" "$host_identifier"
# --- Download images ---
echo -e "\n${C_YELLOW}Pulling Docker images...${C_NC}"
docker pull ${REMOTE_ACCESS_IMAGE}
docker pull ${DHCP_DOCKER_IMAGE}
docker pull ${COMPOSE_DOCKER_IMAGE}
if [[ "${download_keys:-n}" =~ ^[yY]([eE][sS])?$ ]]; then
echo -e "\n${C_YELLOW}Pulling images via docker-compose...${C_NC}"
docker rm compose-pull 2> /dev/null || true
docker run -it --rm --name compose-pull \
--mount type=volume,source=satlab_keys,target=/home/satlab/keys/ \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
-e LABEL="${LABEL}" \
-e VOLUME_KEYS_FOLDER="${VOLUME_KEYS_FOLDER}" \
"${COMPOSE_DOCKER_IMAGE}" pull
echo -e "${C_GREEN}Docker images pulled successfully.${C_NC}"
else
echo -e "\n${C_YELLOW}Skipping Docker image pull (partner user).${C_NC}"
fi
# --- Switch to cgroups v1 ---
echo -e "\n${C_YELLOW}Configuring cgroups for legacy compatibility...${C_NC}"
if ! grep -q "systemd.unified_cgroup_hierarchy=0" /etc/default/grub; then
echo "Adding cgroup kernel parameter to GRUB..."
sed -i 's/\(GRUB_CMDLINE_LINUX_DEFAULT=".*\)"/\1 systemd.unified_cgroup_hierarchy=0"/' /etc/default/grub
update-grub
else
echo "GRUB cgroup parameter already set. Skipping."
fi
# --- Configure Satlab services ---
echo -e "\n${C_YELLOW}Configuring Satlab services...${C_NC}"
ln -sf /etc/satlab/satlab-upstart.service /etc/systemd/system/satlab-upstart.service
ln -sf /etc/satlab/host_server/satlab-host-server.service /etc/systemd/system/satlab-host-server.service
ln -sf /etc/satlab/satlab-apply-staged-updates.service /etc/systemd/system/
ln -sf /etc/satlab/satlab-stage-updates.service /etc/systemd/system/
ln -sf /etc/satlab/satlab-stage-updates.timer /etc/systemd/system/
systemctl daemon-reload
systemctl enable satlab-upstart
systemctl enable satlab-host-server
systemctl enable satlab-apply-staged-updates.service
systemctl enable satlab-stage-updates.service
systemctl enable satlab-stage-updates.timer
# --- Finish the installation ---
echo -e "\n${C_GREEN}Configuration complete! A reboot is required to apply all changes.${C_NC}"
while true; do
read -p "Do you want to reboot now? (y/n) " yn
case $yn in
[Yy]* ) reboot; break;;
[Nn]* ) echo -e "${C_GREEN}Reboot cancelled.${C_NC}"; break;;
* ) echo -e "${C_GREEN}Please answer yes or no.${C_NC}";;
esac
done