Anterior-IaC/uisp/install-full.sh
2025-03-02 00:53:37 -06:00

1496 lines
50 KiB
Bash
Executable File

#!/usr/bin/env bash
set -o nounset
set -o errexit
set -o pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PATH="${PATH}:/usr/local/bin"
TMP_INSTALL_DIR="${SCRIPT_DIR}"
# prerequisites "command|package"
PREREQUISITES=(
"curl|curl"
"sed|sed"
"envsubst|gettext-base"
"useradd|passwd"
)
COMPOSE_PROJECT_NAME="unms"
USERNAME="${UNMS_USER:-unms}"
if getent passwd "${USERNAME}" >/dev/null; then
HOME_DIR="$(getent passwd "${USERNAME}" | cut -d: -f6)"
else
HOME_DIR="${UNMS_HOME_DIR:-"/home/${USERNAME}"}"
fi
# files and directoris
export APP_DIR="${HOME_DIR}/app"
export DATA_DIR="${HOME_DIR}/data"
export RESTORE_DIR="${DATA_DIR}/unms-backups/restore"
export CONFIG_DIR="${APP_DIR}/conf"
export CONFIG_FILE="${APP_DIR}/unms.conf"
export METADATA_FILE="${APP_DIR}/metadata"
export DOCKER_COMPOSE_INSTALL_PATH="/usr/local/bin"
export DOCKER_COMPOSE_FILENAME="docker-compose.yml"
export DOCKER_COMPOSE_PATH="${APP_DIR}/${DOCKER_COMPOSE_FILENAME}"
export DOCKER_COMPOSE_TEMPLATE_FILENAME="docker-compose.yml.template"
export DOCKER_COMPOSE_TEMPLATE_PATH="${APP_DIR}/${DOCKER_COMPOSE_TEMPLATE_FILENAME}"
if [ "${SCRIPT_DIR}" = "${APP_DIR}" ]; then
echo >&2 "Please don't run the installation script in the application directory ${APP_DIR}"
exit 1
fi
# NMS variables
export UNMS_HTTP_PORT="8081"
export UNMS_WS_PORT="8082"
export UNMS_WS_SHELL_PORT="8083"
export UNMS_WS_API_PORT="8084"
export UNMS_POSTGRES_USER="unms"
export UNMS_POSTGRES_DB="unms"
export UNMS_POSTGRES_SCHEMA="unms"
export UNMS_POSTGRES_PASSWORD="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 48 | head -n 1 || true)"
export UNMS_TOKEN="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 48 | head -n 1 || true)"
export UNMS_CLI_TOKEN="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 48 | head -n 1 || true)"
export UNMS_DEPLOYMENT=""
export UNMS_VERSION="$(grep "^version=" "${SCRIPT_DIR}/metadata" | sed 's/version=//')"
export UNMS_IP_WHITELIST=""
export UNMS_FEATURES=""
export ALTERNATIVE_HTTP_PORT="8080"
export ALTERNATIVE_HTTPS_PORT="8443"
export ALTERNATIVE_SUSPEND_PORT="8081"
export ALTERNATIVE_NETFLOW_PORT="2056"
export ALTERNATIVE_REMOTE_UI_WS_PORT_PUBLIC="8189";
export SECURE_LINK_SECRET="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 100 | head -n 1 || true)"
export CLOUD_API_URL=""
export CLOUD_PORTAL_URL=""
export REMOTE_UI_WS_PORT_PUBLIC="8089";
# CRM variables
export UCRM_DOCKER_IMAGE="ubnt/unms-crm"
export UCRM_VERSION="4.4.30"
export UCRM_POSTGRES_PASSWORD="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 48 | head -n 1 || true)"
export UCRM_SECRET="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 48 | head -n 1 || true)"
export UCRM_USER="${USERNAME}"
export UCRM_POSTGRES_USER="ucrm"
export UCRM_POSTGRES_DB="unms"
export UCRM_POSTGRES_SCHEMA="ucrm"
export UCRM_MAILER_ADDRESS="127.1.0.1"
export UCRM_MAILER_USERNAME="username"
export UCRM_MAILER_PASSWORD="password"
export NODE_ENV="production"
# other variables
export HTTP_PORT="80"
export HTTPS_PORT="443"
export SUSPEND_PORT="81"
export WS_PORT=""
export PROXY_HTTPS_PORT=""
export PROXY_WS_PORT=""
export NETFLOW_PORT="2055"
export VERSION="latest"
export DEMO="false"
export USE_LOCAL_IMAGES="false"
export DOCKER_IMAGE="ubnt/unms"
export DOCKER_REGISTRY="docker.io"
export DOCKER_USERNAME=""
export DOCKER_PASSWORD=""
export DOCKER_VERSION=""
export DOCKER_MIN_VERSION="20.10.5"
export DOCKER_STRICT_VERSION="27.3.1"
export DOCKER_COMPOSE_COMMAND="docker compose"
export DOCKER_COMPOSE_PROJECT_NAME="unms"
export DOCKER_COMPOSE_VERSION=""
export SSL_CERT_DIR=""
export SSL_CERT=""
export SSL_CERT_KEY=""
export SSL_CERT_CA=""
export HOST_TAG=""
export UNATTENDED="false"
export UPDATING="false"
export NO_AUTO_UPDATE="false"
export START_CONTAINERS="true"
export USE_LOCAL_DISCOVERY="true"
export USE_ALTERNATIVE_CERT_DIR="false"
export BRANCH="master"
export SUBNET="172.18.251.0/24"
export BRIDGE_NAME_PREFIX=""
export CLUSTER_SIZE="auto"
export IPAM_PUBLIC=""
export IPAM_PRIVATE=""
export NET_DRIVER_OPTS_PUBLIC=""
export NET_DRIVER_OPTS_PRIVATE=""
export CERT_DIR_MAPPING_NGINX=""
export USERCERT_DIR_MAPPING_NGINX=""
export RUN_VACUUM="true"
export POSTGRES_USER="postgres"
export POSTGRES_PASSWORD="$(LC_CTYPE=C tr -dc "a-zA-Z0-9" < /dev/urandom | fold -w 48 | head -n 1 || true)"
export CURRENT_VERSION="$(cat "${METADATA_FILE}" 2>/dev/null | grep '^version=' | sed 's/version=//'|| true)"
export MIN_CURRENT_VERSION="1.3.0"
export DELETE_CRM_DATA="false"
if [ -f /proc/ubnthal/system.info ]; then
export SUBSYSTEM_ID="$(grep systemid /proc/ubnthal/system.info | sed 's/systemid=//')"
export SERVER_MAC="$(grep eth0.macaddr /proc/ubnthal/system.info | sed 's/eth0.macaddr=//')"
else
export SUBSYSTEM_ID=
export SERVER_MAC=
fi
export ENV_DIR=
export ENV_FILES_UNMS=
export ENV_FILES_UCRM=
export ENV_FILES_NETFLOW=
cleanup() {
# Cleanup temp install dir.
if [ "${TMP_INSTALL_DIR}" != "${SCRIPT_DIR}" ] ; then
rm -rf "${TMP_INSTALL_DIR}" || true;
fi
}
fail() {
echo -e "ERROR: $1" >&2
cleanup || true;
exit 1
}
read_previous_config() {
# read WS port settings from existing running container
# they were not saved to config file in versions <=0.7.18
if ! oldEnv="$(docker inspect --format '{{ .Config.Env }}' unms)"; then
echo "Couldn't read WS port config from existing UISP container"
else
WS_PORT="$(docker ps --filter "name=unms$" --filter "status=running" --format "{{.Ports}}" | sed -E "s/.*0.0.0.0:([0-9]+)->8444.*|.*/\1/")"
echo "Setting WS_PORT=${WS_PORT}"
PROXY_WS_PORT="$(echo "${oldEnv}" | sed -E "s/.*[ []PUBLIC_WS_PORT=([0-9]*).*|.*/\1/")"
echo "Setting PROXY_WS_PORT=${PROXY_WS_PORT}"
fi
# read config file
if [ -f "${CONFIG_FILE}" ]; then
echo "Reading configuration file ${CONFIG_FILE}"
sed -i -E 's/^SERVER_MAC="([^"]*)$/SERVER_MAC="\1"/' "${CONFIG_FILE}" 2> /dev/null # fix for corrupted config in 1.4.0-beta.8
if ! source "${CONFIG_FILE}"; then
echo >&2 "Failed to read configuration from ${CONFIG_FILE}"
exit 1
fi
else
echo "Configuration file not found."
fi
UCRM_POSTGRES_DB="${UNMS_POSTGRES_DB}"
}
# parse arguments
while [[ "$#" -gt 0 ]]; do
case "$1" in
--demo)
echo "Setting DEMO=true"
DEMO="true"
;;
--update)
echo "Restoring previous configuration"
read_previous_config
UPDATING="true"
;;
--unattended)
echo "Setting UNATTENDED=true"
UNATTENDED="true"
;;
--no-auto-update)
echo "Setting NO_AUTO_UPDATE=true"
NO_AUTO_UPDATE="true"
;;
--no-local-discovery)
echo "Setting USE_LOCAL_DISCOVERY=false"
USE_LOCAL_DISCOVERY="false"
;;
--no-start-containers)
echo "Setting START_CONTAINERS=false"
START_CONTAINERS="false"
;;
--no-vacuum)
echo "Setting RUN_VACUUM=false"
RUN_VACUUM="false"
;;
--ucrm-version)
echo "Setting UCRM_VERSION=$2"
UCRM_VERSION="$2"
;;
--ucrm-docker-image)
echo "Setting UCRM_DOCKER_IMAGE=$2"
UCRM_DOCKER_IMAGE="$2"
shift # past argument value
;;
--use-local-images)
echo "Setting USE_LOCAL_IMAGES=true"
USE_LOCAL_IMAGES="true"
;;
--use-alt-cert-dir)
echo "Setting USE_ALTERNATIVE_CERT_DIR=true"
USE_ALTERNATIVE_CERT_DIR="true"
;;
-v|--version)
echo "Setting VERSION=$2"
VERSION="$2"
shift # past argument value
;;
--docker-image)
echo "Setting DOCKER_IMAGE=$2"
DOCKER_IMAGE="$2"
shift # past argument value
;;
--docker-registry)
echo "Setting DOCKER_REGISTRY=$2"
DOCKER_REGISTRY="$2"
shift # past argument value
;;
--docker-username)
echo "Setting DOCKER_USERNAME=$2"
DOCKER_USERNAME="$2"
shift # past argument value
;;
--docker-password)
echo "Setting DOCKER_PASSWORD=*****"
DOCKER_PASSWORD="$2"
shift # past argument value
;;
--data-dir)
echo "Setting DATA_DIR=$2"
DATA_DIR="$2"
shift # past argument value
;;
--http-port)
echo "Setting HTTP_PORT=$2"
HTTP_PORT="$2"
shift # past argument value
;;
--https-port)
echo "Setting HTTPS_PORT=$2"
HTTPS_PORT="$2"
shift # past argument value
;;
--suspend-port)
echo "Setting SUSPEND_PORT=$2"
SUSPEND_PORT="$2"
shift # past argument value
;;
--ws-port)
echo "Setting WS_PORT=$2"
WS_PORT="$2"
shift # past argument value
;;
--public-https-port)
echo "Setting PROXY_HTTPS_PORT=$2"
PROXY_HTTPS_PORT="$2"
shift # past argument value
;;
--public-ws-port)
echo "Setting PROXY_WS_PORT=$2"
PROXY_WS_PORT="$2"
shift # past argument value
;;
--netflow-port)
echo "Setting NETFLOW_PORT=$2"
NETFLOW_PORT="$2"
shift # past argument value
;;
--public-remote-ui-ws-port)
echo "Setting REMOTE_UI_WS_PORT_PUBLIC=$2"
REMOTE_UI_WS_PORT_PUBLIC="$2"
shift # past argument value
;;
--ssl-cert-dir)
echo "Setting SSL_CERT_DIR=$2"
SSL_CERT_DIR="$2"
shift # past argument value
;;
--ssl-cert)
echo "Setting SSL_CERT=$2"
SSL_CERT="$2"
shift # past argument value
;;
--ssl-cert-key)
echo "Setting SSL_CERT_KEY=$2"
SSL_CERT_KEY="$2"
shift # past argument value
;;
--ssl-cert-ca)
echo "Setting SSL_CERT_CA=$2"
SSL_CERT_CA="$2"
shift # past argument value
;;
--host-tag)
echo "Setting HOST_TAG=$2"
HOST_TAG="$2"
shift # past argument value
;;
--branch)
echo "Setting BRANCH=$2"
BRANCH="$2"
shift # past argument value
;;
--subnet)
echo "Setting SUBNET=$2"
SUBNET="$2"
shift # past argument value
;;
--bridge-name-prefix)
echo "Setting BRIDGE_NAME_PREFIX=$2"
BRIDGE_NAME_PREFIX="$2"
shift # past argument value
;;
--node-env)
echo "Setting NODE_ENV=$2"
NODE_ENV="$2"
shift # past argument value
;;
--workers)
echo "Setting CLUSTER_SIZE=$2"
[[ "${2}" =~ ^[1-9]$|^[1-4][0-9]$|^50$ ]] || [[ "${2}" = "auto" ]] || fail "--workers argument must be a number in range 1-50 or 'auto'."
CLUSTER_SIZE="$2"
shift # past argument value
;;
--deployment)
echo "Setting UNMS_DEPLOYMENT=$2"
UNMS_DEPLOYMENT=$2
shift # past argument value
;;
--ip-whitelist)
echo "Setting UNMS_IP_WHITELIST=$2"
UNMS_IP_WHITELIST=$2
shift # past argument value
;;
--env-dir)
echo "Setting ENV_DIR=$2"
ENV_DIR=$2
shift # past argument value
;;
--features)
echo "Setting UNMS_FEATURES=$2"
UNMS_FEATURES=$2
shift # past argument value
;;
--cloud-api-url)
echo "Setting CLOUD_API_URL=$2"
CLOUD_API_URL=$2
shift # past argument value
;;
--cloud-portal-url)
echo "Setting CLOUD_PORTAL_URL=$2"
CLOUD_PORTAL_URL=$2
shift # past argument value
;;
*)
# unknown option
;;
esac
shift # past argument key
done
# '+' symbol is not allowed in docker tags, replace with '-'
export DOCKER_TAG=${VERSION//+/-}
export UCRM_DOCKER_TAG=${UCRM_VERSION//+/-}
# check that none or all three SSL variables are set
if [ ! -z "${SSL_CERT_DIR}" ] || [ ! -z "${SSL_CERT}" ] || [ ! -z "${SSL_CERT_KEY}" ]; then
if [ -z "${SSL_CERT_DIR}" ]; then echo >&2 "Please set --ssl-cert-dir"; exit 1; fi
if [ -z "${SSL_CERT}" ]; then echo >&2 "Please set --ssl-cert"; exit 1; fi
if [ -z "${SSL_CERT_KEY}" ]; then echo >&2 "Please set --ssl-cert-key"; exit 1; fi
fi
# check subnet and prepare the networks section of docker compose file
if [ ! -z "${SUBNET}" ]; then
cidrRegex="^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"
if [[ ! "${SUBNET}" =~ ${cidrRegex} ]]; then
echo >&2 "Value of --subnet is invalid. Please use CIDR notation (ex. 172.45.0.1/24)"
exit 1
fi
IFS=/ read -r subnetIp subnetPrefix <<< "${SUBNET}"
if [ "${subnetPrefix}" -gt 27 ]; then
echo >&2 Please specify a subnet with 32 or more addresses
exit 1
fi
dec2ip () {
local ip dec=$@ delim=""
for e in {3..0}; do
((octet = dec / (256 ** e) ))
((dec -= octet * 256 ** e))
ip+=$delim$octet
delim=.
done
printf '%s\n' "$ip"
}
ip2dec () {
local a b c d ip=$@
IFS=. read -r a b c d <<< "$ip"
printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
}
# split subnet into privateSubnet and publicSubnet
subnetIpDec=$( ip2dec "${subnetIp}" )
subnetMask=$(( 0xffffffff - 2 ** ( 32 - ${subnetPrefix} ) + 1 ))
subnetIpMaskedDec=$(( ${subnetIpDec} & ${subnetMask} ))
newSubnetPrefix=$(( ${subnetPrefix} + 1 ))
publicSubnetIpDec=$(( ${subnetIpMaskedDec} ))
privateSubnetIpDec=$(( ${subnetIpMaskedDec} + 2 ** (32 - ${newSubnetPrefix}) ))
publicSubnetIp=$(dec2ip "${publicSubnetIpDec}")
privateSubnetIp=$(dec2ip "${privateSubnetIpDec}")
# prepare subnet section of the docker compose file
IPAM_PUBLIC=$(printf "ipam:\n config:\n - subnet: \"${publicSubnetIp}/${newSubnetPrefix}\"")
IPAM_PRIVATE=$(printf "ipam:\n config:\n - subnet: \"${privateSubnetIp}/${newSubnetPrefix}\"")
fi
if [ -n "${BRIDGE_NAME_PREFIX}" ]; then
NET_DRIVER_OPTS_PUBLIC=$(printf "driver_opts:\n com.docker.network.bridge.name: ${BRIDGE_NAME_PREFIX}-pub")
NET_DRIVER_OPTS_PRIVATE=$(printf "driver_opts:\n com.docker.network.bridge.name: ${BRIDGE_NAME_PREFIX}-priv")
fi
if [ ! -z "${ENV_DIR}" ]; then
# prepare env_file sections of the docker compose file
[ -f ${ENV_DIR}/unms.env ] && ENV_FILES_UNMS=$(printf "env_file:\n - ${ENV_DIR}/unms.env")
[ -f ${ENV_DIR}/ucrm.env ] && ENV_FILES_UCRM=$(printf "env_file:\n - ${ENV_DIR}/ucrm.env")
[ -f ${ENV_DIR}/netflow.env ] && ENV_FILES_NETFLOW=$(printf "env_file:\n - ${ENV_DIR}/netflow.env")
fi
# prepare --silent option for curl
curlSilent=""
if [ "${UNATTENDED}" = "true" ]; then
curlSilent="--silent"
fi
is_decimal_number() {
[[ ${1} =~ ^[0-9]+$ ]]
}
version_equal_or_newer() {
if [[ "$1" == "$2" ]]; then return 0; fi
local IFS=.
local i ver1=($1) ver2=($2)
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do ver1[i]=0; done
for ((i=0; i<${#ver1[@]}; i++)); do
if [[ -z ${ver2[i]} ]]; then ver2[i]=0; fi
if ! is_decimal_number "${ver1[i]}" ; then return 1; fi
if ! is_decimal_number "${ver2[i]}" ; then return 1; fi
if ((10#${ver1[i]} > 10#${ver2[i]})); then return 0; fi
if ((10#${ver1[i]} < 10#${ver2[i]})); then return 1; fi
done
return 0;
}
semver_equal_or_newer() {
if [[ "$1" == "$2" ]]; then return 0; fi
# remove build number
local v1="$(echo "$1" | sed 's/\+.*//')"
local v2="$(echo "$2" | sed 's/\+.*//')"
# split to core and pre-release parts
local IFS=-
local i j parts1=($v1) parts2=($v2)
for ((i=0; i<${#parts1[@]} || i < ${#parts2[@]}; i++)); do
# split parts to items
if [ "${i}" -ge "${#parts1[@]}" ]; then return 0; fi
if [ "${i}" -ge "${#parts2[@]}" ]; then return 1; fi
local part1="${parts1[i]}" part2="${parts2[i]}"
local IFS=.
local j ver1=($part1) ver2=($part2)
for ((j=0; j < ${#ver1[@]} || j < ${#ver2[@]}; j++)); do
# compare items
if [ "${j}" -ge "${#ver1[@]}" ]; then return 1; fi
if [ "${j}" -ge "${#ver2[@]}" ]; then return 0; fi
local item1="${ver1[j]}" item2="${ver2[j]}"
if is_decimal_number "${item1}" && is_decimal_number "${item2}"; then
# compare numerically
if [ "${item1}" -gt "${item2}" ]; then return 0; fi
if [ "${item1}" -lt "${item2}" ]; then return 1; fi
else
# compare alphabetically
if [[ "${item1}" > "${item2}" ]]; then return 0; fi
if [[ "${item1}" < "${item2}" ]]; then return 1; fi
fi
done
done
return 0;
}
version_older() {
! version_equal_or_newer "$1" "$2"
}
# usage: confirm <question>
# Prints given question and asks user to type Y or N.
# Returns 0 if user typed Y, 1 if user typed N.
# Exits if user failed to type Y or N too many times.
# examples:
# confirm "Do you want to continue?" || exit 1
confirm() {
local question="$1"
for i in {0..10}; do
read -p "${question} [Y/N]" -n 1 -r
echo
if [[ ${REPLY} =~ ^[Yy]$ ]]; then
echo "Yes"
return 0
fi
if [[ ${REPLY} =~ ^[Nn]$ ]]; then
echo "No"
return 1
fi
echo "Please type Y or N."
done
echo "Too many failed attempts."
exit 1
}
check_system() {
local architecture
architecture=$(uname -m)
case "${architecture}" in
amd64|x86_64|aarch64)
;;
*)
echo >&2 "Unsupported platform '${architecture}'."
echo >&2 "UISP supports: x86_64/amd64/aarch64."
exit 1
;;
esac
local lsb_dist
local dist_version
if [ -z "${lsb_dist:-}" ] && [ -r /etc/lsb-release ]; then
lsb_dist="$(. /etc/lsb-release && echo "${DISTRIB_ID:-}")"
fi
if [ -z "${lsb_dist:-}" ] && [ -r /etc/debian_version ]; then
lsb_dist="debian"
fi
if [ -z "${lsb_dist:-}" ] && [ -r /etc/fedora-release ]; then
lsb_dist="fedora"
fi
if [ -z "${lsb_dist:-}" ] && [ -r /etc/oracle-release ]; then
lsb_dist="oracleserver"
fi
if [ -z "${lsb_dist:-}" ]; then
if [ -r /etc/centos-release ] || [ -r /etc/redhat-release ]; then
lsb_dist="centos"
fi
fi
if [ -z "${lsb_dist:-}" ] && [ -r /etc/os-release ]; then
lsb_dist="$(. /etc/os-release && echo "${ID:-}")"
fi
lsb_dist="$(echo "${lsb_dist:-}" | tr '[:upper:]' '[:lower:]')"
DISTRO="$lsb_dist"
SUPPORTED_DISTRO=false
local SKIP_PREREQ_CHECK=false
case "$DISTRO" in
ubuntu)
if [ -z "${dist_version:-}" ] && [ -r /etc/lsb-release ]; then
dist_version="$(. /etc/lsb-release && echo "${DISTRIB_RELEASE:-}")"
fi
if version_equal_or_newer "${dist_version}" "16.04"; then
SUPPORTED_DISTRO=true
fi
;;
debian)
dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')"
case "${dist_version}" in
jessie) dist_version=8;;
stretch) dist_version=9;;
buster) dist_version=10;;
esac
if version_equal_or_newer "${dist_version}" "8"; then
SUPPORTED_DISTRO=true
fi
;;
*coreos)
if [ -z "${dist_version:-}" ] && [ -r /etc/lsb-release ]; then
dist_version="$(. /etc/lsb-release && echo "${DISTRIB_RELEASE:-}")"
fi
if version_equal_or_newer "${dist_version}" "1465.6.0"; then
if ! echo $PATH | grep -q "/opt/bin" ; then
export PATH="/opt/bin:$PATH"
fi
if [ ! -d /opt/bin ]; then
mkdir -p /opt/bin
fi
DOCKER_COMPOSE_INSTALL_PATH="/opt/bin"
SUPPORTED_DISTRO=true
fi
;;
ubios)
if [ -z "${dist_version:-}" ] && [ -r /etc/os-release ]; then
dist_version="$(. /etc/os-release && echo "${VERSION_ID:-}")"
fi
SUPPORTED_DISTRO=true
SKIP_PREREQ_CHECK=true
;;
*)
if [ -z "${dist_version:-}" ] && [ -r /etc/os-release ]; then
dist_version="$(. /etc/os-release && echo "${VERSION_ID:-}")"
fi
;;
esac
if [ "${SKIP_PREREQ_CHECK}" = "false" ]; then
for prerequisite in "${PREREQUISITES[@]}"; do
IFS=\| read -r preCommand prePackage <<< "${prerequisite}"
command -v "${preCommand}" >/dev/null 2>&1 || {
if [ "$UNATTENDED" = true ]; then
echo "Warning: this script requires '${preCommand}' from '${prePackage}' but it can't be found. The installation may fail."
else
echo >&2 "This script requires '${preCommand}'. Please install '${prePackage}' and try again. Aborting."
exit 1
fi
}
done
fi
DIST_VERSION="${dist_version:-}"
if [ "${UNATTENDED}" = "false" ] && [ "${SUPPORTED_DISTRO}" = "false" ]; then
echo "Your distribution '${lsb_dist} ${dist_version:-}' is not supported."
echo "We recommend that you install UISP on Ubuntu 18.04, Debian 9 or newer."
confirm "Would you like to continue with the installation anyway?" || exit 1
else
echo "Distribution: '${lsb_dist} ${dist_version:-}'"
fi
if [ "${UNATTENDED}" = "false" ] && [[ -e /proc/meminfo ]]; then
local memory
local memoryUnit
memory="$(awk '/MemTotal/{print $2}' /proc/meminfo)"
if (which bc > /dev/null 2>&1); then
memoryUnit=$(echo "scale=2; ${memory}/1024^2" | bc)
memoryUnit="${memoryUnit} GB"
else
memoryUnit="${memory} KB"
fi
if [[ "${memory}" -lt 1000000 ]]; then
echo >&2 "ERROR: Your system has only ${memoryUnit} of RAM."
echo >&2 "UISP requires at least 1 GB of RAM to run and 2 GB is recommended. Installation aborted."
exit 1
fi
if [[ "${memory}" -lt 2000000 ]]; then
echo >&2 "WARNING: Your system has only ${memoryUnit} RAM."
echo >&2 "We recommend at least 2 GB RAM to run UISP without problems."
fi
fi
if [ "${UNATTENDED}" = "false" ] && docker --version >/dev/null 2>&1; then
# it's possible to have different client and server version
local dockerClientVersion=$(docker version -f '{{.Client.Version}}')
local dockerServerVersion=$(docker version -f '{{.Server.Version}}')
if [ $dockerClientVersion != $dockerServerVersion ]; then
echo "Your docker client version $dockerClientVersion and server version $dockerServerVersion are different. This is highly unrecommended."
confirm "Would you like to continue with the installation anyway?" || exit 1
fi
fi
}
check_update_allowed() {
if [ -z "${CURRENT_VERSION}" ]; then
return 0
fi
if [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
if ! semver_equal_or_newer "${VERSION}" "${CURRENT_VERSION}"; then
fail "Cannot downgrade from version '${CURRENT_VERSION}' to '${VERSION}'."
fi
else
echo "'${VERSION}' is not semver, ignoring downgrade check."
fi
if ! semver_equal_or_newer "${CURRENT_VERSION}" "${MIN_CURRENT_VERSION}"; then
fail "Cannot update to version ${VERSION}. Current version ${CURRENT_VERSION} is too old. Please update to ${MIN_CURRENT_VERSION} with
'sudo ${APP_DIR}/unms-cli update --version ${MIN_CURRENT_VERSION}'"
fi
}
check_forgotten_update_flag() {
if [ "${UPDATING}" = "true" ] || [ "${UNATTENDED}" = "true" ]; then
return 0
fi
if [ -f "${CONFIG_FILE}" ]; then
echo "Found configuration file for a previous UISP installation but --update was not specified."
echo "Proceeding with the installation would overwrite the previous configuration and could make UISP unavailable."
confirm "Do you want to continue without loading the previous configuration?" || exit 1
fi
}
check_custom_cert_path() {
if [ -z "${SSL_CERT_DIR}" ]; then
# Not using custom cert.
return 0
fi
if [ "${EUID}" -ne 0 ]; then
# Ignore cert check during update.
return 0
fi
local cert_path="${SSL_CERT_DIR}/${SSL_CERT}"
local key_path="${SSL_CERT_DIR}/${SSL_CERT_KEY}"
local norm_cert_dir
local norm_cert_path
local norm_key_path
# Check that cert and key files exist.
test -f "${cert_path}" || fail "Cert file '${cert_path}' does not exist. Check the --ssl-cert-dir and --ssl-cert arguments."
test -f "${key_path}" || fail "Key file '${key_path}' does not exist. Check the --ssl-cert-dir and --ssl-cert-key arguments."
# Check that the cert dir is parent of cert and key files.
# Nginx container mounts this directory and if the cert or key file actually placed within another directory it will
# be inaccessible from within the container.
norm_cert_dir=$(readlink -f "${SSL_CERT_DIR}") || fail "Failed to determine real path of cert directory '${SSL_CERT_DIR}'. Check the --ssl-cert-dir argument."
norm_cert_path=$(readlink -f "${cert_path}") || fail "Failed to determine real path of cert file '${cert_path}'. Check the --ssl-cert argument."
norm_key_path=$(readlink -f "${key_path}") || fail "Failed to determine real path of key file '${key_path}'. Check the --ssl-cert-key argument."
[[ "${norm_cert_path}" = "${norm_cert_dir}"* ]] || fail "Cert file: \n${norm_cert_path}\n is not placed in the cert directory:\n${norm_cert_dir}\nCheck --ssl-cert-dir and --ssl-cert arguments for symbolic links. The actual ssl cert file (not just symbolic link) must be within the ssl cert directory or its subdirectories."
[[ "${norm_key_path}" = "${norm_cert_dir}"* ]] || fail "Key file:\n${norm_key_path}\n is not placed in the cert directory:\n${norm_cert_dir}\nCheck --ssl-cert-dir and --ssl-cert-key arguments for symbolic links. The actual ssl key file (not just symbolic link) must be within the ssl cert directory or its subdirectories."
}
check_free_space() {
dockerRootDir="$(docker info --format='{{ print .DockerRootDir }}')"
freeSpace="$(df -m "${dockerRootDir}" | tail -1 | awk '{print $4}')"
local minRequiredSpace=3000 # MB
if [ "${USE_LOCAL_IMAGES}" = "true" ]; then
minRequiredSpace=500 # MB
fi
if [[ "${freeSpace}" -lt "${minRequiredSpace}" ]]; then
echo >&2 "There is not enough disk space available to safely install UISP. At least ${minRequiredSpace} MB is required."
echo >&2 "You have ${freeSpace} MB of available disk space in ${dockerRootDir}"
echo >&2 -e "\n----------------\n"
echo >&2 "We recommend running \"docker system prune -a\" once in a while to clean unused containers, images, etc."
echo >&2 "You can determine how much space can be cleaned up by running \"docker system df\""
exit 1
fi
}
dockerCompose() {
${DOCKER_COMPOSE_COMMAND} -p "${DOCKER_COMPOSE_PROJECT_NAME}" -f "${DOCKER_COMPOSE_PATH}" "$@"
}
install_docker() {
if [ "${DISTRO}" = "ubios" ]; then
return 0
fi
if ! command -v docker > /dev/null 2>&1; then
echo "Download and install Docker"
curl -fsSL https://get.docker.com | sh -s -- --version "${DOCKER_STRICT_VERSION}" > /dev/null
systemctl enable docker
systemctl start docker
fi
if ! command -v docker > /dev/null 2>&1; then
fail "Docker not installed. Please check previous logs. Aborting."
fi
DOCKER_VERSION=$(docker -v | sed 's/.*version v\{0,1\}\([0-9.]*\).*/\1/');
if ! version_equal_or_newer "${DOCKER_VERSION}" "${DOCKER_STRICT_VERSION}" ; then
if [ "${UNATTENDED}" = "false" ] && [ "${SUPPORTED_DISTRO}" = "true" ] && [ "${EUID}" -eq 0 ]; then
if confirm "Docker version ${DOCKER_VERSION} is below recommended version ${DOCKER_STRICT_VERSION}. Would you like to update docker automatically? This action will affect all docker services on this computer, not just UISP."; then
case "${DISTRO}" in
ubuntu|debian)
if realpath "$(command -v docker)" | grep snap > /dev/null 2>&1; then
# current latest stable for snap 24.0.5, to keep the version fixed, once released find revision with docker 25.0.3
snap refresh docker --revision 2915 || fail "Failed to update docker from snap for '${DISTRO}'."
else
curl -fsSL https://get.docker.com | sh -s -- --version "${DOCKER_STRICT_VERSION}" || fail "Failed to update docker for '${DISTRO}'."
fi
;;
*)
fail "Cannot update docker for '${DISTRO}' automatically. Please update docker to ${DOCKER_STRICT_VERSION} or newer and run this installation script again."
;;
esac
fi
fi
# Check that the version was updated.
DOCKER_VERSION=$(docker -v | sed 's/.*version \([0-9.]*\).*/\1/');
if ! version_equal_or_newer "${DOCKER_VERSION}" "${DOCKER_MIN_VERSION}" ; then
fail "Docker version ${DOCKER_VERSION} is not supported. Please upgrade to version ${DOCKER_STRICT_VERSION} or newer."
fi
fi
echo "Docker version client: $(docker version -f '{{.Client.Version}}'), server: $(docker version -f '{{.Server.Version}}')"
}
check_docker_compose() {
if ! ($DOCKER_COMPOSE_COMMAND > /dev/null 2>&1); then
echo "Warning: Docker Compose plugin is not installed. We recommend installing it for compatibility." >&2
DOCKER_COMPOSE_COMMAND="docker-compose"
DOCKER_COMPOSE_VERSION="$(docker-compose -v | sed 's/.*version \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/')";
echo "Docker compose version: ${DOCKER_COMPOSE_VERSION}"
fi
}
configure_containerd() {
if [ -f /lib/systemd/system/containerd.service ] && [ ! -f /lib/systemd/system/containerd.service.d/unms-killmode.conf ]; then
if [ ! -w /lib/systemd/system ]; then
echo "Containerd service found but got no permissions for creating config override, skipping."
return
fi
echo "Containerd service found, updating configuration."
mkdir -p /lib/systemd/system/containerd.service.d
cat > /lib/systemd/system/containerd.service.d/unms-killmode.conf <<EOL
[Service]
KillMode=mixed
EOL
systemctl daemon-reload
fi
}
create_user() {
if [ -z "$(getent passwd ${USERNAME})" ]; then
echo "Creating user ${USERNAME}, home dir '${HOME_DIR}'."
if [ -z "$(getent group ${USERNAME})" ]; then
useradd -m -d "${HOME_DIR}" -G docker "${USERNAME}" || fail "Failed to create user '${USERNAME}'"
else
useradd -m -d "${HOME_DIR}" -g "${USERNAME}" -G docker "${USERNAME}" || fail "Failed to create user '${USERNAME}'"
fi
elif ! getent group docker | grep -q "\b${USERNAME}\b" \
|| ! [ -d "${HOME_DIR}" ] \
|| [ "$(stat -c '%u' "${HOME_DIR}")" != "$(id -u "${USERNAME}")" ]; then
echo >&2 "WARNING: User '${USERNAME}' already exists. We are going to ensure that the"
echo >&2 "user is in the 'docker' group and that its home '${HOME_DIR}' dir exists and"
echo >&2 "is owned by the user."
if ! [ "$UNATTENDED" = true ]; then
confirm "Would you like to continue with the installation?" || exit 1
fi
fi
if ! getent group docker | grep -q "\b${USERNAME}\b"; then
echo "Adding user '${USERNAME}' to docker group."
if ! usermod -aG docker "${USERNAME}"; then
echo >&2 "Failed to add user '${USERNAME}' to docker group."
exit 1
fi
fi
if ! [ -d "${HOME_DIR}" ]; then
echo "Creating home directory '${HOME_DIR}'."
if ! mkdir -p "${HOME_DIR}"; then
echo >&2 "Failed to create home directory '${HOME_DIR}'."
exit 1
fi
fi
if [ "$(stat -c '%u' "${HOME_DIR}")" != "$(id -u "${USERNAME}")" ]; then
chown "${USERNAME}" "${HOME_DIR}"
fi
export USER_ID=$(id -u "${USERNAME}")
}
cleanup_docker() {
# Networks created by the compose v1 are not deleted upon uisp shutdown and
# cause compose v2 "up" to fail. Let's get rid of them.
docker network rm unms_internal unms_public >/dev/null 2>&1 || true
}
start_postgres() {
echo "Starting postgres DB."
dockerCompose up -d postgres >/dev/null || fail "Failed to start Postgres DB."
for delay in 1 2 2 5 10 10 0; do
test "${delay}" != "0" || fail "Postgres DB failed to start in time."
if dockerCompose exec -T postgres pg_isready 2>&1 >/dev/null; then
break
fi
sleep "${delay}"
done
}
stop_postgres() {
if version_equal_or_newer "${DOCKER_VERSION}" "23.0.0" || version_equal_or_newer "${DOCKER_COMPOSE_VERSION}" "1.18.0" ; then
dockerCompose down --timeout 60 || fail "Failed to stop Postgres DB."
else
dockerCompose down || fail "Failed to stop Postgres DB."
fi
}
exec_psql() {
dockerCompose exec -T postgres psql --username postgres --no-password --no-align --tuples-only --quiet --dbname "${UNMS_POSTGRES_DB}" "$@"
}
exec_psql_command() {
exec_psql --command "${1}"
}
fix_postgres() {
test -d "${DATA_DIR}/postgres" || return 0 # do nothing during first installation
test "${RUN_VACUUM}" = "true" || return 0
CURRENT_DB_VERSION="$(head -n 1 "${DATA_DIR}/postgres/PG_VERSION" 2>/dev/null || true)"
if [ "${CURRENT_DB_VERSION}" == "13" ]; then
echo "Reducing Postgres DB size."
start_postgres
exec_psql_command "VACUUM FULL" || fail "Failed to reduce Postgres DB size."
stop_postgres
fi
}
remove_old_restore_files() {
# Make sure that restore dir does not exist. We are now applying any backup in this directory during start
# of UISP container. Under normal circumstances this directory should be empty or not exist at all.
test -d "${RESTORE_DIR}" || return 0 # nothing to delete
rm "${RESTORE_DIR}" -rf || echo "WARNING: Failed to clear restore directory '${RESTORE_DIR}'."
}
migrate_app_files() {
oldConfigFile="${HOME_DIR}/unms.conf"
oldDockerComposeFile="${HOME_DIR}/docker-compose.yml"
oldDockerComposeTemplate="${HOME_DIR}/docker-compose.yml.template"
oldConfigDir="${HOME_DIR}/conf"
mkdir -p -m 700 "${APP_DIR}"
if [ -f "${oldConfigFile}" ]; then mv -u "${oldConfigFile}" "${CONFIG_FILE}"; fi
if [ -f "${oldDockerComposeFile}" ]; then mv -u "${oldDockerComposeFile}" "${DOCKER_COMPOSE_PATH}"; fi
if [ -f "${oldDockerComposeTemplate}" ]; then mv -u "${oldDockerComposeTemplate}" "${DOCKER_COMPOSE_TEMPLATE_PATH}"; fi
if [ -d "${oldConfigDir}" ]; then rm -rf "${oldConfigDir}"; fi
chown -R "${USERNAME}" "${APP_DIR}" || true
}
determine_public_ports() {
# The docker-compose.yml will be broken if we use same port for http and https. Make sure that they are different.
if [ "${HTTP_PORT}" = "${HTTPS_PORT}" ]; then
echo >&2 "ERROR: Port '${HTTP_PORT}' cannot be configured for both http and https. Please choose different ports using --http-port and --https-port arguments"
exit 1
fi
if [ "${WS_PORT}" = "${HTTP_PORT}" ]; then
echo >&2 "ERROR: Port '${HTTP_PORT}' cannot be configured for both http and ws. Please choose different ports using --http-port and --ws-port arguments"
exit 1
fi
# default for PUBLIC_HTTPS_PORT is HTTPS_PORT
PUBLIC_HTTPS_PORT="${HTTPS_PORT}"
# PROXY_HTTPS_PORT overrides PUBLIC_HTTPS_PORT
if [ ! -z "${PROXY_HTTPS_PORT:-}" ]; then
PUBLIC_HTTPS_PORT="${PROXY_HTTPS_PORT}"
fi
# default for PROXY_WS_PORT is PROXY_HTTPS_PORT
if [ -z "${PROXY_WS_PORT:-}" ]; then
PROXY_WS_PORT="${PROXY_HTTPS_PORT}"
fi
# default for PUBLIC_WS_PORT is WS_PORT
PUBLIC_WS_PORT="${WS_PORT}"
# PROXY_WS_PORT overrides PUBLIC_WS_PORT
if [ ! -z "${PROXY_WS_PORT:-}" ]; then
PUBLIC_WS_PORT="${PROXY_WS_PORT}"
fi
# if WS port is different from HTTP port, add port mapping
WS_PORT_MAPPING=
if [ ! -z "${WS_PORT}" ] && [ ! "${WS_PORT}" = "${HTTPS_PORT}" ]; then
WS_PORT_MAPPING="- \"${WS_PORT}:${WS_PORT}\""
fi
export PUBLIC_HTTPS_PORT
export PUBLIC_WS_PORT
export WS_PORT_MAPPING
}
create_docker_compose_file() {
echo "Creating docker-compose.yml"
# Workaround for docker-compose from snap package.
if realpath "$(command -v ${DOCKER_COMPOSE_COMMAND})" | grep snap > /dev/null 2>&1 && [ "${TMP_INSTALL_DIR}" = "${SCRIPT_DIR}" ] ; then
echo "It appears that docker compose was installed using snap package manager. This version of docker compose cannot access files in '/tmp' directory."
TMP_INSTALL_DIR="$(mktemp -d -p "${HOME_DIR}" -t install-XXXXXX)" || fail "Failed to create temporary installation dir."
echo "Moving installation files from '${SCRIPT_DIR}' to '${TMP_INSTALL_DIR}'."
cp -a "${SCRIPT_DIR}/"* "${TMP_INSTALL_DIR}" || fail "Failed to copy installation files to install dir '${TMP_INSTALL_DIR}'."
if [ "${EUID}" -eq 0 ]; then
chown -R "${USERNAME}" "${TMP_INSTALL_DIR}/" || fail "Failed to change install dir '${TMP_INSTALL_DIR}' owner."
fi
fi
envsubst < "${TMP_INSTALL_DIR}/${DOCKER_COMPOSE_TEMPLATE_FILENAME}" > "${TMP_INSTALL_DIR}/${DOCKER_COMPOSE_FILENAME}" || fail "Failed to create docker-compose.yml"
sed -i '/.*=$/d' "${TMP_INSTALL_DIR}/${DOCKER_COMPOSE_FILENAME}" || fail "Failed to remove empty env variables."
}
login_to_dockerhub() {
if [[ ${DOCKER_USERNAME} ]]; then
echo "Logging in to Docker Hub as ${DOCKER_USERNAME}"
docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}" "${DOCKER_REGISTRY}"
fi
}
pull_docker_images() {
if [ "${USE_LOCAL_IMAGES}" = "true" ]; then
echo "Will try to use local Docker images."
return 0
fi
echo "Pulling docker images."
local newDockerComposeFile="${TMP_INSTALL_DIR}/${DOCKER_COMPOSE_FILENAME}"
if [ -f "${newDockerComposeFile}" ]; then
${DOCKER_COMPOSE_COMMAND} -p unms -f "${newDockerComposeFile}" pull || fail "Failed to pull docker images"
fi
docker pull ubnt/ucrm-conntrack || fail "Failed to pull ubnt/ucrm-conntrack image"
}
stop_docker_containers() {
if [ -f "${DOCKER_COMPOSE_PATH}" ]; then
runningContainers="$(dockerCompose ps -q)" || fail "Failed to get running containers."
if [ -n "${runningContainers}" ]; then
echo "Stopping docker containers."
if version_equal_or_newer "${DOCKER_VERSION}" "23.0.0" || version_equal_or_newer "${DOCKER_COMPOSE_VERSION}" "1.18.0" ; then
dockerCompose down --timeout 60 && RC=$? || RC=$?
else
dockerCompose down && RC=$? || RC=$?
fi
if [ "${RC}" -gt 0 ]; then
# Failed to stop UISP. Try to restart it before exiting.
dockerCompose up -d unms netflow || true
fail "Failed to stop docker containers. This usually happens due to problem in docker service. Try restarting docker
service with 'sudo systemctl restart docker' and then retry the installation."
fi
fi
fi
}
check_raw_sockets() {
local error
# Try to set raw sockets capabilities for node. If it fails the docker is running without 'setcap cap_net_raw' support.
error="$(docker run --rm --entrypoint /usr/sbin/setcap "${DOCKER_IMAGE}:${DOCKER_TAG}" \
cap_net_raw=pe /usr/local/bin/node 2>&1)" && RC=$? || RC=$?
if [ "${RC}" -gt 0 ]; then
if echo "${error}" | grep -q "Failed to set capabilities on file"; then
fail "This Docker installation does not support setting file capabilities using 'setcap' command.
This happens usually due to lack of support from Docker's storage driver.
Please check the storage driver used by Docker by running 'docker info | grep \"Storage Driver\"'. It should be
set to overlay2 or similar filesystem with support for extended file attributes.
To change Docker's storage driver to overlay2 first check that the kernel supports it by running \"modprobe -a overlay\".
If that command does not print any error then it means that the overlay2 is supported.
If overlay2 is supported edit /etc/docker/daemon.json and add { \"storage-driver\": \"overlay2\" }.
Restart Docker service with 'sudo systemctl restart docker' and run this installation script again."
else
fail "Failed to check raw sockets: ${error}"
fi
fi
echo "File capabilities are supported."
}
try_to_bind_port() {
local port="${1}"
local protocol="${2}" # tcp or udp
local error
# Try to create docker container that binds given port.
error="$(docker run --rm -p "${port}:${port}/${protocol}" --entrypoint /bin/true "${DOCKER_IMAGE}:${DOCKER_TAG}" 2>&1)" && RC=$? || RC=$?
if [ "${RC}" -gt 0 ]; then
if echo "${error}" | grep -q "already allocated" || echo "${error}" | grep -q "already in use"; then
return 1
else
fail "Failed to check free ports: ${error}"
fi
fi
echo "Port ${port}/${protocol} is free."
return 0
}
check_free_ports() {
echo "Checking available ports"
while ! try_to_bind_port "${HTTP_PORT}" "tcp"; do
test "${UNATTENDED}" = "false" || fail "Port ${HTTP_PORT} is in use."
read -r -p "Port ${HTTP_PORT} is already in use, please choose a different HTTP port for UISP. [${ALTERNATIVE_HTTP_PORT}]: " HTTP_PORT
HTTP_PORT=${HTTP_PORT:-$ALTERNATIVE_HTTP_PORT}
done
while ! try_to_bind_port "${HTTPS_PORT}" "tcp"; do
test "${UNATTENDED}" = "false" || fail "Port ${HTTPS_PORT} is in use."
read -r -p "Port ${HTTPS_PORT} is already in use, please choose a different HTTPS port for UISP. [${ALTERNATIVE_HTTPS_PORT}]: " HTTPS_PORT
HTTPS_PORT=${HTTPS_PORT:-$ALTERNATIVE_HTTPS_PORT}
done
while ! try_to_bind_port "${SUSPEND_PORT}" "tcp"; do
if [ "${UNATTENDED}" = true ]; then
echo >&2 "WARNING: Port ${SUSPEND_PORT} is in use. Selecting ${ALTERNATIVE_SUSPEND_PORT} port for suspension."
SUSPEND_PORT="${ALTERNATIVE_SUSPEND_PORT}"
break
else
read -r -p "Port ${SUSPEND_PORT} is already in use, please choose a different Suspension port for UISP. [${ALTERNATIVE_SUSPEND_PORT}]: " SUSPEND_PORT
SUSPEND_PORT=${SUSPEND_PORT:-$ALTERNATIVE_SUSPEND_PORT}
fi
done
while ! try_to_bind_port "${REMOTE_UI_WS_PORT_PUBLIC}" "tcp"; do
if [ "${UNATTENDED}" = true ]; then
echo >&2 "WARNING: Port ${REMOTE_UI_WS_PORT_PUBLIC} is in use. Selecting ${ALTERNATIVE_REMOTE_UI_WS_PORT_PUBLIC} port for remote device web UI."
REMOTE_UI_WS_PORT_PUBLIC="${ALTERNATIVE_REMOTE_UI_WS_PORT_PUBLIC}"
break
else
read -r -p "Port ${REMOTE_UI_WS_PORT_PUBLIC} is already in use, please choose a different remote device web UI port for UISP. [${ALTERNATIVE_REMOTE_UI_WS_PORT_PUBLIC}]: " REMOTE_UI_WS_PORT_PUBLIC
REMOTE_UI_WS_PORT_PUBLIC=${REMOTE_UI_WS_PORT_PUBLIC:-$ALTERNATIVE_REMOTE_UI_WS_PORT_PUBLIC}
fi
done
while ! try_to_bind_port "${NETFLOW_PORT}" "udp"; do
if [ "${UNATTENDED}" = true ]; then
echo >&2 "WARNING: Port ${NETFLOW_PORT} is in use. Selecting ${ALTERNATIVE_NETFLOW_PORT} port for NetFlow."
NETFLOW_PORT="${ALTERNATIVE_NETFLOW_PORT}"
break
else
read -r -p "Port ${NETFLOW_PORT} is already in use, please choose a different NetFlow port for UISP. [${ALTERNATIVE_NETFLOW_PORT}]: " NETFLOW_PORT
NETFLOW_PORT=${NETFLOW_PORT:-$ALTERNATIVE_NETFLOW_PORT}
fi
done
}
create_data_volumes() {
# some old versions linked data/cert to external cert dir
if [ -L "${DATA_DIR}/cert" ]; then
echo "Deleting old symlink ${DATA_DIR}/cert"
rm -f "${DATA_DIR}/cert";
fi
echo "Creating data volumes in '${DATA_DIR}'."
local defaultNginxCertDir="${DATA_DIR}/cert"
local alternativeNginxCertDir="${HOME_DIR}/cert"
local nginxCertDir
if [ "${USE_ALTERNATIVE_CERT_DIR}" = "true" ]; then
nginxCertDir="${alternativeNginxCertDir}"
if [ -e "${defaultNginxCertDir}" ]; then
test ! -e "${alternativeNginxCertDir}" || fail "Both '${defaultNginxCertDir}' and '${alternativeNginxCertDir}' exist."
mv "${defaultNginxCertDir}" "${alternativeNginxCertDir}" || fail "Failed to move cert dir '${defaultNginxCertDir}' to '${alternativeNginxCertDir}'."
fi
else
nginxCertDir="${defaultNginxCertDir}"
if [ -e "${alternativeNginxCertDir}" ]; then
test ! -e "${defaultNginxCertDir}" || fail "Both '${alternativeNginxCertDir}' and '${defaultNginxCertDir}' exist."
mv "${alternativeNginxCertDir}" "${defaultNginxCertDir}" || fail "Failed to move cert dir '${alternativeNginxCertDir}' to '${defaultNginxCertDir}'."
fi
fi
volumes=(
"${DATA_DIR}"
"${nginxCertDir}"
"${DATA_DIR}/siridb"
"${DATA_DIR}/siridb-cores"
"${DATA_DIR}/rabbitmq"
)
for volume in "${volumes[@]}"; do
mkdir -p -m u+rwX,g-rwx,o-rwx "${volume}" || fail "Failed to create volume '${volume}'."
if [ "${EUID}" -eq 0 ]; then
chown "${USERNAME}" "${volume}" || fail "Failed to change ownership of '${volume}'."
fi
done
# always mount ~unms/data/cert as /cert
# mount either an external cert dir or ~unms/data/cert as /usercert
CERT_DIR_MAPPING_NGINX="- ${nginxCertDir}:/cert"
if [ -z "${SSL_CERT_DIR}" ]; then
USERCERT_DIR_MAPPING_NGINX=""
else
echo "Will mount ${SSL_CERT_DIR} as /usercert"
USERCERT_DIR_MAPPING_NGINX="- ${SSL_CERT_DIR}:/usercert:ro"
fi
}
deploy_templates() {
echo "Deploying templates"
mkdir -p "${APP_DIR}"
cp -r "${TMP_INSTALL_DIR}"/* "${APP_DIR}/" || fail "Failed to deploy templates form '${TMP_INSTALL_DIR}' to '${APP_DIR}'."
}
save_config() {
echo "Writing config file"
if ! cat >"${CONFIG_FILE}" <<EOL
VERSION="${VERSION}"
DEMO="${DEMO}"
NODE_ENV="${NODE_ENV}"
DOCKER_IMAGE="${DOCKER_IMAGE}"
UCRM_DOCKER_IMAGE="${UCRM_DOCKER_IMAGE}"
DATA_DIR="${DATA_DIR}"
HTTP_PORT="${HTTP_PORT}"
HTTPS_PORT="${HTTPS_PORT}"
SUSPEND_PORT="${SUSPEND_PORT}"
NETFLOW_PORT="${NETFLOW_PORT}"
PROXY_HTTPS_PORT="${PROXY_HTTPS_PORT}"
WS_PORT="${WS_PORT}"
PROXY_WS_PORT="${PROXY_WS_PORT}"
REMOTE_UI_WS_PORT_PUBLIC="${REMOTE_UI_WS_PORT_PUBLIC}"
SSL_CERT_DIR="${SSL_CERT_DIR}"
SSL_CERT="${SSL_CERT}"
SSL_CERT_KEY="${SSL_CERT_KEY}"
SSL_CERT_CA="${SSL_CERT_CA}"
HOST_TAG="${HOST_TAG}"
BRANCH="${BRANCH}"
SUBNET="${SUBNET}"
BRIDGE_NAME_PREFIX="${BRIDGE_NAME_PREFIX}"
CLUSTER_SIZE="${CLUSTER_SIZE}"
UNMS_POSTGRES_USER="${UNMS_POSTGRES_USER}"
UNMS_POSTGRES_DB="${UNMS_POSTGRES_DB}"
UNMS_POSTGRES_SCHEMA="${UNMS_POSTGRES_SCHEMA}"
UNMS_POSTGRES_PASSWORD="${UNMS_POSTGRES_PASSWORD}"
UNMS_TOKEN="${UNMS_TOKEN}"
UNMS_CLI_TOKEN="${UNMS_CLI_TOKEN}"
UNMS_DEPLOYMENT="${UNMS_DEPLOYMENT}"
UNMS_IP_WHITELIST="${UNMS_IP_WHITELIST}"
UCRM_POSTGRES_USER="${UCRM_POSTGRES_USER}"
UCRM_POSTGRES_DB="${UCRM_POSTGRES_DB}"
UCRM_POSTGRES_SCHEMA="${UCRM_POSTGRES_SCHEMA}"
UCRM_POSTGRES_PASSWORD="${UCRM_POSTGRES_PASSWORD}"
UCRM_SECRET="${UCRM_SECRET}"
POSTGRES_USER="${POSTGRES_USER}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD}"
USE_LOCAL_DISCOVERY="${USE_LOCAL_DISCOVERY}"
USE_ALTERNATIVE_CERT_DIR="${USE_ALTERNATIVE_CERT_DIR}"
UNMS_FEATURES="${UNMS_FEATURES}"
SUBSYSTEM_ID="${SUBSYSTEM_ID}"
SERVER_MAC="${SERVER_MAC}"
EOL
then
fail "Failed to save config file ${CONFIG_FILE}"
fi
}
setup_auto_update() {
if [ "$NO_AUTO_UPDATE" = true ] || [ "$EUID" -ne 0 ]; then
echo "Skipping auto-update setup."
else
updateScript="${APP_DIR}/update.sh";
if ! chmod +x "${updateScript}"; then
echo >&2 "Failed to setup auto-update script"
exit 1
fi
if [ -d /etc/cron.d ] && command -v crontab > /dev/null 2>&1; then
echo "* * * * * ${USERNAME} ${updateScript} --cron > /dev/null 2>&1 || true" > /etc/cron.d/unms-update
if (crontab -l -u "${USERNAME}" | grep "update.sh --cron"); then
# The per-user crontab was used in previous versions of UISP. Now we are using the global crontab.
# Remove the update script from per-user crontab. There should be no other records by default but
# user may have set up some custom job so remove just the update script.
crontab -l -u "${USERNAME}" | grep -v "update.sh --cron" || true | crontab -u "${USERNAME}" -
fi
else
if [ -d /etc/systemd/system ] && command -v systemctl > /dev/null 2>&1; then
cat > /etc/systemd/system/unms-update.service <<EOL
[Unit]
Description=Auto update UISP
[Service]
User=${USERNAME}
Type=oneshot
ExecStart=${updateScript} --cron
EOL
cat > /etc/systemd/system/unms-update.timer <<EOL
[Unit]
Description=Run unms-update.service every minute
[Timer]
OnCalendar=*:0/1
EOL
systemctl enable unms-update.service &&
systemctl enable unms-update.timer &&
systemctl start unms-update.timer
if [ $? -ne 0 ]; then
echo >&2 "Failed to enable systemd auto update timer and service"
exit 1
fi
else
echo >&2 "Failed to enable auto update. UISP requires either Crontab or systemd timers."
exit 1
fi
fi
fi
}
delete_old_firmwares() {
echo "Deleting old firmwares from ${DATA_DIR}/firmwares/unms/*"
if ! rm -rf "${DATA_DIR}/firmwares/unms"/*; then
echo >&2 "WARNING: Failed to delete old firmwares"
fi
}
change_owner() {
# only necessary when installing for the first time, as root
if [ "${EUID}" -eq 0 ]; then
cd "${HOME_DIR}"
if ! chown -R "${USERNAME}" ./*; then
echo >&2 "Failed to change config files owner"
exit 1
fi
oldUninstallScript="${APP_DIR}/uninstall.sh"
if [ -f "${oldUninstallScript}" ]; then
if rm "${oldUninstallScript}"; then
echo "Removed ${oldUninstallScript}"
else
echo >&2 "Failed to remove ${oldUninstallScript}"
fi
fi
else
echo "Not running as root - will not change config files owner"
fi
if ! chmod +x "${APP_DIR}/unms-cli"; then
echo >&2 "Failed to change permissions on ${APP_DIR}/unms-cli"
exit 1
fi
}
start_docker_containers() {
if [ "${START_CONTAINERS}" = "false" ]; then
return 0
fi
echo "Starting docker containers."
dockerCompose up -d unms netflow || fail "Failed to start docker containers"
}
remove_old_image() {
local containerName="$1"
local imageName="$2"
local currentImage="$(docker ps --format "{{.Image}}" --filter name="^${containerName}$" || true)"
if [ -z "${currentImage}" ]; then
return 0;
fi
local allImages="$(docker images "${imageName}:"* --format "{{.Repository}}:{{.Tag}}" || true)"
if [ -z "${allImages}" ]; then
return 0;
fi
for value in ${allImages}; do
if [ "${value}" != "${currentImage}" ]; then
echo "Removing old image '${value}'"
if ! docker rmi "${value}"; then
echo "Failed to remove old image '${value}'"
fi
fi
done
}
remove_old_images() {
echo "Removing old images"
danglingImages="$(docker images -qf "dangling=true")"
if [ ! -z "${danglingImages}" ]; then
echo "Removing dangling images"
docker rmi ${danglingImages} || true;
fi
remove_old_image "unms" "${DOCKER_IMAGE}"
remove_old_image "unms-netflow" "${DOCKER_IMAGE}-netflow"
remove_old_image "unms-nginx" "${DOCKER_IMAGE}-nginx"
remove_old_image "unms-fluentd" "${DOCKER_IMAGE}-fluentd"
remove_old_image "unms-siridb" "${DOCKER_IMAGE}-siridb"
remove_old_image "ucrm" "${UCRM_DOCKER_IMAGE}"
}
confirm_success() {
if [ "${START_CONTAINERS}" = "false" ]; then
echo "Skipping start of containers, use unms-cli to start UISP manually."
return 0
fi
echo "Waiting for UISP to start"
n=0
until [ ${n} -ge 10 ]
do
sleep 3s
unmsRunning=true
# env -i is to ensure that http[s]_proxy variables are not set
# Otherwise the check would go through proxy.
env -i curl -skL "https://127.0.0.1:${HTTPS_PORT}" > /dev/null && break
echo "."
unmsRunning=false
n=$((n+1))
done
if [ "${unmsRunning}" = "false" ]; then
# Sometimes UISP is not available from localhost. Try to access it through docker interface.
dockerInterfaceIp="$(ip a show docker0 | grep inet | head -n 1 | sed 's/.* inet \([^\/]*\)\/.*/\1/' 2> /dev/null || echo "")"
if [ -n "${dockerInterfaceIp}" ]; then
if env -i curl -skL "https://${dockerInterfaceIp}:${HTTPS_PORT}" > /dev/null; then
unmsRunning=true
fi
fi
fi
docker ps
if [ "${unmsRunning}" = true ]; then
echo "UISP is running"
else
fail "UISP is NOT running"
fi
}
check_system
check_update_allowed
check_forgotten_update_flag
check_custom_cert_path
install_docker
check_docker_compose
configure_containerd
check_free_space
create_user
remove_old_restore_files
migrate_app_files
determine_public_ports # need to set all docker compose variables
create_data_volumes
create_docker_compose_file # compose file for docker-compose down
login_to_dockerhub
pull_docker_images
stop_docker_containers
check_raw_sockets
check_free_ports
determine_public_ports # again - now we have all info
create_docker_compose_file # again - compose file for docker-compose up
deploy_templates
cleanup_docker
fix_postgres
save_config
setup_auto_update
delete_old_firmwares
change_owner
start_docker_containers
remove_old_images
confirm_success
cleanup
exit 0