#!/usr/bin/env bash set -o nounset set -o errexit set -o pipefail # FUNCTIONS function ask_questions { while [[ -z "$API_HOST" ]] && [[ "$API_HOST" != *[.]*[.]* ]] do echo -ne "Enter the subdomain for the backend (e.g. api.example.com): " read API_HOST done echo "API_HOST is set to ${API_HOST}" while [[ -z "$APP_HOST" ]] && [[ "$APP_HOST" != *[.]*[.]* ]] do echo -ne "Enter the subdomain for the frontend (e.g. rmm.example.com): " read APP_HOST done echo "APP_HOST is set to ${APP_HOST}" while [[ -z "$MESH_HOST" ]] && [[ "$MESH_HOST" != *[.]*[.]* ]] do echo -ne "Enter the subdomain for meshcentral (e.g. mesh.example.com): " read MESH_HOST done echo "MESH_HOST is set to ${MESH_HOST}" while [[ -z "$EMAIL" ]] && [[ "$EMAIL" != *[@]*[.]* ]] do echo -ne "Enter a valid email address for django and meshcentral: " read EMAIL done echo "EMAIL is set to ${EMAIL}" while [[ -z "$USERNAME" ]] do echo -ne "Set username for mesh and tactical login: " read USERNAME done echo "USERNAME is set to ${USERNAME}" while [[ -z "$PASSWORD" ]] do echo -ne "Set password for mesh and tactical password: " read PASSWORD done echo "PASSWORD is set" # check if let's encrypt or cert-keys options were set if [[ -z "$LETS_ENCRYPT" ]] && [[ -z "$CERT_PRIV_FILE" ]] || [[ -z "$CERT_PUB_FILE" ]]; then echo -ne "Create a let's encrypt certificate?[Y,n]: " read USE_LETS_ENCRYPT [[ "$USE_LETS_ENCRYPT" == "" ]] || [[ "$USE_LETS_ENCRYPT" ~= [Yy] ]] && LETS_ENCRYPT=1 if [[ -z "$LET_ENCRYPT" ]]; then echo "Let's Encrypt will not be used" echo -ne "Do you want to specify paths to a certificate public key and private key?[Y,n]: " read PRIVATE_CERTS if [[ "$PRIVATE_CERTS" == "" ]] || [[ "$PRIVATE_CERTS" ~= [yY] ]]; then # check for valid public certificate file while [[ ! -f $CERT_PUB_FILE ]] do echo -ne "Enter a valid full path to public key file: " read CERT_PUB_FILE done # check for valid private key file while [[ ! -f $CERT_PRIV_FILE ]] do echo -ne "Enter a valid full path to private key file: " read CERT_PRIV_FILE done fi fi fi } function encode_certificates { echo "Base64 encoding certificates" CERT_PUB_BASE64="$(sudo base64 -w 0 ${CERT_PUB_FILE})" CERT_PRIV_BASE64="$(sudo base64 -w 0 ${CERT_PRIV_FILE})" } function generate_env { [[ -f "$ENV_FILE" ]] && echo "Env file already exists"; return 0; local mongodb_user=$(cat /dev/urandom | tr -dc 'a-z' | fold -w 8 | head -n 1) local mongodb_pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1) local postgres_user=$(cat /dev/urandom | tr -dc 'a-z' | fold -w 8 | head -n 1) local postgres_pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1) echo "Generating env file in ${INSTALL_DIR}" local config_file="$(cat << EOF IMAGE_REPO=${DOCKER_REPO} VERSION=${VERSION} TRMM_USER=${USERNAME} TRMM_PASS=${PASSWORD} APP_HOST=${APP_HOST} API_HOST=${API_HOST} MESH_HOST=${MESH_HOST} MESH_USER=${USERNAME} MESH_PASS=${PASSWORD} MONGODB_USER=${mongogb_user} MONGODB_PASSWORD=${mongodb_pass} POSTGRES_USER=${postgres_user} POSTGRES_PASS=${postgres_pass} EOF )" echo "${env_file}" > "$ENV_FILE" } function update_env_field { } function get_env_field { local search_field="$1" awk -F "=" '{if ($1==$search_field) { print $2" } }' $ENV_FILE } function initiate_letsencrypt { echo "Starting Let's Encrypt" ROOT_DOMAIN=$(echo ${API_HOST} | cut -d "." -f2- ) echo "Root domain is ${ROOTDOMAIN}" sudo certbot certonly --manual -d *.${ROOT_DOMAIN} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${EMAIL} --no-eff-email while [[ $? -ne 0 ]] do sudo certbot certonly --manual -d *.${ROOT_DOMAIN} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${EMAIL} --no-eff-email done CERT_PRIV_FILE=/etc/letsencrypt/live/${ROOT_DOMAIN}/privkey.pem CERT_PUB_FILE=/etc/letsencrypt/live/${ROOT_DOMAIN}/fullchain.pem } # setup defaults # keep track of first arg FIRST_ARG="$1" # defaults DOCKER_REPO="tacticalrmm/" REPO="wh1te909" BRANCH="master" VERSION="latest" # file locations INSTALL_DIR=/opt/tactical ENV_FILE=/opt/tactical/.env # check prerequisites command -v docker >/dev/null 2>&1 || { echo >&2 "Docker must be installed. Exiting..."; exit 1; } command -v docker-compose >/dev/null 2>&1 || { echo >&2 "Docker Compose must be installed. Exiting..."; exit 1; } command -v curl >/dev/null 2>&1 || { echo >&2 "Curl must be installed. Exiting..."; exit 1; } command -v bash >/dev/null 2>&1 || { echo >&2 "Bash must be installed. Exiting..."; exit 1; } # check for arguments [ -z "$1" ] && echo >&2 "No arguments supplied. Exiting..."; exit 1; # parse arguments while [[ $# -gt 0 ]] do key="$1" case $key in # install arg -i|install) [[ "$key" != "$FIRST_ARG" ]] && echo >&2 "install must be the first argument. Exiting.."; exit 1; MODE="install" shift # past argument ;; # update arg -u|update) [[ "$key" != "$FIRST_ARG" ]] && echo >&2 "update must be the first argument. Exiting..."; exit 1; MODE="update" shift # past argument ;; # backup arg -b|backup) [[ "$key" != "$FIRST_ARG" ]] && echo >&2 "backup must be the first argument. Exiting..."; exit 1; MODE="backup" shift # past argument ;; # restore arg -r|restore) [[ "$key" != "$FIRST_ARG" ]] && echo >&2 "restore must be the first argument. Exiting..."; exit 1; MODE="restore" shift # past argument ;; # update-cert arg -c|update-cert) [[ "$key" != "$FIRST_ARG" ]] && echo >&2 "update-cert must be the first argument. Exiting..."; exit 1; MODE="update-cert" shift # past argument ;; # use-lets-encrypt arg --use-lets-encrypt) [[ -z "$MODE" ]] && echo >&2 "Missing install or update-cert as first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update-cert" ]] && \ echo >&2 "--use-lets-encrypt option only valid for install and update-cert. Exiting..."; exit 1; LETS_ENCRYPT=1 shift # past argument ;; # cert-priv-file arg --cert-priv-file) [[ -z "$MODE" ]] && echo >&2 "Missing install or update-cert first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update-cert" ]] && \ echo >&2 "--cert-priv-file option only valid for install and update-cert. Exiting..."; exit 1; shift # past argument [ ! -f "$key" ] && echo >&2 "Certificate private key file $key does not exist. Use absolute paths. Exiting..."; exit 1; CERT_PRIV_FILE="$key" shift # past value ;; # cert-pub-file arg --cert-pub-file) [[ -z "$MODE" ]] && echo >&2 "Missing install or update-cert first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update-cert" ]] && \ echo >&2 "--cert-pub-file option only valid for install and update-cert. Exiting..."; exit 1; shift # past argument [ ! -f "$key" ] && echo >&2 "Certificate public Key file ${key} does not exist. Use absolute paths. Exiting..."; exit 1; CERT_PUB_FILE="$key" shift # past value ;; # local arg --local) [[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \ echo >&2 "--local option only valid for install and update. Exiting..."; exit 1; DOCKER_REPO="" shift # past argument ;; # repo arg --repo) [[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \ echo >&2 "--repo option only valid for install and update. Exiting..."; exit 1; shift # past argument REPO="$key" shift # past value ;; # branch arg --branch) [[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \ echo >&2 "--branch option only valid for install and update. Exiting..."; exit 1; shift # past argument BRANCH="$key" shift # past value ;; # version arg --version) [[ -z "$MODE" ]] && echo >&2 "Missing install or update first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] || [[ "$MODE" != "update" ]] && \ echo ">&2 --version option only valid for install and update. Exiting..."; exit 1; shift # past argument VERSION="$key" shift # past value ;; # noninteractive arg --noninteractive) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--noninteractive option only valid for install. Exiting..."; exit 1; NONINTERACTIVE=1 shift # past argument ;; # app host arg --app-host) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--app-host option only valid for install. Exiting..."; exit 1; shift # past argument APP_HOST="$key" shift # past value ;; # api host arg --api-host) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--api-host option only valid for install. Exiting..."; exit 1; shift # past argument API_HOST="$key" shift # past value ;; # mesh host arg --mesh-host) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--mesh-host option only valid for install. Exiting..."; exit 1; shift # past argument MESH_HOST="$key" shift # past value ;; # tactical user arg --tactical-user) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--tactical-user option only valid for install. Exiting..."; exit 1; shift # past argument USERNAME="$key" shift # past value ;; # tactical password arg --tactical-password) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--tactical-password option only valid for install. Exiting..."; exit 1; shift # past argument PASSWORD="$key" shift # past value ;; # email arg --email) [[ -z "$MODE" ]] && echo >&2 "Missing install first argument. Exiting..."; exit 1; [[ "$MODE" != "install" ]] && echo >&2 "--email option only valid for install. Exiting..."; exit 1; shift # past argument EMAIL="$key" shift # past value ;; # Unknown arg *) echo "Unknown argument ${$1}. Exiting..." exit 1 ;; esac done # for install mode if [[ "$MODE" == "install" ]]; then echo "Starting installation in ${INSTALL_DIR}" # move to install dir mkdir -p "${INSTALL_DIR}" cd "$INSTALL_DIR" # pull docker-compose.yml file echo "Downloading docker-compose.yml from branch ${BRANCH}" COMPOSE_FILE="https://raw.githubusercontent.com/${REPO}/tacticalrmm/${BRANCH}/docker/docker-compose.yml" if ! curl -sS "${COMPOSE_FILE}"; then echo >&2 "Failed to download installation package ${COMPOSE_FILE}" exit 1 fi # check if install is noninteractive if [[ -z "$NONINTERACTIVE" ]]; then # ask user for information not supplied as arguments ask_questions else echo "NonInteractive mode set." # check for required noninteractive arguments [[ -z "$API_HOST" ]] || \ [[ -z "$APP_HOST" ]] || \ [[ -z "$MESH_HOST" ]] || \ [[ -z "$EMAIL" ]] || \ [[ -z "$USERNAME" ]] || \ [[ -z "$PASSWORD" ]] && \ echo "You must supply additional arguments for noninteractive install."; exit 1; fi # if certificates are available base64 encode them if [[ -n "$LET_ENCRYPT" ]] && [[ -z "$NONINTERACTIVE" ]]; then initiate_letsencrypt encode_certificates elif [[ -n "$CERT_PUB_FILE" ]] && [[ -n "$CERT_PRIV_FILE" ]]; then encode_certificates # generate config file generate_config # generate env file generate_env echo "Configuration complete. Starting environment." # start environment docker-compose pull docker-compose up -d fi # for update mode if [[ "$MODE" == "update" ]]; then [[ "$VERSION" != "latest" ]] docker-compose pull docker-compose up -d fi # for update cert mode if [[ "$MODE" == "update-cert" ]]; then # check for required parameters [[ -z "$LET_ENCRYPT" ]] || \ [[ -z "$CERT_PUB_FILE" ]] && \ [[ -z "$CERT_PRIV_FILE" ]] && \ echo >&2 "Provide the --lets-encrypt option or use --cert-pub-file and --cert-priv-file. Exiting..."; exit; if [[ -n "$LET_ENCRYPT" ]]; then initiate_letsencrypt encode_certificates generate_env elif [[ -n "$CERT_PUB_FILE" ]] && [[ -n "$CERT_PRIV_FILE" ]]; then encode_certificates generate_env docker-compose restart fi # for backup mode if [[ "$MODE" == "backup" ]]; then echo "backup not yet implemented" fi # for restore mode if [[ "$MODE" == "restore" ]] then; echo "restore not yet implemented" fi