How can I automate an installation?

I want to install FreeBSD in a certain way, with a certain procedure. Typing everything manually is such a drag.

This in case you're wondering.

Is it possible to script it? My problem is, the system is not even installed yet. Is there somehow I can apply a script to the installation?
 
You can run a tty in the install medium. If you can install packages, like curl, maybe you could download your script upstream, or even attach a device to it and automate the installation. But I do no know or imagine a way to make this being a full automate install without a user setting the shell option or allowing ssh, etc.
 
I want to install FreeBSD in a certain way, with a certain procedure. Typing everything manually is such a drag.

This in case you're wondering.

Is it possible to script it? My problem is, the system is not even installed yet. Is there somehow I can apply a script to the installation?
You could boot mfsBSD from a USB stick, copy a build script to your target system using scp and then run that script after logging in via ssh.
 
Is there somehow I can apply a script to the installation?
Sure, just add the script to a official installation FreeBSD installer media/image.
  1. Fetch preferable a *-memstick.img
  2. copy image on USB (alternatively attach *-memstick.img as a memory disk, mdconfig(8), then copy on USB after modification)
  3. mount USB disk
  4. use /etc/installerconfig file to install scripted, semi-automatic, as described in bsdinstall(8), as SirDice pointed out. Semi-automatic in this case, because you need to enter geli(8) passphrase to initialize and attach the provider.
  5. or rename and replace /etc/rc.local with your custom installation script
If you replace the "ro" mount option for "/" with "rw" in /etc/fstab, you can easily modify the script when needed (not possible with .iso image). After every modification, execute the script directly without rebooting the system (ie # sh /etc/installerconfig or # sh /etc/rc.local.

You could also test the script in a VM before running on bare metal.

Is it possible to ssh into the installation shell? There is no sshd in the output of 'ps aux,' so can I start sshd and how?
The installer image doesn't come with sshd enabled. If you want sshd running during installation, add sshd_enable="YES" to /etc/rc.conf.

With the same method you can add any service or system configuration you need, ie, request a IP address to ssh in.
 
Thank you for the answers, I am going to analyze them, including reading bsdinstall(8).

Meanwhile, I got confused by the idea of adding sshd_enable="YES" to /etc/rc.conf. It's the installation stage. The only available system is running off read-only media. So I don't see the point. It would be cool if I could add sshd_enable="YES" to the booting kernel like we add things to Grub lines in Linux. I don't suppose I can.

I booted into single user mode and found sshd, but it won't work.
"No host key files found"
I naively tried to generate the keys,
# ssh-keygen -A
But of course that doesn't work because the installation media is read-only.

Is it possible to get sshd running at installation stage?

I am going to read bsdinstall(8) in bed before I sleep.
 
Meanwhile, I got confused by the idea of adding sshd_enable="YES" to /etc/rc.conf. It's the installation stage. The only available system is running off read-only media.
I should have explained the situation more in detail in my previous posting to make it clearer, but I assumed that was obvious. My mistake.

FreeBSD installer media = A FreeBSD installer image (file) (.iso or .img) copied on a USB disk.
FreeBSD installer image (file) = A FreeBSD *.iso or *-memstick.img image (file)

A *-memstick.img installer image has a read/write file system, mounted "ro" from /etc/fstab. This can be changed easily to "rw".

Option 1: Mount the on a USB disk copied *-memestick.img FreeBSD installer (or attach the installer image *-memestick.img before copying on a USB disk as a memory disk) to a running system, replace "ro" with "rw". While mounted read/write, copy the script over.

Option 2: Boot the FreeBSD USB installer media (copied *-memstick.img on USB disk) into single-user mode and mount the file system read/write ( # mount -u /), modify /etc/fstab, continue booting into multi-user mode. Copy the script in the way you have chosen.

Is it possible to get sshd running at installation stage?
Regarding sshd_enable="YES" in /etc/rc.conf, I didn't tested that on 14.0. Apparently the service doesn't get started as long the original /etc/rc.local is active. That file has received changes on 14.0. On 13.3 this should still work.

If installation media (or image) is 14.0, add service sshd onestart to /etc/rc.local.

Also, I would set ifconfig_DEFAULT="DHCP" (or a static address instead of DHCP) to have the network interface configured to ssh in.

If you are using /etc/installerconfig or a custom /etc/rc.local this shouldn't be necessary. When file /etc/installerconfig is present, /etc/rc.local isn't read. But I doubt you need a sshd service running when the installation is started scripted from a local script.

Having said that, the best way to install scripted is to place the installation script directly on the (read/write) installer media or image, not to install third party packages to download the script, or ssh in, or execute the script from a attached device.

Modifying (adding a script to) a read-only file system on a *.iso image (file) is possible, but more labourious, not worth the effort.
 
The way that I do such installs is boot via PXE. The OS booted via PXE has my full environment including a CVS tree full of scripts to prepare harddrives with bootable OSes.

So a new machine only needs a LAN connection and an entry in dhcpd.conf on the server and off you go.

Laptops without PXE-capable LAN are a problem, though.
 
Well, the mission failed.

I read about bsdinstall and it seems overly complicated for what I want. Besides, how does one run it? I tried and the system said it's not possible because the system is read-only. Shrug.

I understand the concept of editing the content of the memstick.img image file. But then how is that different from inserting a second USB stick with the script? It's actually easier than hacking memstick.img.

Anyway, I'm not doing it on bare metal, it's a Virtualbox VM. I can only boot ISO images. I tried editing the ISO image with multiple methods and none of them worked. The image always ends up being unbootable. I copied the boot info from the original iso image to the new one. It still won't boot.

I have an awkward workaround. I created another disk, a very small disk, formatted it as FAT and put the script there. After booting the installation image, I could mount that disk and have access to the script. I didn't run it, but it would probably work. I will test it eventually.

Anyway, I had thought of this awkward workaround already. I was really hoping to learn a much better, smarter way though.
 
What I do for semi-automated installations is to add ssh-server to an iso or disk image. I then run a bunch of scripts to do the install. For what I do, I circumvent bsdinstall, do the partitioning, encryption, zfs pool etc using that script and then extract the necessary install packages. I can't give you those scripts - I lost the best versions in a hack, but I can give you what I use to add sshd to an image or iso. Note that modifying iso files is different from modifying disk images.

Note: I didn't know I could set DHCP by default, so I used a very naive way to enable it. I suggest to change that part, although it doesn't really matter much. It's not perfect, but should be good enough at the very least as a starting point.

Code:
#!/bin/sh
# Script to enable SSH & networking on a FreeBSD ISO or IMG file
# For FreeBSD 13 at the very least, possible other ones too

## Sources
# /usr/src/release/amd64/mkisoimages.sh
# /usr/src/tools/boot/install-boot.sh
# https://horrell.ca/2014/12/04/creating-a-custom-freebsd-10-iso-with-automated-installation/ No longer online (but cloudflare shows something from waybackarchive, really cool)

MOUNT_POINT=/tmp/mnt
RESULT_DIR=/tmp/bsdincssh
# temporary memorydisk which will be used to make the modifications without touching disks too much
MEMDISK_SIZE="2G"
# will be set in rc.conf to make it easier to connect (assuming DDNS is used)
KEYMAP="be.kbd"
HOSTNAME="install"
mkdir -p "${MOUNT_POINT}" "${RESULT_DIR}"

fatal() {
    echo "FATAL: $1"
    local EXIT=$2
    if [ -z "${EXIT}" ]; then
        EXIT=1
    fi
    exit ${EXIT}
}

mountISO() {
    ISO="$1"
    if [ -z "${ISO}" ]; then
        fatal "mountISO(): Wrong number of parameters supplied"
    fi
    
    local TEMP_ISO_MOUNT=/tmp/isomnt
    mkdir -p "${TEMP_ISO_MOUNT}"
    local DEV=$(mdconfig -f "${ISO}")

    mount -t cd9660 /dev/${DEV} "${TEMP_ISO_MOUNT}"
    
    # memdisk is used as file storage in the case of an iso
    export MEMDISK_DEV=$(mdconfig -as ${MEMDISK_SIZE})
    newfs /dev/${MEMDISK_DEV} > /dev/null 2>&1
    mount /dev/${MEMDISK_DEV} "${MOUNT_POINT}"
    # TODO archive copy
    cp -a "${TEMP_ISO_MOUNT}/" "${MOUNT_POINT}"
    
    umount "${TEMP_ISO_MOUNT}"
    mdconfig -d -u ${DEV}
}

mountImage() {
    IMAGE="$1"
    if [ -z "${IMAGE}" ]; then
        fatal "mountImage(): Wrong number of parameters supplied"
    fi
    
    local IMAGE_FILE=$(basename "${IMAGE}")
    # ls -l will output the size correctly as well as dd, but I'm not sure this is the best way to do this; du won't return correctly either :'(
    #local BYTES=$(ls -l "${IMAGE}" |awk '{print $5}')
    #export MEMDISK_DEV=$(mdconfig -as ${BYTES})
    # memdisk is bitwise copy from the image file; will grow in size if MEMDISK_SIZE is larger (and it should)
    #dd if="${IMAGE}" of=/dev/${MEMDISK_DEV} bs=64m > /dev/null 2>&1
    # image is copied, then mounted via mdconfig
    cp -r "${IMAGE}" "${RESULT_DIR}/${IMAGE_FILE}"
    export MEMDISK_DEV=$(mdconfig -f "${RESULT_DIR}/${IMAGE_FILE}")
    mount /dev/${MEMDISK_DEV}s2a "${MOUNT_POINT}"
}

enableNetworking() {
    # a bit over the top I think, but I'm guessing this will work in most cases (except for wifi)
    # just loop all kernel modules for interfaces and add them to rc.conf
    echo "Enabling networking for"
    for MOD in $(ls "${MOUNT_POINT}/boot/kernel" | grep "if_" | cut -c4-99); do
        local INTERFACE="${MOD%.*}"
        echo -n "...${INTERFACE}"
        chroot "${MOUNT_POINT}" sysrc "ifconfig_${INTERFACE}0=DHCP" > /dev/null 2>&1
        chroot "${MOUNT_POINT}" sysrc "ifconfig_${INTERFACE}1=DHCP" > /dev/null 2>&1
        chroot "${MOUNT_POINT}" sysrc "ifconfig_${INTERFACE}2=DHCP" > /dev/null 2>&1
        chroot "${MOUNT_POINT}" sysrc "ifconfig_${INTERFACE}3=DHCP" > /dev/null 2>&1
    done
}

enableSSH() {
    chroot "${MOUNT_POINT}" service sshd enable
    echo "Creating SSH host keys"
    chroot "${MOUNT_POINT}" service sshd keygen > /dev/null 2>&1

    # allow passwordless & root login; wouldn't recommend this on an actual system :)
    echo "UseDNS no"             >> "${MOUNT_POINT}/etc/ssh/sshd_config"
    echo "UsePAM no"             >> "${MOUNT_POINT}/etc/ssh/sshd_config"
    echo "PermitRootLogin yes"         >> "${MOUNT_POINT}/etc/ssh/sshd_config"
    echo "PermitEmptyPasswords yes"     >> "${MOUNT_POINT}/etc/ssh/sshd_config"
    echo "PasswordAuthentication yes"     >> "${MOUNT_POINT}/etc/ssh/sshd_config"
}

modifyRC() {
    chroot "${MOUNT_POINT}" sysrc "keymap=${KEYMAP}"
    chroot "${MOUNT_POINT}" sysrc "hostname=${HOSTNAME}"
    
    chroot "${MOUNT_POINT}" service powerd enable
}

disableBsdInstall() {
    chroot "${MOUNT_POINT}" sed 's/dialog --backtitle "FreeBSD Installer"/exit 0 #/' /etc/rc.local
}

installUsefulPackages() {
    pkg -r "${MOUNT_POINT}" install -y storcli #powerd
}

finishImage() {
    IMAGE="$1"
    if [ -z "${IMAGE}" ]; then
        fatal "finishImage(): Wrong number of parameters supplied"
    fi

    local IMAGE_FILE=$(basename "${IMAGE}")
    echo "Written new image file to ${RESULT_DIR}/${IMAGE_FILE}"
    umount "${MOUNT_POINT}"
    #dd if=/dev/${MEMDISK_DEV} of="${RESULT_DIR}/${IMAGE_FILE}" bs=64m > /dev/null 2>&1
    mdconfig -d -u ${MEMDISK_DEV}
}

finishISO() {
    ISO="$1"
    if [ -z "${ISO}" ]; then
        fatal "finishIso(): Wrong number of parameters supplied"
    fi
    
    # based on make_esp_file() in /usr/src/tools/boot/install-boot.sh
    espfilename=$(mktemp /tmp/efiboot.XXXXXX)
    stagedir=$(mktemp -d /tmp/stand-test.XXXXXX)
    mkdir -p "${stagedir}/EFI/BOOT"
    # NOTE: amd64 only!!! see /usr/src/tools/boot/install-boot.sh for others
    cp "${MOUNT_POINT}/boot/loader.efi" "${stagedir}/EFI/BOOT/bootx64.efi"
    makefs -t msdos -o fat_type=12 -o sectors_per_cluster=1 -o volume_label=EFISYS -s 2048k "${espfilename}" "${stagedir}" > /dev/null 2>&1
    rm -rf "${stagedir}"
    
    local ISO_FILE=$(basename "${ISO}")
    echo "writing new ISO file to ${RESULT_DIR}/${ISO_FILE}"
    local LABEL="$(isoinfo -d -i "${ISO}" | grep "Volume id" | awk '{print $3}')"
    local BOOTABLE="-o bootimage=i386;${MOUNT_POINT}/boot/cdboot -o no-emul-boot"
    BOOTABLE="${BOOTABLE} -o bootimage=i386;${espfilename} -o no-emul-boot -o platformid=efi"

    makefs -t cd9660 ${BOOTABLE} -o rockridge -o label="${LABEL}" -o publisher=malavon "${RESULT_DIR}/${ISO_FILE}" "${MOUNT_POINT}"
    
    rm ${espfilename}
    
    umount "${MOUNT_POINT}"
    mdconfig -d -u ${MEMDISK_DEV}
}

doImageOrISO() {
    EXT="$1"
    IMAGE_FUNC="$2"
    ISO_FUNC="$3"
    shift 3 # remove above parameters, but keep all others which can be params to the function
    local PARAMS=$@
    
    if [ -z "${EXT}" ] || [ -z "${IMAGE_FUNC}" ] || [ -z "${ISO_FUNC}" ]; then
        fatal "doImageOrISO(): Wrong number of parameters supplied"
    fi

    case "${EXT}" in
        img)
            eval "${IMAGE_FUNC}" "${PARAMS}"
            ;;
        iso)
            eval "${ISO_FUNC}" "${PARAMS}"
            ;;
        *)
            fatal "This script only works with ISO or image files"
            exit 1
            ;;
    esac
}

##############################
## Actual script starts here
##############################

if [ $(id -u) -ne 0 ]; then
    fatal "Please run this script as root, it needs to mount stuff etc"
    exit 1
fi

FILE="$1"
FILE_LOWER="$(echo "${FILE}" | tr '[:upper:]' '[:lower:]')"
# alternatively could use the file command to find out which one it is; but it's hard to parse
EXT="${FILE_LOWER##*.}"

doImageOrISO "$EXT" "mountImage" "mountISO" "${FILE}"
enableNetworking
enableSSH
modifyRC
disableBsdInstall
installUsefulPackages
doImageOrISO "$EXT" "finishImage" "finishISO" "${FILE}"
 
Back
Top