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.
410 lines
15 KiB
Bash
410 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### salt-roster-generator.sh — Generate Salt SSH roster files from external sources ####
|
|
#### Supports CSV files, Ansible inventory (INI format), and AWS EC2 instances ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.01 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./salt-roster-generator.sh --csv hosts.csv -o roster ####
|
|
#### ./salt-roster-generator.sh --ansible-inventory /etc/ansible/hosts -o roster ####
|
|
#### ./salt-roster-generator.sh --ec2 --region us-east-1 -o roster ####
|
|
#### ./salt-roster-generator.sh --ec2 --tag "Environment=production" -o roster ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
VERSION="1.0"
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
|
|
INPUT_MODE=""
|
|
CSV_FILE=""
|
|
ANSIBLE_FILE=""
|
|
EC2_MODE=false
|
|
EC2_REGION=""
|
|
EC2_RUNNING_ONLY=true
|
|
declare -a EC2_TAGS=()
|
|
|
|
OUTPUT_FILE=""
|
|
DEFAULT_USER="root"
|
|
DEFAULT_PORT="22"
|
|
ENABLE_SUDO=false
|
|
SSH_PRIV=""
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
err() { echo "[ERROR] $*" >&2; }
|
|
die() { err "$@"; exit 1; }
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# USAGE
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${SCRIPT_NAME} — Generate Salt SSH roster files from external inventory sources
|
|
|
|
USAGE:
|
|
${SCRIPT_NAME} [OPTIONS]
|
|
|
|
INPUT SOURCES (pick one):
|
|
--csv FILE CSV file (columns: name,host,user,port)
|
|
--ansible-inventory FILE Ansible INI inventory file
|
|
--ec2 AWS EC2 instances (requires aws cli)
|
|
|
|
EC2 OPTIONS:
|
|
--region REGION AWS region (default: from aws cli config)
|
|
--tag KEY=VALUE Filter by tag (can repeat)
|
|
--running-only Only include running instances (default: true)
|
|
|
|
OUTPUT OPTIONS:
|
|
-o, --output FILE Output roster file (default: stdout)
|
|
|
|
SSH OPTIONS (applied to all hosts):
|
|
--user USER SSH user (default: root)
|
|
--sudo Enable sudo for all hosts
|
|
--priv KEY SSH private key path
|
|
--port PORT SSH port (default: 22)
|
|
|
|
OTHER:
|
|
--help, -h Show this help
|
|
--version Show version
|
|
|
|
EXAMPLES:
|
|
./${SCRIPT_NAME} --csv hosts.csv -o roster
|
|
./${SCRIPT_NAME} --ansible-inventory /etc/ansible/hosts -o roster
|
|
./${SCRIPT_NAME} --ec2 --region us-east-1 -o roster
|
|
./${SCRIPT_NAME} --ec2 --tag "Environment=production" --tag "Role=web" -o roster
|
|
EOF
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# ARGUMENT PARSING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
parse_args() {
|
|
[[ $# -eq 0 ]] && { usage; exit 1; }
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--csv)
|
|
INPUT_MODE="csv"
|
|
CSV_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--ansible-inventory)
|
|
INPUT_MODE="ansible"
|
|
ANSIBLE_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--ec2)
|
|
INPUT_MODE="ec2"
|
|
EC2_MODE=true
|
|
shift
|
|
;;
|
|
--region)
|
|
EC2_REGION="$2"
|
|
shift 2
|
|
;;
|
|
--tag)
|
|
EC2_TAGS+=("$2")
|
|
shift 2
|
|
;;
|
|
--running-only)
|
|
EC2_RUNNING_ONLY=true
|
|
shift
|
|
;;
|
|
-o|--output)
|
|
OUTPUT_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--user)
|
|
DEFAULT_USER="$2"
|
|
shift 2
|
|
;;
|
|
--sudo)
|
|
ENABLE_SUDO=true
|
|
shift
|
|
;;
|
|
--priv)
|
|
SSH_PRIV="$2"
|
|
shift 2
|
|
;;
|
|
--port)
|
|
DEFAULT_PORT="$2"
|
|
shift 2
|
|
;;
|
|
--version)
|
|
echo "${SCRIPT_NAME} v${VERSION}"
|
|
exit 0
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
-*)
|
|
die "Unknown option: $1 — run ${SCRIPT_NAME} --help"
|
|
;;
|
|
*)
|
|
die "Unexpected argument: $1 — run ${SCRIPT_NAME} --help"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# VALIDATION
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
validate() {
|
|
[[ -z "$INPUT_MODE" ]] && die "No input source specified. Use --csv, --ansible-inventory, or --ec2."
|
|
|
|
case "$INPUT_MODE" in
|
|
csv)
|
|
[[ -z "$CSV_FILE" ]] && die "--csv requires a file argument."
|
|
[[ ! -f "$CSV_FILE" ]] && die "CSV file not found: $CSV_FILE"
|
|
;;
|
|
ansible)
|
|
[[ -z "$ANSIBLE_FILE" ]] && die "--ansible-inventory requires a file argument."
|
|
[[ ! -f "$ANSIBLE_FILE" ]] && die "Ansible inventory file not found: $ANSIBLE_FILE"
|
|
;;
|
|
ec2)
|
|
if ! command -v aws &>/dev/null; then
|
|
die "aws cli not found. Install it to use --ec2 mode."
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# ROSTER GENERATION
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
generate_header() {
|
|
local source_label="$1"
|
|
echo "# Salt SSH Roster"
|
|
echo "# Generated by ${SCRIPT_NAME} v${VERSION}"
|
|
echo "# Source: ${source_label}"
|
|
echo "# Date: $(date '+%Y-%m-%d')"
|
|
echo ""
|
|
}
|
|
|
|
generate_entry() {
|
|
local name="$1"
|
|
local host="$2"
|
|
local user="${3:-$DEFAULT_USER}"
|
|
local port="${4:-$DEFAULT_PORT}"
|
|
local sudo="${5:-}"
|
|
local priv="${6:-$SSH_PRIV}"
|
|
|
|
[[ -z "$name" || -z "$host" ]] && return
|
|
|
|
echo "${name}:"
|
|
echo " host: ${host}"
|
|
echo " user: ${user}"
|
|
echo " port: ${port}"
|
|
|
|
if [[ "$ENABLE_SUDO" == true || "$sudo" == "true" || "$sudo" == "True" ]]; then
|
|
echo " sudo: True"
|
|
fi
|
|
|
|
if [[ -n "$priv" ]]; then
|
|
echo " priv: ${priv}"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ── CSV Parser ────────────────────────────────────────────────────────
|
|
parse_csv() {
|
|
local file="$1"
|
|
local header_line
|
|
header_line=$(head -1 "$file")
|
|
|
|
# Map column positions from header
|
|
local -A col_map
|
|
local idx=0
|
|
IFS=',' read -ra cols <<< "$header_line"
|
|
for c in "${cols[@]}"; do
|
|
c=$(echo "$c" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
col_map["$c"]=$idx
|
|
((idx++)) || true
|
|
done
|
|
|
|
# Require name and host columns
|
|
[[ -z "${col_map[name]:-}" ]] && die "CSV missing required column: name"
|
|
[[ -z "${col_map[host]:-}" ]] && die "CSV missing required column: host"
|
|
|
|
local name_idx="${col_map[name]}"
|
|
local host_idx="${col_map[host]}"
|
|
local user_idx="${col_map[user]:-}"
|
|
local port_idx="${col_map[port]:-}"
|
|
local sudo_idx="${col_map[sudo]:-}"
|
|
local priv_idx="${col_map[priv]:-}"
|
|
|
|
generate_header "$file"
|
|
|
|
local line_num=0
|
|
while IFS= read -r line; do
|
|
((line_num++)) || true
|
|
[[ $line_num -eq 1 ]] && continue # skip header
|
|
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
|
|
|
IFS=',' read -ra fields <<< "$line"
|
|
|
|
local name="${fields[$name_idx]:-}"
|
|
local host="${fields[$host_idx]:-}"
|
|
local user="${DEFAULT_USER}"
|
|
local port="${DEFAULT_PORT}"
|
|
local sudo=""
|
|
local priv=""
|
|
|
|
[[ -n "$user_idx" && -n "${fields[$user_idx]:-}" ]] && user="${fields[$user_idx]}"
|
|
[[ -n "$port_idx" && -n "${fields[$port_idx]:-}" ]] && port="${fields[$port_idx]}"
|
|
[[ -n "$sudo_idx" && -n "${fields[$sudo_idx]:-}" ]] && sudo="${fields[$sudo_idx]}"
|
|
[[ -n "$priv_idx" && -n "${fields[$priv_idx]:-}" ]] && priv="${fields[$priv_idx]}"
|
|
|
|
# Trim whitespace
|
|
name=$(echo "$name" | xargs)
|
|
host=$(echo "$host" | xargs)
|
|
user=$(echo "$user" | xargs)
|
|
port=$(echo "$port" | xargs)
|
|
|
|
generate_entry "$name" "$host" "$user" "$port" "$sudo" "$priv"
|
|
done < "$file"
|
|
}
|
|
|
|
# ── Ansible INI Inventory Parser ──────────────────────────────────────
|
|
parse_ansible_inventory() {
|
|
local file="$1"
|
|
|
|
generate_header "$file"
|
|
|
|
while IFS= read -r line; do
|
|
# Strip leading/trailing whitespace
|
|
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
|
|
# Skip empty lines, comments, group headers, group:vars, group:children
|
|
[[ -z "$line" ]] && continue
|
|
[[ "$line" =~ ^# ]] && continue
|
|
[[ "$line" =~ ^\[.*\] ]] && continue
|
|
|
|
# Parse host line: hostname key=value key=value ...
|
|
local hostname
|
|
hostname=$(echo "$line" | awk '{print $1}')
|
|
|
|
local host="$hostname"
|
|
local user="$DEFAULT_USER"
|
|
local port="$DEFAULT_PORT"
|
|
local priv=""
|
|
|
|
# Extract ansible variables
|
|
if echo "$line" | grep -q 'ansible_host='; then
|
|
host=$(echo "$line" | grep -oP 'ansible_host=\K[^\s]+')
|
|
fi
|
|
if echo "$line" | grep -q 'ansible_user='; then
|
|
user=$(echo "$line" | grep -oP 'ansible_user=\K[^\s]+')
|
|
fi
|
|
if echo "$line" | grep -q 'ansible_port='; then
|
|
port=$(echo "$line" | grep -oP 'ansible_port=\K[^\s]+')
|
|
fi
|
|
if echo "$line" | grep -q 'ansible_ssh_private_key_file='; then
|
|
priv=$(echo "$line" | grep -oP 'ansible_ssh_private_key_file=\K[^\s]+')
|
|
fi
|
|
|
|
generate_entry "$hostname" "$host" "$user" "$port" "" "$priv"
|
|
done < "$file"
|
|
}
|
|
|
|
# ── EC2 Parser ────────────────────────────────────────────────────────
|
|
parse_ec2() {
|
|
local -a aws_args=("ec2" "describe-instances")
|
|
|
|
# Region
|
|
if [[ -n "$EC2_REGION" ]]; then
|
|
aws_args+=("--region" "$EC2_REGION")
|
|
fi
|
|
|
|
# Build filters
|
|
local -a filters=()
|
|
if [[ "$EC2_RUNNING_ONLY" == true ]]; then
|
|
filters+=("Name=instance-state-name,Values=running")
|
|
fi
|
|
for tag in "${EC2_TAGS[@]}"; do
|
|
local key="${tag%%=*}"
|
|
local val="${tag#*=}"
|
|
filters+=("Name=tag:${key},Values=${val}")
|
|
done
|
|
if [[ ${#filters[@]} -gt 0 ]]; then
|
|
aws_args+=("--filters" "${filters[@]}")
|
|
fi
|
|
|
|
aws_args+=(
|
|
"--query" "Reservations[].Instances[].[Tags[?Key==\`Name\`].Value | [0], PrivateIpAddress, InstanceId]"
|
|
"--output" "text"
|
|
)
|
|
|
|
local ec2_output
|
|
ec2_output=$(aws "${aws_args[@]}" 2>/dev/null) || die "aws ec2 describe-instances failed. Check credentials and region."
|
|
|
|
[[ -z "$ec2_output" ]] && die "No EC2 instances found matching the specified filters."
|
|
|
|
local source_label="ec2"
|
|
[[ -n "$EC2_REGION" ]] && source_label="ec2 (${EC2_REGION})"
|
|
generate_header "$source_label"
|
|
|
|
while IFS=$'\t' read -r name ip instance_id; do
|
|
# Use instance ID if no Name tag
|
|
if [[ -z "$name" || "$name" == "None" ]]; then
|
|
name="$instance_id"
|
|
fi
|
|
|
|
[[ -z "$ip" || "$ip" == "None" ]] && continue
|
|
|
|
generate_entry "$name" "$ip" "$DEFAULT_USER" "$DEFAULT_PORT" "" ""
|
|
done <<< "$ec2_output"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# OUTPUT
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
write_output() {
|
|
local content="$1"
|
|
|
|
if [[ -n "$OUTPUT_FILE" ]]; then
|
|
echo "$content" > "$OUTPUT_FILE"
|
|
echo "Roster written to ${OUTPUT_FILE} ($(echo "$content" | grep -c ':$' || true) hosts)" >&2
|
|
else
|
|
echo "$content"
|
|
fi
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
validate
|
|
|
|
local roster=""
|
|
|
|
case "$INPUT_MODE" in
|
|
csv) roster=$(parse_csv "$CSV_FILE") ;;
|
|
ansible) roster=$(parse_ansible_inventory "$ANSIBLE_FILE") ;;
|
|
ec2) roster=$(parse_ec2) ;;
|
|
esac
|
|
|
|
write_output "$roster"
|
|
}
|
|
|
|
main "$@"
|