Files
linux-scripts/salt-setup.sh
T

510 lines
14 KiB
Bash

#!/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 <minion_id> # 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