#!/bin/bash # Copyright (C) 2015-2023 YunoHost # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # RUN INSTALL SCRIPT WITH -a FOR NON-INTERACTIVE MODE. set -u # Globals readonly YUNOHOST_LOG="/var/log/yunohost-installation_$(date +%Y%m%d_%H%M%S).log" export DEBIAN_FRONTEND=noninteractive ############################################################################### # Main functions # ############################################################################### function check_connection() { TIMEOUT=$1 while [ $TIMEOUT -gt 0 ]; do ping -c 1 -W 2 yunohost.org 2>&1 >/dev/null && return 0 sleep 1 TIMEOUT=$((TIMEOUT-1)) done return 1 } function usage() { cat << EOF Usage : $(basename $0) [-a] [-d ] [-h] Options : -a Enable automatic mode. No questions are asked. This does not perform the post-install step. -d Choose the distribution to install ('stable', 'testing', 'unstable'). Defaults to 'stable' -f Ignore checks before starting the installation. Use only if you know what you are doing. -h Prints this help and exit EOF } function parse_options() { AUTOMODE=false DISTRIB=stable BUILD_IMAGE=false FORCE=false while getopts ":aid:fh" option; do case $option in a) AUTOMODE=true export DEBIAN_FRONTEND=noninteractive ;; d) DISTRIB=$OPTARG ;; f) FORCE=true ;; i) # This hidden option will allow to build generic image for Rpi/Olimex BUILD_IMAGE=true ;; h) usage exit 0 ;; :) usage exit 1 ;; \?) usage exit 1 ;; esac done } function main() { parse_options "$@" check_assertions || exit 1 confirm_installation || exit 1 upgrade_system || die "Failed to upgrade the system" boring_workarounds || die "Failed to run the boring workarounds" setup_package_source || die "Setting up deb package sources failed" install_yunohost_packages || die "Installation of Yunohost packages failed" # For some reason sometimes dbus is not properly started/enabled ... if [[ "$BUILD_IMAGE" == "false" ]] ; then systemctl is-active dbus >/dev/null || systemctl enable dbus --now fi if [[ "$BUILD_IMAGE" == "true" ]] ; then clean_image || die "Unable to clean image" fi if is_raspbian ; then # FIXME : add a proper conclusion + timer warning? # Reboot should be done before postinstall to be able to run iptables rules reboot fi conclusion exit 0 } ############################################################################### # Helpers # ############################################################################### readonly normal=$(printf '\033[0m') readonly bold=$(printf '\033[1m') readonly faint=$(printf '\033[2m') readonly underline=$(printf '\033[4m') readonly negative=$(printf '\033[7m') readonly red=$(printf '\033[31m') readonly green=$(printf '\033[32m') readonly orange=$(printf '\033[33m') readonly blue=$(printf '\033[34m') readonly yellow=$(printf '\033[93m') readonly white=$(printf '\033[39m') readonly resetline=$(printf '\r\033[K') function success() { local msg=${1} echo "[${bold}${green} OK ${normal}] ${msg}" | tee -a $YUNOHOST_LOG } function info() { local msg=${1} echo "[${bold}${blue}INFO${normal}] ${msg}" | tee -a $YUNOHOST_LOG } function warn() { local msg=${1} echo "[${bold}${orange}WARN${normal}] ${msg}" | tee -a $YUNOHOST_LOG >&2 } function error() { local msg=${1} echo "[${bold}${red}FAIL${normal}] ${msg}" | tee -a $YUNOHOST_LOG >&2 } function die() { error "$1" info "Installation logs are available in $YUNOHOST_LOG" exit 1 } trap trapint 2 function trapint { echo "" die "Aborted" exit 0 } function show_apt_progress { local percent="$1" local title="$2" local message="$3" local done=$((${percent%.*}*40/100)) local todo=$((39 - $done)) local done_sub_bar="$(printf "%${done}s")" local todo_sub_bar="$(printf "%${todo}s")" echo -ne "$resetline $bold$blue$title$normal [${done_sub_bar// /=}>${todo_sub_bar}] ${percent:0:4}% ${message:0:40}" } function _apt() { set -o pipefail echo "===================" >>$YUNOHOST_LOG echo "Running: apt-get $*" >>$YUNOHOST_LOG echo "===================" >>$YUNOHOST_LOG if [[ "$AUTOMODE" == "false" ]]; then local title="" apt-get $* -o 'APT::Status-Fd=3' 3>&1 >>$YUNOHOST_LOG 2>&1 \ | while read line; do local wat=$(echo $line | cut -d: -f1) local percent=$(echo $line | cut -d: -f3) local message=$(echo $line | cut -d: -f2) [[ $wat == "dlstatus" ]] && local title="Downloading" || local title="Installing" show_apt_progress $percent "$title" "$message"; done \ && printf "$resetline $bold${green}Done$normal" \ || { printf "$resetline $bold${red}'apt-get $*' failed.$normal Please check $YUNOHOST_LOG for debugging\n\n"; return 1; } else # Why we need pipefail : https://stackoverflow.com/a/6872163 apt-get $* 2>&1 | tee -a $YUNOHOST_LOG || return 1 fi set +o pipefail } function apt_update() { _apt update --allow-releaseinfo-change } function apt_install() { _apt install --assume-yes -o Dpkg::Options::="--force-confold" $* } ############################################################################### # Installation steps # ############################################################################### function check_assertions() { if [[ $DISTRIB == "stable" ]] then error "Only unstable and testing branches are supported for Bookworm right now. We ABSOLUTELY DISCOURAGE using YunoHost Bookworm in any sort of production setup right now UNLESS YOU ARE A POWER-USER. Everything is in BETA STAGE ONLY." return 1 fi # Assert we're on Debian # Note : we do not rely on lsb_release to avoid installing a dependency # only to check this... [[ -f "/etc/debian_version" ]] || { error "This script can only be ran on Debian 12 (Bookworm)."; return 1; } # Assert we're on Bookworm # Note : we do not rely on lsb_release to avoid installing a dependency # only to check this... # TODO: remove the line with "bookworm/sid" [[ "$(cat /etc/debian_version)" =~ ^12.* ]] \ || [[ "$(cat /etc/debian_version)" =~ "bookworm/sid" ]] \ || { error "YunoHost is only available for the version 12 (Bookworm) of Debian, you are using '$(cat /etc/debian_version)'."; return 1; } # Forbid people from installing on Ubuntu or Linux mint ... if [[ -f "/etc/lsb-release" ]]; then if cat /etc/lsb-release | grep -q -i "Ubuntu\|Mint" then error "Please don't try to install YunoHost on an Ubuntu or Linux Mint system ... You need a 'raw' Debian 12 (Bookworm)." return 1 fi fi # Assert we're root [[ "$(id -u)" == "0" ]] || { error "This script must be run as root. On most setups, the command 'sudo -i' can be run first to become root."; return 1; } # Assert Internet is reachable if ! check_connection 30; then die "You need internet to use this script! yunohost.org did not respond to ping after more than 30s." fi # Assert curl is setup if ! command -v curl 2>&1 >/dev/null; then apt_install curl || { error "Yunohost installer requires curl to be installed, but it failed to install it."; return 1; } fi # Check PATH var [[ "$PATH" == *"/sbin"* ]] || { error "Your environment PATH variable must contains /sbin directory. Maybe try running 'PATH=/sbin:\$PATH' to fix this."; return 1; } # Assert systemd is installed command -v systemctl > /dev/null || { error "YunoHost requires systemd to be installed."; return 1; } # Check that kernel is >= 3.12, otherwise systemd won't work properly. Cf. https://github.com/systemd/systemd/issues/5236#issuecomment-277779394 dpkg --compare-versions "$(uname -r)" "ge" "3.12" || { error "YunoHost requires a kernel >= 3.12. Please consult your hardware documentation or VPS provider to learn how to upgrade your kernel."; return 1; } # Check we aren't running in docker or other weird containers that we can't probably install on systemd-detect-virt | grep -v -q -w "docker\|container-other" || [[ "$FORCE" == "true" ]] \ || { error "It seems like you are trying to install YunoHost in docker or a weird container technology which probably is not supported by this install script (or YunoHost as a whole). If you know what you are doing, you can run this script with -f."; return 1; } # Check possible conflict with apache, bind9. [[ -z "$(dpkg --get-selections | grep -v deinstall | grep 'bind9\s')" ]] || [[ "$FORCE" == "true" ]] \ || { error "Bind9 is installed on your system. Yunohost conflicts with Bind9 because it requires dnsmasq. To be able to run this script, you should first run 'apt remove bind9 --purge --autoremove'."; return 1; } [[ -z "$(dpkg --get-selections | grep -v deinstall | grep 'apache2\s')" ]] || [[ "$FORCE" == "true" ]] \ || { error "Apache is installed on your system. Yunohost conflicts with apache2 because it requires nginx. To be able to run this script, you should first run 'apt remove apache2 --purge --autoremove'."; return 1; } } function confirm_installation() { [[ "$AUTOMODE" == "true" ]] && return 0 cat << EOF | tee -a $YUNOHOST_LOG $bold ╭───────────────────────╮ │ YunoHost Installation │ ╰───────────────────────╯ $normal • Installing YunoHost requires to install various important services, and possibly rework the configuration of some services that may already be installed (such as: nginx, postfix, dovecot, fail2ban, slapd) EOF read -p " Are you sure you want to proceed (y/n) ? " choice < /dev/tty choice="$(echo $choice | tr '[A-Z]' '[a-z]')" [[ "$choice" == "yes" ]] || [[ "$choice" == "y" ]] || { error "Aborting"; return 1; } if [[ "$DISTRIB" == "unstable" ]] then cat << EOF | tee -a $YUNOHOST_LOG • You are installing the unstable/alpha version of YunoHost 12/Bookworm. You should be warned that THIS IS ALPHA-STAGE DEVELOPMENT. WE ABSOLUTELY DISCOURAGE ANY USE OF THIS VERSION IN A PRODUCTION CONTEXT, THIS IS ONLY MEANT FOR *TESTING*. THINGS **WILL** BREAK. EOF read -p " Type 'Yes, I understand' if you understand: " choice < /dev/tty [[ "$choice" == "Yes, I understand" ]] || { error "Aborting"; return 1; } fi # SSH config warning if [[ -f /etc/ssh/sshd_config ]] then # If root login is currently enabled local root_login_warning="" if ! grep -E "^[[:blank:]]*PermitRootLogin[[:blank:]]+no" /etc/ssh/sshd_config ; then root_login_warning=" • SSH login using root will be disabled (except from local network).\n" root_login_warning+=" Instead, you should login using the first YunoHost user." fi # If current conf uses a custom ssh port local ssh_port_warning="" if grep -Ev "^[[:blank:]]*Port[[:blank:]]+22[[:blank:]]*(#.*)?$" /etc/ssh/sshd_config | grep -E "^[[:blank:]]*Port[[:blank:]]+[[:digit:]]+$" ; then ssh_port_warning=" • You will have to connect using port 22 instead of your custom SSH port,\n" ssh_port_warning+=" though you can reconfigure this from YunoHost after the postinstall." fi if [[ -n "$root_login_warning" ]] || [[ -n "$ssh_port_warning" ]] then cat << EOF | tee -a $YUNOHOST_LOG • Additionally, it is encouraged to let YunoHost manage the SSH configuration. However, you should be aware that: $(test -n "$root_login_warning" && echo -e "$root_login_warning") $(test -n "$ssh_port_warning" && echo -e "$ssh_port_warning") (Note that this will only be effective *after* you run YunoHost's postinstall) EOF read -p " Should YunoHost override the SSH configuration (y/n) ? " choice < /dev/tty choice="$(echo $choice | tr '[A-Z]' '[a-z]')" if [[ "$choice" != "yes" ]] && [[ "$choice" != "y" ]] then # Keep a copy to be restored during the postinstall # so that the ssh confs behaves as manually modified. cp /etc/ssh/sshd_config /etc/ssh/sshd_config.before_yunohost fi fi fi cat << EOF | tee -a $YUNOHOST_LOG 🚀 ${bold}Let's go !$normal 📜 Detailed logs will be available in $YUNOHOST_LOG EOF return 0 } function upgrade_system() { echo "" | tee -a $YUNOHOST_LOG echo "$bold 1/5 • Running system upgrades$normal" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG apt_update || return 1 # We need libtext-iconv-perl even before the dist-upgrade, # otherwise the dist-upgrade might fails on some setups because # perl is yolomacnuggets :| # Stuff like "Can't locate object method "new" via package "Text::Iconv"" apt_install libtext-iconv-perl || return 1 # Manually upgrade grub stuff in non-interactive mode, # otherwise a weird technical question is asked to the user # regarding how to upgrade grub's configuration... apt_install --only-upgrade grub-common grub2-common || true _apt dist-upgrade -y -o Dpkg::Options::="--force-confold" || return 1 if is_raspbian ; then apt_install rpi-update || return 1 if [[ "$BUILD_IMAGE" == "false" ]] ; then (rpi-update 2>&1 | tee -a $YUNOHOST_LOG) || return 1 fi fi } function boring_workarounds() { echo "" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG echo "$bold 2/5 • Install dependencies needed before the main install$normal" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG # ###################################################################### # # Dependencies that must be installed prior to the rest, for reasons ... # # (for example https://github.com/YunoHost/issues/issues/1382) # # ###################################################################### # apt_install --no-install-recommends lsb-release dialog curl gnupg apt-transport-https adduser debconf debhelper dh-autoreconf locales echo "" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG echo "$bold 3/5 • Apply various tweaks to prepare installation$normal" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG # #################################### # # Attempt to fix the usual locale mess # # #################################### # # This function tries to fix the whole locale and perl mess about missing locale files # Generate at least en_US.UTF-8 grep -q "^ *en_US.UTF-8" /etc/locale.gen || echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen # FIXME: here some day we should try to identify the user's lang from LANG or LC_ALL and generate the appropriate locale ... # (and set this lang as the default in /etc/env 3 lines below) locale-gen >/dev/null # If no /etc/environment exists, default to en_US.UTF-8 grep -q LC_ALL /etc/environment || echo 'LC_ALL="en_US.UTF-8"' >> /etc/environment source /etc/environment export LC_ALL # ######################## # # Workarounds for fail2ban # # ######################## # # We need to create auth.log in case it does not exists, because in some situation, # this file does not exists, fail2ban will miserably fail to start because # the default fail2ban jail include the sshd jail ... >.> touch /var/log/auth.log # ######################## # # Workarounds for avahi # # ######################## # # When attempting several installation of Yunohost on the same host # with a light VM system like LXC # we hit a bug with avahi-daemon postinstallation # This is described in detail in https://github.com/lxc/lxc/issues/25 # # It makes the configure step of avahi-daemon fail, because the service does # start correctly. Then all other packages depending on avahi-daemon refuse to # configure themselves. # # The workaround we use is to generate a random uid for the avahi user, and # create the user with this id beforehand, so that the avahi-daemon postinst # script does not do it on its own. Our randomized uid has far less chances to # be already in use in another system than the automated one (which tries to use # consecutive uids). # Return without error if avahi already exists if ! id avahi > /dev/null 2>&1; then # Get a random unused uid between 500 and 999 (system-user) local avahi_id=$((500 + RANDOM % 500)) while cut -d ':' -f 3 /etc/passwd | grep -q $avahi_id ; do avahi_id=$((500 + RANDOM % 500)) done #info "Workaround for avahi : creating avahi user with uid $avahi_id" # Use the same adduser parameter as in the avahi-daemon postinst script # Just specify --uid explicitely adduser --disabled-password --quiet --system \ --home /var/run/avahi-daemon --no-create-home \ --gecos "Avahi mDNS daemon" --group avahi \ --uid $avahi_id fi # ########## # # Resolvconf # # ########## # # On some machines (e.g. OVH VPS), the /etc/resolv.conf is immutable # We need to make it mutable for the resolvconf dependency to be installed chattr -i /etc/resolv.conf 2>/dev/null || true # Done printf "$resetline $bold${green}Done$normal" } function setup_package_source() { echo "" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG echo "$bold 4/5 • Adding YunoHost repository to apt$normal" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG local CUSTOMAPT=/etc/apt/sources.list.d/yunohost.list # Debian repository local CUSTOMDEB="deb [signed-by=/usr/share/keyrings/yunohost-bookworm.gpg] http://forge.yunohost.org/debian/ bookworm stable" if [[ "$DISTRIB" == "stable" ]] ; then echo "$CUSTOMDEB" > $CUSTOMAPT elif [[ "$DISTRIB" == "testing" ]] ; then echo "$CUSTOMDEB testing" > $CUSTOMAPT elif [[ "$DISTRIB" == "unstable" ]] ; then echo "$CUSTOMDEB testing unstable" > $CUSTOMAPT fi # Add YunoHost repository key to the keyring curl --fail --silent https://forge.yunohost.org/yunohost_bookworm.asc | gpg --dearmor > /usr/share/keyrings/yunohost-bookworm.gpg apt_update } function install_yunohost_packages() { echo "" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG echo "$bold 5/5 • Installing YunoHost$normal" | tee -a $YUNOHOST_LOG echo "" | tee -a $YUNOHOST_LOG debconf-set-selections << EOF slapd slapd/password1 password yunohost slapd slapd/password2 password yunohost slapd slapd/domain string yunohost.org slapd shared/organization string yunohost.org slapd slapd/allow_ldap_v2 boolean false slapd slapd/invalid_config boolean true slapd slapd/backend select MDB postfix postfix/main_mailer_type select Internet Site postfix postfix/mailname string /etc/mailname nslcd nslcd/ldap-bindpw password nslcd nslcd/ldap-starttls boolean false nslcd nslcd/ldap-reqcert select nslcd nslcd/ldap-uris string ldap://localhost/ nslcd nslcd/ldap-binddn string nslcd nslcd/ldap-base string dc=yunohost,dc=org libnss-ldapd libnss-ldapd/nsswitch multiselect group, passwd, shadow postsrsd postsrsd/domain string yunohost.org EOF # Allow sudo removal even if no root password has been set (on some DO # droplet or Vagrant virtual machines), as YunoHost use sudo-ldap export SUDO_FORCE_REMOVE=yes # Install YunoHost # FIXME : do we still want to install recommends ? apt_install \ -o APT::install-recommends=true \ yunohost yunohost-admin postfix \ || return 1 } function conclusion() { # Get first local IP and global IP local local_ip=$(hostname --all-ip-address | tr ' ' '\n' | grep -v ":" | head -n1) local global_ip=$(curl https://ip.yunohost.org 2>/dev/null) local no_ip="" # Will ignore local ip if it's already the global IP (e.g. for some VPS) [[ "$local_ip" != "$global_ip" ]] || local_ip="" # Formatting local width=79 [[ -z "$local_ip" ]] || { local_ip=$(echo -e "\n │ - https://$local_ip/ (local IP, if self-hosting at home)") local nb_spaces=$(( $width - ${#local_ip} )) local_ip+="$(printf "%${nb_spaces}s")│" } [[ -z "$global_ip" ]] || { global_ip=$(echo -e "\n │ - https://$global_ip/ (global IP, if you're on a VPS)") local nb_spaces=$(( $width - ${#global_ip} )) global_ip+="$(printf "%${nb_spaces}s")│" } [[ -n "$local_ip" ]] || [[ -n "$global_ip" ]] || { no_ip=$(echo -e "\n │ - (no local nor global IP detected ?)") local nb_spaces=$(( $width - ${#no_ip} )) no_ip+="$(printf "%${nb_spaces}s")│" } cat << EOF | tee -a $YUNOHOST_LOG 🎉 ${bold}YunoHost installation completed!$normal ╭───────────────────────────────────────────────────────────────────────────╮ │ You should now proceed with Yunohost post-installation. │ │ This is where you will be asked for: │ │ • the main domain of your server ; │ │ • the administration password ; │ │ • the name and password of the first user, which will also be admin. │ │ │ │ You can perform this step, either: │ │ • from the command line, by running 'yunohost tools postinstall' as root │ │ • or from your web browser, by accessing : │${local_ip}${global_ip}${no_ip} │ │ │ If this is your first time with YunoHost, it is strongly recommended to │ │ take time to read the administator documentation and in particular the │ │ sections 'Finalizing your setup' and 'Getting to know YunoHost'. │ │ │ │ It is available at the following URL : ➡️ https://yunohost.org/admindoc │ ╰───────────────────────────────────────────────────────────────────────────╯ EOF } ############################################################################### # Raspbian specific stuff # ############################################################################### function is_raspbian() { # On Raspbian image lsb_release is available if [[ "$(lsb_release -i -s 2> /dev/null)" != "Raspbian" ]] ; then return 1 fi return 0 } ############################################################################### # Image building specific stuff # ############################################################################### function clean_image() { # Delete SSH keys rm -f /etc/ssh/ssh_host_* >> $YUNOHOST_LOG 2>&1 yes | ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa >> $YUNOHOST_LOG 2>&1 yes | ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa >> $YUNOHOST_LOG 2>&1 yes | ssh-keygen -f /etc/ssh/ssh_host_ecdsa_key -N '' -t ecdsa -b 521 >> $YUNOHOST_LOG 2>&1 # Deleting logs ... find /var/log -type f -exec rm {} \; >> $YUNOHOST_LOG 2>&1 # Purging apt ... apt-get clean >> $YUNOHOST_LOG 2>&1 } ############################################################################### main "$@"