#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" CONFIG_FILE="${1:-${SCRIPT_DIR}/vfire-stack.env}" DRY_RUN=0 APP_USER="vfire-monitor" APP_GROUP="vfire-monitor" APP_HOME="/opt/vfire-monitor" APP_VENV="${APP_HOME}/.venv" APP_STATE_DIR="/var/lib/vfire-monitor" APP_LOG_DIR="/var/log/vfire-monitor" APP_SERVICE="vfire-monitor.service" APP_ENV_FILE="/etc/default/vfire-monitor" APP_SYSTEMD_FILE="/etc/systemd/system/${APP_SERVICE}" ZABBIX_APT_KEYRING="/usr/share/keyrings/zabbix.gpg" ZABBIX_APT_LIST="/etc/apt/sources.list.d/zabbix.list" GRAFANA_APT_KEYRING="/etc/apt/keyrings/grafana.asc" GRAFANA_APT_LIST="/etc/apt/sources.list.d/grafana.list" GRAFANA_DASHBOARD_DIR="/var/lib/grafana/dashboards/vfire" SUMMARY_FILE="/root/vfire-stack-summary.txt" log() { printf '[vfire-installer] %s\n' "$*" } die() { printf '[vfire-installer] ERROR: %s\n' "$*" >&2 exit 1 } run_cmd() { if [[ "${DRY_RUN}" == "1" ]]; then printf '[dry-run] %s\n' "$*" return 0 fi "$@" } parse_args() { local positional=() while (($#)); do case "$1" in --dry-run) DRY_RUN=1 shift ;; *) positional+=("$1") shift ;; esac done if ((${#positional[@]} > 0)); then CONFIG_FILE="${positional[0]}" fi } require_root() { [[ "${EUID}" -eq 0 ]] || die "Execute como root." } require_supported_os() { source /etc/os-release [[ "${ID}" == "debian" ]] || die "Este instalador suporta apenas Debian." [[ "${VERSION_CODENAME:-}" == "trixie" ]] || die "Este instalador foi preparado para Debian 13 (trixie)." } random_secret() { tr -dc 'A-Za-z0-9' /dev/null 2>&1 || die "Comando obrigatorio ausente: ${cmd}" done } assert_ports_available() { local port [[ "${DRY_RUN}" == "1" ]] && return 0 command -v ss >/dev/null 2>&1 || return 0 if [[ "${VFM_APP_PORT}" == "${ZABBIX_WEB_PORT}" || "${VFM_APP_PORT}" == "${GRAFANA_PORT}" || "${ZABBIX_WEB_PORT}" == "${GRAFANA_PORT}" ]]; then die "As portas configuradas para V-Fire Monitor, Zabbix e Grafana precisam ser distintas." fi for port in "${VFM_APP_PORT}" "${ZABBIX_WEB_PORT}" "${GRAFANA_PORT}"; do if ss -ltn "( sport = :${port} )" | tail -n +2 | grep -q .; then log "Porta ${port} ja esta em uso. O instalador vai prosseguir para permitir reexecucoes e atualizacoes." fi done } apt_install() { if [[ "${DRY_RUN}" == "1" ]]; then printf '[dry-run] DEBIAN_FRONTEND=noninteractive apt-get install -y %s\n' "$*" return 0 fi DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" } write_zabbix_repo() { run_cmd install -d -m 0755 /usr/share/keyrings if [[ "${DRY_RUN}" == "1" ]]; then printf '[dry-run] curl -fsSL https://repo.zabbix.com/zabbix-official-repo.key | gpg --dearmor --yes -o %s\n' "${ZABBIX_APT_KEYRING}" else curl -fsSL "https://repo.zabbix.com/zabbix-official-repo.key" | gpg --dearmor --yes -o "${ZABBIX_APT_KEYRING}" fi run_cmd chmod 0644 "${ZABBIX_APT_KEYRING}" cat >"${ZABBIX_APT_LIST}" <"${GRAFANA_APT_LIST}" </dev/null || run_cmd groupadd --system "${APP_GROUP}" id -u "${APP_USER}" >/dev/null 2>&1 || run_cmd useradd --system --gid "${APP_GROUP}" --home "${APP_HOME}" --shell /usr/sbin/nologin "${APP_USER}" run_cmd install -d -o "${APP_USER}" -g "${APP_GROUP}" "${APP_HOME}" "${APP_STATE_DIR}" "${APP_LOG_DIR}" run_cmd rsync -a \ --delete \ --exclude '.git' \ --exclude '.venv' \ --exclude '.pytest_cache' \ --exclude '__pycache__' \ --exclude '*.pyc' \ --exclude 'config_nodes.json' \ --exclude 'mapa_dispositivos.json' \ --exclude 'license.key' \ --exclude 'app_secret.key' \ "${REPO_ROOT}/" "${APP_HOME}/" run_cmd python3 -m venv "${APP_VENV}" run_cmd "${APP_VENV}/bin/python" -m pip install --upgrade pip run_cmd "${APP_VENV}/bin/python" -m pip install -r "${APP_HOME}/requirements.txt" for file_name in config_nodes.json mapa_dispositivos.json license.key app_secret.key; do if [[ -f "${REPO_ROOT}/${file_name}" && ! -f "${APP_STATE_DIR}/${file_name}" ]]; then run_cmd install -m 0600 -o "${APP_USER}" -g "${APP_GROUP}" "${REPO_ROOT}/${file_name}" "${APP_STATE_DIR}/${file_name}" fi done run_cmd chown -R "${APP_USER}:${APP_GROUP}" "${APP_HOME}" "${APP_STATE_DIR}" "${APP_LOG_DIR}" } configure_vfire_service() { log "Configurando servico systemd do V-Fire Monitor" cat >"${APP_ENV_FILE}" <"${APP_SYSTEMD_FILE}" <>"${file}" fi } configure_zabbix() { log "Configurando PostgreSQL e Zabbix" run_cmd systemctl enable --now postgresql ensure_postgres_role ensure_postgres_database import_zabbix_schema_if_needed set_ini_value "/etc/zabbix/zabbix_server.conf" "DBHost" "localhost" set_ini_value "/etc/zabbix/zabbix_server.conf" "DBName" "${ZABBIX_DB_NAME}" set_ini_value "/etc/zabbix/zabbix_server.conf" "DBUser" "${ZABBIX_DB_USER}" set_ini_value "/etc/zabbix/zabbix_server.conf" "DBPassword" "${ZABBIX_DB_PASSWORD}" run_cmd install -d -m 0750 /etc/zabbix/web cat > /etc/zabbix/web/zabbix.conf.php </etc/grafana/provisioning/datasources/zabbix.yaml </etc/systemd/system/grafana-server.service.d/override.conf </etc/grafana/provisioning/dashboards/vfire.yaml </dev/null 2>&1; then return 0 fi sleep "${delay}" done die "Servico indisponivel em ${url}" } zabbix_api_request() { local payload="$1" local auth_token="${2:-}" local curl_args=(-fsS -H 'Content-Type: application/json-rpc') [[ "${DRY_RUN}" == "1" ]] && return 0 if [[ -n "${auth_token}" ]]; then curl_args+=(-H "Authorization: Bearer ${auth_token}") fi curl "${curl_args[@]}" -d "${payload}" "http://127.0.0.1:${ZABBIX_WEB_PORT}/api_jsonrpc.php" } zabbix_error_message() { local response="$1" printf '%s' "${response}" | jq -r '.error.data // .error.message // empty' } zabbix_require_success() { local response="$1" local context="$2" local error_message error_message="$(zabbix_error_message "${response}")" if [[ -n "${error_message}" ]]; then die "${context}: ${error_message}" fi } configure_zabbix_admin_password() { local auth_payload auth_response auth_token user_id wait_for_http "http://127.0.0.1:${ZABBIX_WEB_PORT}/" auth_payload='{"jsonrpc":"2.0","method":"user.login","params":{"username":"Admin","password":"zabbix","userData":true},"id":1}' auth_response="$(zabbix_api_request "${auth_payload}" || true)" auth_token="$(printf '%s' "${auth_response}" | jq -r '.result.sessionid // empty')" user_id="$(printf '%s' "${auth_response}" | jq -r '.result.userid // empty')" if [[ -n "${auth_token}" && -n "${user_id}" && "${ZABBIX_ADMIN_PASSWORD}" != "zabbix" ]]; then auth_response="$(zabbix_api_request "$(jq -cn \ --arg userid "${user_id}" \ --arg current "zabbix" \ --arg passwd "${ZABBIX_ADMIN_PASSWORD}" \ '{jsonrpc:"2.0",method:"user.update",params:{userid:$userid,current_passwd:$current,passwd:$passwd},id:1}')" "${auth_token}" || true)" zabbix_require_success "${auth_response}" "Falha ao atualizar a senha administrativa do Zabbix" return 0 fi auth_payload="$(jq -cn --arg username "${ZABBIX_ADMIN_USERNAME}" --arg password "${ZABBIX_ADMIN_PASSWORD}" '{jsonrpc:"2.0",method:"user.login",params:{username:$username,password:$password},id:1}')" auth_response="$(zabbix_api_request "${auth_payload}" || true)" zabbix_require_success "${auth_response}" "Falha ao autenticar na API do Zabbix com as credenciais administrativas configuradas" auth_token="$(printf '%s' "${auth_response}" | jq -r '.result // empty')" [[ -n "${auth_token}" ]] || die "Nao foi possivel autenticar na API do Zabbix com as credenciais administrativas configuradas." } zabbix_login() { local response response="$(zabbix_api_request "$(jq -cn --arg username "${ZABBIX_ADMIN_USERNAME}" --arg password "${ZABBIX_ADMIN_PASSWORD}" '{jsonrpc:"2.0",method:"user.login",params:{username:$username,password:$password},id:1}')")" zabbix_require_success "${response}" "Falha ao obter token da API do Zabbix" printf '%s' "${response}" | jq -r '.result // empty' } zabbix_get_single_id() { local method="$1" local filter_key="$2" local filter_value="$3" local id_key="$4" local auth="$5" zabbix_api_request "$(jq -cn \ --arg method "${method}" \ --arg key "${filter_key}" \ --arg value "${filter_value}" \ '{jsonrpc:"2.0",method:$method,params:{output:["'"${id_key}"'"],filter:{($key):[$value]}},id:1}')" "${auth}" \ | jq -r ".result[0].${id_key} // empty" } import_zabbix_template() { local auth="$1" local source [[ -f "${ZABBIX_TEMPLATE_FILE}" ]] || die "Template do Zabbix nao encontrado: ${ZABBIX_TEMPLATE_FILE}" source="$(cat "${ZABBIX_TEMPLATE_FILE}")" zabbix_api_request "$(jq -cn \ --arg source "${source}" \ '{jsonrpc:"2.0",method:"configuration.import",params:{format:"yaml",source:$source,rules:{template_groups:{createMissing:true,updateExisting:true},templates:{createMissing:true,updateExisting:true},discoveryRules:{createMissing:true,updateExisting:true,deleteMissing:false},items:{createMissing:true,updateExisting:true,deleteMissing:false},triggers:{createMissing:true,updateExisting:true,deleteMissing:false},valueMaps:{createMissing:true,updateExisting:true,deleteMissing:false},templateDashboards:{createMissing:true,updateExisting:true,deleteMissing:false}}},id:1}')" "${auth}" >/dev/null } ensure_zabbix_host_group() { local auth="$1" local group_id group_id="$(zabbix_get_single_id "hostgroup.get" "name" "${ZABBIX_HOST_GROUP}" "groupid" "${auth}")" if [[ -n "${group_id}" ]]; then printf '%s' "${group_id}" return 0 fi zabbix_api_request "$(jq -cn --arg name "${ZABBIX_HOST_GROUP}" '{jsonrpc:"2.0",method:"hostgroup.create",params:{name:$name},id:1}')" "${auth}" | jq -r '.result.groupids[0]' } ensure_zabbix_monitored_host() { local auth="$1" local group_id="$2" local template_id host_id template_id="$(zabbix_get_single_id "template.get" "host" "${ZABBIX_TEMPLATE_NAME}" "templateid" "${auth}")" [[ -n "${template_id}" ]] || die "Template do Zabbix nao encontrado apos importacao: ${ZABBIX_TEMPLATE_NAME}" host_id="$(zabbix_get_single_id "host.get" "host" "${ZABBIX_MONITORED_HOST}" "hostid" "${auth}")" if [[ -n "${host_id}" ]]; then zabbix_api_request "$(jq -cn \ --arg hostid "${host_id}" \ --arg host "${ZABBIX_MONITORED_HOST}" \ --arg group_id "${group_id}" \ --arg template_id "${template_id}" \ '{jsonrpc:"2.0",method:"host.update",params:{hostid:$hostid,host:$host,name:$host,status:0,groups:[{groupid:$group_id}],templates:[{templateid:$template_id}]},id:1}')" "${auth}" >/dev/null printf '%s' "${host_id}" return 0 fi zabbix_api_request "$(jq -cn \ --arg host "${ZABBIX_MONITORED_HOST}" \ --arg group_id "${group_id}" \ --arg template_id "${template_id}" \ '{jsonrpc:"2.0",method:"host.create",params:{host:$host,name:$host,status:0,groups:[{groupid:$group_id}],templates:[{templateid:$template_id}],tags:[{tag:"application",value:"vfire-monitor"}]},id:1}')" "${auth}" | jq -r '.result.hostids[0]' } seed_vfire_runtime_config() { if [[ "${DRY_RUN}" == "1" ]]; then printf '[dry-run] seed runtime config at %s/config_nodes.json\n' "${APP_STATE_DIR}" return 0 fi "${APP_VENV}/bin/python" - <"${SUMMARY_FILE}" <