aboutsummaryrefslogtreecommitdiff
path: root/files/usr/local/sbin/jailctl.freebsd_hypervisor
diff options
context:
space:
mode:
Diffstat (limited to 'files/usr/local/sbin/jailctl.freebsd_hypervisor')
-rw-r--r--files/usr/local/sbin/jailctl.freebsd_hypervisor1098
1 files changed, 1098 insertions, 0 deletions
diff --git a/files/usr/local/sbin/jailctl.freebsd_hypervisor b/files/usr/local/sbin/jailctl.freebsd_hypervisor
new file mode 100644
index 0000000..05c0158
--- /dev/null
+++ b/files/usr/local/sbin/jailctl.freebsd_hypervisor
@@ -0,0 +1,1098 @@
+#!/bin/sh
+#
+# Jail management utility.
+
+set -eu -o pipefail
+
+. /usr/local/etc/jailctl.conf
+
+cmd::main(){
+ local usage="COMMAND [ARGS]...
+FreeBSD jail management utility.
+Commands:
+ create Create a new jail
+ create-snapshot Take a snapshot of a jail
+ create-template Create a template from a jail
+ destroy-snapshot Delete a jail snapshot
+ destroy-template Delete a template
+ destroy Delete a jail and its dataset
+ download-release Download and create a FreeBSD release template
+ edit Edit a jail's configuration
+ exec Run a command within the jail
+ list-snapshots List jail snapshots
+ list-templates List available templates
+ list List configured jails
+ reprovision Wipe and reprovision an OS dataset from template
+ restart Restart a jail
+ rollback Rollback a jail to a given snapshot
+ shell Run a shell within the jail
+ show Show jail configuration
+ start Start a jail
+ status Show running jail status
+ stop Stop a jail
+ update-release Update a FreeBSD release template"
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -ge 1 ] || cmd::usage 'no comand specified'
+ local cmd=$1; shift
+
+ case $cmd in
+ create) cmd::create "$@" ;;
+ create-snapshot|snapshot|snap) cmd::create_snapshot "$@" ;;
+ create-template) cmd::create_template "$@" ;;
+ destroy-snapshot|rms) cmd::destroy_snapshot "$@" ;;
+ destroy-template|rmt) cmd::destroy_template "$@" ;;
+ destroy|rm) cmd::destroy "$@" ;;
+ download-release) cmd::download_release "$@" ;;
+ edit) cmd::edit "$@" ;;
+ exec) cmd::exec "$@" ;;
+ list-snapshots|lss) cmd::list_snapshots "$@" ;;
+ list-templates|lst) cmd::list_templates "$@" ;;
+ list|ls) cmd::list "$@" ;;
+ reprovision) cmd::reprovision "$@" ;;
+ restart) cmd::restart "$@" ;;
+ rollback) cmd::rollback "$@" ;;
+ shell|sh) cmd::shell "$@" ;;
+ show) cmd::show "$@" ;;
+ start) cmd::start "$@" ;;
+ status) cmd::status "$@" ;;
+ stop) cmd::stop "$@" ;;
+ update-release) cmd::update_release "$@" ;;
+ # The following commands are internal to jailctl. Don't run them manually.
+ _create-epair) cmd::_create_epair "$@" ;;
+ _destroy-epair) cmd::_destroy_epair "$@" ;;
+ *) cmd::usage "unknown command: ${cmd}" ;;
+ esac
+}
+
+################################################################################
+# Standard helper functions.
+################################################################################
+die(){
+ printf '%s: %s\n' jailctl "$*" 1>&2
+ exit 1
+}
+
+warn(){
+ printf '%s\n' "$*" 1>&2
+}
+
+
+################################################################################
+# CLI-related functions.
+################################################################################
+cmd::help(){
+ printf 'Usage: %s %s\n' jailctl "$usage"
+ [ -n "${help:-}" ] && printf '%s\n' "$help"
+ exit 0
+}
+
+cmd::usage(){
+ [ $# -gt 0 ] && printf '%s: %s\n' "jailctl" "$1" 1>&2
+ printf 'Usage: %s %s\n' jailctl "${usage}" 1>&2
+ exit 2
+}
+
+cmd::getopt_help(){
+ local opt
+ while getopts :h opt; do
+ case $opt in
+ h) cmd::help ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+}
+
+cmd::create(){
+ local usage='create [-a IP] [-b] [-c CPUSET] [-d DOMAIN] [-g GATEWAY] [-k SSHKEY]
+ [-m MEMLIMIT] [-n NETMASK] [-q QUOTA] [-Q OS_QUOTA] [-r NAMESERVER]
+ [-s SEARCHDOMAIN] [-v VLANID] NAME TEMPLATE'
+ local help="Create a new jail.
+Options:
+ -a IP IPv4 address
+ -b Enable BPF device (allows DHCP, tcpdump, etc)
+ -c CPUSET CPU list for cpuset(1)
+ -d DOMAIN Host domain name
+ -g GATEWAY Default IPv4 gateway
+ -k SSHKEY Path to SSH pubkey for root's authorized_keys
+ -m MEMLIMIT Virtual memory limit
+ -n NETMASK IPv4 netmask
+ -q QUOTA Quota for delegated dataset
+ -Q OS_QUOTA Quota for root filesystem
+ -r NAMESERVER DNS resolver
+ -s SEARCHDOMAIN DNS search domain
+ -v VLANID VLAN ID number"
+
+ local \
+ bpf_enabled=false \
+ cpuset \
+ data_quota \
+ data_quota=$DEFAULT_DATA_QUOTA \
+ devfs_ruleset=$DEFAULT_DEVFS_RULESET \
+ domain=$DEFAULT_DOMAIN \
+ gateway \
+ ip \
+ memlimit \
+ nameservers \
+ netmask=$DEFAULT_NETMASK \
+ os_quota=$DEFAULT_OS_QUOTA \
+ searchdomains \
+ snapshot \
+ sshkey \
+ vlan=$DEFAULT_VLAN \
+ opt
+
+ while getopts :a:bc:d:g:hk:m:n:q:Q:r:s:v: opt; do
+ case $opt in
+ a) ip=$OPTARG ;;
+ b) bpf_enabled=true ;;
+ c) cpuset=$OPTARG ;;
+ d) domain=$OPTARG ;;
+ g) gateway=$OPTARG ;;
+ h) cmd::help ;;
+ k) sshkey=$OPTARG ;;
+ m) memlimit=$OPTARG ;;
+ n) netmask=$OPTARG ;;
+ q) data_quota=$OPTARG ;;
+ Q) os_quota=$OPTARG ;;
+ r) nameservers="${nameservers:-} ${OPTARG}" ;;
+ s) searchdomains="${searchdomains:-} ${OPTARG}" ;;
+ v) vlan=$OPTARG ;;
+ :) cmd::usage "missing option value: -${OPTARG}" ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'NAME not specified'
+ [ $# -lt 2 ] && cmd::usage 'TEMPLATE not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ local name=$1 template=$2
+
+ jail::exists "$name" && die "jail name already in use: ${name}"
+ template::exists "$template" || die "no such template: ${template}"
+
+ interface::exists "bridge${vlan}" || interface::add_vlan "$vlan"
+
+ if [ -n "${sshkey:-}" ]; then
+ [ -f "$sshkey" ] || die "ssh key ${sshkey}: file not found"
+ fi
+
+ zfs::ensure_snapshot snapshot "${JAIL_DATASET}/templates/${template}"
+
+ # Clone template into new 'os' dataset.
+ zfs create -v "${JAIL_DATASET}/${name}"
+ zfs clone \
+ $ZFS_OPTS \
+ -o quota="$os_quota" \
+ "$snapshot" "${JAIL_DATASET}/${name}/os"
+
+ # Create delegated 'data' dataset.
+ zfs create -v \
+ $ZFS_OPTS \
+ -o mountpoint=none \
+ -o quota="$data_quota" \
+ "${JAIL_DATASET}/${name}/data"
+
+ # Copy timezone configuration from host.
+ cp -v /etc/localtime "${JAIL_HOME}/${name}/os/etc/localtime"
+
+ # Generate /etc/resolv.conf in the jail (word-splitting intentional).
+ [ -n "${nameservers:-}" ] && printf 'nameserver %s\n' $nameservers >> "${JAIL_HOME}/${name}/os/etc/resolv.conf"
+ [ -n "${searchdomains:-}" ] && printf 'search %s\n' "$searchdomains" >> "${JAIL_HOME}/${name}/os/etc/resolv.conf"
+
+ # Get the jail's virtual interface name.
+ local epair_name
+ epair_name=$(interface::epair::derive_name "$name")
+
+ # Set /etc/rc.conf values within the jail.
+ sysrc -v -f "${JAIL_HOME}/${name}/os/etc/rc.conf" \
+ "hostname=${name}.${domain}" \
+ "ifconfig_ej_${epair_name}_name=jail0" \
+ 'ipv6_activate_all_interfaces=NO' \
+ 'syslogd_flags=-ss' \
+ 'sendmail_enable=NONE' \
+ 'dumpdev=NO' \
+ 'pf_enable=YES'
+
+ echo "$DEFAULT_PF_CONF" > "${JAIL_HOME}/${name}/os/etc/pf.conf"
+
+ if [ -n "${ip:-}" ]; then
+ # If $ip set, configure /etc/rc.conf for static IP.
+ sysrc -v -f "${JAIL_HOME}/${name}/os/etc/rc.conf" \
+ "ifconfig_jail0=inet ${ip} netmask ${netmask}" \
+ "defaultrouter=${gateway}"
+ else
+ # Othersie, configure /etc/rc.conf for DHCP.
+ sysrc -v -f "${JAIL_HOME}/${name}/os/etc/rc.conf" "ifconfig_jail0=SYNCDHCP"
+ devfs_ruleset=$BPF_ENABLED_DEVFS_RULESET
+ fi
+
+ if [ -n "${sshkey:-}" ]; then
+ # If $sshkey set, enable sshd and root login.
+ echo "PermitRootLogin prohibit-password" | tee -a "${JAIL_HOME}/${name}/os/etc/ssh/sshd_config"
+ sysrc -v -f "${JAIL_HOME}/${name}/os/etc/rc.conf" 'sshd_enable=YES'
+ # Copy the ssh key into root's authorized_keys within the jail.
+ install -v -d -m 0700 "${JAIL_HOME}/${name}/os/root/.ssh"
+ install -v -m 0600 "$sshkey" "${JAIL_HOME}/${name}/os/root/.ssh/authorized_keys"
+ fi
+
+ # If BPF requested, use our custom bpf ruleset.
+ [ "${bpf_enabled}" = true ] && devfs_ruleset=$BPF_ENABLED_DEVFS_RULESET
+
+ # Generate jail config file.
+ local jailcfg="${JAIL_HOME}/${name}/jail.conf"
+ cat <<EOF > "$jailcfg"
+${name} {
+ path = "${JAIL_HOME}/\$name/os";
+ host.hostname = "\$name.${domain}";
+
+ exec.prestart = "jailctl _create-epair \$name vlan${vlan} bridge${vlan}";
+ exec.created = "zfs set jailed=on ${JAIL_DATASET}/\$name/data";
+ exec.created += "zfs jail \$name ${JAIL_DATASET}/\$name/data";
+ exec.start = "zfs mount -a";
+ exec.start += "/bin/sh /etc/rc";
+ exec.stop = "/bin/sh /etc/rc.shutdown";
+ exec.stop += "zfs list -Ho name,jailed,mounted | awk '\$2 == \\"on\\" && \$3 == \\"yes\\" {print \$1}' | xargs -rtn1 zfs unmount";
+ exec.poststop = "jailctl _destroy-epair \$name";
+ exec.poststop += "rctl -r jail:\$name:";
+ exec.clean;
+
+ exec.system_user = "root";
+ exec.jail_user = "root";
+
+ mount.devfs;
+ devfs_ruleset = "${devfs_ruleset}";
+
+ mount = "tmpfs \$path/tmp tmpfs rw,size=1G 0 0";
+ allow.mount = true;
+ allow.mount.zfs = true;
+ enforce_statfs = 1;
+
+ vnet;
+ vnet.interface = "ej_${epair_name}";
+EOF
+
+ ln -sv "$jailcfg" "/etc/jail.conf.d/${name}.conf"
+
+ # Configure resource limits.
+ [ -n "${cpuset:-}" ] && \
+ echo " exec.created += \"cpuset -j \$name -cl ${cpuset}\";" >> "$jailcfg"
+ [ -n "${memlimit:-}" ] && \
+ echo " exec.prestart += \"rctl -a jail:\$name:memoryuse:deny=${memlimit}\";" >> "$jailcfg"
+
+ # End jail config file.
+ echo '}' >> "$jailcfg"
+
+ # Update host's /etc/rc.conf to start the jail on boot.
+ sysrc -v jail_list+="$name"
+
+ # Start the jail.
+ jail::start "$name"
+}
+
+cmd::create_snapshot(){
+ local usage='create-snapshot [-d] [-o] JAIL [SNAPNAME]'
+ local help='Create a jail snapshot.
+Options:
+ -d Only snapshot the data dataset
+ -o Only snapshot the OS dataset'
+
+ local opt both=true os_only=false data_only=false snapname
+ while getopts :dho opt; do
+ case $opt in
+ d) data_only=true; both=false ;;
+ h) cmd::help ;;
+ o) os_only=true; both=false ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ jail=$1 snapname=${2:-}
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ [ -n "$snapname" ] || snapname=$(date +%Y-%m-%dT%H:%M:%S)
+
+ # Snapshot the OS dataset.
+ if [ "$both" = true ] || [ "$os_only" = true ]; then
+ zfs snapshot "${JAIL_DATASET}/${jail}/os@${snapname}"
+ fi
+
+ # Snapshot the data dataset.
+ if [ "$both" = true ] || [ "$data_only" = true ]; then
+ zfs snapshot "${JAIL_DATASET}/${jail}/data@${snapname}"
+ fi
+}
+
+cmd::create_template(){
+ local usage='create-template NAME JAIL'
+ local help='Create a template from a jail.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'NAME not specified'
+ [ $# -lt 2 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ local name=$1 jail=$2
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ template::exists "$name" && die "template already exists: $name"
+ jail::running "$jail" && die "refusing to create template while jail is running: ${jail}"
+
+ local snapname
+ snapname=$(date +%Y-%m-%dT%H:%M:%S)
+
+ zfs snapshot "${JAIL_DATASET}/${jail}/os@${snapname}"
+ zfs send "${JAIL_DATASET}/${jail}/os@${snapname}" | zfs receive -v "${JAIL_DATASET}/templates/${name}"
+ zfs destroy "${JAIL_DATASET}/${jail}/os@${snapname}"
+}
+
+cmd::destroy_snapshot(){
+ local usage='destroy-snapshot [-y] JAIL SNAPSHOT'
+ local help="Delete a jail snapshot
+Options:
+ -y Don't prompt for confirmation"
+
+ local noconfirm=false answer opt
+
+ while getopts :hy opt; do
+ case $opt in
+ h) cmd::help ;;
+ y) noconfirm=true ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -lt 2 ] && cmd::usage 'SNAPSHOT not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ local jail=$1 snapshot=$2
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ local datasets=''
+
+ if [ "${snapshot#*@}" != "$snapshot" ]; then
+ # If the snapshot name contains '@', then we have something like 'os@snapname'.
+ zfs::dataset_exists "${JAIL_DATASET}/${jail}/${snapshot}" && \
+ datasets="${JAIL_DATASET}/${jail}/${snapshot}"
+ else
+ # Otherwise, check if either os or data dataset contains a matching snapshot.
+ zfs::dataset_exists "${JAIL_DATASET}/${jail}/os@${snapshot}" && \
+ datasets="${JAIL_DATASET}/${jail}/os@${snapshot}"
+
+ zfs::dataset_exists "${JAIL_DATASET}/${jail}/data@${snapshot}" && \
+ datasets="${datasets} ${JAIL_DATASET}/${jail}/data@${snapshot}"
+ fi
+
+ [ -n "$datasets" ] || die "no such snapshot for jail ${jail}: ${snapshot}"
+
+ if [ "$noconfirm" != true ]; then
+ read -rp "Really destroy ${jail} snapshot ${snapshot}? (y/N) " answer
+ case $answer in
+ [yY]|[yY][eE][sS]) : ;;
+ *) die 'operation cancelled' ;;
+ esac
+ fi
+
+ local dataset
+ for dataset in $datasets; do
+ zfs destroy -v "$dataset"
+ done
+}
+
+cmd::destroy_template(){
+ local usage='destroy-template [-y] TEMPLATE'
+ local help="Delete a jail template.
+Options:
+ -y Don't prompt for confirmation
+Notes:
+ A template cannot be deleted while its clones still exist."
+
+ local noconfirm=false answer opt
+
+ while getopts :hy opt; do
+ case $opt in
+ h) cmd::help ;;
+ y) noconfirm=true ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'TEMPLATE not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ template::exists "$jail" || die "no such template: ${jail}"
+
+ if [ "$noconfirm" != true ]; then
+ read -rp "Really destroy template ${jail}? (y/N) " answer
+ case $answer in
+ [yY]|[yY][eE][sS]) : ;;
+ *) die 'operation cancelled' ;;
+ esac
+ fi
+
+ zfs destroy -v -r "${JAIL_DATASET}/templates/${jail}"
+}
+
+cmd::destroy(){
+ local usage='destroy [-y] JAIL'
+ local help="Delete a jail and its dataset.
+Options:
+ -y Don't prompt for confirmation"
+
+ local noconfirm=false answer opt
+
+ while getopts :hy opt; do
+ case $opt in
+ h) cmd::help ;;
+ y) noconfirm=true ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ if [ "$noconfirm" != true ]; then
+ read -rp "Really destroy jail ${jail}? (y/N) " answer
+ case $answer in
+ [yY]|[yY][eE][sS]) : ;;
+ *) die 'operation cancelled' ;;
+ esac
+ fi
+
+ jail::running "$jail" && jail::stop "$jail"
+
+ # Delete jail config file.
+ rm "/etc/jail.conf.d/${jail}.conf" "${JAIL_HOME}/${jail}/jail.conf"
+
+ # Remove the jail from the autostart list.
+ sysrc -v jail_list-="$jail" ||:
+
+ # Destroy the jail's dataset.
+ zfs destroy -v -f -r "${JAIL_DATASET}/${jail}"
+}
+
+cmd::download_release() {
+ local usage='download-release RELEASE'
+ local help='Download and create a FreeBSD release template.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'RELEASE not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local release=$1
+
+ local template
+ template::release2name template "$release"
+
+ template::exists "$template" && die "template already exists: ${template}"
+ template::download_release "$release"
+}
+
+cmd::edit(){
+ local usage='edit JAIL'
+ local help='Edit jail configuration.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ "$EDITOR" "${JAIL_HOME}/${jail}/jail.conf"
+}
+
+cmd::exec(){
+ local usage='exec JAIL COMMAND...'
+ local help='Run a command within the jail.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -lt 2 ] && cmd::usage 'COMMAND not specified'
+ jail=$1; shift
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ jail::exec "$jail" "$@"
+}
+
+cmd::list(){
+ local usage='list [-t]'
+ local help='List configured jails.
+Options:
+ -t Use terse output (jail names only)'
+
+ local file name status opt terse=false
+
+ while getopts :th opt; do
+ case $opt in
+ t) terse=true ;;
+ h) cmd::help ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -eq 0 ] || cmd::usage 'too many arguments'
+
+ { [ $terse = true ] || echo 'JAIL STATUS'
+ for file in "$JAIL_HOME"/*/jail.conf; do
+ [ -e "$file" ] || continue
+
+ name=$(basename "$(dirname "$file")")
+
+ if [ $terse = true ]; then
+ printf '%s\n' "$name"
+ else
+ if jail::running "$name"; then
+ status=running
+ else
+ status=stopped
+ fi
+ printf '%s %s\n' "$name" "$status"
+ fi
+ done
+ } | column -t
+}
+
+cmd::list_templates(){
+ local usage='list-templates'
+ local help='list template datasets.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -eq 0 ] || cmd::usage 'too many arguments'
+
+ ls -1 "${JAIL_HOME}/templates"
+}
+
+cmd::list_snapshots(){
+ local usage='list-snapshots JAIL'
+ local help='List jail snapshots
+Options:
+ -d List snapshots from data dataset only
+ -o List snapshots from OS dataset only
+ -t Use terse output'
+
+ local opt terse=false dataset=''
+ while getopts :dhot opt; do
+ case $opt in
+ d) dataset=/data ;;
+ h) cmd::help ;;
+ o) dataset=/os ;;
+ t) terse=true ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ if [ "$terse" = true ]; then
+ zfs list -r -t snapshot -H -o name -s creation "${JAIL_DATASET}/${jail}${dataset}" 2>/dev/null \
+ | sed 's/^.*\///' \
+ | sort -k1,1 -t@ -s
+ else
+ { echo 'DATASET SNAPSHOT USED REFER'
+ zfs list -r -t snapshot -H -o name,used,refer -s creation "${JAIL_DATASET}/${jail}${dataset}" 2>/dev/null
+ } | sed 's/^.*\///' \
+ | sort -k1,1 -t@ -s \
+ | tr '@' ' ' \
+ | column -t
+ fi
+}
+
+cmd::rollback(){
+ local usage='rollback JAIL SNAPSHOT'
+ local help='Rollback a jail to a given snapshot.
+Options:
+ -d Rollback the data dataset only
+ -f Attempt rollback even if the jail is running
+ -o Rollback the OS dataset only'
+
+ local opt force=false both=true os_only=false data_only=false
+ while getopts :dfho opt; do
+ case $opt in
+ d) data_only=true; both=false ;;
+ f) force=true ;;
+ h) cmd::help ;;
+ o) os_only=true; both=false ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -lt 2 ] && cmd::usage 'SNAPSHOT not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ local jail=$1 snapshot=$2
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ # Ensure OS snapshot exists, if requested.
+ if [ "$both" = true ] || [ "$os_only" = true ]; then
+ zfs::dataset_exists "${JAIL_DATASET}/${jail}/os@${snapshot}" \
+ || die "no such snapshot for ${jail}/os: ${snapshot}"
+ fi
+
+ # Ensure data snapshot exists, if requested.
+ if [ "$both" = true ] || [ "$data_only" = true ]; then
+ zfs::dataset_exists "${JAIL_DATASET}/${jail}/data@${snapshot}" \
+ || die "no such snapshot for ${jail}/data: ${snapshot}"
+ fi
+
+ # Don't rollback while jail is running.
+ if jail::running "$jail" && [ "$force" != true ]; then
+ die "jail ${jail} is running, refusing to rollback (-f to override)"
+ fi
+
+ # Rollback the OS snapshot, if requested.
+ if [ "$both" = true ] || [ "$os_only" = true ]; then
+ zfs rollback -r "${JAIL_DATASET}/${jail}/os@${snapshot}"
+ fi
+
+ # Rollback the data snapshot, if requested.
+ if [ "$both" = true ] || [ "$data_only" = true ]; then
+ zfs rollback -r "${JAIL_DATASET}/${jail}/data@${snapshot}"
+ fi
+}
+
+cmd::start(){
+ local usage='start JAIL'
+ local help='Start a jail.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ jail::running "$jail" && die "jail already running: ${jail}"
+
+ jail::start "$jail"
+}
+
+cmd::stop(){
+ local usage='stop JAIL'
+ local help='Stop a jail.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ jail::running "$jail" || die "jail not running: ${jail}"
+
+ jail::stop "$jail"
+}
+
+cmd::reprovision(){
+ local usage='reprovision JAIL TEMPLATE'
+ local help="Wipe and reprovision a jail's OS dataset from a template."
+
+ local opt noconfirm=false running=false
+ while getopts :hy opt; do
+ case $opt in
+ h) cmd::help ;;
+ y) noconfirm=true ;;
+ ?) cmd::usage "unknown option: -${OPTARG}" ;;
+ esac
+ done
+ shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -lt 2 ] && cmd::usage 'TEMPLATE not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ local jail=$1 template=$2
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ template::exists "$template" || die "no such template: ${template}"
+
+ if [ "$noconfirm" != true ]; then
+ read -rp "Really reprovision ${jail}? (y/N) " answer
+ case $answer in
+ [yY]|[yY][eE][sS]) : ;;
+ *) die 'operation cancelled' ;;
+ esac
+ fi
+
+ # If the jail is running, stop it.
+ if jail::running "$jail"; then
+ running=true
+ jail::stop "$jail"
+ fi
+
+ local snapshot old_quota old_ifconfig old_defaultrouter old_hostname old_resolvconf
+
+ # Get the latest snapshot for the template (if not specified).
+ zfs::ensure_snapshot snapshot "${JAIL_DATASET}/templates/${template}"
+
+ # Stash old configuration data.
+ old_quota=$(zfs get -Hp -o value quota "${JAIL_DATASET}/${jail}/os")
+ old_hostname=$(sysrc -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" -qn hostname)
+ old_ifconfig=$(sysrc -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" -qn ifconfig_jail0)
+ old_defaultrouter=$(sysrc -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" -qn defaultrouter) ||:
+ old_pfenable=$(sysrc -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" -qn pf_enable) ||:
+ old_sshkey=$(cat "${JAIL_HOME}/${jail}/os/root/.ssh/authorized_keys") ||:
+ old_resolvconf=$(cat "${JAIL_HOME}/${jail}/os/etc/resolv.conf") ||:
+
+ # Reprovision OS dataset from template.
+ zfs destroy -v -f -r "${JAIL_DATASET}/${jail}/os"
+ zfs clone \
+ $ZFS_OPTS \
+ -o quota="$old_quota" \
+ "$snapshot" "${JAIL_DATASET}/${jail}/os"
+
+ # Copy timezone configuration from host.
+ cp -v /etc/localtime "${JAIL_HOME}/${jail}/os/etc/localtime"
+
+ # Restore stashed configuration data.
+ sysrc -v -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" \
+ "hostname=${old_hostname}" \
+ "ifconfig_ej_$(interface::epair::derive_name "$jail")_name=jail0" \
+ "ifconfig_jail0=${old_ifconfig}" \
+ 'ipv6_activate_all_interfaces=NO' \
+ 'syslogd_flags=-ss' \
+ 'sendmail_enable=NONE' \
+ 'dumpdev=NO' \
+ "pf_enable=YES"
+
+ [ -n "$old_defaultrouter" ] && sysrc -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" "defaultrouter=${old_defaultrouter}"
+
+ [ -n "$old_resolvconf" ] && printf '%s\n' "$old_resolvconf" | tee "${JAIL_HOME}/${jail}/os/etc/resolv.conf"
+
+ echo "$DEFAULT_PF_CONF" > "${JAIL_HOME}/${jail}/os/etc/pf.conf"
+
+ if [ -n "$old_sshkey" ]; then
+ echo "PermitRootLogin prohibit-password" | tee -a "${JAIL_HOME}/${jail}/os/etc/ssh/sshd_config"
+ sysrc -v -f "${JAIL_HOME}/${jail}/os/etc/rc.conf" 'sshd_enable=YES'
+ install -v -d -m 0700 "${JAIL_HOME}/${jail}/os/root/.ssh"
+ install -v -m 0600 /dev/null "${JAIL_HOME}/${jail}/os/root/.ssh/authorized_keys"
+ printf '%s\n' "$old_sshkey" | tee "${JAIL_HOME}/${jail}/os/root/.ssh/authorized_keys"
+ fi
+
+ # If the jail was running, restart it.
+ if [ "$running" = true ]; then
+ jail::start "$jail"
+ fi
+}
+
+cmd::restart(){
+ local usage='restart JAIL'
+ local help='Restart a jail.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'no jail specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ jail::running "$jail" || die "jail not running: ${jail}"
+
+ jail::restart "$jail"
+}
+
+cmd::shell(){
+ local usage='shell JAIL'
+ local help='Run a shell within the jail.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ jail::running "$jail" || die "jail not running: ${jail}"
+
+ jail::exec "$jail" /bin/csh
+}
+
+cmd::show(){
+ local usage='show JAIL'
+ local help='Show jail configuration.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+
+ printf -- '------------------------- JAIL CONFIGURATION -------------------------\n'
+ cat "${JAIL_HOME}/${jail}/jail.conf"
+ printf -- '\n---------------------------- ZFS DATASET -----------------------------\n'
+ zfs list -o name,quota,used,avail,mountpoint -S name \
+ "${JAIL_DATASET}/${jail}/os" \
+ "${JAIL_DATASET}/${jail}/data"
+}
+
+cmd::status(){
+ local usage='status JAIL'
+ local help='Show running jail status.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ jail::exists "$jail" || die "no such jail: ${jail}"
+ jail::running "$jail" || die "jail not running: ${jail}"
+
+ printf -- '---------------------------- JAIL STATUS -----------------------------\n'
+ jls -j "$jail" -h jid name path osrelease host.hostname 2>/dev/null | column -t
+ printf -- '\n---------------------------- ZFS DATASET -----------------------------\n'
+ zfs list -o name,quota,used,avail,mountpoint -S name \
+ "${JAIL_DATASET}/${jail}/os" \
+ "${JAIL_DATASET}/${jail}/data" \
+ | sed "s|^${JAIL_DATASET}/${jail}/||" \
+ | column -t
+ printf -- '\n--------------------------- RESOURCE USAGE ---------------------------\n'
+ rctl -h -u "jail:${jail}:" \
+ | grep -E '^(maxproc|memoryuse|openfiles|pcpu|memoryuse|swapuse|readbps|writebps|readiops|writeiops)=' \
+ | rs -c= -C' ' -T \
+ | column -t
+ printf -- '\n----------------------------- PROCESSES ------------------------------\n'
+ ps -auxdr -J "$jail"
+}
+
+cmd::update_release(){
+ local usage='update-release TEMPLATE'
+ local help='Update a FreeBSD release template.'
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'TEMPLATE not specified'
+ [ $# -gt 1 ] && cmd::usage 'too many arguments'
+ local jail=$1
+
+ template::exists "$jail" || die "no such template: ${jail}"
+ template::update_release "$jail"
+}
+
+cmd::_create_epair(){
+ local usage='_create-epair JAIL INTERFACE BRIDGE'
+ local help="Create an epair for a VNET jail.
+DO NOT RUN THIS COMMAND MANUALLY. It is invoked by each jail's exec.prestart."
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -lt 2 ] && cmd::usage 'INTERFACE not specified'
+ [ $# -lt 3 ] && cmd::usage 'BRIDGE not specified'
+ [ $# -gt 3 ] && cmd::usage 'too many arguments'
+ local jail=$1 interface=$2 bridge=$3 \
+ new_epair epair_name epair_bridge_mac epair_jail_mac
+
+ interface::exists "$interface" || die "no such interface: ${interface}"
+ interface::exists "$bridge" || die "no such bridge: ${bridge}"
+
+ epair_name=$(interface::epair::derive_name "$jail")
+ interface::epair::derive_mac "$interface" "$jail" epair_bridge_mac epair_jail_mac
+
+ new_epair=$(ifconfig epair create)
+ ifconfig "$bridge" addm "$new_epair"
+
+ ifconfig "$new_epair" name "eb_${epair_name}" descr "jail/${jail}"
+ ifconfig "eb_${epair_name}" up
+ ifconfig "eb_${epair_name}" ether "$epair_bridge_mac"
+
+ ifconfig "${new_epair%a}b" name "ej_${epair_name}"
+ ifconfig "ej_${epair_name}" up
+ ifconfig "ej_${epair_name}" ether "$epair_jail_mac"
+}
+
+cmd::_destroy_epair(){
+ local usage='_destroy-epair JAIL'
+ local help="Destroy an epair for a VNET jail.
+DO NOT RUN THIS COMMAND MANUALLY. It is invoked by each jail's exec.poststop."
+
+ cmd::getopt_help "$@"; shift $((OPTIND - 1))
+
+ [ $# -lt 1 ] && cmd::usage 'JAIL not specified'
+ [ $# -gt 2 ] && cmd::usage 'too many arguments'
+ local jail=$1 epair_name
+
+ epair_name=$(interface::epair::derive_name "$jail")
+ ifconfig "eb_${epair_name}" destroy
+}
+
+
+################################################################################
+# "Library" functions.
+################################################################################
+interface::add_vlan(){
+ local id=$1
+ ifconfig "vlan${id}" create
+ ifconfig "vlan${id}" vlan "$id" vlandev "$TRUNK_INTERFACE" up
+ ifconfig "bridge${id}" create
+ ifconfig "bridge${id}" addm "vlan${id}" up
+ sysrc -v \
+ "cloned_interfaces+=vlan${id} bridge${id}" \
+ "ifconfig_vlan${id}=vlan ${id} vlandev ${TRUNK_INTERFACE} up" \
+ "ifconfig_bridge${id}=addm vlan${id} up"
+}
+
+interface::exists(){
+ ifconfig "$1" > /dev/null 2>&1
+}
+
+interface::epair::derive_mac(){
+ # Derive unique mac addresses for an epair interface, based on the physical
+ # interface address + jail name.
+ local iface=$1 name=$2 upvar_a=$3 upvar_b=$4
+ local hwaddr checksum epair_mac_a epair_mac_b
+
+ # Similar to /usr/share/examples/jails/jib
+ hwaddr=$(ifconfig "$iface" ether | awk '/ether/,$0=$2')
+ # ??:??:??:II:II:II
+ epair_mac_a=${hwaddr#??:??:??} # => :II:II:II
+ # => :SS:SS:II:II:II
+ epair_mac_a=":$(printf '%s' "$name" | md5 | sed 's/^\(..\)\(..\).*/\1:\2/')${epair_mac_a}"
+ # => NP:SS:SS:II:II:II
+ case $hwaddr in
+ ?2:*) epair_mac_a="0a${epair_mac_a}" epair_mac_b="0e${epair_mac_a}" ;;
+ ?[Ee]:*) epair_mac_a="02${epair_mac_a}" epair_mac_b="06${epair_mac_a}" ;;
+ *) epair_mac_a="02${epair_mac_a}" epair_mac_b="0e${epair_mac_a}" ;;
+ esac
+
+ # Sanity check for duplicate MAC
+ for mac in $epair_mac_a $epair_mac_b; do
+ ifconfig | grep -qE "(ether|hwaddr) ${mac}" && die "MAC collision when configuring epair interface!"
+ done
+
+ setvar "$upvar_a" "$epair_mac_a"
+ setvar "$upvar_b" "$epair_mac_b"
+}
+
+interface::epair::derive_name(){
+ # Generate an epair(4) interface suffix, based on the jail name.
+ #
+ # The maximum length of a network interface name on FreeBSD is 15 characters.
+ # We use a prefix of 'eb_' for the bridge side of the interface, and 'ej_'
+ # for the jail side. This leaves 12 characters for a unique suffix for each
+ # jail.
+ #
+ # If the sanitized jail name is less than 12 characters, we'll simply use it
+ # for the suffix. Otherwise, we'll use the last 12 characters of the jail
+ # name's SHA-1 hash.
+ local name=$1 sanitized
+ sanitized=$(printf '%s' "$name" | tr -dC '[:alnum:]_')
+
+ if [ "${#sanitized}" -le 12 ]; then
+ printf '%s' "$sanitized"
+ else
+ printf '%s' "$name" | sha1 | tail -c 12
+ fi
+}
+
+jail::exec() {
+ local jail=$1; shift
+ jexec -l "$jail" "$@"
+}
+
+jail::exists(){
+ test -f "${JAIL_HOME}/${1}/jail.conf"
+}
+
+jail::restart(){
+ jail -v -f "${JAIL_HOME}/${1}/jail.conf" -rc "$1"
+}
+
+jail::running(){
+ jls -j "$1" > /dev/null 2>&1
+}
+
+jail::start(){
+ jail -v -f "${JAIL_HOME}/${1}/jail.conf" -c "$1"
+}
+
+jail::stop(){
+ jail -v -f "${JAIL_HOME}/${1}/jail.conf" -r "$1"
+}
+
+template::exists(){
+ zfs list -H "${JAIL_DATASET}/templates/${1}" > /dev/null 2>&1
+}
+
+template::download_release(){
+ # Download a given FreeBSD release and create a template jail.
+ local release=$1 arch base_tarball template
+
+ arch=$(uname -p)
+ base_tarball="https://download.freebsd.org/releases/${arch}/${release}/base.txz"
+
+ template::release2name template "$release"
+
+ zfs create -v -p $ZFS_OPTS "${JAIL_DATASET}/templates/${template}"
+
+ if ! fetch "$base_tarball" -o - | tar xzf - -C "${JAIL_HOME}/templates/${template}"; then
+ zfs destroy -v "${JAIL_DATASET}/templates/${template}"
+ die "failed to extract base tarball for ${release}"
+ fi
+
+ template::update_release "$template"
+}
+
+template::release2name(){
+ # Convert a FreeBSD release version to a template name.
+ # e.g. for "13.2-RELEASE", return "freebsd13".
+ setvar "$1" "freebsd${2%.*}"
+}
+
+template::update_release(){
+ # Run freebsd-update within a the given template and take a fresh snapshot.
+ local template=$1 snapshot
+
+ PAGER=/bin/cat freebsd-update -b "${JAIL_HOME}/templates/${template}" --not-running-from-cron fetch install
+
+ snapshot=$("${JAIL_HOME}/templates/${template}/bin/freebsd-version")
+ template::exists "${template}@${snapshot}" || zfs snapshot "${JAIL_DATASET}/templates/${template}@${snapshot}"
+}
+
+zfs::ensure_snapshot(){
+ # If the given zfs dataset is a snapshot, return as-is.
+ # Otherwise, get the latest snapshot from the given dataset and return that.
+ local upvar=$1 dataset=$2 latest
+
+ if [ "${dataset#*@}" = "$dataset" ]; then
+ latest=$(zfs list -t snapshot -o name -s creation -H "$dataset" | tail -1)
+ setvar "$upvar" "$latest"
+ else
+ zfs list -t snapshot -H "$dataset" > /dev/null 2>&1 || die "no such snapshot: ${dataset}"
+ setvar "$upvar" "$dataset"
+ fi
+}
+
+zfs::dataset_exists(){
+ zfs list "$1" > /dev/null 2>&1
+}
+
+
+cmd::main "$@"