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

280 lines
9.2 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{ 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