aboutsummaryrefslogblamecommitdiff
path: root/lib/10-core
blob: 60d110105cc8d124ca1eecc04e6f7d0ae7c2de9e (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_decrypt_key(){
  # Decrypt an OpenSSL key file using the vault password.
  # $1 = encrypted key file
  # $2 = plaintext output file (or stdout if unset)
  _boxconf_get_vault_password

  if [ $# -gt 1 ]; then
    PASS=$BOXCONF_VAULT_PASSWORD openssl ec -in "$1" -out "$2" -passin env:PASS
  else
    PASS=$BOXCONF_VAULT_PASSWORD openssl ec -in "$1" -passin env:PASS
  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 _bcs_fullpath; do
    # Calculate the file's path relative to the BOXCONF_ROOT.
    _bcs_relpath=${_bcs_fullpath#${BOXCONF_ROOT}/}

    # Create the file's parent directories (if any) in the stage dir.
    mkdir -p "${_bcs_stagedir}/$(dirname "$_bcs_relpath")"

    # Copy the file to the stage dir, decrypting if necessary.
    if _boxconf_is_encrypted "$_bcs_fullpath"; then
      _boxconf_decrypt "$_bcs_fullpath" "${_bcs_stagedir}/${_bcs_relpath}"
    elif head -n1 "$_bcs_fullpath" | grep -Fxq -- '-----BEGIN ENCRYPTED PRIVATE KEY-----'; then
      _boxconf_decrypt_key "$_bcs_fullpath" "${_bcs_stagedir}/${_bcs_relpath}"
    else
      cp -p "$_bcs_fullpath" "${_bcs_stagedir}/${_bcs_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}.XXXXXX")
  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 ] && [ "${boxconf_reboot:-}" != false ]; then
    log '$BOXCONF_NEED_REBOOT was set. Rebooting host...'
    reboot
  fi
}