Skip to content
This repository has been archived by the owner on Oct 10, 2023. It is now read-only.

installer: atomically copy directory to specified ESP #1

Merged
merged 12 commits into from
Aug 19, 2021
Merged

Conversation

cole-h
Copy link
Member

@cole-h cole-h commented Aug 10, 2021

This copies the files to a temporary directory, then renames them to their
desired destination, and finally removes the temporary directory.

Usable in conjunction with https://github.com/DeterminateSystems/nixpkgs-priv/tree/boot-json and the following module (ripped from my flake; may need modifications):

                ({ pkgs, lib, config, ... }: {
                  boot.loader.manual = {
                    enable = true;
                    installHook =
                      let
                        installer = pkgs.rustPlatform.buildRustPackage rec {
                          pname = "installer";
                          version = "0";
                          src = inputs.experiment;
                          cargoLock.lockFile = src + "/Cargo.lock";
                          buildType = "debug";
                          dontStrip = true;
                        };
                      in
                      pkgs.writeShellScript "install-bootloader" ''
                        set -x
                        cd "$(mktemp -d)" || exit 1

                        ${installer}/bin/generator /nix/var/nix/profiles/system-*-link

                        ${installer}/bin/installer \
                          --toplevel="$1" \
                          --esp="${config.boot.loader.efi.efiSysMountPoint}" \
                          ${lib.optionalString config.boot.loader.efi.canTouchEfiVariables "--touch-efi-vars"} \
                          --console-mode="${config.boot.loader.systemd-boot.consoleMode}" \
                          --timeout="${toString config.boot.loader.timeout}" \
                          --bootctl="${pkgs.systemd}/bin/bootctl" \
                          ${lib.optionalString (config.boot.loader.systemd-boot.configurationLimit != null) ''--configuration-limit="${toString config.boot.loader.systemd-boot.configurationLimit}"''} \
                          --generated-entries="./systemd-boot-entries"
                      '';
                  };
                })

Closes #2.
Closes #4.

@grahamc
Copy link
Member

grahamc commented Aug 13, 2021

    ({ options, lib, ... }: {
      config = lib.mkIf (!(options.boot.loader ? manual)) {
        boot.loader.systemd-boot.enable = true;
        boot.loader.efi.canTouchEfiVariables = true;
      };
    })

console_mode: pico
.value_from_str("--console-mode")
.unwrap_or_else(|_| String::from("keep")),
console_mode: pico.value_from_str("--console-mode")?,
configuration_limit: pico.value_from_str("--configuration-limit")?,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be optional.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corresponding change to your flake example:

${lib.optionalString (config.boot.loader.systemd-boot.configurationLimit != null) ''--configuration-limit="${toString config.boot.loader.systemd-boot.configurationLimit}"''} \

installer/src/main.rs Outdated Show resolved Hide resolved
installer/src/main.rs Outdated Show resolved Hide resolved
@grahamc
Copy link
Member

grahamc commented Aug 13, 2021

grahamc@scruffy:~/scruffy/ > sudo nixos-rebuild boot --flake '.#scruffy' -L
building the system configuration...
++ mktemp -d
+ cd /tmp/tmp.VkmzKrKg8O
+ /nix/store/9lbr2291aibmi8bxl2n7v7srw4r944nw-installer-0/bin/generator /nix/var/nix/profiles/system-1-link /nix/var/nix/profiles/system-2-link /nix/var/nix/profiles/system-3-link /nix/var/nix/profiles/system-4-link /nix/var/nix/profiles/system-5-link /nix/var/nix/profiles/system-6-link /nix/var/nix/profiles/system-7-link
+ /nix/store/9lbr2291aibmi8bxl2n7v7srw4r944nw-installer-0/bin/installer --toplevel=/nix/store/zy41vhmfxx6b4657bl1cqs438y3dg2h2-nixos-system-scruffy-21.11.20210810.6f6ade6 --esp=/boot --console-mode=keep --timeout=5 --bootctl=/nix/store/f0dgfrsfwmnvxzlcm2vv5dy1kmknsmpr-systemd-247.6/bin/bootctl --configuration-limit=120 --generated-entries=./systemd-boot-entries
[installer/src/main.rs:80] &args = Args {
    toplevel: "/nix/store/zy41vhmfxx6b4657bl1cqs438y3dg2h2-nixos-system-scruffy-21.11.20210810.6f6ade6",
    dry_run: false,
    generated_entries: "./systemd-boot-entries",
    timeout: Some(
        5,
    ),
    console_mode: "keep",
    configuration_limit: 120,
    editor: true,
    esp: Some(
        "/boot",
    ),
    can_touch_efi_vars: false,
    bootctl: Some(
        "/nix/store/f0dgfrsfwmnvxzlcm2vv5dy1kmknsmpr-systemd-247.6/bin/bootctl",
    ),
}

@grahamc
Copy link
Member

grahamc commented Aug 13, 2021

Opa! I'm switched!

@cole-h
Copy link
Member Author

cole-h commented Aug 13, 2021

Also, there should probably be some better error handling -- would be nice if there was some way to differentiate between fatal and not-so-fatal errors.

This copies the files to a temporary directory, then renames them to their
desired destination, and finally removes the temporary directory.
Also implements the configuration_limit and editor flags.
We don't need to error if the directory we're trying to clean doesn't exist (it
will be created). We do need to error if `nix-env` returns empty output -- `nix-
env` requires root to list the generations of a profile (for whatever
reason...).
It's a bit annoying having to run this as root even for a dry run (because `nix-
env` needs root to create a lockfile to list generations... for some reason).
So, we just do the work ourselves.
This function isn't only run against the ESP, so we don't want to give that
impression in the function signature.
@cole-h cole-h force-pushed the atomic_copy branch 2 times, most recently from 4995743 to 3b4b29a Compare August 17, 2021 20:42
Comment on lines 195 to 241
pub(crate) fn bootloader_is_old(
bootloader_version: Option<usize>,
systemd_version: usize,
) -> Result<bool> {
if let Some(bootloader_version) = bootloader_version {
let old = systemd_version > bootloader_version;
Ok(old)
} else {
String::from("/nix/var/nix/profiles/system")
warn!("could not find any previously installed systemd-boot");
Ok(false)
}
}

pub(crate) fn get_bootloader_version(bootctl: &Path, esp: &Path) -> Result<Option<usize>> {
trace!("checking bootloader version");

debug!("running `bootctl status`");
let output = Command::new(&bootctl)
.args(&[&format!("--path={}", &esp.display()), "status"])
.output()?
.stdout;

let version = self::parse_bootloader_version(&output)?;

Ok(version)
}

pub(crate) fn parse_bootloader_version(output: &[u8]) -> Result<Option<usize>> {
let output = str::from_utf8(output)?;

// pat in its own str so that `cargo fmt` doesn't choke...
let pat = "^\\W+File:.*/EFI/(BOOT|systemd)/.*\\.efi \\(systemd-boot (?P<version>\\d+)\\)$";

// See enumerate_binaries() in systemd bootctl.c for code which generates this:
// https://github.com/systemd/systemd/blob/788733428d019791ab9d780b4778a472794b3748/src/boot/bootctl.c#L221-L224
let re = RegexBuilder::new(pat)
.multi_line(true)
.case_insensitive(true)
.build()?;
let caps = re.captures(output);

let version = if let Some(caps) = caps {
caps.name("version")
.and_then(|cap| cap.as_str().parse::<usize>().ok())
} else {
None
};
Copy link
Member

@grahamc grahamc Aug 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making more things. For example, we're not just comparing usizes we're comparing installed SystemdBoot versions.

What if there was a struct of SystemdBootVersion with a version field, and you could construct it by calling SystemdBootVersion::detect_version() which executes the command and calls SystemdBootVersion::from_status_output(string) which calls SystemdBootVersion::new(version number)? Then we've got a little library, and a struct we could hang some helper functions on, like Ord for comparing.

All this could go in to a nice, small file with some tests just for it, in an easy to consume nibble of code. Compare that to here: 50 lines of self-contained-ish code burried 200 lines deep in a nearly 500 line long file.

Similar feedback applies to the systemd version stuff.

@cole-h cole-h force-pushed the atomic_copy branch 2 times, most recently from fb3f953 to 0fcb3ba Compare August 18, 2021 17:03
Copy link
Member

@grahamc grahamc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking MUCH better with these improvements. This review is just one one specific commit, though. Maybe toss in a couple tests for cmp and eq?

installer/src/systemd_boot/version/mod.rs Outdated Show resolved Hide resolved
installer/src/systemd_boot/version/systemd.rs Outdated Show resolved Hide resolved
Ok(version)
}

pub fn detect_version(bootctl: &Path, esp: &Path) -> Result<Option<Self>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return type is a bit weird. It might be worth looking out for ways to improve this, like an enum of DetectionResult { ExecError, ParseError, Ok } -- but not sure, and definitely not asking you to do it now.

installer/src/systemd_boot/version/systemd_boot.rs Outdated Show resolved Hide resolved
installer/src/systemd_boot/version/systemd_boot.rs Outdated Show resolved Hide resolved
Copy link
Member

@grahamc grahamc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking pretty good!

.status()?;
}
} else {
warn!("could not find any previously installed systemd-boot");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this is not warning-grade error if this is a new installation, what is the difference between installing and upgrading? Maybe we should automatically install if that fails?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just felt like there should be the "least magic" happening as possible -- if a user wants to install systemd-boot, they should use the --install flag. Otherwise, they want to upgrade their bootloader. If systemd-boot isn't present, then that's fine; we'll just copy the kernels and boot entries to the specified ESP and let them install it either manually (via bootctl) or via this tool.

Does that make sense? This is more a warning that "we'll copy everything you want, but it won't boot yet, because there's no installed bootloader (systemd-boot)".

installer/src/systemd_boot/mod.rs Outdated Show resolved Hide resolved
installer/src/systemd_boot/version/mod.rs Outdated Show resolved Hide resolved
@cole-h cole-h merged commit 39f506a into main Aug 19, 2021
@cole-h cole-h deleted the atomic_copy branch August 19, 2021 16:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support bootctl install Implement some logging
2 participants