Add all 44 scripts, update CI: error severity baseline, PowerShell validation, multi-distro testing
Amp-Thread-ID: https://ampcode.com/threads/T-019cc404-c628-759e-a50b-f5eeea35b91f Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
#!/bin/bash
|
||||
|
||||
################################################
|
||||
#### Salt Key Manager ####
|
||||
#### Automate salt-key operations ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### Version: 1.00-030526 ####
|
||||
################################################
|
||||
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
readonly SCRIPT_NAME
|
||||
|
||||
# Default configuration
|
||||
readonly DEFAULT_STALE_DAYS=30
|
||||
readonly DEFAULT_CACHE_DIR="/var/cache/salt/master/minions"
|
||||
|
||||
# Configuration variables
|
||||
DEBUG=${DEBUG:-}
|
||||
|
||||
# Runtime flags
|
||||
ACTION=""
|
||||
TARGET_MINION=""
|
||||
STALE_DAYS=$DEFAULT_STALE_DAYS
|
||||
EXPORT_PATH=""
|
||||
BULK_FILE=""
|
||||
AUTO_YES=false
|
||||
USE_COLOR=true
|
||||
|
||||
# Colors
|
||||
C_GREEN=""
|
||||
C_YELLOW=""
|
||||
C_RED=""
|
||||
C_CYAN=""
|
||||
C_RESET=""
|
||||
|
||||
handle_error() {
|
||||
local exit_code=$1
|
||||
local line_number=$2
|
||||
echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
trap 'handle_error $? $LINENO' ERR
|
||||
|
||||
debug_echo() {
|
||||
if [[ -n "$DEBUG" ]]; then
|
||||
echo "[DEBUG] $*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo "[INFO] $*"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo "[WARN] $*" >&2
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] $*" >&2
|
||||
}
|
||||
|
||||
setup_colors() {
|
||||
if [[ "$USE_COLOR" == true ]] && [[ -t 1 ]]; then
|
||||
C_GREEN='\033[0;32m'
|
||||
C_YELLOW='\033[0;33m'
|
||||
C_RED='\033[0;31m'
|
||||
C_CYAN='\033[0;36m'
|
||||
C_RESET='\033[0m'
|
||||
fi
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Usage: $SCRIPT_NAME [ACTION] [OPTIONS]
|
||||
|
||||
Manage Salt minion keys — accept, reject, delete, verify, rotate, and
|
||||
clean up stale keys.
|
||||
|
||||
ACTIONS:
|
||||
--list List all keys by status with counts
|
||||
--verify Show pending keys with fingerprints for verification
|
||||
--accept-all Accept all pending keys
|
||||
--accept MINION Accept a specific minion key
|
||||
--reject MINION Reject a specific minion key
|
||||
--delete MINION Delete a specific minion key
|
||||
--rotate MINION Rotate a minion key (delete, re-accept on reconnect)
|
||||
--cleanup-stale [DAYS] Delete keys for minions not seen in DAYS days (default: $DEFAULT_STALE_DAYS)
|
||||
--export PATH Export all accepted key fingerprints to a file
|
||||
--bulk-accept FILE Accept minions listed in a file (one per line)
|
||||
|
||||
OPTIONS:
|
||||
--yes Skip confirmation prompts
|
||||
--no-color Disable colored output
|
||||
--help, -h Show this help message
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
DEBUG Enable debug output
|
||||
|
||||
EXAMPLES:
|
||||
# List all keys with status
|
||||
sudo $SCRIPT_NAME --list
|
||||
|
||||
# Show pending keys for verification
|
||||
sudo $SCRIPT_NAME --verify
|
||||
|
||||
# Accept all pending keys
|
||||
sudo $SCRIPT_NAME --accept-all --yes
|
||||
|
||||
# Accept a specific minion
|
||||
sudo $SCRIPT_NAME --accept web01
|
||||
|
||||
# Clean up minions not seen in 60 days
|
||||
sudo $SCRIPT_NAME --cleanup-stale 60
|
||||
|
||||
# Export fingerprints for auditing
|
||||
sudo $SCRIPT_NAME --export /tmp/salt-keys.txt
|
||||
|
||||
# Bulk accept from a file
|
||||
sudo $SCRIPT_NAME --bulk-accept /tmp/new-minions.txt --yes
|
||||
EOF
|
||||
}
|
||||
|
||||
count_keys() {
|
||||
local status="$1"
|
||||
salt-key --list "$status" 2>/dev/null | grep -cv "^$status\|^$" || echo 0
|
||||
}
|
||||
|
||||
do_list() {
|
||||
echo "Salt Key Status"
|
||||
echo "==============="
|
||||
echo ""
|
||||
|
||||
local accepted unaccepted denied rejected
|
||||
accepted=$(count_keys "accepted")
|
||||
unaccepted=$(count_keys "unaccepted")
|
||||
denied=$(count_keys "denied")
|
||||
rejected=$(count_keys "rejected")
|
||||
|
||||
printf ' %bAccepted:%b %d\n' "$C_GREEN" "$C_RESET" "$accepted"
|
||||
printf ' %bPending:%b %d\n' "$C_YELLOW" "$C_RESET" "$unaccepted"
|
||||
printf ' %bDenied:%b %d\n' "$C_RED" "$C_RESET" "$denied"
|
||||
printf ' %bRejected:%b %d\n' "$C_RED" "$C_RESET" "$rejected"
|
||||
echo ""
|
||||
|
||||
if ((accepted > 0)); then
|
||||
printf '%bAccepted Keys:%b\n' "$C_GREEN" "$C_RESET"
|
||||
salt-key --list accepted 2>/dev/null | grep -v "^Accepted Keys:" | sed 's/^/ /'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if ((unaccepted > 0)); then
|
||||
printf '%bPending Keys:%b\n' "$C_YELLOW" "$C_RESET"
|
||||
salt-key --list unaccepted 2>/dev/null | grep -v "^Unaccepted Keys:" | sed 's/^/ /'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if ((denied > 0)); then
|
||||
printf '%bDenied Keys:%b\n' "$C_RED" "$C_RESET"
|
||||
salt-key --list denied 2>/dev/null | grep -v "^Denied Keys:" | sed 's/^/ /'
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if ((rejected > 0)); then
|
||||
printf '%bRejected Keys:%b\n' "$C_RED" "$C_RESET"
|
||||
salt-key --list rejected 2>/dev/null | grep -v "^Rejected Keys:" | sed 's/^/ /'
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
do_verify() {
|
||||
local pending
|
||||
pending=$(salt-key --list unaccepted 2>/dev/null | grep -v "^Unaccepted Keys:$" | grep -v "^$")
|
||||
|
||||
if [[ -z "$pending" ]]; then
|
||||
log_info "No pending keys to verify"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Master Fingerprint:"
|
||||
printf ' %b' "$C_CYAN"
|
||||
salt-key -F master 2>/dev/null | grep -A1 "master.pub" | tail -1 | tr -d ' '
|
||||
printf '%b\n\n' "$C_RESET"
|
||||
|
||||
echo "Pending Keys with Fingerprints:"
|
||||
echo ""
|
||||
|
||||
while IFS= read -r minion; do
|
||||
[[ -z "$minion" ]] && continue
|
||||
minion=$(echo "$minion" | tr -d '[:space:]')
|
||||
local fingerprint
|
||||
fingerprint=$(salt-key -f "$minion" 2>/dev/null | grep -v "^Unaccepted Keys:" | awk '{print $2}' | head -1)
|
||||
printf ' %b%-30s%b %s\n' "$C_YELLOW" "$minion" "$C_RESET" "${fingerprint:-unknown}"
|
||||
done <<< "$pending"
|
||||
|
||||
echo ""
|
||||
log_info "Verify each fingerprint matches the minion's local fingerprint:"
|
||||
log_info " (on minion) salt-call --local key.finger"
|
||||
}
|
||||
|
||||
do_accept_all() {
|
||||
local pending
|
||||
pending=$(count_keys "unaccepted")
|
||||
|
||||
if ((pending == 0)); then
|
||||
log_info "No pending keys to accept"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Accepting $pending pending key(s)..."
|
||||
|
||||
if [[ "$AUTO_YES" != true ]]; then
|
||||
echo "Accept all $pending pending keys? [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
salt-key -A -y 2>/dev/null
|
||||
log_info "All pending keys accepted"
|
||||
}
|
||||
|
||||
do_accept() {
|
||||
local minion="$1"
|
||||
log_info "Accepting key for: $minion"
|
||||
|
||||
if [[ "$AUTO_YES" != true ]]; then
|
||||
local fingerprint
|
||||
fingerprint=$(salt-key -f "$minion" 2>/dev/null | grep -v "^Unaccepted Keys:" | awk '{print $2}' | head -1)
|
||||
echo "Fingerprint: ${fingerprint:-unknown}"
|
||||
echo "Accept key for $minion? [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
salt-key -a "$minion" -y 2>/dev/null
|
||||
log_info "Key accepted for $minion"
|
||||
}
|
||||
|
||||
do_reject() {
|
||||
local minion="$1"
|
||||
log_info "Rejecting key for: $minion"
|
||||
|
||||
if [[ "$AUTO_YES" != true ]]; then
|
||||
echo "Reject key for $minion? [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
salt-key -r "$minion" -y 2>/dev/null
|
||||
log_info "Key rejected for $minion"
|
||||
}
|
||||
|
||||
do_delete() {
|
||||
local minion="$1"
|
||||
log_info "Deleting key for: $minion"
|
||||
|
||||
if [[ "$AUTO_YES" != true ]]; then
|
||||
echo "Delete key for $minion? This cannot be undone. [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
salt-key -d "$minion" -y 2>/dev/null
|
||||
log_info "Key deleted for $minion"
|
||||
}
|
||||
|
||||
do_rotate() {
|
||||
local minion="$1"
|
||||
log_info "Rotating key for: $minion"
|
||||
log_info "This will delete the current key — the minion must reconnect to get a new key accepted"
|
||||
|
||||
if [[ "$AUTO_YES" != true ]]; then
|
||||
echo "Rotate key for $minion? [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
salt-key -d "$minion" -y 2>/dev/null
|
||||
log_info "Key deleted for $minion — accept the new key when the minion reconnects"
|
||||
log_info "On the minion, restart salt-minion: systemctl restart salt-minion"
|
||||
}
|
||||
|
||||
do_cleanup_stale() {
|
||||
local days="$1"
|
||||
log_info "Finding minions not seen in $days days..."
|
||||
|
||||
if [[ ! -d "$DEFAULT_CACHE_DIR" ]]; then
|
||||
log_error "Minion cache directory not found: $DEFAULT_CACHE_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local stale_minions=()
|
||||
local cutoff
|
||||
cutoff=$(date -d "-${days} days" +%s 2>/dev/null) || cutoff=$(date -v-"${days}"d +%s 2>/dev/null)
|
||||
|
||||
while IFS= read -r minion_dir; do
|
||||
local minion_name
|
||||
minion_name=$(basename "$minion_dir")
|
||||
local last_modified
|
||||
last_modified=$(stat -c %Y "$minion_dir" 2>/dev/null) || last_modified=$(stat -f %m "$minion_dir" 2>/dev/null) || continue
|
||||
|
||||
if ((last_modified < cutoff)); then
|
||||
local days_ago=$(( ($(date +%s) - last_modified) / 86400 ))
|
||||
stale_minions+=("$minion_name")
|
||||
printf ' %b%-30s%b (last seen %d days ago)\n' "$C_RED" "$minion_name" "$C_RESET" "$days_ago"
|
||||
fi
|
||||
done < <(find "$DEFAULT_CACHE_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
|
||||
|
||||
if [[ ${#stale_minions[@]} -eq 0 ]]; then
|
||||
log_info "No stale minions found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "Found ${#stale_minions[@]} stale minion(s)"
|
||||
|
||||
if [[ "$AUTO_YES" != true ]]; then
|
||||
echo "Delete keys for all ${#stale_minions[@]} stale minions? [y/N] "
|
||||
read -r confirm
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
for minion in "${stale_minions[@]}"; do
|
||||
salt-key -d "$minion" -y 2>/dev/null && log_info "Deleted key: $minion"
|
||||
done
|
||||
|
||||
log_info "Stale key cleanup complete"
|
||||
}
|
||||
|
||||
do_export() {
|
||||
local output_path="$1"
|
||||
log_info "Exporting accepted key fingerprints to $output_path..."
|
||||
|
||||
{
|
||||
echo "# Salt Key Fingerprint Export"
|
||||
echo "# Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo "# Master: $(hostname -f 2>/dev/null || hostname)"
|
||||
echo "#"
|
||||
echo "# Format: minion_id fingerprint"
|
||||
echo ""
|
||||
salt-key -F accepted 2>/dev/null | grep -v "^Accepted Keys:" | while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
echo "$line"
|
||||
done
|
||||
} > "$output_path"
|
||||
|
||||
local count
|
||||
count=$(grep -cv "^#\|^$" "$output_path" 2>/dev/null) || count=0
|
||||
log_info "Exported $count key fingerprint(s) to $output_path"
|
||||
}
|
||||
|
||||
do_bulk_accept() {
|
||||
local input_file="$1"
|
||||
|
||||
if [[ ! -f "$input_file" ]]; then
|
||||
log_error "File not found: $input_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local count=0
|
||||
local failed=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
local minion_id="${line%%:*}"
|
||||
minion_id=$(echo "$minion_id" | tr -d '[:space:]')
|
||||
|
||||
if salt-key -a "$minion_id" -y 2>/dev/null; then
|
||||
log_info "Accepted: $minion_id"
|
||||
count=$((count + 1))
|
||||
else
|
||||
log_error "Failed to accept: $minion_id"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done < "$input_file"
|
||||
|
||||
log_info "Bulk accept complete: $count accepted, $failed failed"
|
||||
}
|
||||
|
||||
parse_arguments() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--list)
|
||||
ACTION="list"
|
||||
shift
|
||||
;;
|
||||
--verify)
|
||||
ACTION="verify"
|
||||
shift
|
||||
;;
|
||||
--accept-all)
|
||||
ACTION="accept-all"
|
||||
shift
|
||||
;;
|
||||
--accept)
|
||||
ACTION="accept"
|
||||
TARGET_MINION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--reject)
|
||||
ACTION="reject"
|
||||
TARGET_MINION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--delete)
|
||||
ACTION="delete"
|
||||
TARGET_MINION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--rotate)
|
||||
ACTION="rotate"
|
||||
TARGET_MINION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--cleanup-stale)
|
||||
ACTION="cleanup-stale"
|
||||
if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then
|
||||
STALE_DAYS="$2"
|
||||
shift 2
|
||||
else
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
--export)
|
||||
ACTION="export"
|
||||
EXPORT_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bulk-accept)
|
||||
ACTION="bulk-accept"
|
||||
BULK_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
AUTO_YES=true
|
||||
shift
|
||||
;;
|
||||
--no-color)
|
||||
USE_COLOR=false
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
show_help >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
validate_requirements() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$ACTION" ]]; then
|
||||
log_error "An action is required"
|
||||
show_help >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v salt-key >/dev/null 2>&1; then
|
||||
log_error "salt-key not found — is salt-master installed?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ACTION" == "accept" || "$ACTION" == "reject" || "$ACTION" == "delete" || "$ACTION" == "rotate" ]]; then
|
||||
if [[ -z "$TARGET_MINION" ]]; then
|
||||
log_error "Minion name is required for --$ACTION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$ACTION" == "export" && -z "$EXPORT_PATH" ]]; then
|
||||
log_error "Output path is required for --export"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ACTION" == "bulk-accept" && -z "$BULK_FILE" ]]; then
|
||||
log_error "Input file is required for --bulk-accept"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_arguments "$@"
|
||||
validate_requirements
|
||||
setup_colors
|
||||
|
||||
case "$ACTION" in
|
||||
list) do_list ;;
|
||||
verify) do_verify ;;
|
||||
accept-all) do_accept_all ;;
|
||||
accept) do_accept "$TARGET_MINION" ;;
|
||||
reject) do_reject "$TARGET_MINION" ;;
|
||||
delete) do_delete "$TARGET_MINION" ;;
|
||||
rotate) do_rotate "$TARGET_MINION" ;;
|
||||
cleanup-stale) do_cleanup_stale "$STALE_DAYS" ;;
|
||||
export) do_export "$EXPORT_PATH" ;;
|
||||
bulk-accept) do_bulk_accept "$BULK_FILE" ;;
|
||||
esac
|
||||
|
||||
debug_echo "Script completed successfully"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
Reference in New Issue
Block a user