From 85007db580ccf662a45cf2aaeb83518ad2ddb85a Mon Sep 17 00:00:00 2001
From: Cullum Smith <cullum@sacredheartsc.com>
Date: Thu, 11 Jul 2024 10:55:45 -0400
Subject: initial boxconf scaffolding

---
 lib/10-core    | 340 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 lib/20-strings |  14 +++
 lib/30-files   | 175 +++++++++++++++++++++++++++++
 lib/40-os      |  64 +++++++++++
 lib/50-net     |  37 +++++++
 lib/50-zfs     |  15 +++
 6 files changed, 645 insertions(+)
 create mode 100644 lib/10-core
 create mode 100644 lib/20-strings
 create mode 100644 lib/30-files
 create mode 100644 lib/40-os
 create mode 100644 lib/50-net
 create mode 100644 lib/50-zfs

(limited to 'lib')

diff --git a/lib/10-core b/lib/10-core
new file mode 100644
index 0000000..8e01afc
--- /dev/null
+++ b/lib/10-core
@@ -0,0 +1,340 @@
+#!/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 ${1#${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.
+  _bcs_relevant_files=$(find "${BOXCONF_ROOT}" -type f -and \( \
+    -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}" \
+    \) )
+
+  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=$2; 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
+}
diff --git a/lib/20-strings b/lib/20-strings
new file mode 100644
index 0000000..f04f68d
--- /dev/null
+++ b/lib/20-strings
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+join(){
+  # Join strings by a given delimiter.
+  # $1 = delimiter
+  # $2..$N = strings
+  _bcj_delim=$1; shift
+  _bcj_result=''
+  while [ $# -gt 0 ]; do
+    _bcj_result="${_bcj_result:+${_bcj_result}${_bcj_delim}}${1}"
+    shift
+  done
+  printf '%s' "$_bcj_result"
+}
diff --git a/lib/30-files b/lib/30-files
new file mode 100644
index 0000000..c7b2000
--- /dev/null
+++ b/lib/30-files
@@ -0,0 +1,175 @@
+#!/bin/sh
+
+_boxconf_try_files(){
+  # Get the highest precedence file for a given path.
+  # $1 = target file path
+  for _bcsf_file in                                                                \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_HOSTNAME}"                             \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_HOSTNAME}"                                  \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_HOSTCLASS}.${BOXCONF_OS_DISTRIBUTION}" \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_HOSTCLASS}.${BOXCONF_OS_DISTRIBUTION}"      \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_OS_DISTRIBUTION}.${BOXCONF_HOSTCLASS}" \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_OS_DISTRIBUTION}.${BOXCONF_HOSTCLASS}"      \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_HOSTCLASS}.${BOXCONF_OS_FAMILY}"       \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_HOSTCLASS}.${BOXCONF_OS_FAMILY}"            \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_OS_FAMILY}.${BOXCONF_HOSTCLASS}"       \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_OS_FAMILY}.${BOXCONF_HOSTCLASS}"            \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_HOSTCLASS}"                            \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_HOSTCLASS}"                                 \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_OS_DISTRIBUTION}"                      \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_OS_DISTRIBUTION}"                           \
+    "${BOXCONF_SITE_FILE_DIR}${1}.${BOXCONF_OS_FAMILY}"                            \
+    "${BOXCONF_FILE_DIR}${1}.${BOXCONF_OS_FAMILY}"                                 \
+    "${BOXCONF_SITE_FILE_DIR}${1}.common" \
+    "${BOXCONF_FILE_DIR}${1}.common"
+  do
+    if [ -f "$_bcsf_file" ]; then
+      echo "$_bcsf_file"
+      return
+    fi
+  done
+
+  bug "no source file found for ${1}"
+}
+
+install_file(){
+  # Install the files at the given paths into the target system.
+  # The source file is chosen from the matching file in the boxconf directory with
+  # the highest-precedence suffix.
+  # Takes options similar to the `install` command.
+  _bcif_install_args='-Cv'
+  _bcif_mode=0644
+
+  while getopts m:o:g: _bcif_opt; do
+    case $_bcif_opt in
+      m) _bcif_mode=$OPTARG ;;
+      o) _bcif_install_args="${_bcif_install_args} -o ${OPTARG}" ;;
+      g) _bcif_install_args="${_bcif_install_args} -g ${OPTARG}" ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+
+  while [ $# -gt 0 ]; do
+    _bcif_src=$(_boxconf_try_files "$1")
+    install -m "$_bcif_mode" $_bcif_install_args "$_bcif_src" "$1"
+    shift
+  done
+}
+
+install_directory(){
+  # Create the specified directories in the target system.
+  # Takes options similar to the `install` command.
+  _bcid_install_args='-Cdv'
+  _bcid_mode=0755
+
+  while getopts m:o:g: _bcid_opt; do
+    case $_bcid_opt in
+      m) _bcid_mode=$OPTARG ;;
+      o) _bcid_install_args="${_bcid_install_args} -o ${OPTARG}" ;;
+      g) _bcid_install_args="${_bcid_install_args} -g ${OPTARG}" ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+
+  while [ $# -gt 0 ]; do
+    install -m "$_bcid_mode" $_bcid_install_args "$1"
+    shift
+  done
+}
+
+install_template(){
+  # Install the templatess at the given paths into the target system.
+  # The source template is chosen from the matching file in the boxconf directory
+  # with the highest-precedence suffix. Template is rendered as a shell heredoc.
+  # Takes options similar to the `install` command.
+  _bcit_install_args='-Cv'
+  _bcit_mode=0644
+
+  while getopts m:o:g: _bcit_opt; do
+    case $_bcit_opt in
+      m) _bcit_mode=$OPTARG ;;
+      o) _bcit_install_args="${_bcit_install_args} -o ${OPTARG}" ;;
+      g) _bcit_install_args="${_bcit_install_args} -g ${OPTARG}" ;;
+    esac
+  done
+  shift $((OPTIND - 1 ))
+
+  while [ $# -gt 0 ]; do
+    _bcit_src=$(_boxconf_try_files "$1")
+
+    eval "cat <<__BOXCONF_EOF__ >${_bcit_src}.render
+$(cat "$_bcit_src")
+__BOXCONF_EOF__
+"
+    [ -s "${_bcit_src}.render" ] || bug "failed to render template: ${_bcit_src}"
+    install -m "$_bcit_mode" $_bcit_install_args "${_bcit_src}.render" "$1"
+    shift
+  done
+}
+
+install_certificate(){
+  # Install a certificate from the CA dir into the target system.
+  # Takes options similar to the `install` command.
+  # $1 = certificate name
+  # $2 = target path
+  _bcic_install_args='-Cv'
+  _bcic_mode=0644
+
+  while getopts m:o:g: _bcic_opt; do
+    case $_bcic_opt in
+      m) _bcic_mode=$OPTARG ;;
+      o) _bcic_install_args="${_bcic_install_args} -o ${OPTARG}" ;;
+      g) _bcic_install_args="${_bcic_install_args} -g ${OPTARG}" ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+
+  [ -f "${BOXCONF_CA_DIR}/${BOXCONF_HOSTNAME}/${1}.fullchain.crt" ] \
+    || bug "no certificate exists for ${BOXCONF_HOSTNAME}/${1}"
+
+  install -m "$_bcic_mode" $_bcic_install_args "${BOXCONF_CA_DIR}/${BOXCONF_HOSTNAME}/${1}.fullchain.crt" "$2"
+}
+
+install_certificate_key(){
+  # Install a certificate's private key from the CA dir into the target system.
+  # Takes options similar to the `install` command.
+  # $1 = certificate name
+  # $2 = target path
+  _bcick_install_args='-Cv'
+  _bcick_mode=0600
+
+  while getopts m:o:g: _bcick_opt; do
+    case $_bcick_opt in
+      m) _bcick_mode=$OPTARG ;;
+      o) _bcick_install_args="${_bcick_install_args} -o ${OPTARG}" ;;
+      g) _bcick_install_args="${_bcick_install_args} -g ${OPTARG}" ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+
+  [ -f "${BOXCONF_CA_DIR}/${BOXCONF_HOSTNAME}/${1}.key" ] \
+    || bug "no key exists for ${BOXCONF_HOSTNAME}/${1}"
+
+  install -m "$_bcick_mode" $_bcick_install_args "${BOXCONF_CA_DIR}/${BOXCONF_HOSTNAME}/${1}.key" "$2"
+}
+
+install_ca_certificate(){
+  # Install a the root CA from the CA dir into the target system.
+  # Takes options similar to the `install` command.
+  # $1 = target path
+  _bcicc_install_args='-Cv'
+  _bcicc_mode=0644
+
+  while getopts m:o:g: _bcicc_opt; do
+    case $_bcicc_opt in
+      m) _bcicc_mode=$OPTARG ;;
+      o) _bcicc_install_args="${_bcicc_install_args} -o ${OPTARG}" ;;
+      g) _bcicc_install_args="${_bcicc_install_args} -g ${OPTARG}" ;;
+    esac
+  done
+  shift $((OPTIND - 1))
+
+  [ -f "${BOXCONF_CA_DIR}/ca.crt" ] || bug 'CA certificate not found'
+
+  install -m "$_bcicc_mode" $_bcicc_install_args "${BOXCONF_CA_DIR}/ca.crt" "$1"
+}
diff --git a/lib/40-os b/lib/40-os
new file mode 100644
index 0000000..cedeb86
--- /dev/null
+++ b/lib/40-os
@@ -0,0 +1,64 @@
+#!/bin/sh
+
+set_sysctl(){
+  # Set sysctl value(s) and persist them to /etc/sysctl.conf.
+  # $1..$N = sysctl values (as "name=value" strings)
+  while [ $# -gt 0 ]; do
+    sysctl "$1"
+    sed -i.bak "/^${1%%=*}=/{
+h
+s/=.*/=${1#*=}/
+}
+\${
+x
+/^\$/{
+s//${1}/
+H
+}
+x
+}" /etc/sysctl.conf
+    shift
+  done
+  rm -f /etc/sysctl.conf.bak
+}
+
+set_loader_conf(){
+  # Set the FreeBSD bootloader options in /boot/loader.conf.
+  # The host will be rebooted if the file is changed.
+  # $1..$N = bootloader options (as "name=value" strings)
+  [ "$BOXCONF_OS_FAMILY" = freebsd ] || bug 'set_loader_conf can only be used on FreeBSD'
+
+  while [ $# -gt 0 ]; do
+    grep -qxF "${1%%=*}=\"${1#*=}\"" /boot/loader.conf || BOXCONF_NEED_REBOOT=true
+    sed -i.bak "/^${1%%=*}=/{
+h
+s/=.*/=\"${1#*=}\"/
+}
+\${
+x
+/^\$/{
+s//${1%%=*}=\"${1#*=}\"/
+H
+}
+x
+}" /boot/loader.conf
+    shift
+  done
+  rm -f /boot/loader.conf.bak
+}
+
+load_kernel_module(){
+  # Ensure the given kernel modules are loaded.
+  # $1..$N = bootloader options (as "name=value" strings)
+  case $BOXCONF_OS_FAMILY in
+    freebsd)
+      while [ $# -gt 0 ]; do
+        kldstat -qn "$1" || kldload -v "$1"
+        shift
+      done
+      ;;
+    *)
+      die "load_kernel_module unimplemented for ${BOXCONF_OS_FAMILY}"
+      ;;
+  esac
+}
diff --git a/lib/50-net b/lib/50-net
new file mode 100644
index 0000000..37b1cb3
--- /dev/null
+++ b/lib/50-net
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+_boxconf_ip2dec(){
+  # helper function for ip_in_subnet
+  while [ $# -gt 0 ]; do
+    echo "$1" | {
+      IFS=./ read -r _bcipd_a _bcipd_b _bcipd_c _bcipd_d _bcipd_e
+      [ -n "$_bcipd_e" ] || _bcipd_e=32
+      printf '%s %s ' "$((_bcipd_a<<24|_bcipd_b<<16|_bcipd_c<<8|_bcipd_d))" "$((-1<<(32-_bcipd_e)))"
+    }
+    shift
+  done
+}
+
+ip_in_subnet(){
+  # Check if an IP address is contained within a subnet.
+  # $1 = IPv4 address
+  # $2 = network cidr
+  _boxconf_ip2dec "$1" "$2" | {
+    read -r _bciis_a1 _bciis_m1 _bciis_a2 _bciis_m2 ||:
+    test "$(( (_bciis_a1 & _bciis_m2) == (_bciis_a2 & _bciis_m2) && _bciis_m1 >= _bciis_m2 ))" -eq 1
+  }
+}
+
+prefix2netmask(){
+  # Convert a network prefix to its netmask address.
+  # For example, 24 returns '255.255.255.0'
+  # $1 = network prefix value
+  _bcp2n_val=$(( 0xffffffff ^ ((1 << (32 - $1)) - 1) ))
+  echo "$(( (_bcp2n_val >> 24) & 0xff )).$(( (_bcp2n_val >> 16) & 0xff )).$(( (_bcp2n_val >> 8) & 0xff )).$(( _bcp2n_val & 0xff ))"
+}
+
+ip2rdns(){
+  # Convert an IPv4 address to its in-addr.arpa reverse DNS name.
+  # $1 = IPv4 address
+  echo "$1" | awk -F. '{print $4"."$3"."$2"."$1".in-addr.arpa"}'
+}
diff --git a/lib/50-zfs b/lib/50-zfs
new file mode 100644
index 0000000..2948601
--- /dev/null
+++ b/lib/50-zfs
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+dataset_exists(){
+  # Check if a ZFS dataset exists.
+  # $1 = dataset name
+  zfs list "$1" > /dev/null 2>&1
+}
+
+create_dataset(){
+  # Create a ZFS dataset if it doesn't already exists.
+  # All options are passed directly to `zfs create`. Assumes the dataset name is
+  # passed as the final argument.
+  eval "_bccd_dataset=\$$#"
+  dataset_exists "$_bccd_dataset" || zfs create -v "$@"
+}
-- 
cgit v1.2.3