Harden DetMir runtime hot paths
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MODE="repo"
|
||||
AW_API="${AW_API:-http://127.0.0.1:5600}"
|
||||
AW_WORKTIME_API="${AW_WORKTIME_API:-http://127.0.0.1:5610}"
|
||||
AW_LOGICAL_HOST_ID="${AW_LOGICAL_HOST_ID:-${AW_MONITORED_WINDOWS_HOSTNAME:-SHARKON2025}}"
|
||||
AW_READINESS_TIMEOUT_SECONDS="${AW_READINESS_TIMEOUT_SECONDS:-60}"
|
||||
AW_READINESS_INTERVAL_SECONDS="${AW_READINESS_INTERVAL_SECONDS:-2}"
|
||||
AW_EVENTS_LIMIT="${AW_EVENTS_LIMIT:-100}"
|
||||
AW_EVENTS_MAX_SECONDS="${AW_EVENTS_MAX_SECONDS:-15}"
|
||||
HAYA_ROOT="${AW_HAYABUSA_ROOT:-/opt/hayabusa}"
|
||||
HAYA_DROP_DIR="${AW_HAYABUSA_DROP_DIR:-/opt/activitywatch/aw-rus-ops/drop}"
|
||||
SQLITE_DB="${AW_SQLITE_DB:-/var/lib/activitywatch/aw-server-rust/sqlite.db}"
|
||||
MAX_INCOMING_AGE_SECONDS="${MAX_HAYABUSA_INCOMING_AGE_SECONDS:-900}"
|
||||
STRICT_SECRETS="${DETMIR_RESILIENCE_STRICT_SECRETS:-0}"
|
||||
EXPECT_DLP_PROFILE="${DETMIR_RESILIENCE_EXPECT_DLP_PROFILE:-light}"
|
||||
EXPECT_OPTIONAL_DLP_OFF="${DETMIR_RESILIENCE_EXPECT_OPTIONAL_DLP_OFF:-0}"
|
||||
EXPECT_LOKI_OFF="${DETMIR_RESILIENCE_EXPECT_LOKI_OFF:-1}"
|
||||
|
||||
DLP_RUNTIME_UNITS=(
|
||||
aw-dlp-influx-exporter.timer
|
||||
aw-dlp-influx-exporter.service
|
||||
activitywatch-dlp-aggregator.timer
|
||||
activitywatch-dlp-aggregator.service
|
||||
aw-dlp-report-scheduler.timer
|
||||
aw-dlp-report-scheduler.service
|
||||
aw-dlp-syslog-forwarder.timer
|
||||
aw-dlp-syslog-forwarder.service
|
||||
aw-dlp-webhook-sender.timer
|
||||
aw-dlp-webhook-sender.service
|
||||
aw-dlp-cef-exporter.timer
|
||||
aw-dlp-cef-exporter.service
|
||||
aw-dlp-ioc-refresh.timer
|
||||
aw-dlp-ioc-refresh.service
|
||||
aw-dlp-policy-engine.service
|
||||
aw-dlp-case-management.service
|
||||
detmir-portal-evidence.service
|
||||
)
|
||||
|
||||
DLP_LIGHT_ALLOWED_UNITS=(
|
||||
activitywatch-dlp-aggregator.timer
|
||||
activitywatch-dlp-aggregator.service
|
||||
aw-dlp-ioc-refresh.timer
|
||||
aw-dlp-ioc-refresh.service
|
||||
detmir-dlp-load-guard.timer
|
||||
detmir-dlp-load-guard.service
|
||||
)
|
||||
|
||||
DLP_HEAVY_RUNTIME_UNITS=(
|
||||
aw-dlp-influx-exporter.timer
|
||||
aw-dlp-influx-exporter.service
|
||||
aw-dlp-report-scheduler.timer
|
||||
aw-dlp-report-scheduler.service
|
||||
aw-dlp-syslog-forwarder.timer
|
||||
aw-dlp-syslog-forwarder.service
|
||||
aw-dlp-webhook-sender.timer
|
||||
aw-dlp-webhook-sender.service
|
||||
aw-dlp-cef-exporter.timer
|
||||
aw-dlp-cef-exporter.service
|
||||
aw-dlp-policy-engine.service
|
||||
aw-dlp-case-management.service
|
||||
detmir-portal-evidence.service
|
||||
)
|
||||
|
||||
LOKI_RUNTIME_UNITS=(
|
||||
loki.service
|
||||
promtail.service
|
||||
)
|
||||
|
||||
OK_COUNT=0
|
||||
WARN_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/detmir_resilience_check.sh [--repo|--live|--all]
|
||||
|
||||
Modes:
|
||||
--repo check repository hardening and docs only (default, CI-safe)
|
||||
--live read-only checks for the local AW server/Hayabusa host
|
||||
--all repo + live
|
||||
|
||||
Environment:
|
||||
AW_API=http://127.0.0.1:5600
|
||||
AW_WORKTIME_API=http://127.0.0.1:5610
|
||||
AW_LOGICAL_HOST_ID=SHARKON2025
|
||||
AW_READINESS_TIMEOUT_SECONDS=60
|
||||
AW_EVENTS_MAX_SECONDS=15
|
||||
AW_HAYABUSA_ROOT=/opt/hayabusa
|
||||
AW_HAYABUSA_DROP_DIR=/opt/activitywatch/aw-rus-ops/drop
|
||||
AW_SQLITE_DB=/var/lib/activitywatch/aw-server-rust/sqlite.db
|
||||
DETMIR_RESILIENCE_EXPECT_DLP_PROFILE=light
|
||||
DETMIR_RESILIENCE_EXPECT_OPTIONAL_DLP_OFF=0
|
||||
DETMIR_RESILIENCE_EXPECT_LOKI_OFF=1
|
||||
DETMIR_RESILIENCE_STRICT_SECRETS=1
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--repo) MODE="repo"; shift ;;
|
||||
--live) MODE="live"; shift ;;
|
||||
--all) MODE="all"; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ok() {
|
||||
OK_COUNT=$((OK_COUNT + 1))
|
||||
printf '[OK] %s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
WARN_COUNT=$((WARN_COUNT + 1))
|
||||
printf '[WARN] %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
printf '[FAIL] %s\n' "$*"
|
||||
}
|
||||
|
||||
have() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
require_file() {
|
||||
local path="$1"
|
||||
if [[ -f "$ROOT_DIR/$path" ]]; then
|
||||
ok "file exists: $path"
|
||||
else
|
||||
fail "missing file: $path"
|
||||
fi
|
||||
}
|
||||
|
||||
require_pattern() {
|
||||
local path="$1"
|
||||
local pattern="$2"
|
||||
local label="$3"
|
||||
if grep -Eq "$pattern" "$ROOT_DIR/$path"; then
|
||||
ok "$label"
|
||||
else
|
||||
fail "$label"
|
||||
fi
|
||||
}
|
||||
|
||||
check_repo() {
|
||||
printf '== repo resilience checks ==\n'
|
||||
|
||||
require_file "scripts/detmir_resilience_check.sh"
|
||||
require_file "docs/DETMIR_RESILIENCE_HARDENING_RU.md"
|
||||
require_file "docs/DLP_RESOURCE_PROFILES_RU.md"
|
||||
require_file "docs/DLP_OPTIONAL_RUNTIME_RU.md"
|
||||
require_file "scripts/detmir_dlp_load_guard.sh"
|
||||
require_file "scripts/detmir_dlp_warehouse_sync.sh"
|
||||
require_file "aw-server/hayabusa/aw-hayabusa.sh"
|
||||
require_file "adk-rust/crates/hayabusa-tools/src/bin/autoprocess.rs"
|
||||
require_file "windows/AWatchRusCollectorGuardService.cs"
|
||||
require_file "windows/install-collector-guard-service.ps1"
|
||||
|
||||
require_pattern "aw-server/hayabusa/aw-hayabusa.sh" "HAYA_QUARANTINE_DIR" "Hayabusa wrapper has quarantine root"
|
||||
require_pattern "aw-server/hayabusa/aw-hayabusa.sh" "quarantine_incoming_package" "Hayabusa wrapper isolates incoming poison packages"
|
||||
require_pattern "adk-rust/crates/hayabusa-tools/src/bin/autoprocess.rs" "validate_drop_inputs" "Hayabusa autoprocess validates drop package before accept"
|
||||
require_pattern "adk-rust/crates/hayabusa-tools/src/bin/autoprocess.rs" "quarantine_drop_package" "Hayabusa autoprocess quarantines bad drop package"
|
||||
require_pattern "windows/AWatchRusCollectorGuardService.cs" "Process\\.Exited|ChildExited" "Collector guard service watches child exit"
|
||||
require_pattern "windows/AWatchRusCollectorGuardService.cs" "MaxChildRestartsInWindow" "Collector guard service has bounded child restart budget"
|
||||
require_pattern "windows/install-collector-guard-service.ps1" "failureflag" "Collector guard installer enables SCM failureflag"
|
||||
require_pattern "docs/DETMIR_RESILIENCE_HARDENING_RU.md" "Hayabusa poison-package isolation" "Resilience doc records Hayabusa hardening"
|
||||
require_pattern "docs/DETMIR_RESILIENCE_HARDENING_RU.md" "Windows collector guard service child watchdog" "Resilience doc records guard child watchdog"
|
||||
require_pattern "docs/DLP_RESOURCE_PROFILES_RU.md" "core_only" "DLP resource profiles document core_only"
|
||||
require_pattern "docs/DLP_RESOURCE_PROFILES_RU.md" "auto.?disable|автоотключ" "DLP resource profiles document auto-disable guard"
|
||||
require_pattern "docs/DLP_RESOURCE_PROFILES_RU.md" "rollback" "DLP resource profiles document rollback"
|
||||
require_pattern "docs/DETMIR_CURRENT_STATE_RU.md" "AW_DLP_PROFILE=light" "Current state records DLP light profile"
|
||||
require_pattern "scripts/detmir_dlp_runtime_control.sh" "set-profile" "DLP runtime control supports profile switching"
|
||||
require_pattern "scripts/detmir_dlp_runtime_control.sh" "rollback_dlp" "DLP runtime control supports rollback"
|
||||
require_pattern "scripts/detmir_dlp_load_guard.sh" "set-profile core_only" "DLP load guard can auto-disable DLP to core_only"
|
||||
require_pattern "scripts/detmir_dlp_load_guard.sh" "STRIKES_REQUIRED" "DLP load guard requires consecutive overload checks"
|
||||
require_pattern "scripts/detmir_dlp_warehouse_sync.sh" "sqlite3 .*\\.backup" "DLP warehouse sync uses SQLite backup"
|
||||
require_pattern "ansible/group_vars/all.yml" 'aw_dlp_profile: "light"' "Production defaults keep DLP profile light"
|
||||
require_pattern "ansible/group_vars/all.yml" 'aw_dlp_enabled: true' "Production defaults enable lightweight DLP"
|
||||
require_pattern "ansible/group_vars/all.yml" 'aw_dlp_influx_enabled: false' "Production defaults keep DLP Influx disabled"
|
||||
require_pattern "ansible/group_vars/all.yml" 'aw_dlp_light_collector_enabled: true' "Production defaults enable lightweight DLP collector"
|
||||
require_pattern "ansible/group_vars/all.yml" 'aw_dlp_light_guard_enabled: true' "Production defaults enable DLP load guard"
|
||||
require_pattern "ansible/group_vars/all.yml" 'detmir_portal_dlp_module_enabled_override: true' "Production defaults expose DLP light status to portal"
|
||||
require_pattern "ansible/deploy_aw_server.yml" "detmir-dlp-load-guard.service" "AW server deploy installs DLP load guard service"
|
||||
require_pattern "ansible/deploy_aw_server.yml" "CPUQuota=.*aw_dlp_aggregator_cpu_quota" "DLP aggregator has systemd CPU quota"
|
||||
require_pattern "ansible/deploy_detmir_portal.yml" "detmir-dlp-warehouse-sync.service" "Portal deploy installs DLP warehouse sync service"
|
||||
require_pattern "ansible/deploy_detmir_portal.yml" "detmir_portal_dlp_module_enabled_override \\| default\\(false\\)" "Portal deploy defaults DLP module to disabled"
|
||||
|
||||
if [[ -f "$ROOT_DIR/ansible/inventory.ini" ]] && grep -Eq '(^|[[:space:]])ansible_(become_)?password[[:space:]]*=[[:space:]]*[^<{]' "$ROOT_DIR/ansible/inventory.ini"; then
|
||||
if [[ "$STRICT_SECRETS" == "1" ]]; then
|
||||
fail "ansible/inventory.ini appears to contain literal password assignments; move them to vault/env"
|
||||
else
|
||||
warn "ansible/inventory.ini appears to contain literal password assignments; strict mode would fail"
|
||||
fi
|
||||
else
|
||||
ok "no literal ansible password assignments detected in ansible/inventory.ini"
|
||||
fi
|
||||
}
|
||||
|
||||
check_systemd_unit() {
|
||||
local unit="$1"
|
||||
if ! have systemctl; then
|
||||
warn "systemctl unavailable; skipping $unit"
|
||||
return
|
||||
fi
|
||||
if systemctl is-active --quiet "$unit"; then
|
||||
ok "systemd active: $unit"
|
||||
else
|
||||
fail "systemd not active: $unit"
|
||||
fi
|
||||
}
|
||||
|
||||
check_aw_api() {
|
||||
if ! have curl; then
|
||||
warn "curl unavailable; skipping AW API check"
|
||||
return
|
||||
fi
|
||||
local deadline last_code elapsed
|
||||
deadline=$((SECONDS + AW_READINESS_TIMEOUT_SECONDS))
|
||||
last_code=""
|
||||
while (( SECONDS <= deadline )); do
|
||||
elapsed="$(
|
||||
curl -sS --connect-timeout 3 --max-time 8 -o /dev/null -w '%{http_code} %{time_total}' \
|
||||
"$AW_API/api/0/info" 2>/dev/null || true
|
||||
)"
|
||||
last_code="${elapsed%% *}"
|
||||
if [[ "$last_code" == "200" ]]; then
|
||||
ok "ActivityWatch API readiness /api/0/info returns 200 (${elapsed#* }s)"
|
||||
return
|
||||
fi
|
||||
sleep "$AW_READINESS_INTERVAL_SECONDS"
|
||||
done
|
||||
case "$last_code" in
|
||||
503) fail "ActivityWatch API readiness ended on 503; possible datastore lock poisoning" ;;
|
||||
""|000) fail "ActivityWatch API did not become ready within ${AW_READINESS_TIMEOUT_SECONDS}s" ;;
|
||||
*) fail "ActivityWatch API readiness unexpected final HTTP status: $last_code" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_aw_hot_path() {
|
||||
if ! have curl; then
|
||||
warn "curl unavailable; skipping AW hot-path event check"
|
||||
return
|
||||
fi
|
||||
local url result code elapsed
|
||||
url="${AW_API%/}/api/0/buckets/aw-worktime-sessions_${AW_LOGICAL_HOST_ID}/events?limit=${AW_EVENTS_LIMIT}"
|
||||
result="$(curl -sS --connect-timeout 3 --max-time "$AW_EVENTS_MAX_SECONDS" -o /dev/null -w '%{http_code} %{time_total}' "$url" 2>/dev/null || true)"
|
||||
code="${result%% *}"
|
||||
elapsed="${result#* }"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
fail "ActivityWatch worktime events hot path returned HTTP ${code:-none}"
|
||||
return
|
||||
fi
|
||||
if awk -v t="$elapsed" -v max="$AW_EVENTS_MAX_SECONDS" 'BEGIN { exit !(t <= max) }'; then
|
||||
ok "ActivityWatch worktime events hot path returns 200 (${elapsed}s, limit=${AW_EVENTS_LIMIT})"
|
||||
else
|
||||
fail "ActivityWatch worktime events hot path exceeded ${AW_EVENTS_MAX_SECONDS}s (${elapsed}s)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_worktime_api() {
|
||||
if ! have curl; then
|
||||
warn "curl unavailable; skipping Worktime API check"
|
||||
return
|
||||
fi
|
||||
local tmp code
|
||||
tmp="$(mktemp)"
|
||||
code="$(curl -sS --connect-timeout 3 --max-time 12 -o "$tmp" -w '%{http_code}' \
|
||||
"${AW_WORKTIME_API%/}/reports/worktime/today?format=json&host=${AW_LOGICAL_HOST_ID}&allow_stale=1" 2>/dev/null || true)"
|
||||
if [[ "$code" != "200" ]]; then
|
||||
fail "Worktime API today report returned HTTP ${code:-none}"
|
||||
rm -f "$tmp"
|
||||
return
|
||||
fi
|
||||
if have jq; then
|
||||
if jq -e '(.degraded // false) == false' "$tmp" >/dev/null 2>&1; then
|
||||
ok "Worktime API today report is not degraded"
|
||||
else
|
||||
fail "Worktime API today report is degraded"
|
||||
fi
|
||||
if jq -e '((.rows // .users // []) | length) > 0' "$tmp" >/dev/null 2>&1; then
|
||||
ok "Worktime API today report has employee rows"
|
||||
else
|
||||
fail "Worktime API today report has no employee rows"
|
||||
fi
|
||||
else
|
||||
ok "Worktime API today report returns 200"
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
check_failed_units() {
|
||||
if ! have systemctl; then
|
||||
return
|
||||
fi
|
||||
local failed
|
||||
failed="$(systemctl --failed --no-legend --plain 2>/dev/null | wc -l | tr -d ' ')"
|
||||
if [[ "$failed" == "0" ]]; then
|
||||
ok "systemd failed units count is 0"
|
||||
else
|
||||
fail "systemd failed units count is $failed"
|
||||
fi
|
||||
}
|
||||
|
||||
count_files() {
|
||||
local dir="$1"
|
||||
local pattern="$2"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
printf '0\n'
|
||||
return
|
||||
fi
|
||||
find "$dir" -maxdepth 1 -type f -name "$pattern" | wc -l | tr -d ' '
|
||||
}
|
||||
|
||||
check_hayabusa_queues() {
|
||||
local incoming_dir="$HAYA_ROOT/inbox/incoming"
|
||||
local quarantine_dir="$HAYA_ROOT/quarantine"
|
||||
local incoming_count drop_count old_count quarantine_count
|
||||
incoming_count="$(count_files "$incoming_dir" '*.zip')"
|
||||
drop_count="$(count_files "$HAYA_DROP_DIR" '*.zip')"
|
||||
old_count="0"
|
||||
if [[ -d "$incoming_dir" ]]; then
|
||||
old_count="$(find "$incoming_dir" -maxdepth 1 -type f -name '*.zip' -mmin "+$((MAX_INCOMING_AGE_SECONDS / 60))" | wc -l | tr -d ' ')"
|
||||
fi
|
||||
if [[ "$incoming_count" == "0" ]]; then
|
||||
ok "Hayabusa incoming zip count is 0"
|
||||
else
|
||||
warn "Hayabusa incoming zip count is $incoming_count"
|
||||
fi
|
||||
if [[ "$drop_count" == "0" ]]; then
|
||||
ok "Hayabusa drop zip count is 0"
|
||||
else
|
||||
warn "Hayabusa drop zip count is $drop_count"
|
||||
fi
|
||||
if [[ "$old_count" == "0" ]]; then
|
||||
ok "Hayabusa incoming has no stale zip older than ${MAX_INCOMING_AGE_SECONDS}s"
|
||||
else
|
||||
fail "Hayabusa incoming has $old_count stale zip package(s)"
|
||||
fi
|
||||
if [[ -d "$quarantine_dir" ]]; then
|
||||
quarantine_count="$(find "$quarantine_dir" -type f -name reason.json | wc -l | tr -d ' ')"
|
||||
if [[ "$quarantine_count" == "0" ]]; then
|
||||
ok "Hayabusa quarantine reason count is 0"
|
||||
else
|
||||
warn "Hayabusa quarantine reason count is $quarantine_count; review/replay policy required"
|
||||
fi
|
||||
else
|
||||
warn "Hayabusa quarantine root not found yet: $quarantine_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
check_sqlite_files() {
|
||||
if [[ ! -f "$SQLITE_DB" ]]; then
|
||||
warn "AW SQLite DB not found at $SQLITE_DB; skipping local DB size check"
|
||||
return
|
||||
fi
|
||||
local db_size wal_size
|
||||
db_size="$(stat -c '%s' "$SQLITE_DB")"
|
||||
wal_size="0"
|
||||
[[ -f "$SQLITE_DB-wal" ]] && wal_size="$(stat -c '%s' "$SQLITE_DB-wal")"
|
||||
if (( db_size > 5 * 1024 * 1024 * 1024 )); then
|
||||
fail "AW SQLite DB exceeds 5GiB"
|
||||
elif (( db_size > 2 * 1024 * 1024 * 1024 )); then
|
||||
warn "AW SQLite DB exceeds 2GiB"
|
||||
else
|
||||
ok "AW SQLite DB size below 2GiB"
|
||||
fi
|
||||
if (( wal_size > 1024 * 1024 * 1024 )); then
|
||||
fail "AW SQLite WAL exceeds 1GiB"
|
||||
elif (( wal_size > 256 * 1024 * 1024 )); then
|
||||
warn "AW SQLite WAL exceeds 256MiB"
|
||||
else
|
||||
ok "AW SQLite WAL size below 256MiB"
|
||||
fi
|
||||
}
|
||||
|
||||
check_sqlite_hot_path_index() {
|
||||
if [[ ! -f "$SQLITE_DB" ]]; then
|
||||
warn "AW SQLite DB not found at $SQLITE_DB; skipping hot-path index check"
|
||||
return
|
||||
fi
|
||||
if ! have sqlite3; then
|
||||
warn "sqlite3 unavailable; skipping hot-path index check"
|
||||
return
|
||||
fi
|
||||
local index_exists plan
|
||||
index_exists="$(sqlite3 "$SQLITE_DB" "SELECT name FROM sqlite_master WHERE type='index' AND name='events_bucketrow_starttime_desc_index';" 2>/dev/null || true)"
|
||||
if [[ "$index_exists" == "events_bucketrow_starttime_desc_index" ]]; then
|
||||
ok "AW SQLite hot-path index exists"
|
||||
else
|
||||
fail "AW SQLite hot-path index events_bucketrow_starttime_desc_index is missing"
|
||||
return
|
||||
fi
|
||||
plan="$(
|
||||
sqlite3 "$SQLITE_DB" "EXPLAIN QUERY PLAN SELECT id,starttime,endtime,data FROM events WHERE bucketrow=(SELECT id FROM buckets WHERE name='aw-worktime-sessions_${AW_LOGICAL_HOST_ID}') ORDER BY starttime DESC LIMIT ${AW_EVENTS_LIMIT};" 2>/dev/null || true
|
||||
)"
|
||||
if printf '%s' "$plan" | grep -q 'events_bucketrow_starttime_desc_index'; then
|
||||
ok "AW SQLite worktime event query uses hot-path index"
|
||||
else
|
||||
fail "AW SQLite worktime event query does not use hot-path index"
|
||||
fi
|
||||
if printf '%s' "$plan" | grep -q 'TEMP B-TREE'; then
|
||||
fail "AW SQLite worktime event query still builds TEMP B-TREE"
|
||||
else
|
||||
ok "AW SQLite worktime event query avoids TEMP B-TREE"
|
||||
fi
|
||||
}
|
||||
|
||||
check_units_inactive() {
|
||||
local label="$1"
|
||||
shift
|
||||
if ! have systemctl; then
|
||||
warn "systemctl unavailable; skipping $label runtime check"
|
||||
return
|
||||
fi
|
||||
local active_units=()
|
||||
local unit active
|
||||
for unit in "$@"; do
|
||||
active="$(systemctl is-active "$unit" 2>/dev/null || true)"
|
||||
if [[ "$active" == "active" || "$active" == "activating" ]]; then
|
||||
active_units+=("$unit:$active")
|
||||
fi
|
||||
done
|
||||
if [[ "${#active_units[@]}" -eq 0 ]]; then
|
||||
ok "$label runtime units are inactive"
|
||||
else
|
||||
fail "$label runtime units active: ${active_units[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_live() {
|
||||
printf '== live resilience checks ==\n'
|
||||
check_systemd_unit "activitywatch-server"
|
||||
check_systemd_unit "aw-worktime-api"
|
||||
check_aw_api
|
||||
check_aw_hot_path
|
||||
check_worktime_api
|
||||
check_failed_units
|
||||
check_hayabusa_queues
|
||||
check_sqlite_files
|
||||
check_sqlite_hot_path_index
|
||||
if [[ "$EXPECT_OPTIONAL_DLP_OFF" == "1" || "$EXPECT_DLP_PROFILE" == "core_only" ]]; then
|
||||
check_units_inactive "optional DLP" "${DLP_RUNTIME_UNITS[@]}"
|
||||
elif [[ "$EXPECT_DLP_PROFILE" == "light" ]]; then
|
||||
check_units_inactive "heavy DLP" "${DLP_HEAVY_RUNTIME_UNITS[@]}"
|
||||
else
|
||||
warn "optional DLP runtime inactive check skipped for profile=$EXPECT_DLP_PROFILE"
|
||||
fi
|
||||
if [[ "$EXPECT_LOKI_OFF" == "1" ]]; then
|
||||
check_units_inactive "Loki" "${LOKI_RUNTIME_UNITS[@]}"
|
||||
else
|
||||
warn "Loki inactive check skipped by env"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
repo) check_repo ;;
|
||||
live) check_live ;;
|
||||
all) check_repo; check_live ;;
|
||||
*) fail "invalid mode: $MODE" ;;
|
||||
esac
|
||||
|
||||
printf 'summary: ok=%s warn=%s fail=%s\n' "$OK_COUNT" "$WARN_COUNT" "$FAIL_COUNT"
|
||||
if (( FAIL_COUNT > 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user