#!/bin/sh

umask 022
PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
export PATH
set -e
trap '[ $? -eq 0 ] || printf "WARNING: %s did not complete successfully.\n" "$0" >&2' EXIT
trap 'exit 1' INT HUP TERM

# Stage-0 bootstrap: NFS is not yet mounted, so /opt/aitken/admin does not exist.
# On the web tree, aitken.conf and common.sh sit one level up from this script
# (admin/aitken.conf, admin/common.sh).  setup.sh is always fetched and run
# from the web tree for stage-0; it is not sourced directly from the repo.
_dir="$(cd "$(dirname "$0")" && pwd)"
. "${_dir}/../aitken.conf"
. "${_dir}/../common.sh"

# Shorthand for processor scripts; these live under ADMIN_BASE so they are
# only accessible after NFS is mounted (steps below).  bootstrap.sh stages this
# script in a directory named for its edition, so that name locates them.
_PROC="${ADMIN_BASE}/system/os/$(basename "$_dir")/processors"


#
# Validate hostname
#
[ -n "$1" ] || die "Usage: $0 <fqdn>"
hostname="$1"
validate_fqdn "$hostname"
# shellcheck disable=SC2086
require_known_domain "$hostname" $VALID_DOMAINS
# The keytab is collected for the name given here, but Kerberos logins and ksu
# match the host's own name, set in the installer.
_running="$(hostname)"
_rcconf="$(sysrc -n hostname 2>/dev/null || true)"
if [ "$_running" != "$hostname" ] || [ "$_rcconf" != "$hostname" ]; then
    die "Hostname mismatch: given '${hostname}', running '${_running}', rc.conf '${_rcconf}'.
  Fix:  sysrc hostname=\"${hostname}\" && hostname ${hostname}"
fi

log ""
log "Setting up a new FreeBSD $(freebsd-version) host (${hostname})."
log "Press Enter to continue, or ^C to abort."
read -r _x


#
# Remove local ZFS home dataset
#
# The default FreeBSD ZFS-on-root layout mounts zroot/home at /home.  It must
# be destroyed before NFS can own /home.  This runs pre-NFS (from /tmp) so the
# script itself is never in /home.
#
log "Removing local ZFS home dataset (if present)..."
if zfs list zroot/home > /dev/null 2>&1; then
    if mount -t nfs | grep -q " on /home "; then
        log "  /home already NFS-mounted; skipping zfs unmount/destroy."
    else
        zfs unmount zroot/home
        zfs destroy zroot/home
        syslog "zroot/home removed."
    fi
else
    log "  zroot/home does not exist, skipping."
fi


#
# Update system base
#
# Run freebsd-update non-interactively before enabling NFS so updates apply
# against an unmodified system tree.  Some versions exit 1 when already
# up to date, so we allow failure here.
#
log "Updating system base..."
printf '\n' | PAGER=cat freebsd-update --not-running-from-cron fetch || true
printf '\n' | PAGER=cat freebsd-update install || true
syslog "freebsd-update complete."


#
# Configure DNS resolvers
#
# Write /etc/resolv.conf authoritatively from aitken.conf.  Runs before the
# NFS mount so DNS is correct when the NFS server hostname is resolved.
# On a static-IP host nothing overwrites resolv.conf after boot.
#
log "Configuring DNS resolvers..."
{
    printf 'search %s\n' "$DNS_SEARCH"
    # shellcheck disable=SC2086
    for _ns in $DNS_RESOLVERS; do
        printf 'nameserver %s\n' "$_ns"
    done
} > /etc/resolv.conf
syslog "DNS configured: search=${DNS_SEARCH} resolvers=${DNS_RESOLVERS}"


#
# Mount NFS filesystems
#
# nfsv3 forces NFSv3 to avoid NFSv4 UID/GID mapping issues with TrueNAS Core.
# See README.md for background.
#
log "Installing NFS client and mounting filesystems..."

sysrc rpcbind_enable="YES"
sysrc nfs_client_enable="YES"
sysrc rpc_lockd_enable="YES"
sysrc rpc_statd_enable="YES"

service rpcbind status > /dev/null 2>&1 || service rpcbind start
service nfsclient status > /dev/null 2>&1 || service nfsclient start
service statd status > /dev/null 2>&1 || service statd start
service lockd status > /dev/null 2>&1 || service lockd start

ensure_dir /media/pics   root wheel 755
ensure_dir /media/videos root wheel 755
ensure_dir "$ADMIN_MOUNT" root wheel 755

# Fed by here-doc, NOT `printf | while`: the right side of a pipe runs in a
# subshell, where `die` cannot stop this script and _admin_is_local would not
# survive the loop.  NFS_MOUNTS carries the access mode (ro|rw); the transport
# options are FreeBSD's and belong here, not in the config.
_admin_is_local=
while IFS=' 	' read -r _export _mnt _mode; do
    [ -z "$_export" ] && continue
    case "$_mode" in
        ro|rw) ;;
        "")    die "NFS_MOUNTS: no access mode for ${_mnt} (expected ro or rw)" ;;
        *)     die "NFS_MOUNTS: bad access mode '${_mode}' for ${_mnt} (expected ro or rw)" ;;
    esac
    # Never NFS-mount an export this host serves itself.
    if [ "${_export%%:*}" = "${hostname%%.*}" ]; then
        log "  ${_mnt}: served locally by this host, not mounting over NFS."
        if [ "$_mnt" = "$ADMIN_MOUNT" ]; then
            _admin_is_local=1
        fi
        continue
    fi
    append_once "${_export}" /etc/fstab \
        "${_export} ${_mnt} nfs ${_mode},nfsv3 0 0"
done <<EOF
$NFS_MOUNTS
EOF

mkdir -p /cdrom
append_once "/dev/cd0" /etc/fstab \
    "/dev/cd0	/cdrom	cd9660	ro,noauto	0	0"

mount -a -t nfs
df -h

require_nfs_mount /home
# The admin/build host serves this tree itself; everywhere else it must be NFS.
[ "$_admin_is_local" = 1 ] || require_nfs_mount "$ADMIN_MOUNT"

#
# Pin the NFS servers in /etc/hosts, so later boots mount without DNS:
# mountcritremote runs before any resolver on this host starts, and a host
# whose resolver is down cannot look the servers up either.  The list is
# generated (site.nfs_servers); the tree it lives in is mounted by now.
#
while read -r _ip _fqdn _short; do
    case "$_ip" in ''|'#'*) continue ;; esac
    append_once "$_fqdn" /etc/hosts "${_ip}	${_fqdn} ${_short}"
done < "${ADMIN_BASE}/system/generated/hosts-nfs"


#
# Disable extra virtual terminals
#
log "Disabling extra virtual terminals..."
"${_PROC}/freebsd-disable-extra-vtys.sh"
kill -1 1


#
# Bootstrap pkg(8) and install the packages that must exist BEFORE the dispatcher
#
# WHAT BELONGS HERE IS A SHORT LIST, and the test is "does something earlier than
# the dispatcher need it".  Everything else the fleet carries -- the operator
# toolkit, the guest agent -- is in the inventory as a `method: pkg` row and is
# installed by the dispatcher, so ADR-0004's drift gate can see its version.  A
# bare `pkg install` here records no version and compares nothing, which is how a
# `bat` 0.25.0 -> 0.26.1 move went unnoticed.
#
log "Setting up pkg(8)..."
pkg bootstrap -y

#
# bash is INSTALLED IN TWO PLACES on purpose: here, because the login shell for
# root and the admin user is set below and the shell has to exist first, and as
# an inventory row so its version is visible to the drift gate.  It is one of
# two packages with that shape (the other is pam_krb5); bash-completion has
# neither property and is now a dispatcher row only.
#
log "Installing bash..."
pkg install -y --quiet shells/bash
[ -L /bin/bash ] || ln -s /usr/local/bin/bash /bin/bash


#
# Configure Kerberos
#
# This host's identity is MINTED ON THE BUILD HOST and staged for collection
# (ADR-0033); nothing here writes to the realm, so no administrative credential
# reaches this machine and no KDC needs to be reachable for a write.  Run
# tools/provision-host.py on the build host before booting into this script.
#
log "Configuring Kerberos..."

#
# THE REALM'S CLIENT IS BASE HEIMDAL, and no MIT package is installed here.  Base
# sshd and the pam_krb5 port both link against base libkrb5, as does every
# from-source package that authenticates, so the client half of Kerberos is
# already present on a stock FreeBSD host.
#
# MIT is what runs the DAEMONS and what mints principals, and it arrives as the
# dispatcher's `mit_kerberos` row on the hosts that do those jobs.  Nothing
# before the dispatcher needs it: the only place the two implementations are not
# interchangeable is kadmin(1), and minting moved to the build host (ADR-0033),
# so this host receives a keytab and a config file rather than an admin session.
#
# Anything in this repo that must have MIT calls it by absolute path.
#
#
# The fleet client config, and /etc/krb5.conf IS THE REAL FILE.  That is where
# base Heimdal reads it, and where MIT will read it from FreeBSD 15.x on, when
# MIT is in base and its sysconfdir stops being /usr/local/etc.  The hosts that
# carry the 14.x MIT PACKAGE get /usr/local/etc/krb5.conf as a symlink pointing
# here, installed by `mit_kerberos` -- the package whose presence is exactly
# "MIT lives under /usr/local on this host".  Both names resolve to one file, so
# the two implementations cannot disagree about the realm.
#
install_file "${ADMIN_BASE}/system/generated/krb5.conf" /etc/krb5.conf 0644

#
# Collect this host's keytab.
#
# Skip if one is already installed: re-running setup.sh must not disturb a
# working host.  Re-minting is the build host's job and is deliberate there
# (provision-host.py --reprovision), never a side effect of a re-run here.
#
_staged="${KERBEROS_KEYTAB_STAGE}/${hostname}.keytab"

if [ -s /etc/krb5.keytab ]; then
    log "  /etc/krb5.keytab already present -- leaving it alone."
    if [ -f "${_staged}" ]; then
        warn "  A keytab is ALSO staged at ${_staged} and was not used."
        warn "  Remove /etc/krb5.keytab first if you meant to adopt the staged one."
    fi
elif [ -f "${_staged}" ]; then
    log "  Installing the staged keytab for ${hostname}."
    install -o root -g wheel -m 0600 "${_staged}" /etc/krb5.keytab

    # Consumed: one host, one build window.  A staged keytab left lying around
    # is a credential for this host readable by anything that can reach the
    # share, and it has already served its purpose.
    rm -f "${_staged}" \
        || warn "  Could not remove ${_staged} -- delete it from the build host."

    #
    # ktutil(8), not klist(1): base Heimdal's klist reads a ticket CACHE only and
    # has no keytab mode.  The EXIT STATUS is the whole test -- 1 for absent,
    # unreadable, or not a keytab -- and nothing parses the listing, which is
    # formatted differently from MIT's.
    #
    /usr/sbin/ktutil -k /etc/krb5.keytab list >/dev/null 2>&1 \
        || die "/etc/krb5.keytab was installed but is not a readable keytab"
    log "  Keytab installed and verified."
else
    die "No keytab for ${hostname}.
  Expected: ${_staged}
  Mint it on the BUILD HOST first (ADR-0033):
      tools/provision-host.py <device-id>
  This host cannot create its own identity -- that is the point."
fi

#
# ksu(1) MUST BE SETUID -- base ships it 0555 and it cannot change uid without
# root.  Base Heimdal's ksu is the only one on a host that carries no MIT
# package, so this is the fleet's ksu and setting the bit is ours to do.
#
# A deliberate deviation from base: `freebsd-update IDS` reports it, and
# freebsd-update.conf's KeepModifiedMetadata preserves the mode across updates.
# See system/MAINTENANCE.md.
#
chmod 4755 /usr/bin/ksu


#
# Configure SSH (Kerberos authentication via Include drop-ins)
#
# An Include directive is inserted at the top of both ssh_config and
# sshd_config so they pull in *.conf from their respective .d directories --
# the same drop-ins Ubuntu uses.  The Include directive is supported by
# FreeBSD's ssh_config(5) and sshd_config(5) though not present by default.
#
# Verification: after reboot, confirm with:
#   ssh -G localhost | grep gssapi
#   sshd -T | grep gssapi
#
log "Configuring SSH..."
#
# pam_krb5 is INSTALLED IN TWO PLACES on purpose: here, because it is required
# for proper sshd operation, and as an inventory row so its version is visible
# to the drift gate.  It is one of two packages with that shape (the other is
# bash).
#
# THE BARE PORT NAME, never a version-pinned one.  `pam_krb5-4.11_2` names an
# exact PORTVERSION+PORTREVISION, and a PORTREVISION is the one component the
# fleet treats as not-drift (pkg_upstream_version strips it) -- so a plain
# rebuild to `_3` would make this line unsatisfiable and kill the build HERE, on
# a host that already has a keytab installed and does not yet have sshd
# reconfigured.
#
pkg install -y --quiet pam_krb5

ensure_dir /etc/ssh/ssh_config.d  root wheel 755
ensure_dir /etc/ssh/sshd_config.d root wheel 755

"${_PROC}/freebsd-ssh-include.sh" /etc/ssh/ssh_config

"${_PROC}/freebsd-ssh-include.sh" /etc/ssh/sshd_config

install_file "${ADMIN_BASE}/system/files/etc/ssh/ssh_config.d/10-aitken-ssh.conf" \
    /etc/ssh/ssh_config.d/10-aitken-ssh.conf 0644
install_file "${ADMIN_BASE}/system/files/etc/ssh/sshd_config.d/10-aitken-sshd.conf" \
    /etc/ssh/sshd_config.d/10-aitken-sshd.conf 0644
install_file "${ADMIN_BASE}/system/files/etc/issue" /etc/issue 0644

"${_PROC}/freebsd-pam-krb5.sh"

service sshd restart


#
# Configure sendmail
#
# Non-mailserver hosts: disable inbound sendmail, keep outbound MSP queue.
# Root alias forwards system mail to the admin address.
#
log "Configuring sendmail..."
_tab=$(printf '\t')
if ! grep -qF "root:${_tab}${ADMIN_USER}@${PRIMARY_DOMAIN}" /etc/mail/aliases; then
    sed -i '' "s|^# root:.*|root:${_tab}${ADMIN_USER}@${PRIMARY_DOMAIN}|" \
        /etc/mail/aliases
fi
newaliases

sysrc sendmail_enable="NO"
sysrc sendmail_submit_enable="NO"
sysrc sendmail_outbound_enable="YES"
sysrc sendmail_msp_queue_enable="YES"

service sendmail status > /dev/null 2>&1 && service sendmail restart || true


#
# Configure admin syslog routing
#
# Route ADMIN_SYSLOG_FACILITY to ADMIN_LOG_FILE.  Written to /etc/syslog.d so
# we never touch the freebsd-update-managed /etc/syslog.conf.  Relies on
# stock /etc/syslog.conf including /etc/syslog.d (verify: grep include
# /etc/syslog.conf).  The newsyslog drop-in handles log rotation.
#
log "Configuring admin syslog routing..."
printf '# aitken.com admin logging -- facility defined in aitken.conf\n%s.*\t%s\n' \
    "$ADMIN_SYSLOG_FACILITY" "$ADMIN_LOG_FILE" \
    > /etc/syslog.d/40-aitken-admin.conf
chmod 0644 /etc/syslog.d/40-aitken-admin.conf
touch "$ADMIN_LOG_FILE"
chmod 0640 "$ADMIN_LOG_FILE"
install_file "${ADMIN_BASE}/system/files/etc/newsyslog.conf.d/aitken-admin.conf" \
    /etc/newsyslog.conf.d/aitken-admin.conf 0644
service syslogd restart
syslog "admin syslog routing active: ${ADMIN_SYSLOG_FACILITY}.* -> ${ADMIN_LOG_FILE}"


#
# Update login.conf default PATH
#
# Promotes /usr/local/bin and /usr/local/sbin ahead of the system directories
# so locally-installed binaries take precedence.
#
log "Updating login.conf default PATH..."
"${_PROC}/freebsd-set-default-path.sh"


#
# Configure user accounts
#
log "Configuring user accounts..."

# Set useradd defaults: home base /home, no password, bash login shell.
pw useradd -D -b /home -w no -s /usr/local/bin/bash

# Remove the unused toor account; ignore error if already gone.
pw userdel toor > /dev/null 2>&1 || true

# Reconfigure root shell and description.
pw usermod 0 -c "System Administrator" -s /usr/local/bin/bash

install_file "${ADMIN_BASE}/system/files/root/.k5login" /root/.k5login 0600

# Create admin user if absent; home directory is on NFS so we create it
# separately without skel files -- pw useradd -m would clobber existing dotfiles.
id "${ADMIN_USER}" > /dev/null 2>&1 || \
    pw useradd "${ADMIN_USER}" -u "${ADMIN_UID}" -c "Jeff Aitken" \
               -G "${ADMIN_GROUP}"


#
# Set MOTD
#
log "Setting MOTD..."
sed "s/MYHOSTNAME/${hostname}/g" \
    "${ADMIN_BASE}/system/files/etc/motd.template" > /etc/motd.template


log ""
log "Setup complete.  Reboot now."
exit 0
