nixos/grub: Add the ability to mirror grub to multiple partitions

This commit is contained in:
William A. Kennington III 2015-05-25 14:57:20 -07:00
parent 255c0903a1
commit fd5b273e82
2 changed files with 120 additions and 37 deletions

View file

@ -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}";
}));
})
];

View file

@ -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, "</boot/grub/state" or return $defaultGrubState;
open FILE, "<$bootPath/grub/state" or return $defaultGrubState;
local $/ = "\n";
my $version = <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;