From c84a9d07b16507d572a5fd0dcde1790b404ead20 Mon Sep 17 00:00:00 2001 From: sadnub Date: Thu, 10 Dec 2020 23:43:14 -0500 Subject: [PATCH 1/3] tactical-cli for managing docker installations --- docker/install.sh | 44 +++++ docker/tactical-cli | 439 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 483 insertions(+) create mode 100755 docker/install.sh create mode 100644 docker/tactical-cli diff --git a/docker/install.sh b/docker/install.sh new file mode 100755 index 00000000..fd788245 --- /dev/null +++ b/docker/install.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -o nounset +set -o errexit +set -o pipefail + +temp="/tmp/tactical" + +args="$*" +version="latest" +branch="master" + +branchRegex=" --branch ([^ ]+)" +if [[ " ${args}" =~ ${branchRegex} ]]; then + branch="${BASH_REMATCH[1]}" +fi + +echo "branch=${branch}" +tactical_cli="https://raw.githubusercontent.com/wh1te909/tacticalrmm/${branch}/docker/tactical-cli" + +versionRegex=" --version ([^ ]+)" +if [[ " ${args}" =~ ${versionRegex} ]]; then + version="${BASH_REMATCH[1]}" +fi + +rm -rf "${temp}" +if ! mkdir "${temp}"; then + echo >&2 "Failed to create temporary directory" + exit 1 +fi + +cd "${temp}" +echo "Downloading tactical-cli from branch ${branch}" +if ! curl -sS "${tactical_cli}"; then + echo >&2 "Failed to download installation package ${tactical_cli}" + exit 1 +fi + +chmod +x tactical-cli +./tactical-cli ${args} --version "${version}" 2>&1 | tee -a ~/install.log + +cd ~ +if ! rm -rf "${temp}"; then + echo >&2 "Warning: Failed to remove temporary directory ${temp}" +fi diff --git a/docker/tactical-cli b/docker/tactical-cli new file mode 100644 index 00000000..17dc93e1 --- /dev/null +++ b/docker/tactical-cli @@ -0,0 +1,439 @@ +#!/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=${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 +REPO="tacticalrmm/" +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; + REPO="" + shift # past argument + ;; + + # 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/wh1te909/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 From 9254532baaab6143f8170353bff2ed991ddbd823 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 16 Dec 2020 20:37:11 +0000 Subject: [PATCH 2/3] fix applying default policies in certain situations --- api/tacticalrmm/automation/models.py | 4 ++-- api/tacticalrmm/automation/tasks.py | 24 +++++++++++++++++-- api/tacticalrmm/core/views.py | 14 +++++++---- .../migrations/0006_auto_20201210_2145.py | 6 ++++- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/api/tacticalrmm/automation/models.py b/api/tacticalrmm/automation/models.py index 27b438d7..31aa7ff6 100644 --- a/api/tacticalrmm/automation/models.py +++ b/api/tacticalrmm/automation/models.py @@ -80,7 +80,7 @@ class Policy(BaseAuditModel): default_policy = CoreSettings.objects.first().server_policy client_policy = client.server_policy site_policy = site.server_policy - else: + elif agent.monitoring_type == "workstation": default_policy = CoreSettings.objects.first().workstation_policy client_policy = client.workstation_policy site_policy = site.workstation_policy @@ -132,7 +132,7 @@ class Policy(BaseAuditModel): default_policy = CoreSettings.objects.first().server_policy client_policy = client.server_policy site_policy = site.server_policy - else: + elif agent.monitoring_type == "workstation": default_policy = CoreSettings.objects.first().workstation_policy client_policy = client.workstation_policy site_policy = site.workstation_policy diff --git a/api/tacticalrmm/automation/tasks.py b/api/tacticalrmm/automation/tasks.py index d517d161..d7581ca8 100644 --- a/api/tacticalrmm/automation/tasks.py +++ b/api/tacticalrmm/automation/tasks.py @@ -19,7 +19,17 @@ def generate_agent_checks_from_policies_task( ): policy = Policy.objects.get(pk=policypk) - for agent in policy.related_agents(): + + if policy.is_default_server_policy and policy.is_default_workstation_policy: + agents = Agent.objects.all() + elif policy.is_default_server_policy: + agents = Agent.objects.filter(monitoring_type="server") + elif policy.is_default_workstation_policy: + agents = Agent.objects.filter(monitoring_type="workstation") + else: + agents = policy.related_agents() + + for agent in agents: agent.generate_checks_from_policies(clear=clear) if create_tasks: agent.generate_tasks_from_policies( @@ -86,7 +96,17 @@ def update_policy_check_fields_task(checkpk): def generate_agent_tasks_from_policies_task(policypk, clear=False): policy = Policy.objects.get(pk=policypk) - for agent in policy.related_agents(): + + if policy.is_default_server_policy and policy.is_default_workstation_policy: + agents = Agent.objects.all() + elif policy.is_default_server_policy: + agents = Agent.objects.filter(monitoring_type="server") + elif policy.is_default_workstation_policy: + agents = Agent.objects.filter(monitoring_type="workstation") + else: + agents = policy.related_agents() + + for agent in agents: agent.generate_tasks_from_policies(clear=clear) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index 1380702f..73334efc 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -42,18 +42,24 @@ def get_core_settings(request): @api_view(["PATCH"]) def edit_settings(request): - settings = CoreSettings.objects.first() - serializer = CoreSettingsSerializer(instance=settings, data=request.data) + coresettings = CoreSettings.objects.first() + old_server_policy = coresettings.server_policy + old_workstation_policy = coresettings.workstation_policy + serializer = CoreSettingsSerializer(instance=coresettings, data=request.data) serializer.is_valid(raise_exception=True) new_settings = serializer.save() + print(coresettings.server_policy) + print(coresettings.workstation_policy) + print(new_settings.server_policy) + print(new_settings.workstation_policy) # check if default policies changed - if settings.server_policy != new_settings.server_policy: + if old_server_policy != new_settings.server_policy: generate_all_agent_checks_task.delay( mon_type="server", clear=True, create_tasks=True ) - if settings.workstation_policy != new_settings.workstation_policy: + if old_workstation_policy != new_settings.workstation_policy: generate_all_agent_checks_task.delay( mon_type="workstation", clear=True, create_tasks=True ) diff --git a/api/tacticalrmm/scripts/migrations/0006_auto_20201210_2145.py b/api/tacticalrmm/scripts/migrations/0006_auto_20201210_2145.py index af22f325..c9ecfd33 100644 --- a/api/tacticalrmm/scripts/migrations/0006_auto_20201210_2145.py +++ b/api/tacticalrmm/scripts/migrations/0006_auto_20201210_2145.py @@ -13,7 +13,11 @@ def move_scripts_to_db(apps, schema_editor): for script in Script.objects.all(): if not script.script_type == "builtin": - filepath = f"{settings.SCRIPTS_DIR}/userdefined/{script.filename}" + if script.filename: + filepath = f"{settings.SCRIPTS_DIR}/userdefined/{script.filename}" + else: + print(f"No filename on script found. Skipping") + continue # test if file exists if os.path.exists(filepath): From 40db5d4aa86a4d6a554e91d8448c939cf41148d4 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Wed, 16 Dec 2020 21:50:43 +0000 Subject: [PATCH 3/3] remove debug print --- api/tacticalrmm/core/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/tacticalrmm/core/views.py b/api/tacticalrmm/core/views.py index 73334efc..21629bf1 100644 --- a/api/tacticalrmm/core/views.py +++ b/api/tacticalrmm/core/views.py @@ -49,10 +49,6 @@ def edit_settings(request): serializer.is_valid(raise_exception=True) new_settings = serializer.save() - print(coresettings.server_policy) - print(coresettings.workstation_policy) - print(new_settings.server_policy) - print(new_settings.workstation_policy) # check if default policies changed if old_server_policy != new_settings.server_policy: generate_all_agent_checks_task.delay(