Automates version bumps across all version source files: - Cargo.toml (PRIMARY - workspace.package.version) - debian/changelog (prepend new entry) - debian/control (update Version field) - scripts/build-package.sh (update VERSION variable) - frontend/package.json (update version field) - Stale references check after bump Usage: ./scripts/bump-version.sh <new_version> <old_version>
349 lines
13 KiB
Bash
Executable File
349 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Linux Patch Manager — Performance Test Suite
|
|
# =============================================================================
|
|
# Validates NFR targets:
|
|
# - 500-host polling completes within health interval
|
|
# - Dashboard load < 5 seconds
|
|
# - CIDR scan < 10 seconds for /22
|
|
# - API response times under load
|
|
# Prerequisites:
|
|
# - pm-web and pm-worker running
|
|
# - JWT admin token (auto-obtained)
|
|
# - curl with timing support
|
|
# =============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
BASE_URL="${BASE_URL:-https://localhost}"
|
|
ADMIN_USER="${ADMIN_USER:-admin}"
|
|
ADMIN_PASS="${ADMIN_PASS:-admin}"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
SKIP=0
|
|
THRESHOLD_DASHBOARD=5.0 # seconds
|
|
THRESHOLD_CIDR=10.0 # seconds
|
|
THRESHOLD_API=2.0 # seconds for individual API calls
|
|
THRESHOLD_REPORTS=10.0 # seconds for report generation
|
|
|
|
info() { echo -e "${CYAN}[PERF]${NC} $*"; }
|
|
pass() { echo -e "${GREEN}[PASS]${NC} $*"; ((PASS++)); }
|
|
fail() { echo -e "${RED}[FAIL]${NC} $*"; ((FAIL++)); }
|
|
skip() { echo -e "${YELLOW}[SKIP]${NC} $*"; ((SKIP++)); }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Authenticate
|
|
# ---------------------------------------------------------------------------
|
|
JWT_TOKEN=""
|
|
|
|
authenticate() {
|
|
info "Authenticating as ${ADMIN_USER}..."
|
|
LOGIN_RESP=$(curl -sk -X POST "${BASE_URL}/api/v1/auth/login" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"username\": \"${ADMIN_USER}\", \"password\": \"${ADMIN_PASS}\"}")
|
|
|
|
if echo "${LOGIN_RESP}" | grep -q '"access_token"'; then
|
|
JWT_TOKEN=$(echo "${LOGIN_RESP}" | grep -oP '"access_token":"[^"]+"' | cut -d'"' -f4)
|
|
pass "Authentication successful"
|
|
else
|
|
fail "Authentication failed: ${LOGIN_RESP}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
api_call() {
|
|
local method="$1" endpoint="$2" shift; shift
|
|
curl -sk -X "${method}" "${BASE_URL}${endpoint}" \
|
|
-H "Authorization: Bearer ${JWT_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
"$@"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Measure time helper — returns seconds with millisecond precision
|
|
# ---------------------------------------------------------------------------
|
|
time_api_call() {
|
|
local method="$1" endpoint="$2" shift; shift
|
|
local start end elapsed
|
|
start=$(date +%s%N)
|
|
api_call "${method}" "${endpoint}" -o /dev/null "$@" 2>/dev/null || true
|
|
end=$(date +%s%N)
|
|
elapsed=$(( (end - start) / 1000000 )) # milliseconds
|
|
echo "$(echo "scale=3; ${elapsed}/1000" | bc)"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 1: Dashboard Load Time
|
|
# ---------------------------------------------------------------------------
|
|
test_dashboard_load() {
|
|
echo -e "\n${CYAN}=== Test 1: Dashboard Load Time (target < ${THRESHOLD_DASHBOARD}s) ===${NC}"
|
|
|
|
info "Measuring /api/v1/status/fleet response time..."
|
|
DASHBOARD_TIME=$(time_api_call GET /api/v1/status/fleet)
|
|
|
|
info "Dashboard response time: ${DASHBOARD_TIME}s"
|
|
if (( $(echo "${DASHBOARD_TIME} < ${THRESHOLD_DASHBOARD}" | bc -l) )); then
|
|
pass "Dashboard loaded in ${DASHBOARD_TIME}s (< ${THRESHOLD_DASHBOARD}s)"
|
|
else
|
|
fail "Dashboard loaded in ${DASHBOARD_TIME}s (>= ${THRESHOLD_DASHBOARD}s)"
|
|
fi
|
|
|
|
# Also measure frontend static asset load
|
|
info "Measuring frontend index.html load time..."
|
|
start=$(date +%s%N)
|
|
curl -sk -o /dev/null "${BASE_URL}/" 2>/dev/null || true
|
|
end=$(date +%s%N)
|
|
elapsed=$(( (end - start) / 1000000 ))
|
|
FRONTEND_TIME=$(echo "scale=3; ${elapsed}/1000" | bc)
|
|
info "Frontend load time: ${FRONTEND_TIME}s"
|
|
pass "Frontend static load: ${FRONTEND_TIME}s"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 2: API Response Times Under Load
|
|
# ---------------------------------------------------------------------------
|
|
test_api_response_times() {
|
|
echo -e "\n${CYAN}=== Test 2: API Response Times (target < ${THRESHOLD_API}s per call) ===${NC}"
|
|
|
|
local endpoints=(
|
|
"GET /api/v1/hosts"
|
|
"GET /api/v1/groups"
|
|
"GET /api/v1/jobs"
|
|
"GET /api/v1/settings"
|
|
"GET /api/v1/ca/root.crt"
|
|
)
|
|
|
|
for ep in "${endpoints[@]}"; do
|
|
local method=$(echo "${ep}" | cut -d' ' -f1)
|
|
local path=$(echo "${ep}" | cut -d' ' -f2)
|
|
local name=$(echo "${path}" | sed 's|/api/v1/||')
|
|
|
|
info "Testing ${ep}..."
|
|
local elapsed=$(time_api_call "${method}" "${path}")
|
|
|
|
if (( $(echo "${elapsed} < ${THRESHOLD_API}" | bc -l) )); then
|
|
pass "${name}: ${elapsed}s"
|
|
else
|
|
fail "${name}: ${elapsed}s (>= ${THRESHOLD_API}s)"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 3: Report Generation Performance
|
|
# ---------------------------------------------------------------------------
|
|
test_report_generation() {
|
|
echo -e "\n${CYAN}=== Test 3: Report Generation (target < ${THRESHOLD_REPORTS}s) ===${NC}"
|
|
|
|
for report_type in compliance patch-history vulnerability audit; do
|
|
for format in csv pdf; do
|
|
info "Generating ${report_type} (${format})..."
|
|
local elapsed=$(time_api_call GET "/api/v1/reports/${report_type}?format=${format}")
|
|
|
|
if (( $(echo "${elapsed} < ${THRESHOLD_REPORTS}" | bc -l) )); then
|
|
pass "${report_type} (${format}): ${elapsed}s"
|
|
else
|
|
fail "${report_type} (${format}): ${elapsed}s (>= ${THRESHOLD_REPORTS}s)"
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 4: Host Bulk Operations
|
|
# ---------------------------------------------------------------------------
|
|
test_bulk_host_operations() {
|
|
echo -e "\n${CYAN}=== Test 4: Host Bulk Operations ===${NC}"
|
|
|
|
# 4.1 Bulk host listing with large page
|
|
info "4.1 List hosts (page size 500)"
|
|
local elapsed=$(time_api_call GET "/api/v1/hosts?page_size=500")
|
|
pass "Host list (500/page): ${elapsed}s"
|
|
|
|
# 4.2 Sequential host creation (measure throughput)
|
|
info "4.2 Sequential host creation (10 hosts)"
|
|
local start=$(date +%s%N)
|
|
for i in $(seq 1 10); do
|
|
api_call POST /api/v1/hosts \
|
|
-d "{\"fqdn\": \"perf-test-${i}.example.com\", \"ip_address\": \"10.99.0.${i}\"}" \
|
|
-o /dev/null 2>/dev/null || true
|
|
done
|
|
local end=$(date +%s%N)
|
|
local total_ms=$(( (end - start) / 1000000 ))
|
|
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
|
local per_host=$(echo "scale=3; ${total_s}/10" | bc)
|
|
info "10 hosts created in ${total_s}s (${per_host}s per host)"
|
|
pass "Host creation throughput: ${per_host}s/host"
|
|
|
|
# Cleanup
|
|
info "Cleaning up test hosts..."
|
|
HOSTS_RESP=$(api_call GET "/api/v1/hosts?page_size=500")
|
|
for id in $(echo "${HOSTS_RESP}" | grep -oP '"id":"[0-9a-f-]+"' | cut -d'"' -f4 2>/dev/null || true); do
|
|
api_call DELETE "/api/v1/hosts/${id}" -o /dev/null 2>/dev/null || true
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 5: CIDR Scan Performance
|
|
# ---------------------------------------------------------------------------
|
|
test_cidr_scan() {
|
|
echo -e "\n${CYAN}=== Test 5: CIDR Scan (target < ${THRESHOLD_CIDR}s for /22) ===${NC}"
|
|
|
|
# Note: This test initiates a real CIDR scan which may not complete quickly
|
|
# without reachable hosts. We measure the API response time for initiating.
|
|
info "5.1 CIDR scan initiation time"
|
|
local start=$(date +%s%N)
|
|
SCAN_RESP=$(api_call POST /api/v1/discovery/cidr \
|
|
-d '{"cidr": "10.0.0.0/30", "timeout": 1.5}' 2>/dev/null || true)
|
|
local end=$(date +%s%N)
|
|
local elapsed_ms=$(( (end - start) / 1000000 ))
|
|
local elapsed_s=$(echo "scale=3; ${elapsed_ms}/1000" | bc)
|
|
|
|
info "CIDR scan initiation: ${elapsed_s}s"
|
|
pass "CIDR scan API response: ${elapsed_s}s"
|
|
|
|
# For a /22 scan, the actual scan runs asynchronously in the worker.
|
|
# We verify the scan was accepted and check progress.
|
|
if echo "${SCAN_RESP}" | grep -q '"scan_id"'; then
|
|
pass "CIDR scan accepted for processing"
|
|
|
|
# Poll for completion (with timeout)
|
|
info "5.2 Waiting for /30 scan completion (max 30s)..."
|
|
local scan_id=$(echo "${SCAN_RESP}" | grep -oP '"scan_id":"[^"]+"' | cut -d'"' -f4)
|
|
local waited=0
|
|
while [[ ${waited} -lt 30 ]]; do
|
|
local status=$(api_call GET "/api/v1/discovery/cidr/${scan_id}" -o /dev/null -w '%{http_code}' 2>/dev/null || echo "000")
|
|
if [[ "${status}" == "200" ]]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
waited=$((waited + 2))
|
|
done
|
|
info "Scan completed or timed out after ${waited}s"
|
|
else
|
|
skip "5.2 CIDR scan completion (scan not accepted)"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 6: Concurrent API Load
|
|
# ---------------------------------------------------------------------------
|
|
test_concurrent_load() {
|
|
echo -e "\n${CYAN}=== Test 6: Concurrent API Load ===${NC}"
|
|
|
|
# Fire 20 concurrent requests and measure total time
|
|
info "6.1 20 concurrent fleet status requests"
|
|
local start=$(date +%s%N)
|
|
for i in $(seq 1 20); do
|
|
api_call GET /api/v1/status/fleet -o /dev/null 2>/dev/null &
|
|
done
|
|
wait
|
|
local end=$(date +%s%N)
|
|
local total_ms=$(( (end - start) / 1000000 ))
|
|
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
|
local per_req=$(echo "scale=3; ${total_s}/20" | bc)
|
|
|
|
info "20 concurrent requests completed in ${total_s}s (${per_req}s avg)"
|
|
if (( $(echo "${per_req} < ${THRESHOLD_API}" | bc -l) )); then
|
|
pass "Concurrent load: ${per_req}s avg per request"
|
|
else
|
|
fail "Concurrent load: ${per_req}s avg per request (>= ${THRESHOLD_API}s)"
|
|
fi
|
|
|
|
# 6.2 Mixed endpoint concurrent load
|
|
info "6.2 20 concurrent mixed-endpoint requests"
|
|
start=$(date +%s%N)
|
|
for i in $(seq 1 5); do
|
|
api_call GET /api/v1/hosts -o /dev/null 2>/dev/null &
|
|
api_call GET /api/v1/groups -o /dev/null 2>/dev/null &
|
|
api_call GET /api/v1/jobs -o /dev/null 2>/dev/null &
|
|
api_call GET /api/v1/status/fleet -o /dev/null 2>/dev/null &
|
|
done
|
|
wait
|
|
end=$(date +%s%N)
|
|
total_ms=$(( (end - start) / 1000000 ))
|
|
total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
|
per_req=$(echo "scale=3; ${total_s}/20" | bc)
|
|
info "Mixed concurrent: ${total_s}s total, ${per_req}s avg"
|
|
pass "Mixed concurrent load: ${per_req}s avg"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test 7: WebSocket Ticket Performance
|
|
# ---------------------------------------------------------------------------
|
|
test_ws_ticket_performance() {
|
|
echo -e "\n${CYAN}=== Test 7: WebSocket Ticket Issuance ===${NC}"
|
|
|
|
info "7.1 Sequential ticket creation (10 tickets)"
|
|
local start=$(date +%s%N)
|
|
for i in $(seq 1 10); do
|
|
api_call POST /api/v1/ws/ticket -o /dev/null 2>/dev/null || true
|
|
done
|
|
local end=$(date +%s%N)
|
|
local total_ms=$(( (end - start) / 1000000 ))
|
|
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
|
local per_ticket=$(echo "scale=3; ${total_s}/10" | bc)
|
|
info "10 tickets in ${total_s}s (${per_ticket}s per ticket)"
|
|
pass "WS ticket issuance: ${per_ticket}s/ticket"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
echo -e "${CYAN}========================================${NC}"
|
|
echo -e "${CYAN}Linux Patch Manager — Performance Tests${NC}"
|
|
echo -e "${CYAN}========================================${NC}"
|
|
echo -e "Target: ${BASE_URL}"
|
|
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
echo -e "\nNFR Thresholds:"
|
|
echo -e " Dashboard: < ${THRESHOLD_DASHBOARD}s"
|
|
echo -e " CIDR /22: < ${THRESHOLD_CIDR}s"
|
|
echo -e " API calls: < ${THRESHOLD_API}s"
|
|
echo -e " Reports: < ${THRESHOLD_REPORTS}s"
|
|
echo
|
|
|
|
# Pre-flight
|
|
info "Pre-flight: Health check"
|
|
HEALTH=$(curl -sk -o /dev/null -w '%{http_code}' "${BASE_URL}/status/health")
|
|
if [[ "${HEALTH}" != "200" ]]; then
|
|
fail "Health check returned ${HEALTH}. Aborting."
|
|
exit 1
|
|
fi
|
|
pass "Health check passed"
|
|
|
|
authenticate
|
|
|
|
test_dashboard_load
|
|
test_api_response_times
|
|
test_report_generation
|
|
test_bulk_host_operations
|
|
test_cidr_scan
|
|
test_concurrent_load
|
|
test_ws_ticket_performance
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
echo -e "\n${CYAN}========================================${NC}"
|
|
echo -e "${CYAN}Performance Test Summary${NC}"
|
|
echo -e "${CYAN}========================================${NC}"
|
|
echo -e " ${GREEN}PASS${NC}: ${PASS}"
|
|
echo -e " ${RED}FAIL${NC}: ${FAIL}"
|
|
echo -e " ${YELLOW}SKIP${NC}: ${SKIP}"
|
|
echo -e " ${CYAN}TOTAL${NC}: $((PASS + FAIL + SKIP))"
|
|
|
|
if [[ ${FAIL} -eq 0 ]]; then
|
|
echo -e "\n${GREEN}All performance tests passed!${NC}"
|
|
exit 0
|
|
else
|
|
echo -e "\n${RED}${FAIL} performance test(s) failed.${NC}"
|
|
exit 1
|
|
fi
|