From df9d898cceb4ff3732f0ff7f49a2aa2f53390912 Mon Sep 17 00:00:00 2001 From: Phill Kelley <34226495+Paraphraser@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:39:11 +1100 Subject: [PATCH] 2023-10-24 replacement for install.sh script - master branch One of the most surprising things about #729 (at least to me) was how it implied that someone had actually used the `install.sh` script. I say this with the utmost respect for the original author and subsequent contributors (who did all the work while I just sat on my hands) but, after studying the existing script, I reached the conclusion that the wisest course of action was to start from scratch. There were seemed to be so many issues in the existing `install.sh` that it was difficult to know how much could be salvaged: * It creates `.new_install` in the current working directory (typically `~`) **before** cloning IOTstack. The menu, of course, expects that file to be in `~/IOTstack` so the menu thinks no installation work has been done. * It (correctly) uses the "convenience script" to install `docker` but then uses `apt` to install `docker-compose` (which is wrong). The result is the Python version of `docker-compose` being installed and, as a side effect, `docker` is unconditionally downgraded to a compatible version (which is where the problematic `+dfsg1` version suffix comes from - see #496). The long-term effect is that both `docker` and `docker-compose` become pinned and are never subsequently upgraded by `apt`. Another side-effect is the version of `docker-compose-plugin` installed by the convenience script becomes inaccessible. * The `usermod` commands for `bluetooth` appear twice; those for the `docker` group three times. This probably doesn't actually harm anything but it certainly doesn't lend itself to clarity of intention. * The version-checking is brute force and makes assumptions that won't always necessarily hold, such as that there are always three SEMVER components and that suffixes like `+dfsg1` won't result in a mess, like they did in similar code in the menu (again I refer to #496. And #503. And #585). Indeed, it's really only luck which means the `+dfsg1` doesn't appear until after `install.sh` has finished its work. What this replacement script attempts to do is: 1. Use absolute paths throughout so there is no ambiguity about where files/folders are located. Although this replacement script defaults to `~/IOTstack` the default can be overridden by prepending the correct path, as in: ``` $ IOTSTACK="$HOME/TestIOTstack" ./install.sh ``` 2. Use a **rational** method of version checking (specifically `dpkg --compare-versions`) which can actually handle the cases of one, two, three or more SEMVER fields correctly and which isn't fazed by weird suffixes. 3. Install the minimum set of dependencies needed by IOTstack. This is on the assumption that `install.sh` may be being used on either a green-fields system or an existing system. PiBuilder excels at green-fields but could prove problematic were it to be used on an already-highly-customised working system. My own view is that a clean slate plus PiBuilder produces a better outcome but there is definitely a case to be made for supporting adding IOTstack to an existing system. 4. Install `docker` and `docker-compose-plugin` correctly so both `docker-compose` (with hyphen) and `docker compose` (without hyphen) are the same binary and produce the same result. One side effect of correct installation is that both `docker` and `docker-compose-plugin` are updated by `apt`. 5. Adds the user to the required groups (once!). 6. Installs Python dependencies in a Bookworm-friendly manner. And, yes, I do realise using `--break-system-packages` is suboptimal but that's something that can be addressed by people with Python expertise (and, given no PRs have been submitted to attend to this, those people are probably a bit thin on the ground). In my view a dash of *sub-optimal* is better than not working at all on Bookworm. 7. Sets Raspberry Pi cmdline options on a per-option basis, rather than assuming the options will always appear in the same order. This replacement script is also specifically designed to be run multiple times without doing any harm. It is also designed so that it can be run safely *after* a PiBuilder run. This serves four purposes: 1. If the script is being run on an existing system where, say, an obsolete version of `docker` is installed, the script will explain how to remove `docker`, after which the script should be re-run. This basic approach of check, explain how to recover, re-try, continues until the script completes normally. 2. Ultimately, my intention is to propose another PR to remove **all** "installation" and version-checking tasks from the menu. To that end, this replacement script writes its exit status to `~/IOTstack/.new_install`. Eventually, I see the menu behaving like this: - if `.new_install` is not present or is present and contains a non-zero exit code, the menu will prompt the user to run the (replacement) `install.sh`. 3. As a guided-migration tool. Once the menu has been changed as above, the mostly likely situation a user will encounter after the subsequent pull from GitHub is the menu recommending that (the new) `install.sh` should be run. If the script finds the user's environment is obsolete (eg ancient pinned versions of `docker` or `docker-compose`) the script will guide the user through the upgrade. Rinse, repeat and eventually the script will complete normally, after which the user's system will be fully up-to-date and the menu will just get on with the job of being the menu. 4. As a general-purpose "fixup" tool. Anyone reporting problems with IOTstack which implicate anything this replacement script is designed to handle can be instructed to run `install.sh` and see what happens. This replacement script updates minimum version numbers to something more recent: * docker version 24 or later (previously 18.2.0 or later) * docker-compose version 2.20 or later (previously not checked) * Python version 3.9 or later (previously 3.6.9 or later) I have tested this script on: 1. Raspberry Pi 4B running Bullseye and Bookworm. 2. Debian on Proxmox, running Bullseye and Bookworm. 3. Multiple runs of this script on each of the above (to ensure second or subsequent runs do no harm). 4. After running PiBuilder on clean installs of all four test platforms (ie 1+2 above), also to ensure a run does no harm. 5. After running the **existing** `install.sh` on all four test platforms, to ensure any damage (eg pinned obsolete versions) is discovered and reported, and that by following the repair instructions and re-running the **new** `install.sh` ultimately gives the platform a clean bill of health. Documentation will be added to #737 shortly. Signed-off-by: Phill Kelley <34226495+Paraphraser@users.noreply.github.com> --- install.sh | 620 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 373 insertions(+), 247 deletions(-) diff --git a/install.sh b/install.sh index e1d9df8a..55a83dcd 100755 --- a/install.sh +++ b/install.sh @@ -1,293 +1,419 @@ #!/bin/bash -# Minimum Software Versions -REQ_DOCKER_VERSION=18.2.0 -REQ_PYTHON_VERSION=3.6.9 - -PYTHON_CMD=python3 - -sys_arch=$(uname -m) - -while test $# -gt 0 -do - case "$1" in - --no-ask) NOASKCONFIRM="true" - ;; - --*) echo "bad option $1" - ;; - esac - shift -done - -echo "IOTstack Installation" -if [ "$EUID" -eq "0" ]; then - echo "Please do not run as root" - exit +echo " " +echo " _____ ____ _______ _ _ " +echo " |_ _/ __ \\__ __|| | installer | | " +echo " | || | | | | |___| |_ __ _ ___| | __" +echo " | || | | | | / __| __/ _\` |/ __| |/ /" +echo " _| || |__| | | \\__ \\ || (_| | (__| < " +echo " |_____\\____/ |_|___/\\__\\__,_|\\___|_|\\_\\" +echo " " +echo " " + +#---------------------------------------------------------------------- +# The intention of this script is that it should be able to be run +# multiple times WITHOUT doing any harm. If you propose changes, please +# make sure you test the script in both a "green fields" system AND on +# a working system where docker, docker-compose and IOTstack are already +# installed. +#---------------------------------------------------------------------- + +# overuse of sudo is a very common problem among new IOTstack users +[ "$EUID" -eq 0 ] && echo "This script should NOT be run using sudo" && exit 1 + +# the name of this script is +SCRIPT=$(basename "$0") + +# this script should be run without arguments +[ $# -ne 0 ] && echo "$SCRIPT parameter(s) $@ ignored" + +# assumption(s) which can be overridden +IOTSTACK=${IOTSTACK:-"$HOME/IOTstack"} + +# form absolute path +IOTSTACK=$(realpath "$IOTSTACK") + +# derived path(s) - note that the menu knows about most of these so +# they can't just be changed without a lot of care. +IOTSTACK_ENV="$IOTSTACK/.env" +IOTSTACK_MENU_REQUIREMENTS="$IOTSTACK/requirements-menu.txt" +IOTSTACK_MENU_VENV_DIR="$IOTSTACK/.virtualenv-menu" +IOTSTACK_INSTALLER_HINT="$IOTSTACK/.new_install" + +# the expected installation location of docker-compose-plugin is +COMPOSE_PLUGIN_PATH="/usr/libexec/docker/cli-plugins/docker-compose" + +# the default location of a symlink in the PATH pointing to the above is +COMPOSE_SYMLINK_PATH="/usr/local/bin/docker-compose" + +# add these to /boot/cmdline.txt (if it exists) +CMDLINE_OPTIONS="cgroup_memory=1 cgroup_enable=memory" + +# dependencies installed via apt +APT_DEPENDENCIES="curl git jq python3-pip python3-dev python3-virtualenv uuid-runtime whiptail" + +# minimum version requirements +DOCKER_VERSION_MINIMUM="24" +COMPOSE_VERSION_MINIMUM="2.20" +PYTHON_VERSION_MINIMUM="3.9" + +# best-practice for group membership +DESIRED_GROUPS="docker bluetooth" + +# what to do at script completion (reboot takes precedence) +REBOOT_REQUIRED=false +LOGOUT_REQUIRED=false + +#---------------------------------------------------------------------- +# Check script dependencies +#---------------------------------------------------------------------- + +echo -e -n "\nChecking operating-system environment - " +# This script assumes apt and dpkg are available. That's more-or-less +# the same as saying Debian oe Debian-derived. If apt and/or dpkg are +# missing then there's not much that can be done. +if [ -z $(which apt) -o -z $(which dpkg) ] ; then + echo "fail" + unset ID + [ -f "/etc/os-release" ] && eval $(grep "^ID=" /etc/os-release) + if [ "$ID" = "debian" ] ; then + echo "This system looks like it is based on Debian but seems to be missing" + echo "some key utilities (apt and/or dpkg). That suggests something is wrong." + echo "This script can't proceed until those issues are resolved." + else + echo "Some key utilities that are needed by this script seem to be missing" + echo "from this system. Both the Advanced Package Tool (apt) and the Debian" + echo "Package Manager (dpkg) are core components of Debian and Debian-derived" + echo "distributions like Raspberry Pi OS (aka Raspbian). It looks like you" + echo "might be trying to install IOTstack on a system which isn't based on" + echo "Debian. IOTstack has only ever been tested on Debian-based distributions" + echo "and is not qualified for other Linux or Unix distributions. This script" + echo "can't proceed." + fi + # direct exit - not via handle_exit() + exit 1 +else + echo "pass" fi -function command_exists() { - command -v "$@" > /dev/null 2>&1 -} -function minimum_version_check() { - # Usage: minimum_version_check required_version current_major current_minor current_build - # Example: minimum_version_check "1.2.3" 1 2 3 - REQ_MIN_VERSION_MAJOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 1) - REQ_MIN_VERSION_MINOR=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 2) - REQ_MIN_VERSION_BUILD=$(echo "$1"| cut -d' ' -f 2 | cut -d'.' -f 3) - - CURR_VERSION_MAJOR=$2 - CURR_VERSION_MINOR=$3 - CURR_VERSION_BUILD=$4 - - VERSION_GOOD="Unknown" - - if [ -z "$CURR_VERSION_MAJOR" ]; then - echo "$VERSION_GOOD" - return 1 - fi +#---------------------------------------------------------------------- +# script memory (exit conditions) +#---------------------------------------------------------------------- - if [ -z "$CURR_VERSION_MINOR" ]; then - echo "$VERSION_GOOD" - return 1 - fi +function handle_exit() { - if [ -z "$CURR_VERSION_BUILD" ]; then - echo "$VERSION_GOOD" - return 1 - fi + # record the exit condition (if possible) + [ -d "$IOTSTACK" ] && echo "$1" >"$IOTSTACK_INSTALLER_HINT" - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi + # inform the user + echo -n "$SCRIPT completed" - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ - [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" + # reboot takes precedence over logout + if [ "$REBOOT_REQUIRED" = "true" ] ; then + echo " - a reboot is required." + sleep 2 + sudo reboot + elif [ "$LOGOUT_REQUIRED" = "true" ] ; then + echo " - a logout is required." + sleep 2 + kill -HUP "$PPID" fi - if [ "${CURR_VERSION_MAJOR}" -ge $REQ_MIN_VERSION_MAJOR ] && \ - [ "${CURR_VERSION_MINOR}" -ge $REQ_MIN_VERSION_MINOR ] && \ - [ "${CURR_VERSION_BUILD}" -ge $REQ_MIN_VERSION_BUILD ]; then - VERSION_GOOD="true" - echo "$VERSION_GOOD" - return 0 - else - VERSION_GOOD="false" - fi + # exit as instructed + echo "" + exit $1 - echo "$VERSION_GOOD" } -function user_in_group() -{ - if grep -q $1 /etc/group ; then - if id -nGz "$USER" | grep -qzxF "$1"; then - echo "true" - else - echo "false" - fi - else - echo "notgroup" - fi -} -function install_python3_and_deps() { - CURR_PYTHON_VER="${1:-Unknown}" - CURR_VIRTUALENV="${2:-Unknown}" - if ([ "$NOASKCONFIRM" == "true" ] || - whiptail --title "Python 3 and virtualenv" --yesno "Python 3.6.9 or later (Current = $CURR_PYTHON_VER) and virtualenv (Installed = $CURR_VIRTUALENV) are required for the main menu. Install these now?" 20 78); then - echo "Installing Python3 and virtualenv" - sudo apt install -y python3-dev python3-virtualenv - if [ $? -eq 0 ]; then - PYTHON_VERSION_GOOD="true" - else - echo "Failed to install Python and virtualenv" >&2 - exit 1 - fi - fi -} +#---------------------------------------------------------------------- +# IOTstack dependencies installed via apt +#---------------------------------------------------------------------- -function install_docker() { - if command_exists docker; then - echo "Docker already installed" >&2 - else - echo "Install Docker" >&2 - curl -fsSL https://get.docker.com | sh - sudo usermod -aG docker $USER - fi - - if command_exists docker-compose; then - echo "docker-compose already installed" >&2 - else - echo "Install docker-compose" >&2 - sudo apt install -y docker-compose - fi - - echo "" >&2 - echo "You should now restart your system" >&2 -} +echo -e "\nUpdating Advanced Package Tool (apt) caches" +sudo apt update -function update_docker() { - sudo apt upgrade docker docker-compose -} +echo -e "\nInstalling/updating IOTstack dependencies" +sudo apt install -y $APT_DEPENDENCIES -function do_python3_checks() { - VIRTUALENV_GOOD="false" - if command_exists virtualenv; then - VIRTUALENV_GOOD="true" - echo "Python virtualenv found." >&2 + +#---------------------------------------------------------------------- +# docker + compose installation +#---------------------------------------------------------------------- + +# is docker installed? +if [ -z $(which docker) ] ; then + # no! use the convenience script + echo -e "\nInstalling docker and docker-compose-plugin using the 'convenience script'" + echo "from https://get.docker.com ..." + curl -fsSL https://get.docker.com | sudo sh + if [ $? -eq 0 ] ; then + echo -e "\nInstallation of docker and docker-compose-plugin completed normally." + REBOOT_REQUIRED=true + else + echo -e "\nThe 'convenience script' returned an error. Unable to proceed." + handle_exit 1 fi - PYTHON_VERSION_GOOD="false" - if command_exists $PYTHON_CMD; then - PYTHON_VERSION=$($PYTHON_CMD --version 2>/dev/null) - PYTHON_VERSION_MAJOR=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d' ' -f 2 | cut -d'.' -f 1) - PYTHON_VERSION_MINOR=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 2) - PYTHON_VERSION_BUILD=$(echo "$PYTHON_VERSION"| cut -d' ' -f 2 | cut -d'.' -f 3) - - printf "Python Version: '${PYTHON_VERSION:-Unknown}'. " - if [ "$(minimum_version_check $REQ_PYTHON_VERSION $PYTHON_VERSION_MAJOR $PYTHON_VERSION_MINOR $PYTHON_VERSION_BUILD)" == "true" -a "$VIRTUALENV_GOOD" == "true" ]; then - PYTHON_VERSION_GOOD="true" - echo "Python and virtualenv is up to date." >&2 - else - echo "Python is outdated or virtualenv is missing" >&2 - install_python3_and_deps "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR.$PYTHON_VERSION_BUILD" "$VIRTUALENV_GOOD" - return 1 - fi +else + echo -e -n "\nDocker is already installed - checking your version - " + DOCKER_VERSION_INSTALLED="$(docker version -f "{{.Server.Version}}")" + if dpkg --compare-versions "$DOCKER_VERSION_MINIMUM" "gt" "$DOCKER_VERSION_INSTALLED" ; then + echo "fail" + echo "You have an obsolete version of Docker installed:" + echo " Minimum version required: $DOCKER_VERSION_MINIMUM" + echo " Version currently installed: $DOCKER_VERSION_INSTALLED" + echo "Try updating your system by running:" + echo " \$ sudo apt update && sudo apt upgrade -y" + echo " \$ docker version -f {{.Server.Version}}" + echo "If the version number changes, try re-running this script. If the" + echo "version number does not change, you may need to uninstall both" + echo "docker and docker-compose. If any containers are running, stop" + echo "them, then run:" + echo " \$ sudo systemctl stop docker.service" + echo " \$ sudo systemctl disable docker.service" + echo " \$ sudo apt -y purge docker-ce docker-ce-cli containerd.io docker-compose" + echo " \$ sudo apt -y autoremove" + echo " \$ sudo reboot" + echo "and then re-run this script after the reboot." + handle_exit 1 else - install_python3_and_deps - return 1 + echo "pass" fi +fi + + +#---------------------------------------------------------------------- +# group memberships +#---------------------------------------------------------------------- + +function should_add_user_to_group() +{ + # sense group does not exist + grep -q "^$1:" /etc/group || return 1 + # sense group exists and user is already a member + groups | grep -q "\b$1\b" && return 1 + # group exists, user should be added + return 0 } -function do_env_setup() { - echo "Setting up environment:" - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - sudo usermod -G "bluetooth" -a $USER +# check group membership +echo -e -n "\nChecking group memberships" +for GROUP in $DESIRED_GROUPS ; do + echo -n " - $GROUP " + if should_add_user_to_group $GROUP ; then + echo -n "adding $USER" + sudo /usr/sbin/usermod -G $GROUP -a $USER + LOGOUT_REQUIRED=true + else + echo -n "pass" fi +done +echo "" + +#---------------------------------------------------------------------- +# docker-compose setup/verification +#---------------------------------------------------------------------- + +# Correct installation of docker-compose is defined as the result of +# `which docker-compose` (typically $COMPOSE_SYMLINK_PATH) being a +# symlink pointing to the expected location of docker-compose-plugin as +# it is installed by the convenience script ($COMPOSE_PLUGIN_PATH). +# Alternatively, if `which docker-compose` returns null but the plugin +# is in the expected location, the necessary symlink can be created by +# this script and then docker-compose will be installed "correctly". + +function is_running_OS_release() { + unset VERSION_CODENAME + [ -f "/etc/os-release" ] && eval $(grep "^VERSION_CODENAME=" /etc/os-release) + [ "$VERSION_CODENAME" = "$1" ] && return 0 + return 1 +} - if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - sudo usermod -G "docker" -a $USER - fi +function is_python_script() { + [ $(file -b "$1" | grep -c "^Python script") -gt 0 ] && return 0 + return 1 } -function do_docker_checks() { - if command_exists docker; then - DOCKER_VERSION_GOOD="false" - DOCKER_VERSION=$(docker version -f "{{.Server.Version}}") - if [ -z "$DOCKER_VERSION" ]; then - echo "Error getting docker version. Error when running docker command. Check that docker is installed correctly." +# presume docker-compose not installed correctly +COMPOSE_INSTALLED_CORRECTLY=false + +# search for docker-compose in the PATH +COMPOSE_CMD_PATH=$(which docker-compose) + +# is docker-compose in the PATH? +echo -e -n "\nChecking whether docker-compose is installed correctly - " +if [ -n "$COMPOSE_CMD_PATH" ] ; then + # yes! is it a symlink and does the symlink point to a file? + if [ -L "$COMPOSE_CMD_PATH" -a -f "$COMPOSE_CMD_PATH" ] ; then + # yes! fetch the inode of what the link points to + COMPOSE_CMD_INODE=$(stat -c "%i" -L "$COMPOSE_CMD_PATH") + # does the plugin exist at the expected path? + if [ -f "$COMPOSE_PLUGIN_PATH" ] ; then + # yes! fetch the plugin's inode + COMPOSE_PLUGIN_INODE=$(stat -c "%i" "$COMPOSE_PLUGIN_PATH") + # are the inodes the same? + if [ $COMPOSE_CMD_INODE -eq $COMPOSE_PLUGIN_INODE ] ; then + # yes! thus docker-compose is installed correctly + COMPOSE_INSTALLED_CORRECTLY=true + fi fi - DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 1) - DOCKER_VERSION_MINOR=$(echo "$DOCKER_VERSION"| cut -d'.' -f 2) - DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION"| cut -d'.' -f 3) - DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION_BUILD"| cut -f1 -d"-") - DOCKER_VERSION_BUILD=$(echo "$DOCKER_VERSION_BUILD"| cut -f1 -d"+") - - if [ "$(minimum_version_check $REQ_DOCKER_VERSION $DOCKER_VERSION_MAJOR $DOCKER_VERSION_MINOR $DOCKER_VERSION_BUILD )" == "true" ]; then - [ -f .docker_outofdate ] && rm .docker_outofdate - DOCKER_VERSION_GOOD="true" - echo "Docker version $DOCKER_VERSION >= $REQ_DOCKER_VERSION. Docker is good to go." >&2 + fi +else + # no! does the plugin exist at the expected location? + if [ -f "$COMPOSE_PLUGIN_PATH" ] ; then + # yes! so, no command, but plugin present. Fix with symlink + sudo ln -s "$COMPOSE_PLUGIN_PATH" "$COMPOSE_SYMLINK_PATH" + # and now compose is installed correctly + COMPOSE_INSTALLED_CORRECTLY=true + else + echo "fail" + echo "Your system has docker installed but doesn't seem to have either" + echo "docker-compose or docker-compose-plugin. Try running:" + echo " \$ sudo apt install -y docker-compose-plugin" + echo "and then try re-running this script." + handle_exit 1 + fi +fi + +# is docker-compose installed correctly? +if [ "$COMPOSE_INSTALLED_CORRECTLY" = "true" ] ; then + echo "pass" + echo -e -n "\nChecking your version of docker-compose - " + COMPOSE_VERSION_INSTALLED="$(docker-compose version --short)" + if dpkg --compare-versions "$COMPOSE_VERSION_MINIMUM" "gt" "$COMPOSE_VERSION_INSTALLED" ; then + echo "fail" + echo "You have an obsolete version of docker-compose installed:" + echo " Minimum version required: $COMPOSE_VERSION_MINIMUM" + echo " Version currently installed: $COMPOSE_VERSION_INSTALLED" + echo "Try updating your system by running:" + echo " \$ sudo apt update && sudo apt upgrade -y" + echo "and then try re-running this script." + handle_exit 1 + else + echo "pass" + fi +else + echo "fail" + echo "docker-compose is not installed correctly. The most common reason is" + echo "having installed docker and docker-compose without using the official" + echo "'convenience script'. You may be able to solve this problem by running" + if is_python_script "$COMPOSE_CMD_PATH" ; then + if is_running_OS_release bookworm ; then + echo " \$ pip3 uninstall -y --break-system-packages docker-compose" + echo " \$ sudo pip3 uninstall -y --break-system-packages docker-compose" else - if [ "$NOASKCONFIRM" == "true" ]; then - update_docker - else - if [ ! -f .docker_outofdate ]; then - if (whiptail --title "Docker and Docker-Compose Version Issue" --yesno "Docker version is currently $DOCKER_VERSION which is less than $REQ_DOCKER_VERSION consider upgrading or you may experience issues. You will not be prompted again. You can manually upgrade by typing:\n sudo apt upgrade docker docker-compose\n\nAttempt to upgrade now?" 20 78); then - update_docker - else - touch .docker_outofdate - fi - fi - fi + echo " \$ pip3 uninstall -y docker-compose" + echo " \$ sudo pip3 uninstall -y docker-compose" fi + echo " (ignore any errors from those commands)" else - [ -f .docker_outofdate ] && rm .docker_outofdate - echo "Docker not installed" >&2 - if [ "$NOASKCONFIRM" == "true" ]; then - do_env_setup - install_docker - else - if [ ! -f .docker_notinstalled ]; then - if (whiptail --title "Docker and Docker-Compose" --yesno "Docker is not currently installed, and is required to run IOTstack. Would you like to install docker and docker-compose now?\nYou will not be prompted again." 20 78); then - [ -f .docker_notinstalled ] && rm .docker_notinstalled - do_env_setup - install_docker - else - touch .docker_notinstalled - fi - fi - fi + echo " \$ sudo apt purge -y docker-compose" fi -} - -function do_env_checks() { - GROUPSGOOD=0 + echo "and then try re-running this script." + handle_exit 1 +fi - if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - GROUPSGOOD=1 - echo "User is NOT in 'bluetooth' group" >&2 - fi - if [[ ! "$(user_in_group docker)" == "true" ]]; then - GROUPSGOOD=1 - echo "User is NOT in 'docker' group" >&2 - fi +#---------------------------------------------------------------------- +# Clone IOTstack repo +#---------------------------------------------------------------------- - if [ "$GROUPSGOOD" == 1 ]; then - echo "!! You might experience issues with docker or bluetooth. To fix run: ./menu.sh --run-env-setup" +# does the IOTstack folder already exist? +if [ ! -d "$IOTSTACK" ] ; then + # no! clone from GitHub + echo -e "\nCloning the IOTstack repository from GitHub" + git clone https://github.com/SensorsIot/IOTstack.git "$IOTSTACK" + if [ $? -eq 0 -a -d "$IOTSTACK" ] ; then + echo "IOTstack cloned successfully into $IOTSTACK" + mkdir -p "$IOTSTACK/backups" "$IOTSTACK/services" + else + echo "Unable to clone IOTstack (likely a git or network error)" + handle_exit 1 fi -} +else + echo -e "\n$IOTSTACK already exists - no need to clone from GitHub" +fi -function do_kernel_checks() { - if ! grep -q "cgroup_memory=1 cgroup_enable=memory" /boot/cmdline.txt; then - echo "Kernel cgroups not enabled. Adding kernel parameters." >&2 - echo "You will need to restart your system before the changes take effect." - echo $(cat /boot/cmdline.txt) cgroup_memory=1 cgroup_enable=memory | sudo tee /boot/cmdline.txt - fi -} +# initialise docker-compose global environment file with system timezone +if [ ! -f "$IOTSTACK_ENV" ] || [ $(grep -c "^TZ=" "$IOTSTACK_ENV") -eq 0 ] ; then + echo "TZ=$(cat /etc/timezone)" >>"$IOTSTACK_ENV" +fi -touch .new_install -echo "Enter in the sudo password when prompted, to install dependencies" +#---------------------------------------------------------------------- +# Python support +#---------------------------------------------------------------------- -sudo apt-get install git -y -git clone https://github.com/SensorsIot/IOTstack.git -cd IOTstack +# make sure "python" invokes "python3" +PYTHON_INVOKES=$(update-alternatives --list python 2>/dev/null) +PYTHON3_PATH=$(which python3) +if [ "$PYTHON_INVOKES" != "$PYTHON3_PATH" ] ; then + echo -e "\nMaking python3 the default" + sudo update-alternatives --install /usr/bin/python python "$PYTHON3_PATH" 1 +fi -if [ $? -eq 0 ]; then - echo "IOTstack cloned" +echo -e -n "\nChecking your version of Python - " +PYTHON_VERSION_INSTALLED="$(python --version)" +PYTHON_VERSION_INSTALLED="${PYTHON_VERSION_INSTALLED#*Python }" +if dpkg --compare-versions "$PYTHON_VERSION_MINIMUM" "gt" "$PYTHON_VERSION_INSTALLED" ; then + echo "fail" + echo "You have an obsolete version of python installed:" + echo " Minimum version required: $PYTHON_VERSION_MINIMUM" + echo " Version currently installed: $PYTHON_VERSION_INSTALLED" + echo "Try updating your system by running:" + echo " \$ sudo apt update && sudo apt upgrade -y" + echo " \$ python --version" + echo "If the version number changes, try re-running this script. If not, you" + echo "may need to reinstall python3-pip, python3-dev and python3-virtualenv." + handle_exit 1 else - echo "Could not find IOTstack directory" - exit 5 + echo "pass" fi -do_python3_checks -do_docker_checks -echo "Setting up environment:" -if [[ ! "$(user_in_group bluetooth)" == "notgroup" ]] && [[ ! "$(user_in_group bluetooth)" == "true" ]]; then - echo "User is NOT in 'bluetooth' group. Adding:" >&2 - echo "sudo usermod -G bluetooth -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "bluetooth" -a $USER +# implement menu requirements +if [ -e "$IOTSTACK_MENU_REQUIREMENTS" ] ; then + echo -e "\nChecking and updating IOTstack dependencies (pip)" + unset PYTHON_OPTIONS + if is_running_OS_release bookworm ; then + echo "Note: pip3 installs bypass externally-managed environment check" + PYTHON_OPTIONS="--break-system-packages" + fi + pip3 install -U $PYTHON_OPTIONS -r "$IOTSTACK_MENU_REQUIREMENTS" fi -if [ ! "$(user_in_group docker)" == "true" ]; then - echo "User is NOT in 'docker' group. Adding:" >&2 - echo "sudo usermod -G docker -a $USER" >&2 - echo "You will need to restart your system before the changes take effect." - sudo usermod -G "docker" -a $USER +# trigger re-creation of venv on next menu launch. Strictly speaking, +# sudo is not required for this but it protects against accidental prior +# use of sudo when the venv was created +sudo rm -rf "$IOTSTACK_MENU_VENV_DIR" + + +#---------------------------------------------------------------------- +# Raspberry Pi boot options +#---------------------------------------------------------------------- + +# set cmdline options (if possible - Raspberry Pi dependency) +TARGET="/boot/cmdline.txt" +if [ -e "$TARGET" ] ; then + echo -e -n "\nChecking Raspberry Pi boot-time options - " + unset APPEND + for OPTION in $CMDLINE_OPTIONS ; do + if [ $(grep -c "$OPTION" "$TARGET") -eq 0 ] ; then + APPEND="$APPEND $OPTION" + fi + done + if [ -n "$APPEND" ] ; then + echo "appending$APPEND" + sudo sed -i.bak "s/$/$APPEND/" "$TARGET" + REBOOT_REQUIRED=true + else + echo "no modifications needed" + fi fi -do_env_checks -do_kernel_checks + + +#---------------------------------------------------------------------- +# normal exit +#---------------------------------------------------------------------- + +echo "$SCRIPT completed." +handle_exit 0