How to put multiple bash scripts in a single package?

Hi,

I would like to automate the initial install of NixOS on multiple servers by using a set of custom bash scripts and package them to build a custom ISO installer.

The goal of this initial install is to configure networking with networkd, bonding and static IP addresses in order to be able to deploy full server configurations with NixOps.

So far, I managed to package this bash script:

#!/usr/bin/env bash
#
# Install NixOS on a server

function usage() {
  cat << EOF
Install NixOS on a server.

Usage:
  $(basename $0) -d|--disk <path> -s|--swap <size>
  $(basename $0) -h|--help

Options:
  -d, --disk <path>  Disk path on which to install NixOS (e.g.: /dev/disk/by-path/pci-0000:03:00.0-nvme-1).
  -s, --swap <size>  Size of the swap (e.g.: 16G).

  -h, --help         Display this help message.
EOF
}

function err() {
  echo "$@" >&2
}

function usage_err() {
  err "Try '$(basename $0) --help' for more information."
}

function wait_until_exists() {
  local FILE="$1"

  echo -n "Wait for file $FILE to exist..."

  while [ ! -e "${FILE}" ]; do
    echo -n "."
    sleep 0.1
  done

  echo " done"
}

function main() {

  local OPTIONS=

  OPTIONS=$(getopt \
    --name 'ff-install' \
    --options hd:s: \
    --longoptions help,disk:,swap: \
    -- "$@")

  if [ $? != 0 ] ; then
    usage_err
    exit 1
  fi

  set -e
  eval set -- "$OPTIONS"

  local DISK_PATH=
  local SWAP_SIZE=

  while true; do
    case "$1" in
      -d | --disk )
        DISK_PATH="$2"
        shift 2
        ;;
      -s | --swap )
        SWAP_SIZE="$2"
        shift 2
        ;;
      -h | --help )
        usage
        exit 0
        ;;
      -- )
        shift
        break
        ;;
      * )
        break
        ;;
    esac
  done

  if [[ ! "${DISK_PATH}" =~ ^/dev/disk/by-(id|path)/.+$ ]] ; then
    err "Disk path must start with /dev/disk/by-id/ or /dev/disk/by-path/."
    usage_err
    exit 1
  fi

  if [ ! -e "${DISK_PATH}" ] ; then
    err "Disk path '${DISK_PATH}' does not exit."
    exit 1
  fi

  if [ -z "${SWAP_SIZE}" ] ; then
    err "Swap size cannot be empty."
    usage_err
    exit 1
  fi

  # Partition disk
  echo "Partition disk ${DISK_PATH} …"

  # BIOS boot partition (EF02)
  sgdisk --set-alignment 1 --new 1:34:2047 --typecode 1:EF02 "${DISK_PATH}"
  # EFI system partition (EF00)
  sgdisk --new 2:1M:+512M --typecode 2:EF00 "${DISK_PATH}"
  # Main partition (BF01)
  sgdisk --new 3:0:0 --typecode 3:BF01 "${DISK_PATH}"

  local EFI_PART="${DISK_PATH}-part2"
  local MAIN_PART="${DISK_PATH}-part3"

  wait_until_exists "${MAIN_PART}"

  # Create ZFS pool
  echo "Create ZFS pool …"
  zpool create \
    -O mountpoint=none \
    -O compression=lz4 \
    -O xattr=sa \
    -O acltype=posixacl \
    -R /mnt \
    rpool \
    "${MAIN_PART}"

  # rpool/swap dataset
  echo "Create ZFS swap dataset …"
  zfs create \
    -V "${SWAP_SIZE}" \
    -b $(getconf PAGESIZE) \
    -o compression=zle \
    -o logbias=throughput \
    -o sync=always \
    -o primarycache=metadata \
    -o secondarycache=none \
    -o com.sun:auto-snapshot=false \
    rpool/swap
  wait_until_exists /dev/zvol/rpool/swap
  mkswap \
    -L swap \
    /dev/zvol/rpool/swap
  swapon /dev/zvol/rpool/swap

  # Create ZFS datasets
  echo "Create ZFS other datasets …"

  # rpool/root dataset
  zfs create \
    -o mountpoint=legacy \
    -o com.sun:auto-snapshot=true \
    rpool/root
  mount -t zfs rpool/root /mnt

  # rpool/nix dataset
  zfs create \
    -o mountpoint=legacy \
    -o com.sun:auto-snapshot=false \
    rpool/nix
  mkdir /mnt/nix
  mount -t zfs rpool/nix /mnt/nix

  # rpool/home dataset
  zfs create \
    -o mountpoint=legacy \
    -o com.sun:auto-snapshot=true \
    rpool/home
  mkdir /mnt/home
  mount -t zfs rpool/home /mnt/home

  # Create boot partition
  echo "Create boot partition …"
  mkfs.fat \
    -F 32 \
    -n boot \
    "${EFI_PART}"
  mkdir /mnt/boot
  mount -t vfat "${EFI_PART}" /mnt/boot

  # Generate NixOS configuration files
  echo "Generate NixOS configuration …"
  nixos-generate-config --root /mnt

  # Modify NixOS configuration files
  hostId=`head -c4 /dev/urandom | od -t x4 -A none | tr -d " "`
  sed -i "s/<HOST-ID>/${hostId}/g" /mnt/etc/nixos/configuration.nix
  hostSuffix=`head /dev/urandom | tr -dc A-Z0-9 | head -c4`
  hostName="ff-server-${hostSuffix}"
  sed -i "s/<HOST-NAME>/${hostName}/g" /mnt/etc/nixos/configuration.nix

  # Install NixOS
  echo "Install NixOS …"
  nixos-install

  # Umount installer
  echo "Umount installer …"
  umount /mnt/{nix,home,boot}
  umount /mnt
  swapoff -a

  # End
  echo "NixOS is installed (hostname ${hostName})."
  echo 'Enter `reboot` to boot on the newly installed NixOS.'

}

main "$@"

by using this NIX expression:

{ stdenv, fetchurl }:

stdenv.mkDerivation rec {
  name = "ff-install-unstable-2020-03-17";
  version = "0.1.0-dev";
  src = ./ff-install.sh;
  phases = "installPhase fixupPhase";
  installPhase = ''
    mkdir -p $out/bin
    cp ${src} $out/bin/ff-install
    chmod +x $out/bin/ff-install
  '';
}

You may have noticed than the scripts modify the content of the generated configuration.nix with the help of placeholders. Indeed, I am using a custom default template by setting the NixOS option system.nixos-generate-config.configuration.

The script is starting to do too many things and I would like to split it into several individual ones:

  • ff-partition-disk: create partitions
  • ff-init-filesystem: create filesystems on partitions
  • ff-generate-config: generate the config and modify it
  • ff-prepare-install: wrap call to the scripts above
  • ff-install: install NixOS after letting the user to manually edit the configuration files

I do not fully understand how stdenv.mkDerivation and I was thinking to build a single derivation with all the install scripts into it. Would you recommend this? If yes, how can I package multiple scripts in a single derivation?

In the same spirit, I would also like to write some templates of nixos modules that my ff-generate-config script would include in the main configuration file. So I would need to package these template files in the derivation.

After 2 days of research, I cannot find a way to do that (I am still learning nixpkgs). Is that even a good idea in your opinion?

Thank you

1 Like

Instead of making src a single file, make it a directory, then copy more files during the install phase.

1 Like

Thank you very much!

I managed to package 2 scripts and 1 NixOS module file (bond.nix) with the following expression:

{ stdenv }:

stdenv.mkDerivation rec {
  name = "ff-install-unstable-2020-07-17";
  version = "0.1.0-dev";
  src = ./src;
  phases = "installPhase fixupPhase";
  installPhase = ''
    mkdir -p $out/bin
    mkdir -p $out/etc/config/
    cp ${src}/ff-prepare-install.sh $out/bin/ff-prepare-install
    cp ${src}/ff-install.sh $out/bin/ff-install
    cp ${src}/config/bond.nix $out/etc/config/bond.nix
    chmod +x $out/bin/ff-prepare-install
    chmod +x $out/bin/ff-install
  '';
}

However, I do not understand the right way of knowing the path of bond.nix in the nix store from my ff-prepare-install script. I see several ways:

  • Retrieve the path of ff-prepare-install script using dirname and deduce the relative path of bond.nix from it. I tried and got /run/current-system/sw/bin for the path of ff-prepare-install. It is not the nix store path, so I could not deduce the path of bond.nix from that.
    Maybe there is a way to link the file in something like /run/current-system/sw/etc/ff-install/config/bond.nix. Is there?
  • Use nix to embed the nix store path of bond.nix directly within ff-prepare-install in the same spirit as when the fixupPhase change the shebang path. That seems like a better way. I am not sure how to achieve this.

There may be an obvious way to achieve this but the packaging task is still too confusing for me to get it. Is there any documentation that could guide / point me to the right direction?

1 Like

I think your second option is good. Search in the nixpkgs repo for substituteInPlace.

1 Like

Thanks a lot. It helped me getting in the right direction.

1 Like