blob: 5fa2a1651f949bcd99b55553d968df148e47ba89 (
plain) (
tree)
|
|
#!/bin/sh
BOXCONF_REMOTE_PATH=/root/boxconf
BOXCONF_SSH_ARGS='-l root -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlPath=~/.ssh/%r@%h:%p -o ControlMaster=auto -o ControlPersist=10m'
BOXCONF_SUPPORTED_OS='linux freebsd'
BOXCONF_SUPPORTED_DISTROS='debian'
BOXCONF_SCRIPT_DIR="${BOXCONF_ROOT}/scripts"
BOXCONF_VAR_DIR="${BOXCONF_ROOT}/vars"
BOXCONF_FILE_DIR="${BOXCONF_ROOT}/files"
BOXCONF_SITE_VAR_DIR="${BOXCONF_ROOT}/site/vars"
BOXCONF_SITE_FILE_DIR="${BOXCONF_ROOT}/site/files"
BOXCONF_SITE_SCRIPT_DIR="${BOXCONF_ROOT}/site/scripts"
BOXCONF_CA_DIR="${BOXCONF_ROOT}/site/ca"
BOXCONF_VAULT_PASSWORD_FILE="${BOXCONF_ROOT}/.vault_password"
BOXCONF_VAULT_CIPHER=aes256
log(){
printf '%s: %s\n' "$PROGNAME" "$1"
}
debug(){
printf '%s: DEBUG: %s\n' "$PROGNAME" "$1" 1>&2
}
warn(){
printf '%s: WARNING: %s\n' "$PROGNAME" "$1" 1>&2
}
die(){
printf '%s: ERROR: %s\n' "$PROGNAME" "$1" 1>&2
exit "${2:-1}"
}
bug(){
printf '%s: BUG: %s\n' "$PROGNAME" "$1" 1>&2
exit 255
}
_boxconf_read_password(){
# Read a password from stdin with TTY echo disabled.
# $1 = prompt
# $2 = upvar
if [ -t 0 ]; then
_bcrp_stty=$(stty -g)
stty -echo
fi
printf '%s ' "$1" 1>&2
read -r "$2"
if [ -t 0 ]; then
stty "$_bcrp_stty"
echo
fi
}
_boxconf_get_vault_password(){
# Acquire the vault password.
# If the BOXCONF_VAULT_PASSWORD environment variable is set, use that.
# Next, try reading the password from the .vault_password file.
# If all else fails, prompt interactively.
if [ -z "${BOXCONF_VAULT_PASSWORD:-}" ]; then
if [ -f "${BOXCONF_VAULT_PASSWORD_FILE}" ]; then
BOXCONF_VAULT_PASSWORD=$(cat "${BOXCONF_VAULT_PASSWORD_FILE}")
else
_boxconf_read_password 'Enter vault password:' BOXCONF_VAULT_PASSWORD
fi
fi
}
_boxconf_decrypt(){
# Decrypt a file using the vault password.
# $1 = encrypted file
# $2 = plaintext output file (or stdout if unset)
_boxconf_get_vault_password
if [ $# -gt 1 ]; then
PASS=$BOXCONF_VAULT_PASSWORD openssl enc -in "$1" -out "$2" -d "-${BOXCONF_VAULT_CIPHER}" -pass env:PASS -pbkdf2
else
PASS=$BOXCONF_VAULT_PASSWORD openssl enc -in "$1" -d "-${BOXCONF_VAULT_CIPHER}" -pass env:PASS -pbkdf2
fi
}
_boxconf_is_encrypted(){
# Check if a given file is encrypted.
head -n1 "$1" | grep -q '^Salted__'
}
_boxconf_include(){
# Source the script (or scripts) at the given path.
# If the path is a directory, source all its files in glob order.
# $1 = path
while [ $# -gt 0 ]; do
if [ -f "$1" ]; then
log "sourcing ${1#${BOXCONF_ROOT}/}"
BOXCONF_SOURCE=$1
. "$BOXCONF_SOURCE"
elif [ -d "$1" ]; then
for _bci_file in "$1"/*; do
if [ -f "$_bci_file" ]; then
log "sourcing ${_bci_file#${BOXCONF_ROOT}/}"
BOXCONF_SOURCE=$_bci_file
. "$BOXCONF_SOURCE"
fi
done
fi
shift
done
}
_boxconf_get_hostclass(){
# For a given hostname, find its hostclass and store it in $BOXCONF_HOSTCLASS.
# Hostclass regexes are specified in the hostclasses file.
# If no hostclass matches, the hostclass is 'undefined'.
# $1 = hostname
BOXCONF_HOSTCLASS=undefined
while read -r _bcc_hostclass _bcc_regex; do
if printf '%s\n' "$1" | grep -Eq "$_bcc_regex"; then
BOXCONF_HOSTCLASS=$_bcc_hostclass
break
fi
done < "${BOXCONF_ROOT}/hostclasses"
log "using hostclass ${BOXCONF_HOSTCLASS}"
}
_boxconf_stage(){
# Construct a directory tree containing all files required to configure a host.
# Encrypted files will be copied in plaintext.
# $1 = target hostnmae
# $2 = staging directory path
_bcs_hostname=$1
_bcs_stagedir=$2
log "generating configuration tarball for ${_bcs_hostname} at ${_bcs_stagedir}"
_boxconf_get_hostclass "$_bcs_hostname"
cp -RpL \
"${BOXCONF_ROOT}/boxconf" \
"${BOXCONF_ROOT}/hostclasses" \
"${BOXCONF_ROOT}/lib" \
"$_bcs_stagedir"
# Compex find expression to only copy files necessary for the target host.
# This avoids leaking site-wide secrets to hosts that don't require them.
set -f
_bcs_relevant_files=$(find -L "$BOXCONF_ROOT" -type f -and \( \
-path "${BOXCONF_CA_DIR}/ca.crt" \
-or -path "${BOXCONF_CA_DIR}/${_bcs_hostname}" \
-or -path "${BOXCONF_VAR_DIR}/common" \
-or -path "${BOXCONF_VAR_DIR}/common/*" \
-or -path "${BOXCONF_VAR_DIR}/os/*" \
-or -path "${BOXCONF_VAR_DIR}/distro/*" \
-or -path "${BOXCONF_VAR_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
-or -path "${BOXCONF_VAR_DIR}/hostclass/${BOXCONF_HOSTCLASS}/*" \
-or -path "${BOXCONF_VAR_DIR}/hostname/${_bcs_hostname}" \
-or -path "${BOXCONF_VAR_DIR}/hostname/${_bcs_hostname}/*" \
-or -path "${BOXCONF_SITE_VAR_DIR}/common" \
-or -path "${BOXCONF_SITE_VAR_DIR}/common/*" \
-or -path "${BOXCONF_SITE_VAR_DIR}/os/*" \
-or -path "${BOXCONF_SITE_VAR_DIR}/distro/*" \
-or -path "${BOXCONF_SITE_VAR_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
-or -path "${BOXCONF_SITE_VAR_DIR}/hostclass/${BOXCONF_HOSTCLASS}/*" \
-or -path "${BOXCONF_SITE_VAR_DIR}/hostname/${_bcs_hostname}" \
-or -path "${BOXCONF_SITE_VAR_DIR}/hostname/${_bcs_hostname}/*" \
-or -path "${BOXCONF_SCRIPT_DIR}/common" \
-or -path "${BOXCONF_SCRIPT_DIR}/common/*" \
-or -path "${BOXCONF_SCRIPT_DIR}/os/*" \
-or -path "${BOXCONF_SCRIPT_DIR}/distro/*" \
-or -path "${BOXCONF_SCRIPT_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
-or -path "${BOXCONF_SCRIPT_DIR}/hostclass/${BOXCONF_HOSTCLASS}/*" \
-or -path "${BOXCONF_SCRIPT_DIR}/hostname/${_bcs_hostname}" \
-or -path "${BOXCONF_SCRIPT_DIR}/hostname/${_bcs_hostname}/*" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/common" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/common/*" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/os/*" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/distro/*" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/hostclass/${BOXCONF_HOSTCLASS}/*" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/hostname/${_bcs_hostname}" \
-or -path "${BOXCONF_SITE_SCRIPT_DIR}/hostname/${_bcs_hostname}/*" \
-or -path "${BOXCONF_FILE_DIR}/*.common" \
$(printf -- "-or -path ${BOXCONF_FILE_DIR}/*.%s " ${BOXCONF_SUPPORTED_OS}) \
$(printf -- "-or -path ${BOXCONF_FILE_DIR}/*.%s " ${BOXCONF_SUPPORTED_DISTROS}) \
$(printf -- "-or -path ${BOXCONF_FILE_DIR}/*.${BOXCONF_HOSTCLASS}.%s " ${BOXCONF_SUPPORTED_OS}) \
$(printf -- "-or -path ${BOXCONF_FILE_DIR}/*.%s.${BOXCONF_HOSTCLASS} " ${BOXCONF_SUPPORTED_OS}) \
$(printf -- "-or -path ${BOXCONF_FILE_DIR}/*.${BOXCONF_HOSTCLASS}.%s " ${BOXCONF_SUPPORTED_DISTROS}) \
$(printf -- "-or -path ${BOXCONF_FILE_DIR}/*.%s.${BOXCONF_HOSTCLASS} " ${BOXCONF_SUPPORTED_DISTROS}) \
-or -path "${BOXCONF_FILE_DIR}/*.${BOXCONF_HOSTCLASS}" \
-or -path "${BOXCONF_FILE_DIR}/*.${_bcs_hostname}" \
-or -path "${BOXCONF_SITE_FILE_DIR}/*.common" \
$(printf -- "-or -path ${BOXCONF_SITE_FILE_DIR}/*.%s " ${BOXCONF_SUPPORTED_OS}) \
$(printf -- "-or -path ${BOXCONF_SITE_FILE_DIR}/*.%s " ${BOXCONF_SUPPORTED_DISTROS}) \
$(printf -- "-or -path ${BOXCONF_SITE_FILE_DIR}/*.${BOXCONF_HOSTCLASS}.%s " ${BOXCONF_SUPPORTED_OS}) \
$(printf -- "-or -path ${BOXCONF_SITE_FILE_DIR}/*.%s.${BOXCONF_HOSTCLASS} " ${BOXCONF_SUPPORTED_OS}) \
$(printf -- "-or -path ${BOXCONF_SITE_FILE_DIR}/*.${BOXCONF_HOSTCLASS}.%s " ${BOXCONF_SUPPORTED_DISTROS}) \
$(printf -- "-or -path ${BOXCONF_SITE_FILE_DIR}/*.%s.${BOXCONF_HOSTCLASS} " ${BOXCONF_SUPPORTED_DISTROS}) \
-or -path "${BOXCONF_SITE_FILE_DIR}/*.${BOXCONF_HOSTCLASS}" \
-or -path "${BOXCONF_SITE_FILE_DIR}/*.${_bcs_hostname}" \
\) )
set +f
OIFS=$IFS; IFS=$'\n'
set -- $_bcs_relevant_files
IFS=$OIFS
for _bc_stage_fullpath; do
# Calculate the file's path relative to the BOXCONF_ROOT.
_bc_stage_relpath=${_bc_stage_fullpath#${BOXCONF_ROOT}/}
# Create the file's parent directories (if any) in the stage dir.
mkdir -p "${_bcs_stagedir}/$(dirname "$_bc_stage_relpath")"
# Copy the file to the stage dir, decrypting if necessary.
if _boxconf_is_encrypted "$_bc_stage_fullpath"; then
_boxconf_decrypt "$_bc_stage_fullpath" "${_bcs_stagedir}/${_bc_stage_relpath}"
else
cp -p "$_bc_stage_fullpath" "${_bcs_stagedir}/${_bc_stage_relpath}"
fi
done
}
_boxconf_deploy(){
# Build a configuration tarball and SCP it to a host, then extract and run it.
# $1 = target hostname/IP
# $2 = hostname used for configuration
# $3..$N = original boxconf CLI args
_bc_deploy_target=$1; shift
_bc_deploy_hostname=$1; shift
_bc_stagedir=$(mktemp -d -t "boxconf-${_bc_deploy_hostname}")
trap 'rm -rf -- "$_bc_stagedir"' HUP INT QUIT TERM ABRT EXIT
_boxconf_stage "$_bc_deploy_hostname" "$_bc_stagedir"
log "deploying tarball for ${_bc_deploy_hostname} to ${_bc_deploy_target}:${BOXCONF_REMOTE_PATH}"
# Create the boxconf directory with mode 700 on the target host.
ssh ${BOXCONF_SSH_ARGS} "$_bc_deploy_target" -- rm -rf "$BOXCONF_REMOTE_PATH" '&&' install -d -m 700 "$BOXCONF_REMOTE_PATH"
# Send the boxconf tarball to the target host, and extract it.
tar -C "$_bc_stagedir" -czf - ./ \
| ssh ${BOXCONF_SSH_ARGS} "$_bc_deploy_target" -- tar -xzf - -C "$BOXCONF_REMOTE_PATH"
# Re-exec boxconf on the target host.
ssh ${BOXCONF_SSH_ARGS} "$_bc_deploy_target" -- "${BOXCONF_REMOTE_PATH}/boxconf" -X "$@"
}
_boxconf_run(){
# This is the main entry point for boxconf when running on the target host.
# Gather basic info about the target system, then source all the vars and scripts
# files, in order of lowest to highest precedence.
log "now running on target host (current hostname: $(hostname -s))"
# Determine OS family.
case "$(uname)" in
Linux) BOXCONF_OS=linux ;;
FreeBSD) BOXCONF_OS=freebsd ;;
*) die "unsupported os family: $(uname)" ;;
esac
log "detected os ${BOXCONF_OS}"
# Determine default interface and IPv4 address.
case $BOXCONF_OS in
freebsd)
BOXCONF_DEFAULT_INTERFACE=$(route -4n get default | awk '$1 == "interface:" { print $2 }')
BOXCONF_DEFAULT_IPV4=$(ifconfig "$BOXCONF_DEFAULT_INTERFACE" | awk '$1 == "inet" { print $2 }')
;;
linux)
BOXCONF_DEFAULT_INTERFACE=$(ip -4 -o route get to 1 | awk '{print $5}')
BOXCONF_DEFAULT_IPV4=$(ip -4 -o route get to 1 | awk '{print $7}')
;;
esac
log "detected default interface ${BOXCONF_DEFAULT_INTERFACE}"
log "detected default ip ${BOXCONF_DEFAULT_IPV4}"
# Determine OS distribution.
if [ -f /etc/os-release ]; then
BOXCONF_DISTRO=$(. /etc/os-release; printf '%s' "$ID")
BOXCONF_OS_VERSION=$(. /etc/os-release; printf '%s' "$VERSION_ID")
else
die 'unknown os distribution'
fi
log "detected distro ${BOXCONF_DISTRO}"
log "detected os version ${BOXCONF_OS_VERSION}"
case $BOXCONF_DISTRO in
freebsd|debian) : ;; # supported
*) die "unsupported os distribution: ${BOXCONF_DISTRO}" ;;
esac
# Determine virtualization type.
BOXCONF_VIRTUALIZATION_TYPE=none
case $BOXCONF_OS in
linux)
grep -q '^flags.* hypervisor' /proc/cpuinfo && BOXCONF_VIRTUALIZATION_TYPE=vm
;;
freebsd)
if [ "$(sysctl -n security.jail.jailed)" = 1 ]; then
BOXCONF_VIRTUALIZATION_TYPE=jail
elif [ -n "$(sysctl -n hw.hv_vendor)" ]; then
BOXCONF_VIRTUALIZATION_TYPE=vm
fi
;;
esac
log "detected virtualization type ${BOXCONF_VIRTUALIZATION_TYPE}"
# Determine hostname.
: "${BOXCONF_HOSTNAME:=$(hostname -s)}"
log "using hostname ${BOXCONF_HOSTNAME}"
# Determine hostclass.
_boxconf_get_hostclass "$BOXCONF_HOSTNAME"
[ "$BOXCONF_HOSTCLASS" = undefined ] && warn 'unable to determine hostclass'
# Source vars, then scripts, each in order of lowest to highest precedence.
_boxconf_include \
"${BOXCONF_VAR_DIR}/common" \
"${BOXCONF_SITE_VAR_DIR}/common" \
"${BOXCONF_VAR_DIR}/os/${BOXCONF_OS}" \
"${BOXCONF_SITE_VAR_DIR}/os/${BOXCONF_OS}" \
"${BOXCONF_VAR_DIR}/distro/${BOXCONF_DISTRO}" \
"${BOXCONF_SITE_VAR_DIR}/distro/${BOXCONF_DISTRO}" \
"${BOXCONF_VAR_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
"${BOXCONF_SITE_VAR_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
"${BOXCONF_VAR_DIR}/hostname/${BOXCONF_HOSTNAME}" \
"${BOXCONF_SITE_VAR_DIR}/hostname/${BOXCONF_HOSTNAME}" \
"${BOXCONF_SCRIPT_DIR}/common" \
"${BOXCONF_SITE_SCRIPT_DIR}/common" \
"${BOXCONF_SCRIPT_DIR}/os/${BOXCONF_OS}" \
"${BOXCONF_SITE_SCRIPT_DIR}/os/${BOXCONF_OS}" \
"${BOXCONF_SCRIPT_DIR}/distro/${BOXCONF_DISTRO}" \
"${BOXCONF_SITE_SCRIPT_DIR}/distro/${BOXCONF_DISTRO}" \
"${BOXCONF_SCRIPT_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
"${BOXCONF_SITE_SCRIPT_DIR}/hostclass/${BOXCONF_HOSTCLASS}" \
"${BOXCONF_SCRIPT_DIR}/hostname/${BOXCONF_HOSTNAME}" \
"${BOXCONF_SITE_SCRIPT_DIR}/hostname/${BOXCONF_HOSTNAME}"
# Reboot the target host if requested.
if [ "${BOXCONF_NEED_REBOOT:-}" = true ]; then
log '$BOXCONF_NEED_REBOOT was set. Rebooting host...'
reboot
fi
}
|