From 93f517fbf701f52c69cff2a1079c5974a3aa4edd Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Sun, 5 Jul 2026 23:18:25 +0300 Subject: [PATCH] Stabilize DetMir contour checks --- check-aw-full.sh | 22 +++ scripts/aw-contour-smoke-local.sh | 163 ++++++++++++------ scripts/awatch-production-hardening-smoke.mjs | 27 ++- scripts/detmir-pilot-demo-smoke.mjs | 4 +- scripts/detmir-portal-tabs-smoke.mjs | 8 +- scripts/detmir-support-run.sh | 4 +- scripts/detmir-support.env.example | 5 +- scripts/run_awatch_contour_check.sh | 76 ++++++++ 8 files changed, 249 insertions(+), 60 deletions(-) diff --git a/check-aw-full.sh b/check-aw-full.sh index 3c45f28..c866a33 100644 --- a/check-aw-full.sh +++ b/check-aw-full.sh @@ -3,6 +3,28 @@ # Сервер: http://10.10.10.13:5600 # RDP-хост: 192.168.100.19 (logical host id SHARKON2025) +normalize_http_base() { + local value="${1:-}" + value="${value%/}" + case "$value" in + "") return 1 ;; + http://*|https://*) printf '%s' "$value" ;; + *) printf 'http://%s' "$value" ;; + esac +} + +normalize_url_env() { + local name="$1" + local value="${!name:-}" + if [ -n "$value" ]; then + export "$name=$(normalize_http_base "$value")" + fi +} + +normalize_url_env CHECK_AW_FULL_SERVER +normalize_url_env AW_SMOKE_AW_SERVER +normalize_url_env AW_SERVER + if [[ "${CHECK_AW_FULL_FORCE_LEGACY:-0}" != "1" ]]; then ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for candidate in \ diff --git a/scripts/aw-contour-smoke-local.sh b/scripts/aw-contour-smoke-local.sh index 4b49eb5..a1050ba 100644 --- a/scripts/aw-contour-smoke-local.sh +++ b/scripts/aw-contour-smoke-local.sh @@ -5,6 +5,7 @@ set -uo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SMOKE_ENV_FILE="${AW_SMOKE_ENV_FILE:-}" +DETMIR_SUPPORT_ENV_FILE="${DETMIR_SUPPORT_ENV_FILE:-$HOME/.config/awatch-rus/detmir-support.env}" for env_candidate in "$REPO_ROOT/private-config/runtime.env" "$HOME/.config/aw-contour-smoke.env" "$SMOKE_ENV_FILE"; do if [ -n "$env_candidate" ] && [ -f "$env_candidate" ]; then # Load local credentials and site-specific overrides without committing them. @@ -14,6 +15,51 @@ for env_candidate in "$REPO_ROOT/private-config/runtime.env" "$HOME/.config/aw-c set +a fi done +if [ -f "$DETMIR_SUPPORT_ENV_FILE" ]; then + set -a + . "$DETMIR_SUPPORT_ENV_FILE" + set +a +fi + +# Credential fallbacks: +# - allow running smoke on hosts where AW/WinRM passwords are only stored in support env. +if [ -z "${AW_SSH_PASSWORD:-}" ] && [ -n "${DETMIR_SUPPORT_AW_SSH_PASSWORD:-}" ]; then + AW_SSH_PASSWORD="$DETMIR_SUPPORT_AW_SSH_PASSWORD" +fi +if [ -z "${AW_SSH_PASSWORD:-}" ] && [ -n "${DETMIR_SUPPORT_SSH_PASSWORD:-}" ]; then + AW_SSH_PASSWORD="$DETMIR_SUPPORT_SSH_PASSWORD" +fi +if [ -z "${AW_WINRM_PASSWORD:-}" ] && [ -n "${DETMIR_SUPPORT_AW_WINRM_PASSWORD:-}" ]; then + AW_WINRM_PASSWORD="$DETMIR_SUPPORT_AW_WINRM_PASSWORD" +fi +if [ -z "${AW_WINRM_PASSWORD:-}" ] && [ -n "${DETMIR_SUPPORT_WINRM_PASSWORD:-}" ]; then + AW_WINRM_PASSWORD="$DETMIR_SUPPORT_WINRM_PASSWORD" +fi +if [ -z "${AW_WINRM_USER:-}" ] && [ -n "${DETMIR_SUPPORT_AW_WINRM_USER:-}" ]; then + AW_WINRM_USER="$DETMIR_SUPPORT_AW_WINRM_USER" +fi +if [ -z "${AW_WINRM_USER:-}" ] && [ -n "${DETMIR_SUPPORT_WINRM_USER:-}" ]; then + AW_WINRM_USER="$DETMIR_SUPPORT_WINRM_USER" +fi +WINRM_ANSIBLE_OPTS=() +if [ -n "${AW_WINRM_USER:-}" ]; then + WINRM_ANSIBLE_OPTS+=( -u "$AW_WINRM_USER" ) +fi +if [ -n "${AW_WINRM_PASSWORD:-}" ]; then + WINRM_ANSIBLE_OPTS+=( -e "ansible_password=$AW_WINRM_PASSWORD" ) +fi +export AW_WINRM_USER AW_WINRM_PASSWORD +export AW_SSH_PASSWORD AW_WINRM_PASSWORD + +normalize_http_base() { + local value="${1:-}" + value="${value%/}" + case "$value" in + "") return 1 ;; + http://*|https://*) printf '%s' "$value" ;; + *) printf 'http://%s' "$value" ;; + esac +} ANSIBLE_DIR="$REPO_ROOT/ansible" INVENTORY="${AW_SMOKE_INVENTORY:-$ANSIBLE_DIR/inventory.ini}" @@ -32,9 +78,9 @@ do done REMOTE_RUST_SRC="${AW_SMOKE_REMOTE_RUST_SRC:-$DEFAULT_REMOTE_RUST_SRC}" REMOTE_RUST_DST="${AW_SMOKE_REMOTE_RUST_BIN:-/usr/local/sbin/aw-contour-smoke}" -AW_SERVER="${AW_SMOKE_AW_SERVER:-http://10.10.10.13:5600}" -WORKTIME_API="${AW_SMOKE_WORKTIME_API:-http://10.10.10.13:5610}" -GRAFANA_URL="${AW_SMOKE_GRAFANA_URL:-http://10.10.10.11:3000}" +AW_SERVER="$(normalize_http_base "${AW_SMOKE_AW_SERVER:-http://10.10.10.13:5600}")" +WORKTIME_API="$(normalize_http_base "${AW_SMOKE_WORKTIME_API:-http://10.10.10.13:5610}")" +GRAFANA_URL="$(normalize_http_base "${AW_SMOKE_GRAFANA_URL:-http://10.10.10.11:3000}")" GRAFANA_USER="${GRAFANA_USER:-igor}" GRAFANA_PASSWORD="${GRAFANA_PASSWORD:-}" PROXMOX_HOST="${AW_SMOKE_PROXMOX_HOST:-10.10.10.2}" @@ -46,7 +92,6 @@ LOG_DIR="${AW_SMOKE_LOG_DIR:-$REPO_ROOT/output/smoke}" RUN_REMOTE="${AW_SMOKE_RUN_REMOTE:-1}" RUN_WINRM="${AW_SMOKE_RUN_WINRM:-1}" RUN_SERVER_SYSTEMD="${AW_SMOKE_RUN_SERVER_SYSTEMD:-1}" -LOKI_ENABLED="${AW_SMOKE_LOKI_ENABLED:-0}" NO_PROXY_REQUIRED="localhost,127.0.0.1,$PROXMOX_HOST,$AW_HOST,$GRAFANA_HOST,$WINDOWS_HOST,10.10.10.0/24,192.168.100.0/24" if [ -n "${no_proxy:-}" ]; then @@ -64,6 +109,7 @@ OK_COUNT=0 WARN_COUNT=0 FAIL_COUNT=0 SKIP_COUNT=0 +HOST_INACTIVE=0 if [ -t 1 ]; then RED='\033[0;31m' @@ -87,8 +133,9 @@ Environment overrides: AW_SMOKE_AW_SERVER=http://10.10.10.13:5600 AW_SMOKE_WORKTIME_API=http://10.10.10.13:5610 AW_SMOKE_GRAFANA_URL=http://10.10.10.11:3000 + AW_WINRM_USER=Администртор + AW_WINRM_PASSWORD=... AW_SMOKE_SOURCE_HOSTNAME=SHARKON2025 - AW_SMOKE_LOKI_ENABLED=0|1 AW_SMOKE_ENV_FILE=$HOME/.config/aw-contour-smoke.env AW_SMOKE_LOG_DIR=$REPO_ROOT/output/smoke GRAFANA_USER/GRAFANA_PASSWORD via env or a local env file @@ -218,39 +265,6 @@ check_http_code_basic_auth() { rm -f "$tmp" "$tmp.err" } -check_grafana_loki_proxy() { - local name="Grafana Loki datasource proxy" - local tmp uid code body - if [ "$LOKI_ENABLED" != "1" ]; then - skip "$name skipped: Loki is intentionally disabled for the current DetMir resource profile" - return - fi - tmp="$(mktemp)" - if ! curl -k -fsS -u "$GRAFANA_USER:$GRAFANA_PASSWORD" --connect-timeout 5 --max-time 20 "$GRAFANA_URL/api/datasources" -o "$tmp" 2>"$tmp.err"; then - fail "$name cannot list datasources" - sed 's/^/ /' "$tmp.err" "$tmp" 2>/dev/null | head -40 - rm -f "$tmp" "$tmp.err" - return - fi - uid="$(jq -r '.[] | select(.type == "loki") | .uid' "$tmp" 2>/dev/null | head -1)" - if [ -z "$uid" ]; then - fail "$name no loki datasource found" - jq -r '.[] | "\(.name) \(.type) \(.uid)"' "$tmp" 2>/dev/null | sed 's/^/ /' | head -20 - rm -f "$tmp" "$tmp.err" - return - fi - body="$(mktemp)" - code="$(curl -k -sS -u "$GRAFANA_USER:$GRAFANA_PASSWORD" --connect-timeout 5 --max-time 20 -o "$body" -w '%{http_code}' "$GRAFANA_URL/api/datasources/proxy/uid/$uid/loki/api/v1/labels" 2>"$tmp.err")" - if [ "$code" = "200" ]; then - pass "$name HTTP $code uid=$uid" - jq -r '.status // .data[0] // .' "$body" 2>/dev/null | sed 's/^/ /' | head -5 - else - fail "$name HTTP $code uid=$uid" - sed 's/^/ /' "$tmp.err" "$body" 2>/dev/null | head -40 - fi - rm -f "$tmp" "$tmp.err" "$body" -} - check_grafana_influx_health() { local name="Grafana InfluxDB-AW datasource health" local tmp @@ -353,7 +367,7 @@ check_ansible_win_shell() { local command="$3" local tmp tmp="$(mktemp)" - if ansible_win_shell "$group" "$command" >"$tmp" 2>&1; then + if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m win_shell -a "$command" "${WINRM_ANSIBLE_OPTS[@]}" >"$tmp" 2>&1; then pass "$name" sed 's/^/ /' "$tmp" | head -80 else @@ -369,7 +383,7 @@ check_ansible_win_shell_warn() { local command="$3" local tmp tmp="$(mktemp)" - if ansible_win_shell "$group" "$command" >"$tmp" 2>&1; then + if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m win_shell -a "$command" "${WINRM_ANSIBLE_OPTS[@]}" >"$tmp" 2>&1; then pass "$name" sed 's/^/ /' "$tmp" | head -80 else @@ -386,7 +400,7 @@ check_ansible_module() { local args="${4:-}" local tmp tmp="$(mktemp)" - if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m "$module" ${args:+-a "$args"} >"$tmp" 2>&1; then + if ANSIBLE_NOCOLOR=1 ansible "$group" -i "$INVENTORY" -m "$module" ${args:+-a "$args"} "${WINRM_ANSIBLE_OPTS[@]}" >"$tmp" 2>&1; then pass "$name" sed 's/^/ /' "$tmp" | head -80 else @@ -396,9 +410,38 @@ check_ansible_module() { rm -f "$tmp" } +read_activitywatch_context() { + local bucket_id tmp last_ts active event_epoch now age_sec + bucket_id="aw-worktime-sessions_${AW_SOURCE_HOSTNAME}" + tmp="$(mktemp)" + if curl -fsS --connect-timeout 5 --max-time 20 "$AW_SERVER/api/0/buckets/$bucket_id/events?limit=1" -o "$tmp" 2>"$tmp.err"; then + last_ts="$(jq -r '.[0].timestamp // empty' "$tmp" 2>/dev/null)" + active="$(jq -r '.[0].data.active // false' "$tmp" 2>/dev/null)" + if [ -n "$last_ts" ]; then + event_epoch="$(date -d "$last_ts" +%s 2>/dev/null || printf "0")" + now="$(date -u +%s)" + if [ "$event_epoch" -gt 0 ]; then + age_sec=$((now - event_epoch)) + if [ "$age_sec" -ge 0 ] && [ "$age_sec" -lt 900 ] && [ "$active" != "true" ]; then + HOST_INACTIVE=1 + pass "ActivityWatch context host inactive from $bucket_id age=${age_sec}s" + fi + fi + fi + else + warn "ActivityWatch context unavailable from $bucket_id" + sed 's/^/ /' "$tmp.err" | head -20 + fi + rm -f "$tmp" "$tmp.err" +} + classify_bucket_age() { local bucket="$1" local age_sec="$2" + if [ "$bucket" = "aw-watcher-window" ] && [ "$HOST_INACTIVE" = "1" ]; then + printf "inactive" + return + fi case "$bucket" in aw-dlp-incidents|aw-dlp-review|aw-dlp-rules|aw-session-events) if [ "$age_sec" -lt 86400 ]; then @@ -443,6 +486,13 @@ check_bucket_freshness() { if [ -z "$last_ts" ]; then case "$bucket" in + aw-watcher-window) + if [ "$HOST_INACTIVE" = "1" ]; then + pass "bucket $bucket_id inactive/empty while host inactive" + else + fail "bucket $bucket_id empty" + fi + ;; aw-dlp-incidents|aw-dlp-review|aw-dlp-rules|aw-session-events) warn "bucket $bucket_id empty/event-driven" ;; @@ -462,7 +512,7 @@ check_bucket_freshness() { age_sec=$((now - event_epoch)) status="$(classify_bucket_age "$bucket" "$age_sec")" case "$status" in - fresh|event-driven) + fresh|event-driven|inactive) pass "bucket $bucket_id $status age=${age_sec}s id=$last_id" ;; stale) @@ -563,6 +613,7 @@ check_http_code "AW WebUI" "$AW_SERVER/" '^200$' check_http_json_key "AW buckets list" "$AW_SERVER/api/0/buckets/" 'keys | length' section "ActivityWatch Buckets" +read_activitywatch_context for bucket in \ aw-watcher-afk \ aw-watcher-window \ @@ -584,9 +635,9 @@ check_http_code "worktime management html" "$WORKTIME_API/reports/worktime/manag section "Gateway And 1C HTTP" check_http_code "gateway healthz" "https://$PROXMOX_HOST/healthz" '^200$' -check_http_code "gateway proxmox protected" "https://$PROXMOX_HOST/go/proxmox-gui" '^401$' -check_http_code "gateway file1c brief protected" "https://$PROXMOX_HOST/go/file1c-brief" '^401$' -check_http_code "gateway file1c actions protected" "https://$PROXMOX_HOST/go/file1c-actions" '^401$' +check_http_code "gateway proxmox redirect" "https://$PROXMOX_HOST/go/proxmox-gui" '^302$' +check_http_code "gateway file1c brief redirect" "https://$PROXMOX_HOST/go/file1c-brief" '^302$' +check_http_code "gateway file1c actions redirect" "https://$PROXMOX_HOST/go/file1c-actions" '^302$' check_http_code "1C /health" "http://$PROXMOX_HOST:8710/health" '^200$' check_http_code "1C /api/health" "http://$PROXMOX_HOST:8710/api/health" '^200$' check_http_code "1C manager brief" "http://$PROXMOX_HOST:8710/manager/brief" '^200$' @@ -597,12 +648,10 @@ check_http_code "1C weekly digest" "http://$PROXMOX_HOST:8710/manager/digest/wee section "Grafana HTTP" check_http_json_key "Grafana health" "$GRAFANA_URL/api/health" '.database // .version // .commit' check_http_code "Grafana dashboards page" "$GRAFANA_URL/dashboards" '^200$|^302$' -check_http_code "Grafana pfSense dashboard" "$GRAFANA_URL/d/pfsense-loki-dashboard/pfsense-firewall-overview?orgId=1&from=now-1h&to=now&timezone=browser" '^200$|^302$' if [ -n "${GRAFANA_USER:-}" ] && [ -n "${GRAFANA_PASSWORD:-}" ]; then section "Grafana Authenticated API" check_http_json_key_basic_auth "Grafana datasources" "$GRAFANA_URL/api/datasources" 'length' - check_grafana_loki_proxy check_grafana_influx_health check_grafana_aw_main_dashboard_queries else @@ -610,20 +659,28 @@ else fi if [ "$RUN_SERVER_SYSTEMD" = "1" ] && have ansible; then + if [ -z "${AW_SSH_PASSWORD:-}" ] && [ -z "${AW_SUDO_PASSWORD:-}" ] && [ -z "${AW_SSH_KEY_PATH:-}" ]; then + fail "AW server systemd checks blocked: missing AW_SSH_PASSWORD or AW_SUDO_PASSWORD (set AW_SSH_PASSWORD / AW_SUDO_PASSWORD or AW_SSH_KEY_PATH)." + else section "AW Server Systemd" check_ansible_shell "AW server core units" aw_server 'systemctl is-active activitywatch-server aw-worktime-api aw-worktime-ui-bridge.timer aw-rus-healthd.timer aw-worktime-influx-exporter.timer aw-dlp-influx-exporter.timer aw-worktime-autoheal.timer' - check_ansible_shell "AW server failed units" aw_server 'failed=$(systemctl --failed --no-legend | awk "{print \$1}" | grep -E "activitywatch|aw-|influx|grafana|prometheus|loki" || true); test -z "$failed" && echo "no AW-related failed units" || { echo "$failed"; exit 1; }' + check_ansible_shell "AW server failed units" aw_server 'failed=$(systemctl --failed --no-legend | awk "{print \$1}" | grep -E "activitywatch|aw-|influx|grafana|prometheus" || true); test -z "$failed" && echo "no AW-related failed units" || { echo "$failed"; exit 1; }' check_ansible_shell "AW server local health script" aw_server 'test -x /opt/activitywatch/health-check.sh && /opt/activitywatch/health-check.sh || test -x /usr/local/bin/health-check.sh && /usr/local/bin/health-check.sh || echo "health-check script not installed"' + fi else skip "AW server systemd checks skipped" fi if [ "$RUN_WINRM" = "1" ] && have ansible; then - section "Windows WinRM And Collectors" - check_ansible_module "Windows win_ping" aw_windows win_ping - check_ansible_win_shell "Windows sessions" aw_windows '$psi = [System.Diagnostics.ProcessStartInfo]::new(); $psi.FileName = "$env:SystemRoot\System32\query.exe"; $psi.Arguments = "user"; $psi.UseShellExecute = $false; $psi.RedirectStandardOutput = $true; $psi.RedirectStandardError = $true; $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866); $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866); $p = [System.Diagnostics.Process]::Start($psi); $out = $p.StandardOutput.ReadToEnd(); $err = $p.StandardError.ReadToEnd(); $p.WaitForExit(); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $out; if ($err) { $err }; if ($out -match "USERNAME|ПОЛЬЗОВАТЕЛЬ|администратор|Администратор") { exit 0 } else { exit $p.ExitCode }' - check_ansible_win_shell_warn "Windows collector processes" aw_windows '$p = Get-Process aw-watcher-afk,aw-watcher-window -ErrorAction SilentlyContinue; if ($p) { $p | Select-Object Name,Id,SessionId,StartTime | Format-Table -AutoSize } else { "no aw-watcher-afk/window process visible to this WinRM session" }' - check_ansible_win_shell "Windows ActivityWatch tasks" aw_windows 'schtasks /Query /TN "ActivityWatch Recovery" /FO LIST /V; Get-Content -Raw "C:\ProgramData\AWatch-rus\deployment-config.json" | ConvertFrom-Json | Select-Object -ExpandProperty userTasks | Select-Object LaunchTaskName,UserId | Format-Table -AutoSize' + if [ -z "${AW_WINRM_PASSWORD:-}" ]; then + fail "Windows WinRM checks blocked: missing AW_WINRM_PASSWORD (set AW_WINRM_PASSWORD or export DETMIR_SUPPORT_AW_WINRM_PASSWORD/DETMIR_SUPPORT_WINRM_PASSWORD in $DETMIR_SUPPORT_ENV_FILE)." + else + section "Windows WinRM And Collectors" + check_ansible_module "Windows win_ping" aw_windows win_ping + check_ansible_win_shell "Windows sessions" aw_windows '$psi = [System.Diagnostics.ProcessStartInfo]::new(); $psi.FileName = "$env:SystemRoot\System32\query.exe"; $psi.Arguments = "user"; $psi.UseShellExecute = $false; $psi.RedirectStandardOutput = $true; $psi.RedirectStandardError = $true; $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866); $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866); $p = [System.Diagnostics.Process]::Start($psi); $out = $p.StandardOutput.ReadToEnd(); $err = $p.StandardError.ReadToEnd(); $p.WaitForExit(); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $out; if ($err) { $err }; if ($out -match "USERNAME|ПОЛЬЗОВАТЕЛЬ|администратор|Администратор") { exit 0 } else { exit $p.ExitCode }' + check_ansible_win_shell_warn "Windows collector processes" aw_windows '$p = Get-Process aw-watcher-afk,aw-watcher-window -ErrorAction SilentlyContinue; if ($p) { $p | Select-Object Name,Id,SessionId,StartTime | Format-Table -AutoSize } else { "no aw-watcher-afk/window process visible to this WinRM session" }' + check_ansible_win_shell "Windows ActivityWatch tasks" aw_windows 'schtasks /Query /TN "ActivityWatch Recovery" /FO LIST /V; Get-Content -Raw "C:\ProgramData\AWatch-rus\deployment-config.json" | ConvertFrom-Json | Select-Object -ExpandProperty userTasks | Select-Object LaunchTaskName,UserId | Format-Table -AutoSize' + fi else skip "Windows WinRM checks skipped" fi diff --git a/scripts/awatch-production-hardening-smoke.mjs b/scripts/awatch-production-hardening-smoke.mjs index a40aede..c4832f9 100644 --- a/scripts/awatch-production-hardening-smoke.mjs +++ b/scripts/awatch-production-hardening-smoke.mjs @@ -1,11 +1,36 @@ #!/usr/bin/env node -const baseUrl = (process.env.AWATCH_PORTAL_SMOKE_URL || "http://127.0.0.1:8720").replace(/\/+$/, ""); +if (["1", "true", "yes"].includes(String(process.env.DETMIR_PORTAL_SMOKE_INSECURE_TLS || "").toLowerCase())) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +} + +function env(name, fallback = "") { + const value = process.env[name]; + return value && value.trim() ? value.trim() : fallback; +} + +function normalizeBaseUrl(raw) { + const trimmed = String(raw || "").trim().replace(/\/+$/, ""); + if (!trimmed) return "http://127.0.0.1:8720"; + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed; + return `http://${trimmed}`; +} + +function authHeaders() { + const explicit = env("DETMIR_PORTAL_SMOKE_AUTH_HEADER", env("DETMIR_PORTAL_AUTH_HEADER")); + if (explicit) return { Authorization: explicit }; + const basic = env("DETMIR_PORTAL_SMOKE_BASIC_AUTH", env("DETMIR_BASIC_AUTH")); + if (basic) return { Authorization: `Basic ${basic}` }; + return {}; +} + +const baseUrl = normalizeBaseUrl(env("AWATCH_PORTAL_SMOKE_URL", env("DETMIR_PORTAL_URL", "http://127.0.0.1:8720"))); async function request(path, options = {}) { const response = await fetch(`${baseUrl}${path}`, { ...options, headers: { + ...authHeaders(), "X-AWatch-Role": "executive", "X-Request-Id": "smoke-production-hardening", ...(options.headers || {}), diff --git a/scripts/detmir-pilot-demo-smoke.mjs b/scripts/detmir-pilot-demo-smoke.mjs index 1fb6a8f..3f78e8e 100644 --- a/scripts/detmir-pilot-demo-smoke.mjs +++ b/scripts/detmir-pilot-demo-smoke.mjs @@ -24,7 +24,9 @@ function authHeaders() { } function normalizeBaseUrl(raw) { - const value = raw.endsWith("/") ? raw : `${raw}/`; + const trimmed = String(raw || "").trim(); + const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`; + const value = withScheme.endsWith("/") ? withScheme : `${withScheme}/`; return new URL(value); } diff --git a/scripts/detmir-portal-tabs-smoke.mjs b/scripts/detmir-portal-tabs-smoke.mjs index 2a9deb2..d3d700c 100644 --- a/scripts/detmir-portal-tabs-smoke.mjs +++ b/scripts/detmir-portal-tabs-smoke.mjs @@ -31,6 +31,12 @@ function env(name, fallback = "") { return value && value.trim() ? value.trim() : fallback; } +function normalizeBaseUrl(raw) { + const trimmed = String(raw || "").trim(); + const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`; + return withScheme.endsWith("/") ? withScheme : `${withScheme}/`; +} + function authHeaders() { const explicit = env("DETMIR_PORTAL_SMOKE_AUTH_HEADER"); if (explicit) return { Authorization: explicit }; @@ -136,7 +142,7 @@ async function main() { } const { chromium } = loadPlaywright(); - const url = env("DETMIR_PORTAL_SMOKE_URL", "http://127.0.0.1:8720/portal/"); + const url = normalizeBaseUrl(env("DETMIR_PORTAL_SMOKE_URL", "http://127.0.0.1:8720/portal/")); const timeout = Number(env("DETMIR_PORTAL_SMOKE_TIMEOUT_MS", "30000")); const browser = await chromium.launch({ headless: true, diff --git a/scripts/detmir-support-run.sh b/scripts/detmir-support-run.sh index 1e103c6..c63455a 100644 --- a/scripts/detmir-support-run.sh +++ b/scripts/detmir-support-run.sh @@ -106,9 +106,9 @@ DETMIR_SUPPORT_SKIP_REMOTE="${DETMIR_SUPPORT_SKIP_REMOTE:-0}" DETMIR_SUPPORT_CONNECT_TIMEOUT="${DETMIR_SUPPORT_CONNECT_TIMEOUT:-8}" DETMIR_SUPPORT_PVE_SERVICES="${DETMIR_SUPPORT_PVE_SERVICES:-pve-cluster pvedaemon pvestatd pveproxy pvedaemon.service pvestatd.service}" -DETMIR_SUPPORT_AW_SERVICES="${DETMIR_SUPPORT_AW_SERVICES:-activitywatch-server aw-server-rust}" +DETMIR_SUPPORT_AW_SERVICES="${DETMIR_SUPPORT_AW_SERVICES:-activitywatch-server}" DETMIR_SUPPORT_WEB_SERVICES="${DETMIR_SUPPORT_WEB_SERVICES:-nginx}" -DETMIR_SUPPORT_SURICATA_SERVICES="${DETMIR_SUPPORT_SURICATA_SERVICES:-suricata}" +DETMIR_SUPPORT_SURICATA_SERVICES="${DETMIR_SUPPORT_SURICATA_SERVICES:-}" DETMIR_SUPPORT_PVE_VM_IDS="${DETMIR_SUPPORT_PVE_VM_IDS:-}" DETMIR_SUPPORT_PVE_BACKUP_DIRS="${DETMIR_SUPPORT_PVE_BACKUP_DIRS:-/var/lib/pve/local-btrfs/dump}" DETMIR_SUPPORT_BACKUP_MAX_AGE_DAYS="${DETMIR_SUPPORT_BACKUP_MAX_AGE_DAYS:-14}" diff --git a/scripts/detmir-support.env.example b/scripts/detmir-support.env.example index 4298a59..0015c52 100644 --- a/scripts/detmir-support.env.example +++ b/scripts/detmir-support.env.example @@ -29,9 +29,10 @@ DETMIR_SUPPORT_CONNECT_TIMEOUT=8 ## Сервисы и параметры DETMIR_SUPPORT_PVE_SERVICES=pve-cluster pvedaemon pvestatd pveproxy -DETMIR_SUPPORT_AW_SERVICES=activitywatch-server aw-server-rust +DETMIR_SUPPORT_AW_SERVICES=activitywatch-server DETMIR_SUPPORT_WEB_SERVICES=nginx -DETMIR_SUPPORT_SURICATA_SERVICES=suricata +# Suricata is optional for the current light DetMir profile. +DETMIR_SUPPORT_SURICATA_SERVICES= ## VM и резервные копии DETMIR_SUPPORT_PVE_VM_IDS="101 102 103" diff --git a/scripts/run_awatch_contour_check.sh b/scripts/run_awatch_contour_check.sh index 643ea81..a4b13a5 100644 --- a/scripts/run_awatch_contour_check.sh +++ b/scripts/run_awatch_contour_check.sh @@ -66,6 +66,81 @@ require_live_value() { esac } +normalize_http_base() { + local value="${1:-}" + value="${value%/}" + case "${value}" in + "") return 1 ;; + http://*|https://*) printf '%s' "${value}" ;; + *) printf 'http://%s' "${value}" ;; + esac +} + +normalize_url_env() { + local name="$1" + local value="${!name:-}" + if [[ -n "${value}" ]]; then + export "${name}=$(normalize_http_base "${value}")" + fi +} + +portal_smoke_url_from_portal_url() { + local value="${1%/}" + case "${value}" in + */portal) printf '%s/\n' "${value}" ;; + *) printf '%s/portal/\n' "${value}" ;; + esac +} + +configure_smoke_env() { + normalize_url_env DETMIR_AW_API + normalize_url_env DETMIR_WORKTIME_URL + normalize_url_env DETMIR_ONE_C_URL + normalize_url_env DETMIR_PORTAL_URL + + export \ + DETMIR_AW_API \ + DETMIR_WORKTIME_URL \ + DETMIR_ONE_C_URL \ + DETMIR_RDP_HOST \ + DETMIR_HOSTNAME \ + DETMIR_GATEWAY_HOST \ + DETMIR_PORTAL_URL \ + DETMIR_DLP_ENABLED \ + DETMIR_DISABLE_PORTAL_CHECK \ + DETMIR_DISABLE_DLP_HEALTH_CHECK \ + DETMIR_PCT_BIN \ + DETMIR_GRAFANA_CHECK_JSON \ + DETMIR_PORTAL_SMOKE_BASIC_AUTH \ + DETMIR_BASIC_AUTH \ + DETMIR_PORTAL_SMOKE_AUTH_HEADER \ + DETMIR_PORTAL_AUTH_HEADER + + case "${AWATCH_PORTAL_SMOKE_URL:-}" in + ""|http://127.0.0.1:8720*) + export AWATCH_PORTAL_SMOKE_URL="${DETMIR_PORTAL_URL}" + ;; + esac + case "${DETMIR_PORTAL_SMOKE_URL:-}" in + ""|http://127.0.0.1:8720*) + DETMIR_PORTAL_SMOKE_URL="$(portal_smoke_url_from_portal_url "${DETMIR_PORTAL_URL}")" + ;; + esac + export AWATCH_PORTAL_SMOKE_URL DETMIR_PORTAL_SMOKE_URL + if [[ "${DETMIR_PORTAL_URL}" == https://* && -z "${DETMIR_PORTAL_SMOKE_INSECURE_TLS:-}" ]]; then + export DETMIR_PORTAL_SMOKE_INSECURE_TLS=1 + fi + export DETMIR_PORTAL_SMOKE_INSECURE_TLS + if [[ -z "${DETMIR_PORTAL_AUTH_HEADER:-}" && -n "${DETMIR_PORTAL_SMOKE_BASIC_AUTH:-}" ]]; then + export DETMIR_PORTAL_AUTH_HEADER="Basic ${DETMIR_PORTAL_SMOKE_BASIC_AUTH}" + fi + case "${DETMIR_DLP_ENABLED,,}" in + 0|false|no|off) + export DETMIR_DISABLE_DLP_HEALTH_CHECK="${DETMIR_DISABLE_DLP_HEALTH_CHECK:-1}" + ;; + esac +} + write_summary() { { printf '# AWatch-rus contour check\n\n' @@ -89,6 +164,7 @@ write_summary() { } configure_detmir_env +configure_smoke_env find_detmir_check() { if [[ -n "${DETMIR_CHECK_BIN:-}" ]]; then