nixos-configuration/digital-ocean/make-disk-image.nix

280 lines
9.2 KiB
Nix
Raw Normal View History

2023-12-11 15:18:52 +00:00
{ pkgs
, lib
, name ? "nixos-disk-image"
, # The NixOS configuration to be installed onto the disk image.
config
, # The size of the disk, in megabytes. If "auto", the size is calculated based
# on the contents copied to it and `extraSize` is taken into account.
diskSize ? "auto"
, # Extra disk space, in megabytes. Added to the image if diskSize "auto" is
# used.
extraSize ? 512
, # Swap space, in megabytes. Addeded to the image unless set to null.
swapSize ? 512
, # The unique identifier for the swap partition.
swapUUID ? "44444444-4444-4444-8888-888888888887"
, # The label given to the swap partition.
swapLabel ? "swap"
, # Whether to invoke `switch-to-configuration boot` during image creation.
installBootLoader ? true
, # The filesystem label.
label ? "nixos"
, # The initial NixOS configuration file set at `/etc/nixos/configuration.nix`.
configFile ? null
, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
format ? "raw"
, # The root Filesystem Unique Identifier.
rootFSUID ? "F222513B-DED1-49FA-B591-20CE86A2FE7F"
, # Whether a nix channel based on the current source tree should be
# made available inside the image. Useful for interactive use of nix
# utils, but changes the hash of the image when the sources are
# updated.
copyChannel ? true
, # Shell code executed after the VM has finished.
postVM ? ""
, # Guest memory size
memSize ? 1024
}:
let
format' = if format == "qcow2-compressed" then "qcow2" else format;
compress = lib.optionalString (format == "qcow2-compressed") "-c";
filename = "nixos." + {
qcow2 = "qcow2";
vdi = "vdi";
vpc = "vhd";
raw = "img";
}.${format'} or format';
swapPartition = "1";
rootPartition = "2";
swapEnd = toString (1 + swapSize);
nixpkgs = lib.cleanSource pkgs.path;
# FIXME: merge with channel.nix / make-channel.nix.
channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} ''
mkdir -p $out
cp -prd ${nixpkgs.outPath} $out/nixos
chmod -R u+w $out/nixos
if [ ! -e $out/nixos/nixpkgs ]; then
ln -s . $out/nixos/nixpkgs
fi
rm -rf $out/nixos/.git
echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
'';
binPath = lib.makeBinPath (with pkgs; [
rsync
util-linux
parted
e2fsprogs
lkl
config.system.build.nixos-install
config.system.build.nixos-enter
nix
systemdMinimal
gptfdisk
] ++ stdenv.initialPath);
closureInfo = pkgs.closureInfo {
rootPaths = [ config.system.build.toplevel ] ++
lib.optional copyChannel channelSources;
};
# ext4fs block size (not block device sector size)
blockSize = toString (4 * 1024);
prepareImage = ''
export PATH=${binPath}
# Yes, mkfs.ext4 takes different units in different contexts. Fun.
sectorsToKilobytes() {
echo $(( ( "$1" * 512 ) / 1024 ))
}
sectorsToBytes() {
echo $(( "$1" * 512 ))
}
# Given lines of numbers, adds them together
sum_lines() {
local acc=0
while read -r number; do
acc=$((acc+number))
done
echo "$acc"
}
mebibyte=$(( 1024 * 1024 ))
# Approximative percentage of reserved space in an ext4 fs over 512MiB.
# 0.05208587646484375 × 1000, integer part: 52
compute_fudge() {
echo $(( $1 * 52 / 1000 ))
}
mkdir $out
root="$PWD/root"
mkdir -p $root
export HOME=$TMPDIR
# Provide a Nix database so that nixos-install can copy closures.
export NIX_STATE_DIR=$TMPDIR/state
nix-store --load-db < ${closureInfo}/registration
chmod 755 "$TMPDIR"
echo "running nixos-install..."
nixos-install --root $root --no-bootloader --no-root-passwd \
--system ${config.system.build.toplevel} \
${if copyChannel then "--channel ${channelSources}" else "--no-channel-copy"} \
--substituters ""
diskImage=nixos.raw
${if diskSize == "auto" then ''
# Add the 1MiB aligned reserved space (includes MBR)
reservedSpace=$(( mebibyte ))
swapSpace=$((
$(numfmt --from=iec '${toString swapSize}M') + reservedSpace
))
extraSpace=$((
$(numfmt --from=iec '${toString extraSize}M') + reservedSpace
))
# Compute required space in filesystem blocks
diskUsage=$(
find . ! -type d -print0 |
du --files0-from=- --apparent-size --block-size "${blockSize}" |
cut -f1 |
sum_lines
)
# Each inode takes space!
numInodes=$(find . | wc -l)
# Convert to bytes, inodes take two blocks each!
diskUsage=$(( (diskUsage + 2 * numInodes) * ${blockSize} ))
# Then increase the required space to account for the reserved blocks.
fudge=$(compute_fudge $diskUsage)
requiredFilesystemSpace=$(( diskUsage + fudge ))
diskSize=$(( requiredFilesystemSpace + swapSpace + extraSpace ))
# Round up to the nearest mebibyte. This ensures whole 512 bytes sector
# sizes in the disk image and helps towards aligning partitions optimally.
if (( diskSize % mebibyte )); then
diskSize=$(( ( diskSize / mebibyte + 1) * mebibyte ))
fi
truncate -s "$diskSize" $diskImage
printf "Automatic disk size...\n"
printf " Closure space use: %d bytes\n" $diskUsage
printf " fudge: %d bytes\n" $fudge
printf " Filesystem size needed: %d bytes\n" $requiredFilesystemSpace
printf " Swap space: %d bytes\n" $swapSpace
printf " Extra space: %d bytes\n" $extraSpace
printf " Disk image size: %d bytes\n" $diskSize
'' else ''
truncate -s ${toString diskSize}M $diskImage
''}
parted --script $diskImage -- mklabel msdos
parted --script $diskImage -- \
mkpart primary linux-swap 1MiB ${swapEnd}MiB \
mkpart primary ext4 ${swapEnd}MiB -1
# Get start & length of the root partition in sectors to $START and
# $SECTORS.
eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
mkfs.ext4 -b ${blockSize} -F -L ${label} $diskImage -E \
offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
echo "copying staging root to image..."
cptofs -p -P ${rootPartition} -t ext4 -i $diskImage $root/* / ||
(echo >&2 "ERROR: cptofs failed. diskSize might be too small for closure."; exit 1)
'';
moveOrConvertImage = ''
${if format' == "raw" then "mv $diskImage $out/${filename}" else ''
${pkgs.qemu-utils}/bin/qemu-img convert -f raw -O ${format'} ${compress} \
$diskImage $out/${filename}
''}
diskImage=$out/${filename}
'';
buildImage = pkgs.vmTools.runInLinuxVM (
pkgs.runCommand name {
preVM = prepareImage;
buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ];
postVM = moveOrConvertImage + postVM;
inherit memSize;
} ''
export PATH=${binPath}:$PATH
rootDisk="/dev/vda${rootPartition}"
# It is necessary to set root filesystem unique identifier in advance,
# otherwise the bootloader might get the wrong one and fail to boot. At
# the end, we reset again because we want deterministic timestamps.
tune2fs -T now -U ${rootFSUID} -c 0 -i 0 $rootDisk
# Make systemd-boot find ESP without udev.
mkdir /dev/block
ln -s /dev/vda1 /dev/block/254:1
mountPoint=/mnt
mkdir $mountPoint
mount $rootDisk $mountPoint
# Create the swapspace without turning it on.
mkswap -U ${swapUUID} -L ${swapLabel} /dev/vda${swapPartition}
swapon /dev/vda${swapPartition}
# Install a configuration.nix
mkdir -p /mnt/etc/nixos
${lib.optionalString (configFile != null) ''
cp ${configFile} /mnt/etc/nixos/configuration.nix
''}
${lib.optionalString installBootLoader ''
# In this throwaway resource, we only have `/dev/vda`, but the actual VM
# may refer to another disk for bootloader, e.g. `/dev/vdb`. Use this
# option to create a symlink from vda to any arbitrary device you want.
${lib.optionalString (
config.boot.loader.grub.enable &&
config.boot.loader.grub.device != "/dev/vda"
) ''
mkdir -p $(dirname ${config.boot.loader.grub.device})
ln -s /dev/vda ${config.boot.loader.grub.device}
''}
# Set up core system link, bootloader (sd-boot, GRUB, uboot, etc.), etc.
NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- \
/nix/var/nix/profiles/system/bin/switch-to-configuration boot
# The above scripts will generate a random machine-id and we don't want
# to bake a single ID into all our images.
rm -f $mountPoint/etc/machine-id
''}
umount -R /mnt
# Make sure resize2fs works. Note that resize2fs has stricter criteria for
# resizing than a normal mount, so the `-c 0` and `-i 0` don't affect it.
# Setting it to `now` doesn't produce deterministic output, of course, but
# we can fix that when/if we start making images deterministic. This is
# fixed to 1970-01-01 (UNIX timestamp 0). This two-step approach is
# necessary otherwise `tune2fs` will want a fresher filesystem to perform
# some changes.
tune2fs -T now -U ${rootFSUID} -c 0 -i 0 $rootDisk
tune2fs -f -T 19700101 $rootDisk
''
);
in
buildImage