#!/bin/bash ################################################ #### Salt Master/Minion Setup Automation #### #### Install and configure SaltStack #### #### #### #### 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_SALT_VERSION="latest" readonly DEFAULT_FILE_ROOTS="/srv/salt" readonly DEFAULT_PILLAR_ROOTS="/srv/pillar" readonly DEFAULT_MASTER_INTERFACE="0.0.0.0" readonly DEFAULT_MASTER_PORT_PUB=4505 readonly DEFAULT_MASTER_PORT_RET=4506 # Configuration variables (can be overridden by environment) SALT_VERSION=${SALT_VERSION:-$DEFAULT_SALT_VERSION} FILE_ROOTS=${FILE_ROOTS:-$DEFAULT_FILE_ROOTS} PILLAR_ROOTS=${PILLAR_ROOTS:-$DEFAULT_PILLAR_ROOTS} DEBUG=${DEBUG:-} # Runtime flags MODE="" MASTER_IP="" MINION_ID="" AUTO_ACCEPT=false AUTO_YES=false PKG_MANAGER="" OS_FAMILY="" OS_VERSION="" 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 } show_help() { cat << EOF Usage: $SCRIPT_NAME [OPTIONS] Automate Salt master and/or minion installation and configuration. Supports Ubuntu/Debian and RHEL/AlmaLinux. Adds the Salt Project repository, installs packages, configures services, creates directory structure, and opens firewall ports. OPTIONS: --mode master|minion|both What to install (required) --master-ip ADDRESS Salt master IP or hostname (required for minion/both) --minion-id NAME Custom minion ID (default: system hostname) --auto-accept Enable auto_accept on master (NOT for production) --salt-version VERSION Pin Salt version (default: latest) --yes Skip confirmation prompts --help, -h Show this help message ENVIRONMENT VARIABLES: SALT_VERSION Salt version to install (default: $DEFAULT_SALT_VERSION) FILE_ROOTS Master file_roots path (default: $DEFAULT_FILE_ROOTS) PILLAR_ROOTS Master pillar_roots path (default: $DEFAULT_PILLAR_ROOTS) DEBUG Enable debug output EXAMPLES: # Install salt-master sudo $SCRIPT_NAME --mode master --yes # Install salt-minion pointing to master sudo $SCRIPT_NAME --mode minion --master-ip 10.0.0.1 # Install both on the same node sudo $SCRIPT_NAME --mode both --master-ip localhost --yes # Install with custom minion ID sudo $SCRIPT_NAME --mode minion --master-ip salt.example.com --minion-id web01 # Install specific Salt version sudo $SCRIPT_NAME --mode master --salt-version 3006 --yes EOF } detect_os() { if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 source /etc/os-release OS_VERSION="$VERSION_ID" case "$ID" in ubuntu|debian) OS_FAMILY="debian" PKG_MANAGER="apt" ;; rhel|centos|rocky|almalinux|ol|fedora) OS_FAMILY="rhel" if command -v dnf >/dev/null 2>&1; then PKG_MANAGER="dnf" else PKG_MANAGER="yum" fi ;; *) log_error "Unsupported OS: $ID" exit 1 ;; esac else log_error "Cannot detect OS — /etc/os-release not found" exit 1 fi debug_echo "Detected OS: $OS_FAMILY ($PKG_MANAGER) version $OS_VERSION" } get_cpu_count() { nproc 2>/dev/null || echo 2 } add_salt_repo_debian() { log_info "Adding Salt Project repository (Debian/Ubuntu)..." apt-get update -qq apt-get install -y -qq curl gnupg2 >/dev/null local keyring="/etc/apt/keyrings/salt-archive-keyring.gpg" mkdir -p /etc/apt/keyrings curl -fsSL "https://repo.saltproject.io/salt/py3/ubuntu/${OS_VERSION}/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg" \ -o "$keyring" local repo_url="https://repo.saltproject.io/salt/py3/ubuntu/${OS_VERSION}/amd64" if [[ "$SALT_VERSION" != "latest" ]]; then repo_url="${repo_url}/${SALT_VERSION}" fi echo "deb [signed-by=${keyring}] ${repo_url} ${VERSION_CODENAME} main" \ > /etc/apt/sources.list.d/salt.list apt-get update -qq log_info "Salt repository added" } add_salt_repo_rhel() { log_info "Adding Salt Project repository (RHEL)..." local major_ver="${OS_VERSION%%.*}" local repo_url="https://repo.saltproject.io/salt/py3/redhat/${major_ver}/x86_64" if [[ "$SALT_VERSION" != "latest" ]]; then repo_url="${repo_url}/${SALT_VERSION}" fi cat > /etc/yum.repos.d/salt.repo << REPOEOF [salt] name=Salt Project for RHEL ${major_ver} baseurl=${repo_url} enabled=1 gpgcheck=1 gpgkey=https://repo.saltproject.io/salt/py3/redhat/${major_ver}/x86_64/SALT-PROJECT-GPG-PUBKEY-2023.pub REPOEOF "$PKG_MANAGER" clean expire-cache -q log_info "Salt repository added" } install_master() { log_info "Installing salt-master..." case "$PKG_MANAGER" in apt) apt-get install -y -qq salt-master >/dev/null ;; dnf|yum) "$PKG_MANAGER" install -y -q salt-master ;; esac log_info "salt-master installed" } install_minion() { log_info "Installing salt-minion..." case "$PKG_MANAGER" in apt) apt-get install -y -qq salt-minion >/dev/null ;; dnf|yum) "$PKG_MANAGER" install -y -q salt-minion ;; esac log_info "salt-minion installed" } configure_master() { log_info "Configuring salt-master..." local worker_threads worker_threads=$(get_cpu_count) if [[ -f /etc/salt/master ]]; then cp /etc/salt/master /etc/salt/master.bak."$(date +%Y%m%d%H%M%S)" log_info "Backed up existing /etc/salt/master" fi cat > /etc/salt/master << MASTEREOF ##### Salt Master Configuration ##### ##### Managed by salt-setup.sh ##### interface: ${DEFAULT_MASTER_INTERFACE} file_roots: base: - ${FILE_ROOTS} pillar_roots: base: - ${PILLAR_ROOTS} worker_threads: ${worker_threads} timeout: 30 state_events: True presence_events: True MASTEREOF if [[ "$AUTO_ACCEPT" == true ]]; then { echo "" echo "# WARNING: NOT recommended for production" echo "auto_accept: True" } >> /etc/salt/master log_warn "auto_accept enabled — NOT recommended for production" else { echo "" echo "auto_accept: False" } >> /etc/salt/master fi log_info "Master configuration written to /etc/salt/master" } configure_minion() { log_info "Configuring salt-minion..." local minion_id minion_id="${MINION_ID:-$(hostname -f 2>/dev/null || hostname)}" if [[ -f /etc/salt/minion ]]; then cp /etc/salt/minion /etc/salt/minion.bak."$(date +%Y%m%d%H%M%S)" log_info "Backed up existing /etc/salt/minion" fi cat > /etc/salt/minion << MINIONEOF ##### Salt Minion Configuration ##### ##### Managed by salt-setup.sh ##### master: ${MASTER_IP} id: ${minion_id} # grains: # role: webserver # environment: production MINIONEOF log_info "Minion configured (id: ${minion_id}, master: ${MASTER_IP})" } create_directory_structure() { log_info "Creating Salt directory structure..." mkdir -p "${FILE_ROOTS}" "${PILLAR_ROOTS}" if [[ ! -f "${FILE_ROOTS}/top.sls" ]]; then cat > "${FILE_ROOTS}/top.sls" << 'TOPEOF' base: '*': [] # - common # - packages TOPEOF log_info "Created ${FILE_ROOTS}/top.sls" fi if [[ ! -f "${PILLAR_ROOTS}/top.sls" ]]; then cat > "${PILLAR_ROOTS}/top.sls" << 'PTOPEOF' base: '*': [] # - common PTOPEOF log_info "Created ${PILLAR_ROOTS}/top.sls" fi } open_firewall_ports() { log_info "Configuring firewall for Salt master ports..." if command -v ufw >/dev/null 2>&1; then if ufw status | grep -q "Status: active"; then ufw allow ${DEFAULT_MASTER_PORT_PUB}/tcp >/dev/null ufw allow ${DEFAULT_MASTER_PORT_RET}/tcp >/dev/null log_info "Opened ports ${DEFAULT_MASTER_PORT_PUB}/${DEFAULT_MASTER_PORT_RET} in ufw" else debug_echo "ufw not active — skipping" fi elif command -v firewall-cmd >/dev/null 2>&1; then if firewall-cmd --state >/dev/null 2>&1; then firewall-cmd --permanent --add-port=${DEFAULT_MASTER_PORT_PUB}/tcp >/dev/null firewall-cmd --permanent --add-port=${DEFAULT_MASTER_PORT_RET}/tcp >/dev/null firewall-cmd --reload >/dev/null log_info "Opened ports ${DEFAULT_MASTER_PORT_PUB}/${DEFAULT_MASTER_PORT_RET} in firewalld" else debug_echo "firewalld not running — skipping" fi else log_warn "No supported firewall detected — manually open ports ${DEFAULT_MASTER_PORT_PUB} and ${DEFAULT_MASTER_PORT_RET}" fi } start_service() { local service="$1" log_info "Enabling and starting ${service}..." systemctl enable "$service" >/dev/null 2>&1 systemctl restart "$service" if systemctl is-active "$service" >/dev/null 2>&1; then log_info "${service} is running" else log_error "${service} failed to start" systemctl status "$service" --no-pager return 1 fi } show_summary() { echo "" echo "============================================" echo " Salt Setup Complete" echo "============================================" if [[ "$MODE" == "master" || "$MODE" == "both" ]]; then echo "" echo " Master:" echo " Config: /etc/salt/master" echo " File roots: ${FILE_ROOTS}" echo " Pillar roots: ${PILLAR_ROOTS}" echo " Ports: ${DEFAULT_MASTER_PORT_PUB}, ${DEFAULT_MASTER_PORT_RET}" echo "" echo " Master fingerprint:" salt-key -F master 2>/dev/null | grep -A1 "master.pub" || echo " (not yet generated — restart may be needed)" echo "" echo " Next steps:" echo " salt-key -L # List pending keys" echo " salt-key -a # Accept a minion key" echo " salt '*' test.ping # Test connectivity" fi if [[ "$MODE" == "minion" || "$MODE" == "both" ]]; then local minion_id minion_id="${MINION_ID:-$(hostname -f 2>/dev/null || hostname)}" echo "" echo " Minion:" echo " Config: /etc/salt/minion" echo " Master: ${MASTER_IP}" echo " Minion ID: ${minion_id}" echo "" echo " Next steps:" echo " salt-call test.ping # Test master connectivity" if [[ "$AUTO_ACCEPT" != true ]]; then echo " (on master) salt-key -a ${minion_id}" fi fi echo "" echo "============================================" } parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in --mode) MODE="$2" if [[ "$MODE" != "master" && "$MODE" != "minion" && "$MODE" != "both" ]]; then log_error "Mode must be 'master', 'minion', or 'both'" exit 1 fi shift 2 ;; --master-ip) MASTER_IP="$2" shift 2 ;; --minion-id) MINION_ID="$2" shift 2 ;; --auto-accept) AUTO_ACCEPT=true shift ;; --salt-version) SALT_VERSION="$2" shift 2 ;; --yes) AUTO_YES=true 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 "$MODE" ]]; then log_error "--mode is required (master, minion, or both)" show_help >&2 exit 1 fi if [[ "$MODE" == "minion" || "$MODE" == "both" ]]; then if [[ -z "$MASTER_IP" ]]; then log_error "--master-ip is required for minion/both modes" exit 1 fi fi detect_os } main() { parse_arguments "$@" validate_requirements echo "============================================" echo " Salt Setup" echo " Mode: $MODE" echo " OS: $OS_FAMILY ($PKG_MANAGER)" if [[ -n "$MASTER_IP" ]]; then echo " Master: $MASTER_IP" fi echo "============================================" echo "" if [[ "$AUTO_YES" != true ]]; then echo "Press Enter to continue, or Ctrl+C to abort..." read -r fi case "$OS_FAMILY" in debian) add_salt_repo_debian ;; rhel) add_salt_repo_rhel ;; esac if [[ "$MODE" == "master" || "$MODE" == "both" ]]; then install_master configure_master create_directory_structure open_firewall_ports start_service salt-master fi if [[ "$MODE" == "minion" || "$MODE" == "both" ]]; then install_minion configure_minion start_service salt-minion fi show_summary debug_echo "Script completed successfully" } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi