a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
849 lines
25 KiB
Bash
849 lines
25 KiB
Bash
#!/bin/bash
|
|
################################################################################
|
|
# Script Name: alertmanager-silence-manager.sh
|
|
# Version: 1.0
|
|
# Description: CLI tool for managing Prometheus Alertmanager silences.
|
|
# Create, bulk-create, extend, expire, list, audit, and export
|
|
# silences via the Alertmanager API v2. Supports dry-run mode,
|
|
# pattern-based operations, and YAML bulk silence files.
|
|
#
|
|
# Author: Phil Connor
|
|
# Contact: contact@mylinux.work
|
|
# Website: https://mylinux.work
|
|
# License: MIT
|
|
#
|
|
# Prerequisites:
|
|
# - curl
|
|
# - jq
|
|
# - Alertmanager running and accessible
|
|
#
|
|
# Usage:
|
|
# # Create a single silence
|
|
# ./alertmanager-silence-manager.sh create --matcher 'alertname=HighCPU' --duration 2h --comment "Maintenance"
|
|
#
|
|
# # Bulk create from YAML
|
|
# ./alertmanager-silence-manager.sh bulk-create --file maintenance.yaml
|
|
#
|
|
# # List active silences
|
|
# ./alertmanager-silence-manager.sh list --state active
|
|
#
|
|
# # Extend a silence
|
|
# ./alertmanager-silence-manager.sh extend --id abc12345 --duration 1h
|
|
#
|
|
# # Expire a silence
|
|
# ./alertmanager-silence-manager.sh expire --id abc12345
|
|
#
|
|
# # Export active silences to YAML
|
|
# ./alertmanager-silence-manager.sh export --output silences.yaml
|
|
#
|
|
# # Audit silences
|
|
# ./alertmanager-silence-manager.sh audit
|
|
#
|
|
# Configuration:
|
|
# ALERTMANAGER_URL Alertmanager base URL (default: http://localhost:9093)
|
|
# SILENCE_AUTHOR Author name for silences (default: current user)
|
|
# SILENCE_COMMENT_PREFIX Prefix for all silence comments (default: none)
|
|
#
|
|
################################################################################
|
|
|
|
# ============================================================================
|
|
# CONFIGURATION VARIABLES
|
|
# ============================================================================
|
|
|
|
AM_URL="${ALERTMANAGER_URL:-http://localhost:9093}"
|
|
AUTHOR="${SILENCE_AUTHOR:-$(whoami)}"
|
|
COMMENT_PREFIX="${SILENCE_COMMENT_PREFIX:-}"
|
|
DRY_RUN=false
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# ============================================================================
|
|
# HELPER FUNCTIONS
|
|
# ============================================================================
|
|
|
|
show_usage() {
|
|
cat <<EOF
|
|
Usage: $0 <command> [OPTIONS]
|
|
|
|
Manage Prometheus Alertmanager silences via the API.
|
|
|
|
COMMANDS:
|
|
create Create a single silence
|
|
bulk-create Create silences from a YAML file
|
|
list List silences in table format
|
|
extend Extend a silence by duration
|
|
expire Expire silences by ID or pattern
|
|
export Export active silences to YAML
|
|
audit Show detailed silence audit info
|
|
|
|
CREATE OPTIONS:
|
|
--matcher STR Label matcher (e.g., 'alertname=HighCPU'), repeatable
|
|
--duration STR Duration (e.g., 2h, 30m, 1d)
|
|
--comment STR Silence comment/reason
|
|
--dry-run Preview without creating
|
|
|
|
BULK-CREATE OPTIONS:
|
|
--file PATH Path to YAML silence definitions
|
|
--dry-run Preview without creating
|
|
|
|
LIST OPTIONS:
|
|
--state STR Filter: active, pending, expired, all (default: active)
|
|
|
|
EXTEND OPTIONS:
|
|
--id ID Silence ID to extend
|
|
--duration STR Additional duration (e.g., 1h)
|
|
--dry-run Preview without extending
|
|
|
|
EXPIRE OPTIONS:
|
|
--id ID Silence ID to expire
|
|
--match STR Pattern match on comment (e.g., 'comment=~maintenance.*')
|
|
--dry-run Preview without expiring
|
|
|
|
EXPORT OPTIONS:
|
|
--output PATH Output file (default: stdout)
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
ALERTMANAGER_URL Base URL (default: http://localhost:9093)
|
|
SILENCE_AUTHOR Author name (default: current user)
|
|
SILENCE_COMMENT_PREFIX Prefix for comments
|
|
|
|
EXAMPLES:
|
|
$0 create --matcher 'alertname=HighCPU' --matcher 'instance=web-01' --duration 2h --comment "Maintenance"
|
|
$0 bulk-create --file maintenance-window.yaml --dry-run
|
|
$0 list --state active
|
|
$0 extend --id 4a2f8c3e --duration 1h
|
|
$0 expire --match 'comment=~maintenance.*'
|
|
$0 export --output silences-backup.yaml
|
|
$0 audit
|
|
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
log_dry_run() { echo -e "${YELLOW}[DRY RUN]${NC} $*"; }
|
|
|
|
check_requirements() {
|
|
local missing=0
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
log_error "curl not found"
|
|
missing=1
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
log_error "jq not found"
|
|
missing=1
|
|
fi
|
|
|
|
return $missing
|
|
}
|
|
|
|
check_connectivity() {
|
|
local status
|
|
status=$(curl -sf --connect-timeout 5 --max-time 10 -o /dev/null -w "%{http_code}" "${AM_URL}/api/v2/status" 2>/dev/null)
|
|
|
|
if [ "$status" != "200" ]; then
|
|
log_error "Cannot reach Alertmanager at ${AM_URL} (HTTP $status)"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Query Alertmanager API
|
|
# Args: $1 - method, $2 - endpoint, $3 - data (optional)
|
|
am_api() {
|
|
local method="$1" endpoint="$2" data="$3"
|
|
|
|
if [ -n "$data" ]; then
|
|
curl -sf --connect-timeout 5 --max-time 15 \
|
|
-X "$method" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$data" \
|
|
"${AM_URL}${endpoint}" 2>/dev/null
|
|
else
|
|
curl -sf --connect-timeout 5 --max-time 15 \
|
|
-X "$method" \
|
|
"${AM_URL}${endpoint}" 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
# Parse duration string to seconds
|
|
# Supports: 30s, 5m, 2h, 1d
|
|
parse_duration() {
|
|
local input="$1"
|
|
local num unit
|
|
|
|
num=$(echo "$input" | sed 's/[^0-9]//g')
|
|
unit=$(echo "$input" | sed 's/[0-9]//g')
|
|
|
|
case "$unit" in
|
|
s) echo "$num" ;;
|
|
m) echo $((num * 60)) ;;
|
|
h) echo $((num * 3600)) ;;
|
|
d) echo $((num * 86400)) ;;
|
|
*) log_error "Invalid duration unit: $unit (use s/m/h/d)"; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Get ISO 8601 timestamp for now + offset seconds
|
|
# Args: $1 - offset in seconds (default: 0)
|
|
iso_timestamp() {
|
|
local offset="${1:-0}"
|
|
date -u -d "+${offset} seconds" "+%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || \
|
|
date -u -v "+${offset}S" "+%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null
|
|
}
|
|
|
|
# Parse a matcher string like 'alertname=HighCPU' or 'instance=~web-0[1-3]'
|
|
# Returns JSON object
|
|
parse_matcher() {
|
|
local input="$1"
|
|
local name value is_regex is_equal
|
|
|
|
if [[ "$input" == *"=~"* ]]; then
|
|
name="${input%%=~*}"
|
|
value="${input#*=~}"
|
|
is_regex="true"
|
|
is_equal="true"
|
|
elif [[ "$input" == *"!~"* ]]; then
|
|
name="${input%%!~*}"
|
|
value="${input#*!~}"
|
|
is_regex="true"
|
|
is_equal="false"
|
|
elif [[ "$input" == *"!="* ]]; then
|
|
name="${input%%!=*}"
|
|
value="${input#*!=}"
|
|
is_regex="false"
|
|
is_equal="false"
|
|
elif [[ "$input" == *"="* ]]; then
|
|
name="${input%%=*}"
|
|
value="${input#*=}"
|
|
is_regex="false"
|
|
is_equal="true"
|
|
else
|
|
log_error "Invalid matcher format: $input"
|
|
return 1
|
|
fi
|
|
|
|
jq -n --arg n "$name" --arg v "$value" \
|
|
--argjson r "$is_regex" --argjson e "$is_equal" \
|
|
'{name: $n, value: $v, isRegex: $r, isEqual: $e}'
|
|
}
|
|
|
|
# Truncate string to max length
|
|
truncate() {
|
|
local str="$1" max="${2:-30}"
|
|
if [ ${#str} -gt "$max" ]; then
|
|
echo "${str:0:$((max-2))}.."
|
|
else
|
|
echo "$str"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# CREATE COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_create() {
|
|
local matchers_json="[]"
|
|
local duration=""
|
|
local comment=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--matcher)
|
|
local m
|
|
m=$(parse_matcher "$2") || exit 1
|
|
matchers_json=$(echo "$matchers_json" | jq --argjson m "$m" '. + [$m]')
|
|
shift 2 ;;
|
|
--duration) duration="$2"; shift 2 ;;
|
|
--comment) comment="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
*) log_error "Unknown option for create: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ "$(echo "$matchers_json" | jq 'length')" -eq 0 ]; then
|
|
log_error "At least one --matcher is required"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$duration" ]; then
|
|
log_error "--duration is required"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$comment" ]; then
|
|
log_error "--comment is required"
|
|
exit 1
|
|
fi
|
|
|
|
local duration_secs
|
|
duration_secs=$(parse_duration "$duration") || exit 1
|
|
|
|
local starts_at ends_at
|
|
starts_at=$(iso_timestamp 0)
|
|
ends_at=$(iso_timestamp "$duration_secs")
|
|
|
|
local full_comment="${COMMENT_PREFIX}${comment}"
|
|
|
|
local payload
|
|
payload=$(jq -n \
|
|
--argjson matchers "$matchers_json" \
|
|
--arg startsAt "$starts_at" \
|
|
--arg endsAt "$ends_at" \
|
|
--arg createdBy "$AUTHOR" \
|
|
--arg comment "$full_comment" \
|
|
'{matchers: $matchers, startsAt: $startsAt, endsAt: $endsAt, createdBy: $createdBy, comment: $comment}')
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log_dry_run "Would create silence:"
|
|
echo "$payload" | jq .
|
|
return
|
|
fi
|
|
|
|
local response
|
|
response=$(am_api POST "/api/v2/silences" "$payload")
|
|
|
|
if [ $? -eq 0 ] && [ -n "$response" ]; then
|
|
local sid
|
|
sid=$(echo "$response" | jq -r '.silenceID // empty')
|
|
if [ -n "$sid" ]; then
|
|
log_info "Silence created: ${sid}"
|
|
else
|
|
log_info "Silence created"
|
|
fi
|
|
else
|
|
log_error "Failed to create silence"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# BULK-CREATE COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_bulk_create() {
|
|
local file=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--file) file="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
*) log_error "Unknown option for bulk-create: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$file" ] || [ ! -f "$file" ]; then
|
|
log_error "Valid --file is required"
|
|
exit 1
|
|
fi
|
|
|
|
local success=0 failed=0
|
|
local in_silence=false
|
|
local in_matchers=false
|
|
local matchers_json="[]"
|
|
local duration="" comment=""
|
|
local current_matcher_name="" current_matcher_value="" current_matcher_regex="false"
|
|
|
|
flush_silence() {
|
|
if [ -z "$duration" ] || [ "$(echo "$matchers_json" | jq 'length')" -eq 0 ]; then
|
|
return
|
|
fi
|
|
|
|
local duration_secs starts_at ends_at full_comment payload
|
|
duration_secs=$(parse_duration "$duration") || { ((failed++)); return; }
|
|
starts_at=$(iso_timestamp 0)
|
|
ends_at=$(iso_timestamp "$duration_secs")
|
|
full_comment="${COMMENT_PREFIX}${comment}"
|
|
|
|
payload=$(jq -n \
|
|
--argjson matchers "$matchers_json" \
|
|
--arg startsAt "$starts_at" \
|
|
--arg endsAt "$ends_at" \
|
|
--arg createdBy "$AUTHOR" \
|
|
--arg comment "$full_comment" \
|
|
'{matchers: $matchers, startsAt: $startsAt, endsAt: $endsAt, createdBy: $createdBy, comment: $comment}')
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log_dry_run "Would create silence:"
|
|
echo "$payload" | jq .
|
|
echo ""
|
|
((success++))
|
|
return
|
|
fi
|
|
|
|
local response
|
|
response=$(am_api POST "/api/v2/silences" "$payload")
|
|
|
|
if [ $? -eq 0 ] && [ -n "$response" ]; then
|
|
local sid
|
|
sid=$(echo "$response" | jq -r '.silenceID // empty')
|
|
log_info "Silence created: ${sid:-ok} (${comment})"
|
|
((success++))
|
|
else
|
|
log_error "Failed to create silence: ${comment}"
|
|
((failed++))
|
|
fi
|
|
}
|
|
|
|
flush_matcher() {
|
|
if [ -n "$current_matcher_name" ]; then
|
|
local m
|
|
m=$(jq -n --arg n "$current_matcher_name" --arg v "$current_matcher_value" \
|
|
--argjson r "$current_matcher_regex" \
|
|
'{name: $n, value: $v, isRegex: $r, isEqual: true}')
|
|
matchers_json=$(echo "$matchers_json" | jq --argjson m "$m" '. + [$m]')
|
|
current_matcher_name=""
|
|
current_matcher_value=""
|
|
current_matcher_regex="false"
|
|
fi
|
|
}
|
|
|
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
# Strip leading/trailing whitespace for comparison
|
|
local trimmed
|
|
trimmed=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
|
|
|
|
# Skip comments and empty lines
|
|
[[ "$trimmed" =~ ^# ]] && continue
|
|
[ -z "$trimmed" ] && continue
|
|
|
|
# New silence block
|
|
if [[ "$trimmed" == "- matchers:" ]]; then
|
|
flush_matcher
|
|
if [ "$in_silence" = true ]; then
|
|
flush_silence
|
|
fi
|
|
in_silence=true
|
|
in_matchers=true
|
|
matchers_json="[]"
|
|
duration=""
|
|
comment=""
|
|
continue
|
|
fi
|
|
|
|
# Matcher entry
|
|
if [[ "$trimmed" == "- name:"* ]] && [ "$in_matchers" = true ]; then
|
|
flush_matcher
|
|
current_matcher_name=$(echo "$trimmed" | sed 's/^- name:[[:space:]]*//')
|
|
continue
|
|
fi
|
|
|
|
if [[ "$trimmed" == "value:"* ]] && [ "$in_matchers" = true ]; then
|
|
current_matcher_value=$(echo "$trimmed" | sed 's/^value:[[:space:]]*//')
|
|
continue
|
|
fi
|
|
|
|
if [[ "$trimmed" == "isRegex:"* ]] && [ "$in_matchers" = true ]; then
|
|
current_matcher_regex=$(echo "$trimmed" | sed 's/^isRegex:[[:space:]]*//')
|
|
continue
|
|
fi
|
|
|
|
# Duration
|
|
if [[ "$trimmed" == "duration:"* ]]; then
|
|
flush_matcher
|
|
in_matchers=false
|
|
duration=$(echo "$trimmed" | sed 's/^duration:[[:space:]]*//')
|
|
continue
|
|
fi
|
|
|
|
# Comment
|
|
if [[ "$trimmed" == "comment:"* ]]; then
|
|
comment=$(echo "$trimmed" | sed 's/^comment:[[:space:]]*//' | sed 's/^"//' | sed 's/"$//')
|
|
continue
|
|
fi
|
|
|
|
done < "$file"
|
|
|
|
# Flush last silence
|
|
flush_matcher
|
|
if [ "$in_silence" = true ]; then
|
|
flush_silence
|
|
fi
|
|
|
|
echo ""
|
|
log_info "Bulk create complete: ${success} succeeded, ${failed} failed"
|
|
}
|
|
|
|
# ============================================================================
|
|
# LIST COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_list() {
|
|
local state="active"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--state) state="$2"; shift 2 ;;
|
|
*) log_error "Unknown option for list: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
local silences_json
|
|
silences_json=$(am_api GET "/api/v2/silences")
|
|
|
|
if [ -z "$silences_json" ]; then
|
|
log_error "Failed to fetch silences"
|
|
exit 1
|
|
fi
|
|
|
|
# Filter by state
|
|
local filtered
|
|
if [ "$state" = "all" ]; then
|
|
filtered="$silences_json"
|
|
else
|
|
filtered=$(echo "$silences_json" | jq --arg s "$state" '[.[] | select(.status.state == $s)]')
|
|
fi
|
|
|
|
local count
|
|
count=$(echo "$filtered" | jq 'length')
|
|
|
|
if [ "$count" -eq 0 ]; then
|
|
log_info "No ${state} silences found"
|
|
return
|
|
fi
|
|
|
|
printf "${BLUE}%-10s %-10s %-12s %-30s %-20s %-20s %s${NC}\n" \
|
|
"ID" "STATE" "AUTHOR" "MATCHERS" "STARTS" "ENDS" "COMMENT"
|
|
printf '%.0s-' {1..120}
|
|
echo ""
|
|
|
|
echo "$filtered" | jq -r '.[] | [
|
|
.id[0:8],
|
|
.status.state,
|
|
.createdBy,
|
|
([.matchers[] | "\(.name)=\(.value)"] | join(", ")),
|
|
.startsAt[0:19],
|
|
.endsAt[0:19],
|
|
.comment
|
|
] | @tsv' | while IFS=$'\t' read -r id st author matchers starts ends comment; do
|
|
printf "%-10s %-10s %-12s %-30s %-20s %-20s %s\n" \
|
|
"$id" "$st" "$(truncate "$author" 12)" "$(truncate "$matchers" 30)" \
|
|
"$starts" "$ends" "$(truncate "$comment" 40)"
|
|
done
|
|
|
|
echo ""
|
|
log_info "${count} silence(s) found"
|
|
}
|
|
|
|
# ============================================================================
|
|
# EXTEND COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_extend() {
|
|
local silence_id="" duration=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--id) silence_id="$2"; shift 2 ;;
|
|
--duration) duration="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
*) log_error "Unknown option for extend: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$silence_id" ]; then
|
|
log_error "--id is required"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$duration" ]; then
|
|
log_error "--duration is required"
|
|
exit 1
|
|
fi
|
|
|
|
local duration_secs
|
|
duration_secs=$(parse_duration "$duration") || exit 1
|
|
|
|
# Find the silence (match by prefix)
|
|
local silences_json silence
|
|
silences_json=$(am_api GET "/api/v2/silences")
|
|
silence=$(echo "$silences_json" | jq --arg id "$silence_id" '[.[] | select(.id | startswith($id)) | select(.status.state == "active")] | first // empty')
|
|
|
|
if [ -z "$silence" ] || [ "$silence" = "null" ]; then
|
|
log_error "Active silence not found matching ID: ${silence_id}"
|
|
exit 1
|
|
fi
|
|
|
|
local full_id
|
|
full_id=$(echo "$silence" | jq -r '.id')
|
|
|
|
# Build new silence with extended endsAt
|
|
local new_ends_at
|
|
new_ends_at=$(iso_timestamp "$duration_secs")
|
|
|
|
local payload
|
|
payload=$(echo "$silence" | jq --arg endsAt "$new_ends_at" \
|
|
'del(.id, .status, .updatedAt) | .endsAt = $endsAt')
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log_dry_run "Would extend silence ${full_id} to ${new_ends_at}:"
|
|
echo "$payload" | jq .
|
|
return
|
|
fi
|
|
|
|
local response
|
|
response=$(am_api POST "/api/v2/silences" "$payload")
|
|
|
|
if [ $? -eq 0 ] && [ -n "$response" ]; then
|
|
local new_id
|
|
new_id=$(echo "$response" | jq -r '.silenceID // empty')
|
|
log_info "Silence extended: ${new_id:-ok} (new end: ${new_ends_at})"
|
|
else
|
|
log_error "Failed to extend silence"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# EXPIRE COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_expire() {
|
|
local silence_id="" match_pattern=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--id) silence_id="$2"; shift 2 ;;
|
|
--match) match_pattern="$2"; shift 2 ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
*) log_error "Unknown option for expire: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$silence_id" ] && [ -z "$match_pattern" ]; then
|
|
log_error "Either --id or --match is required"
|
|
exit 1
|
|
fi
|
|
|
|
local silences_json
|
|
silences_json=$(am_api GET "/api/v2/silences")
|
|
|
|
if [ -z "$silences_json" ]; then
|
|
log_error "Failed to fetch silences"
|
|
exit 1
|
|
fi
|
|
|
|
local ids_to_expire=()
|
|
|
|
if [ -n "$silence_id" ]; then
|
|
# Match by ID prefix
|
|
local matched
|
|
matched=$(echo "$silences_json" | jq -r --arg id "$silence_id" \
|
|
'.[] | select(.id | startswith($id)) | select(.status.state == "active") | .id')
|
|
while IFS= read -r id; do
|
|
[ -n "$id" ] && ids_to_expire+=("$id")
|
|
done <<< "$matched"
|
|
fi
|
|
|
|
if [ -n "$match_pattern" ]; then
|
|
# Parse pattern: 'comment=~regex'
|
|
local field pattern
|
|
if [[ "$match_pattern" == *"=~"* ]]; then
|
|
field="${match_pattern%%=~*}"
|
|
pattern="${match_pattern#*=~}"
|
|
else
|
|
log_error "Match pattern must use =~ syntax (e.g., 'comment=~maintenance.*')"
|
|
exit 1
|
|
fi
|
|
|
|
local matched
|
|
if [ "$field" = "comment" ]; then
|
|
matched=$(echo "$silences_json" | jq -r --arg p "$pattern" \
|
|
'.[] | select(.status.state == "active") | select(.comment | test($p)) | .id')
|
|
else
|
|
matched=$(echo "$silences_json" | jq -r --arg f "$field" --arg p "$pattern" \
|
|
'.[] | select(.status.state == "active") | select(.matchers[] | select(.name == $f) | .value | test($p)) | .id')
|
|
fi
|
|
|
|
while IFS= read -r id; do
|
|
[ -n "$id" ] && ids_to_expire+=("$id")
|
|
done <<< "$matched"
|
|
fi
|
|
|
|
if [ ${#ids_to_expire[@]} -eq 0 ]; then
|
|
log_warn "No matching active silences found"
|
|
return
|
|
fi
|
|
|
|
local success=0 failed=0
|
|
|
|
for id in "${ids_to_expire[@]}"; do
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log_dry_run "Would expire silence: ${id}"
|
|
((success++))
|
|
continue
|
|
fi
|
|
|
|
if curl -sf --connect-timeout 5 --max-time 10 \
|
|
-X DELETE "${AM_URL}/api/v2/silence/${id}" >/dev/null 2>&1; then
|
|
log_info "Expired silence: ${id}"
|
|
((success++))
|
|
else
|
|
log_error "Failed to expire silence: ${id}"
|
|
((failed++))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
log_info "Expire complete: ${success} succeeded, ${failed} failed"
|
|
}
|
|
|
|
# ============================================================================
|
|
# EXPORT COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_export() {
|
|
local output=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--output) output="$2"; shift 2 ;;
|
|
*) log_error "Unknown option for export: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
local silences_json
|
|
silences_json=$(am_api GET "/api/v2/silences")
|
|
|
|
if [ -z "$silences_json" ]; then
|
|
log_error "Failed to fetch silences"
|
|
exit 1
|
|
fi
|
|
|
|
local active
|
|
active=$(echo "$silences_json" | jq '[.[] | select(.status.state == "active")]')
|
|
local count
|
|
count=$(echo "$active" | jq 'length')
|
|
|
|
if [ "$count" -eq 0 ]; then
|
|
log_warn "No active silences to export"
|
|
return
|
|
fi
|
|
|
|
local yaml_output
|
|
yaml_output=$(echo "$active" | jq -r '
|
|
"silences:",
|
|
(.[] |
|
|
" - matchers:",
|
|
(.matchers[] |
|
|
" - name: \(.name)",
|
|
" value: \(.value)",
|
|
" isRegex: \(.isRegex)"
|
|
),
|
|
" duration: \(
|
|
((.endsAt | fromdateiso8601) - (.startsAt | fromdateiso8601)) |
|
|
if . >= 86400 then "\(. / 86400 | floor)d"
|
|
elif . >= 3600 then "\(. / 3600 | floor)h"
|
|
elif . >= 60 then "\(. / 60 | floor)m"
|
|
else "\(.)s"
|
|
end
|
|
)",
|
|
" comment: \"\(.comment)\""
|
|
)
|
|
')
|
|
|
|
if [ -n "$output" ]; then
|
|
echo "$yaml_output" > "$output"
|
|
log_info "Exported ${count} silence(s) to ${output}"
|
|
else
|
|
echo "$yaml_output"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# AUDIT COMMAND
|
|
# ============================================================================
|
|
|
|
cmd_audit() {
|
|
local silences_json
|
|
silences_json=$(am_api GET "/api/v2/silences")
|
|
|
|
if [ -z "$silences_json" ]; then
|
|
log_error "Failed to fetch silences"
|
|
exit 1
|
|
fi
|
|
|
|
local active
|
|
active=$(echo "$silences_json" | jq '[.[] | select(.status.state == "active")]')
|
|
local count
|
|
count=$(echo "$active" | jq 'length')
|
|
|
|
if [ "$count" -eq 0 ]; then
|
|
log_info "No active silences"
|
|
return
|
|
fi
|
|
|
|
printf "${BLUE}%-10s %-12s %-20s %-20s %-10s %-28s %s${NC}\n" \
|
|
"ID" "AUTHOR" "CREATED" "EXPIRES" "DURATION" "MATCHERS" "COMMENT"
|
|
printf '%.0s-' {1..130}
|
|
echo ""
|
|
|
|
echo "$active" | jq -r '.[] | [
|
|
.id[0:8],
|
|
.createdBy,
|
|
.startsAt[0:19],
|
|
.endsAt[0:19],
|
|
(
|
|
((.endsAt | fromdateiso8601) - (.startsAt | fromdateiso8601)) |
|
|
if . >= 86400 then "\(. / 86400 | floor)d"
|
|
elif . >= 3600 then "\(. / 3600 | floor)h"
|
|
elif . >= 60 then "\(. / 60 | floor)m"
|
|
else "\(.)s"
|
|
end
|
|
),
|
|
([.matchers[] | "\(.name)=\(.value)"] | join(", ")),
|
|
.comment
|
|
] | @tsv' | while IFS=$'\t' read -r id author created expires duration matchers comment; do
|
|
printf "%-10s %-12s %-20s %-20s %-10s %-28s %s\n" \
|
|
"$id" "$(truncate "$author" 12)" "$created" "$expires" "$duration" \
|
|
"$(truncate "$matchers" 28)" "$(truncate "$comment" 40)"
|
|
done
|
|
|
|
echo ""
|
|
log_info "${count} active silence(s)"
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN
|
|
# ============================================================================
|
|
|
|
main() {
|
|
if [ $# -eq 0 ]; then
|
|
show_usage
|
|
fi
|
|
|
|
check_requirements || exit 1
|
|
|
|
local command="$1"
|
|
shift
|
|
|
|
case "$command" in
|
|
-h|--help) show_usage ;;
|
|
create|bulk-create|list|extend|expire|export|audit) ;;
|
|
*) log_error "Unknown command: $command"; echo ""; show_usage ;;
|
|
esac
|
|
|
|
# All commands except --help require connectivity
|
|
check_connectivity
|
|
|
|
case "$command" in
|
|
create) cmd_create "$@" ;;
|
|
bulk-create) cmd_bulk_create "$@" ;;
|
|
list) cmd_list "$@" ;;
|
|
extend) cmd_extend "$@" ;;
|
|
expire) cmd_expire "$@" ;;
|
|
export) cmd_export "$@" ;;
|
|
audit) cmd_audit "$@" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|