I want to create a custom installation ISO that can be used for unattended installs. I want the boot menu to give you two options. The first option would do the unattended install and then immediately reboot. The second option would boot into a normal Bash prompt (so that you could rescue a system or debug the unattended install script).
Here’s what I’ve tried so far:
# 🅭🄍1.0 This file is dedicated to the public domain using the CC0 1.0 Universal
# Public Domain Dedication:
# <https://creativecommons.org/publicdomain/zero/1.0/>
let
pkgs = import <nixpkgs> { };
configuration = { modulesPath, ... }: {
imports = [ "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" ];
specialisation.unattendedInstall = {
inheritParentConfig = true;
configuration = { pkgs, ... }: {
# In the future, I’ll set systemd.defaultUnit to something that does an
# unattended install, but for the sake of testing, I’m just installing
# an additional package.
environment.systemPackages = [ pkgs.hello ];
};
};
};
nixOSPackage = pkgs.nixos configuration;
in nixOSPackage.config.system.build.isoImage
Building that expression does indeed produce an ISO, and when I boot that ISO, I can see that /run/current-system/specialisation/unattendedInstall/
does indeed exist, but there’s no GRUB entry for the unattendedInstall
specialisation. How do I add boot menu entries for specialisations to an ISO? Should I be using something other than specialisations in order to accomplish what I’m trying to accomplish?
This is probably an issue with iso-image.nix since it manually specifies the boot entries and doesn’t use the grub module (which does add entries for specialisations).
OK, so I figured out how to do what I was trying to do, but it’s really overly complicated. Here’s a summary of how it works:
-
Create a systemd service that does the unattended install (unattended-install.service
).
-
Create a systemd target that activates that service (unattended-install.target
).
-
Use an apply
function to override the value of isoImage.contents
.
-
In the body of the apply
function, locate a path that’s part of the eifDir
package.
-
Create a new customizedEfiDir
package that copies all of the files from efiDir
’s output.
-
Make customizedEfiDir
override the contents of ${efiDir}/EFI/boot/grub.cfg
.
-
Find one of the menu entry declarations in grub.cfg
.
-
Create a new menu entry that’s a copy of the first one.
-
In the new menu entry, find where the Linux kernel command-line arguments are specified.
-
Add this command-line argument: systemd.unit=unattended-install.target
.
-
Write the newly generated menu entry to the bottom of grub.cfg
.
-
In the isoImage.contents
list, replace the item that referred to efiDir
with an item that refers to customizedEfiDir
.
Now, there’s another package named eifImg
that depends on the contents of the original efiDir
package. I haven’t figured out how to override that package, but it doesn’t seem to matter.
Here’s the full code:
# 🅭🄍1.0 This file is dedicated to the public domain using the CC0 1.0 Universal
# Public Domain Dedication:
# <https://creativecommons.org/publicdomain/zero/1.0/>
let
configuration = { pkgs, lib, modulesPath, ... }: {
imports = [ "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" ];
config.systemd = {
services.unattended-install = let
dependencies = [ "network-online.target" ];
in {
wants = dependencies;
after = dependencies;
description = "automatic NixOS installer";
script = ''
# This is where the actual unattended installation script would go.
exit 1
'';
serviceConfig = {
User = "nixos";
Group = "users";
};
unitConfig = {
SuccessAction = "reboot";
# This allows me to debug the unattended installation script if it
# fails.
OnFailure = "multi-user.target";
};
};
targets.unattended-install = {
wants = [ "unattended-install.service" ];
description = "automatic installation environment";
};
};
# This allows us to modify config.isoImage.contents after the rest of the
# config has set it to something. See
# <https://github.com/NixOS/nixpkgs/issues/16884#issuecomment-238814281>
options.isoImage.contents = lib.options.mkOption {
apply = list: lib.lists.forEach list (item:
if item.target == "/EFI"
then item // {
source = let
# I chose this name because the original package that I’m modifying
# is called efi-directory:
# <https://github.com/NixOS/nixpkgs/blob/ee76c70ea139274f1afd5f7d287c0489b4750fee/nixos/modules/installer/cd-dvd/iso-image.nix#L238>
pkgName = "efi-directory-customized";
customizedEfiDir = pkgs.runCommand pkgName {
nativeBuildInputs = [
pkgs.buildPackages.grub2_efi
pkgs.sift
];
} ''
set -e
mkdir "$out"
cp -r ${item.source} "$out/EFI"
readonly grub_cfg="$out/EFI/boot/grub.cfg"
existing_boot_entry="$(
echo -n "menuentry 'Unattended Install' "
sift \
--multiline \
--regexp='--class installer\s.*?{.*?}\n' \
--only-matching \
--limit=1 \
"$grub_cfg"
)"
readonly existing_boot_entry
new_boot_entry="$(
# Thanks to Cyrus (https://askubuntu.com/users/336375/cyrus)
# for this Ask Ubuntu comment:
# <https://askubuntu.com/questions/537967/appending-to-end-of-a-line-using-sed#comment735811_537969>
echo -nE "$existing_boot_entry" \
| sed 's/linux.*/& systemd.unit=unattended-install.target/'
)"
readonly new_boot_entry
chmod +w "$grub_cfg"
echo -nE "$new_boot_entry" >> "$grub_cfg"
chmod -w "$grub_cfg"
grub-script-check "$grub_cfg"
'';
in "${customizedEfiDir}/EFI";
}
else item
);
};
};
pkgs = import <nixpkgs> { };
nixOSPackage = pkgs.nixos configuration;
in nixOSPackage.config.system.build.isoImage