From fd5b273e82cf7e78a94479730cf1f51d3a19c637 Mon Sep 17 00:00:00 2001 From: "William A. Kennington III" Date: Mon, 25 May 2015 14:57:20 -0700 Subject: [PATCH] nixos/grub: Add the ability to mirror grub to multiple partitions --- .../modules/system/boot/loader/grub/grub.nix | 116 +++++++++++++++--- .../system/boot/loader/grub/install-grub.pl | 41 ++++--- 2 files changed, 120 insertions(+), 37 deletions(-) diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix index e8e7727d319..e64c41b7f48 100644 --- a/nixos/modules/system/boot/loader/grub/grub.nix +++ b/nixos/modules/system/boot/loader/grub/grub.nix @@ -27,7 +27,7 @@ let f = x: if x == null then "" else "" + x; - grubConfig = pkgs.writeText "grub-config.xml" (builtins.toXML + grubConfig = args: pkgs.writeText "grub-config.xml" (builtins.toXML { splashImage = f config.boot.loader.grub.splashImage; grub = f grub; grubTarget = f (grub.grubTarget or ""); @@ -35,11 +35,14 @@ let fullVersion = (builtins.parseDrvName realGrub.name).version; grubEfi = f grubEfi; grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else ""; - inherit (efi) efiSysMountPoint canTouchEfiVariables; + bootPath = args.path; + efiSysMountPoint = if args.efiSysMountPoint == null then args.path else args.efiSysMountPoint; + inherit (args) devices; + inherit (efi) canTouchEfiVariables; inherit (cfg) version extraConfig extraPerEntryConfig extraEntries extraEntriesBeforeNixOS extraPrepareConfig configurationLimit copyKernels timeout - default devices fsIdentifier efiSupport; + default fsIdentifier efiSupport; path = (makeSearchPath "bin" ([ pkgs.coreutils pkgs.gnused pkgs.gnugrep pkgs.findutils pkgs.diffutils pkgs.btrfsProgs pkgs.utillinux ] ++ (if cfg.efiSupport && (cfg.version == 2) then [pkgs.efibootmgr ] else []) @@ -48,6 +51,9 @@ let ]); }); + bootDeviceCounters = fold (device: attr: attr // { "${device}" = (attr."${device}" or 0) + 1; }) {} + (concatMap (args: args.devices) cfg.mirroredBoots); + in { @@ -101,6 +107,53 @@ in ''; }; + mirroredBoots = mkOption { + default = [ ]; + example = [ + { path = "/boot1"; devices = [ "/dev/sda" ]; } + { path = "/boot2"; devices = [ "/dev/sdb" ]; } + ]; + description = '' + Mirror the boot configuration to multiple partitions and install grub + to the respective devices corresponding to those partitions. + ''; + + type = types.listOf types.optionSet; + + options = { + + path = mkOption { + example = "/boot1"; + type = types.str; + description = '' + The path to the boot directory where grub will be written. Generally + this boot parth should double as an efi path. + ''; + }; + + efiSysMountPoint = mkOption { + default = null; + example = "/boot1/efi"; + type = types.nullOr types.str; + description = '' + The path to the efi system mount point. Usually this is the same + partition as the above path and can be left as null. + ''; + }; + + devices = mkOption { + default = [ ]; + example = [ "/dev/sda" "/dev/sdb" ]; + type = types.listOf types.str; + description = '' + The path to the devices which will have the grub mbr written. + Note these are typically device paths and not paths to partitions. + ''; + }; + + }; + }; + configurationName = mkOption { default = ""; example = "Stable 2.6.21"; @@ -291,13 +344,18 @@ in boot.loader.grub.devices = optional (cfg.device != "") cfg.device; - system.build.installBootLoader = - if cfg.devices == [] then - throw "You must set the option ‘boot.loader.grub.device’ to make the system bootable." - else - "PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX ListCompare ])} " + - (if cfg.enableCryptodisk then "GRUB_ENABLE_CRYPTODISK=y " else "") + - "${pkgs.perl}/bin/perl ${./install-grub.pl} ${grubConfig}"; + boot.loader.grub.mirroredBoots = optionals (cfg.devices != [ ]) [ + { path = "/boot"; inherit (cfg) devices; inherit (efi) efiSysMountPoint; } + ]; + + system.build.installBootLoader = pkgs.writeScript "install-grub.sh" ('' + #!${pkgs.stdenv.shell} + set -e + export PERL5LIB=${makePerlPath (with pkgs.perlPackages; [ FileSlurp XMLLibXML XMLSAX ListCompare ])} + ${optionalString cfg.enableCryptodisk "export GRUB_ENABLE_CRYPTODISK=y"} + '' + flip concatMapStrings cfg.mirroredBoots (args: '' + ${pkgs.perl}/bin/perl ${./install-grub.pl} ${grubConfig args} + '')); system.build.grub = grub; @@ -312,13 +370,37 @@ in ${pkgs.coreutils}/bin/cp -pf "${v}" "/boot/${n}" '') config.boot.loader.grub.extraFiles); - assertions = [{ assertion = !cfg.zfsSupport || cfg.version == 2; - message = "Only grub version 2 provides zfs support";}] - ++ flip map cfg.devices (dev: { - assertion = dev == "nodev" || hasPrefix "/" dev; - message = "Grub devices must be absolute paths, not ${dev}"; - }); - + assertions = [ + { + assertion = !cfg.zfsSupport || cfg.version == 2; + message = "Only grub version 2 provides zfs support"; + } + { + assertion = cfg.mirroredBoots != [ ]; + message = "You must set the option ‘boot.loader.grub.devices’ or " + + "'boot.loader.grub.mirroredBoots' to make the system bootable."; + } + { + assertion = all (c: c < 2) (mapAttrsToList (_: c: c) bootDeviceCounters); + message = "You cannot have duplicated devices in mirroredBoots"; + } + ] ++ flip concatMap cfg.mirroredBoots (args: [ + { + assertion = args.devices != [ ]; + message = "A boot path cannot have an empty devices string in ${arg.path}"; + } + { + assertion = hasPrefix "/" args.path; + message = "Boot paths must be absolute, not ${args.path}"; + } + { + assertion = hasPrefix "/" args.efiSysMountPoint; + message = "Efi paths must be absolute, not ${args.efiSysMountPoint}"; + } + ] ++ flip map args.devices (device: { + assertion = device == "nodev" || hasPrefix "/" device; + message = "Grub devices must be absolute paths, not ${dev} in ${args.path}"; + })); }) ]; diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl index 81009e9fb82..a0384d23f82 100644 --- a/nixos/modules/system/boot/loader/grub/install-grub.pl +++ b/nixos/modules/system/boot/loader/grub/install-grub.pl @@ -11,7 +11,7 @@ require List::Compare; use POSIX; use Cwd; -my $defaultConfig = $ARGV[1] or die; +my $defaultConfig = $ARGV[0] or die; my $dom = XML::LibXML->load_xml(location => $ARGV[0]); @@ -54,6 +54,7 @@ my $defaultEntry = int(get("default")); my $fsIdentifier = get("fsIdentifier"); my $grubEfi = get("grubEfi"); my $grubTargetEfi = get("grubTargetEfi"); +my $bootPath = get("bootPath"); my $canTouchEfiVariables = get("canTouchEfiVariables"); my $efiSysMountPoint = get("efiSysMountPoint"); $ENV{'PATH'} = get("path"); @@ -62,16 +63,16 @@ die "unsupported GRUB version\n" if $grubVersion != 1 && $grubVersion != 2; print STDERR "updating GRUB $grubVersion menu...\n"; -mkpath("/boot/grub", 0, 0700); +mkpath("$bootPath/grub", 0, 0700); -# Discover whether /boot is on the same filesystem as / and +# Discover whether the bootPath is on the same filesystem as / and # /nix/store. If not, then all kernels and initrds must be copied to -# /boot. -if (stat("/boot")->dev != stat("/nix/store")->dev) { +# the bootPath. +if (stat($bootPath)->dev != stat("/nix/store")->dev) { $copyKernels = 1; } -# Discover information about the location of /boot +# Discover information about the location of the bootPath struct(Fs => { device => '$', type => '$', @@ -206,7 +207,7 @@ sub GrubFs { } return Grub->new(path => $path, search => $search); } -my $grubBoot = GrubFs("/boot"); +my $grubBoot = GrubFs($bootPath); my $grubStore; if ($copyKernels == 0) { $grubStore = GrubFs("/nix/store"); @@ -221,7 +222,7 @@ if ($grubVersion == 1) { timeout $timeout "; if ($splashImage) { - copy $splashImage, "/boot/background.xpm.gz" or die "cannot copy $splashImage to /boot\n"; + copy $splashImage, "$bootPath/background.xpm.gz" or die "cannot copy $splashImage to $bootPath\n"; $conf .= "splashimage " . $grubBoot->path . "/background.xpm.gz\n"; } } @@ -264,7 +265,7 @@ else { if ($splashImage) { # FIXME: GRUB 1.97 doesn't resize the background image if it # doesn't match the video resolution. - copy $splashImage, "/boot/background.png" or die "cannot copy $splashImage to /boot\n"; + copy $splashImage, "$bootPath/background.png" or die "cannot copy $splashImage to $bootPath\n"; $conf .= " insmod png if background_image " . $grubBoot->path . "/background.png; then @@ -285,14 +286,14 @@ $conf .= "$extraConfig\n"; $conf .= "\n"; my %copied; -mkpath("/boot/kernels", 0, 0755) if $copyKernels; +mkpath("$bootPath/kernels", 0, 0755) if $copyKernels; sub copyToKernelsDir { my ($path) = @_; return $grubStore->path . substr($path, length("/nix/store")) unless $copyKernels; $path =~ /\/nix\/store\/(.*)/ or die; my $name = $1; $name =~ s/\//-/g; - my $dst = "/boot/kernels/$name"; + my $dst = "$bootPath/kernels/$name"; # Don't copy the file if $dst already exists. This means that we # have to create $dst atomically to prevent partially copied # kernels or initrd if this script is ever interrupted. @@ -396,14 +397,14 @@ if ($extraPrepareConfig ne "") { } # Atomically update the GRUB config. -my $confFile = $grubVersion == 1 ? "/boot/grub/menu.lst" : "/boot/grub/grub.cfg"; +my $confFile = $grubVersion == 1 ? "$bootPath/grub/menu.lst" : "$bootPath/grub/grub.cfg"; my $tmpFile = $confFile . ".tmp"; writeFile($tmpFile, $conf); rename $tmpFile, $confFile or die "cannot rename $tmpFile to $confFile\n"; -# Remove obsolete files from /boot/kernels. -foreach my $fn (glob "/boot/kernels/*") { +# Remove obsolete files from $bootPath/kernels. +foreach my $fn (glob "$bootPath/kernels/*") { next if defined $copied{$fn}; print STDERR "removing obsolete file $fn\n"; unlink $fn; @@ -422,7 +423,7 @@ struct(GrubState => { }); sub readGrubState { my $defaultGrubState = GrubState->new(version => "", efi => "", devices => "", efiMountPoint => "" ); - open FILE, "; chomp($version); @@ -491,10 +492,10 @@ if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { next if $dev eq "nodev"; print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n"; if ($grubTarget eq "") { - system("$grub/sbin/grub-install", "--recheck", Cwd::abs_path($dev)) == 0 + system("$grub/sbin/grub-install", "--recheck", "--boot-directory=$bootPath", Cwd::abs_path($dev)) == 0 or die "$0: installation of GRUB on $dev failed\n"; } else { - system("$grub/sbin/grub-install", "--recheck", "--target=$grubTarget", Cwd::abs_path($dev)) == 0 + system("$grub/sbin/grub-install", "--recheck", "--boot-directory=$bootPath", "--target=$grubTarget", Cwd::abs_path($dev)) == 0 or die "$0: installation of GRUB on $dev failed\n"; } } @@ -505,10 +506,10 @@ if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) { if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) { print STDERR "installing the GRUB $grubVersion EFI boot loader into $efiSysMountPoint...\n"; if ($canTouchEfiVariables eq "true") { - system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--efi-directory=$efiSysMountPoint") == 0 + system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint") == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; } else { - system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--efi-directory=$efiSysMountPoint", "--no-nvram") == 0 + system("$grubEfi/sbin/grub-install", "--recheck", "--target=$grubTargetEfi", "--boot-directory=$bootPath", "--efi-directory=$efiSysMountPoint", "--no-nvram") == 0 or die "$0: installation of GRUB EFI into $efiSysMountPoint failed\n"; } } @@ -516,7 +517,7 @@ if (($requireNewInstall != 0) && ($efiTarget eq "only" || $efiTarget eq "both")) # update GRUB state file if ($requireNewInstall != 0) { - open FILE, ">/boot/grub/state" or die "cannot create /boot/grub/state: $!\n"; + open FILE, ">$bootPath/grub/state" or die "cannot create $bootPath/grub/state: $!\n"; print FILE get("fullVersion"), "\n" or die; print FILE $efiTarget, "\n" or die; print FILE join( ":", @deviceTargets ), "\n" or die;