add diskco pkg

This commit is contained in:
2024-12-29 21:35:58 -05:00
parent dca840c3f1
commit da11e4ce56
142 changed files with 10235 additions and 59 deletions

841
pkgs/disko/lib/default.nix Normal file
View File

@@ -0,0 +1,841 @@
{ lib ? import <nixpkgs/lib>
, rootMountPoint ? "/mnt"
, makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>
, eval-config ? import <nixpkgs/nixos/lib/eval-config.nix>
}:
let
outputs = import ../default.nix { inherit lib diskoLib; };
diskoLib = {
testLib = import ./tests.nix { inherit lib makeTest eval-config; };
# like lib.types.oneOf but instead of a list takes an attrset
# uses the field "type" to find the correct type in the attrset
subType = { types, extraArgs ? { parent = { type = "rootNode"; name = "root"; }; } }: lib.mkOptionType {
name = "subType";
description = "one of ${lib.concatStringsSep "," (lib.attrNames types)}";
check = x: if x ? type then types.${x.type}.check x else throw "No type option set in:\n${lib.generators.toPretty {} x}";
merge = loc: lib.foldl'
(_res: def: types.${def.value.type}.merge loc [
# we add a dummy root parent node to render documentation
(lib.recursiveUpdate { value._module.args = extraArgs; } def)
])
{ };
nestedTypes = types;
};
# option for valid contents of partitions (basically like devices, but without tables)
_partitionTypes = { inherit (diskoLib.types) btrfs filesystem zfs mdraid luks lvm_pv swap; };
partitionType = extraArgs: lib.mkOption {
type = lib.types.nullOr (diskoLib.subType {
types = diskoLib._partitionTypes;
inherit extraArgs;
});
default = null;
description = "The type of partition";
};
# option for valid contents of devices
_deviceTypes = { inherit (diskoLib.types) table gpt btrfs filesystem zfs mdraid luks lvm_pv swap; };
deviceType = extraArgs: lib.mkOption {
type = lib.types.nullOr (diskoLib.subType {
types = diskoLib._deviceTypes;
inherit extraArgs;
});
default = null;
description = "The type of device";
};
/* deepMergeMap takes a function and a list of attrsets and deep merges them
deepMergeMap :: (AttrSet -> AttrSet ) -> [ AttrSet ] -> Attrset
Example:
deepMergeMap (x: x.t = "test") [ { x = { y = 1; z = 3; }; } { x = { bla = 234; }; } ]
=> { x = { y = 1; z = 3; bla = 234; t = "test"; }; }
*/
deepMergeMap = f: lib.foldr (attr: acc: (lib.recursiveUpdate acc (f attr))) { };
/* get a device and an index to get the matching device name
deviceNumbering :: str -> int -> str
Example:
deviceNumbering "/dev/sda" 3
=> "/dev/sda3"
deviceNumbering "/dev/disk/by-id/xxx" 2
=> "/dev/disk/by-id/xxx-part2"
*/
deviceNumbering = dev: index:
let inherit (lib) match; in
if match "/dev/([vs]|(xv)d).+" dev != null then
dev + toString index # /dev/{s,v,xv}da style
else if match "/dev/(disk|zvol)/.+" dev != null then
"${dev}-part${toString index}" # /dev/disk/by-id/xxx style, also used by zfs's zvolumes
else if match "/dev/((nvme|mmcblk).+|md/.*[[:digit:]])" dev != null then
"${dev}p${toString index}" # /dev/nvme0n1p1 style
else if match "/dev/md/.+" dev != null then
"${dev}${toString index}" # /dev/md/raid1 style
else if match "/dev/mapper/.+" dev != null then
"${dev}${toString index}" # /dev/mapper/vg-lv1 style
else if match "/dev/loop[[:digit:]]+" dev != null
then "${dev}p${toString index}" # /dev/mapper/vg-lv1 style
else
abort ''
${dev} seems not to be a supported disk format. Please add this to disko in https://github.com/nix-community/disko/blob/master/lib/default.nix
'';
/* Escape a string as required to be used in udev symlinks
The allowed characters are "0-9A-Za-z#+-.:=@_/", valid UTF-8 character sequences, and "\x00" hex encoding.
Everything else is escaped as "\xXX" where XX is the hex value of the character.
The source of truth for the list of allowed characters is the udev documentation:
https://www.freedesktop.org/software/systemd/man/latest/udev.html#SYMLINK1
This function is implemented as a best effort. It is not guaranteed to be 100% in line
with the udev implementation, and we hope that you're not crazy enough to try to break it.
hexEscapeUdevSymlink :: str -> str
Example:
hexEscapeUdevSymlink "Boot data partition"
=> "Boot\x20data\x20partition"
hexEscapeUdevSymlink "Even(crazier)par&titi^onName"
=> "Even\x28crazier\x29par\x26titi\x5EonName"
hexEscapeUdevSymlink "all0these@char#acters+_are-allow.ed"
=> "all0these@char#acters+_are-allow.ed"
*/
hexEscapeUdevSymlink =
let
allowedChars = "[0-9A-Za-z#+-.:=@_/]";
charToHex = c: lib.toHexString (lib.strings.charToInt c);
in
lib.stringAsChars
(c: if lib.match allowedChars c != null || c == "" then c else "\\x" + charToHex c);
/* get the index an item in a list
indexOf :: (a -> bool) -> [a] -> int -> int
Example:
indexOf (x: x == 2) [ 1 2 3 ] 0
=> 2
indexOf (x: x == "x") [ 1 2 3 ] 0
=> 0
*/
indexOf = f: list: fallback:
let
iter = index: list:
if list == [ ] then
fallback
else if f (lib.head list) then
index
else
iter (index + 1) (lib.tail list);
in
iter 1 list;
/* indent takes a multiline string and indents it by 2 spaces starting on the second line
indent :: str -> str
Example:
indent "test\nbla"
=> "test\n bla"
*/
indent = lib.replaceStrings [ "\n" ] [ "\n " ];
/* A nix option type representing a json datastructure, vendored from nixpkgs to avoid dependency on pkgs */
jsonType =
let
valueType = lib.types.nullOr
(lib.types.oneOf [
lib.types.bool
lib.types.int
lib.types.float
lib.types.str
lib.types.path
(lib.types.attrsOf valueType)
(lib.types.listOf valueType)
]) // {
description = "JSON value";
};
in
valueType;
/* Given a attrset of deviceDependencies and a devices attrset
returns a sorted list by deviceDependencies. aborts if a loop is found
sortDevicesByDependencies :: AttrSet -> AttrSet -> [ [ str str ] ]
*/
sortDevicesByDependencies = deviceDependencies: devices:
let
dependsOn = a: b:
lib.elem a (lib.attrByPath b [ ] deviceDependencies);
maybeSortedDevices = lib.toposort dependsOn (diskoLib.deviceList devices);
in
if (lib.hasAttr "cycle" maybeSortedDevices) then
abort "detected a cycle in your disk setup: ${maybeSortedDevices.cycle}"
else
maybeSortedDevices.result;
/* Takes a devices attrSet and returns it as a list
deviceList :: AttrSet -> [ [ str str ] ]
Example:
deviceList { zfs.pool1 = {}; zfs.pool2 = {}; mdadm.raid1 = {}; }
=> [ [ "zfs" "pool1" ] [ "zfs" "pool2" ] [ "mdadm" "raid1" ] ]
*/
deviceList = devices:
lib.concatLists (lib.mapAttrsToList (n: v: (map (x: [ n x ]) (lib.attrNames v))) devices);
/* Takes either a string or null and returns the string or an empty string
maybeStr :: Either (str null) -> str
Example:
maybeStr null
=> ""
maybeSTr "hello world"
=> "hello world"
*/
maybeStr = x: lib.optionalString (x != null) x;
/* Takes a Submodules config and options argument and returns a serializable
subset of config variables as a shell script snippet.
*/
defineHookVariables = { options }:
let
sanitizeName = lib.replaceStrings [ "-" ] [ "_" ];
isAttrsOfSubmodule = o: o.type.name == "attrsOf" && o.type.nestedTypes.elemType.name == "submodule";
isSerializable = n: o: !(
lib.hasPrefix "_" n
|| lib.hasSuffix "Hook" n
|| isAttrsOfSubmodule o
# TODO don't hardcode diskoLib.subType options.
|| n == "content" || n == "partitions" || n == "datasets" || n == "swap"
|| n == "mode"
);
in
lib.toShellVars
(lib.mapAttrs'
(n: o: lib.nameValuePair (sanitizeName n) o.value)
(lib.filterAttrs isSerializable options));
mkHook = description: lib.mkOption {
inherit description;
type = lib.types.lines;
default = "";
};
mkSubType = module: lib.types.submodule [
module
{
options = {
preCreateHook = diskoLib.mkHook "shell commands to run before create";
postCreateHook = diskoLib.mkHook "shell commands to run after create";
preMountHook = diskoLib.mkHook "shell commands to run before mount";
postMountHook = diskoLib.mkHook "shell commands to run after mount";
preUnmountHook = diskoLib.mkHook "shell commands to run before unmount";
postUnmountHook = diskoLib.mkHook "shell commands to run after unmount";
};
config._module.args = {
inherit diskoLib rootMountPoint;
};
}
];
mkCreateOption = { config, options, default }@attrs:
lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.str;
default = ''
( # ${config.type} ${lib.concatMapStringsSep " " (n: toString (config.${n} or "")) ["name" "device" "format" "mountpoint"]} #
${diskoLib.indent (diskoLib.defineHookVariables { inherit options; })}
${diskoLib.indent config.preCreateHook}
${diskoLib.indent attrs.default}
${diskoLib.indent config.postCreateHook}
)
'';
description = "Creation script";
};
mkMountOption = { config, options, default }@attrs:
lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default = lib.mapAttrsRecursive
(_name: value:
if builtins.isString value then ''
(
${diskoLib.indent (diskoLib.defineHookVariables { inherit options; })}
${diskoLib.indent config.preMountHook}
${diskoLib.indent value}
${diskoLib.indent config.postMountHook}
)
'' else value)
attrs.default;
description = "Mount script";
};
mkUnmountOption = { config, options, default }@attrs:
lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default = lib.mapAttrsRecursive
(_name: value:
if builtins.isString value then ''
(
${diskoLib.indent (diskoLib.defineHookVariables { inherit options; })}
${diskoLib.indent config.preUnmountHook}
${diskoLib.indent value}
${diskoLib.indent config.postUnmountHook}
)
'' else value)
attrs.default;
description = "Unmount script";
};
/* Writer for optionally checking bash scripts before writing them to the store
writeCheckedBash :: AttrSet -> str -> str -> derivation
*/
writeCheckedBash = { pkgs, checked ? false, noDeps ? false }: pkgs.writers.makeScriptWriter {
interpreter = if noDeps then "/usr/bin/env bash" else "${pkgs.bash}/bin/bash";
check = lib.optionalString (checked && !pkgs.stdenv.hostPlatform.isRiscV64 && !pkgs.stdenv.hostPlatform.isx86_32) (pkgs.writeScript "check" ''
set -efu
# SC2054: our toShellVars function doesn't quote list elements with commas
# SC2034: We don't use all variables exported by hooks.
${pkgs.shellcheck}/bin/shellcheck -e SC2034,SC2054 "$1"
'');
};
/* Takes a disko device specification, returns an attrset with metadata
meta :: lib.types.devices -> AttrSet
*/
meta = toplevel: toplevel._meta;
/* Takes a disko device specification and returns a string which formats the disks
create :: lib.types.devices -> str
*/
create = toplevel: toplevel._create;
/* Takes a disko device specification and returns a string which mounts the disks
mount :: lib.types.devices -> str
*/
mount = toplevel: toplevel._mount;
/* takes a disko device specification and returns a string which unmounts, destroys all disks and then runs create and mount
zapCreateMount :: lib.types.devices -> str
*/
zapCreateMount = toplevel:
''
set -efux
${toplevel._disko}
'';
/* Takes a disko device specification and returns a nixos configuration
config :: lib.types.devices -> nixosConfig
*/
config = toplevel: toplevel._config;
/* Takes a disko device specification and returns a function to get the needed packages to format/mount the disks
packages :: lib.types.devices -> pkgs -> [ derivation ]
*/
packages = toplevel: toplevel._packages;
/* Checks whether nixpkgs is recent enough for vmTools to support the customQemu argument.
Returns false, which is technically incorrect, for a few commits on 2024-07-08, but we can't be more accurate.
Make sure to pass lib, not pkgs.lib! See https://github.com/nix-community/disko/issues/904
vmToolsSupportsCustomQemu :: final_lib -> bool
*/
vmToolsSupportsCustomQemu = final_lib: lib.versionAtLeast final_lib.version "24.11.20240709";
optionTypes = rec {
filename = lib.mkOptionType {
name = "filename";
check = lib.isString;
merge = lib.mergeOneOption;
description = "A filename";
};
absolute-pathname = lib.mkOptionType {
name = "absolute pathname";
check = x: lib.isString x && lib.substring 0 1 x == "/" && pathname.check x;
merge = lib.mergeOneOption;
description = "An absolute path";
};
pathname = lib.mkOptionType {
name = "pathname";
check = x:
with lib; let
# The filter is used to normalize paths, i.e. to remove duplicated and
# trailing slashes. It also removes leading slashes, thus we have to
# check for "/" explicitly below.
xs = filter (s: stringLength s > 0) (splitString "/" x);
in
isString x && (x == "/" || (length xs > 0 && all filename.check xs));
merge = lib.mergeOneOption;
description = "A path name";
};
};
/* topLevel type of the disko config, takes attrsets of disks, mdadms, zpools, nodevs, and lvm vgs.
*/
toplevel = lib.types.submodule (cfg:
let
devices = { inherit (cfg.config) disk mdadm zpool lvm_vg nodev; };
in
{
options = {
disk = lib.mkOption {
type = lib.types.attrsOf diskoLib.types.disk;
default = { };
description = "Block device";
};
mdadm = lib.mkOption {
type = lib.types.attrsOf diskoLib.types.mdadm;
default = { };
description = "mdadm device";
};
zpool = lib.mkOption {
type = lib.types.attrsOf diskoLib.types.zpool;
default = { };
description = "ZFS pool device";
};
lvm_vg = lib.mkOption {
type = lib.types.attrsOf diskoLib.types.lvm_vg;
default = { };
description = "LVM VG device";
};
nodev = lib.mkOption {
type = lib.types.attrsOf diskoLib.types.nodev;
default = { };
description = "A non-block device";
};
_meta = lib.mkOption {
internal = true;
description = ''
meta informationen generated by disko
currently used for building a dependency list so we know in which order to create the devices
'';
default = diskoLib.deepMergeMap (dev: dev._meta) (lib.flatten (map lib.attrValues (lib.attrValues devices)));
};
_packages = lib.mkOption {
internal = true;
description = ''
packages required by the disko configuration
coreutils is always included
'';
default = pkgs: with lib; unique ((flatten (map (dev: dev._pkgs pkgs) (flatten (map attrValues (attrValues devices))))) ++ [ pkgs.coreutils-full ]);
};
_scripts = lib.mkOption {
internal = true;
description = ''
The scripts generated by disko
'';
default = { pkgs, checked ? false }:
let
throwIfNoDisksDetected = _: v: if devices.disk == { } then throw "No disks defined, did you forget to import your disko config?" else v;
destroyDependencies = with pkgs; [
util-linux
e2fsprogs
mdadm
zfs
lvm2
bash
jq
gnused
gawk
coreutils-full
];
in
lib.mapAttrs throwIfNoDisksDetected {
destroy = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-destroy" ''
export PATH=${lib.makeBinPath destroyDependencies}:$PATH
${cfg.config._destroy}
'';
format = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-format" ''
export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
${cfg.config._create}
'';
mount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-mount" ''
export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
${cfg.config._mount}
'';
unmount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-unmount" ''
export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
${cfg.config._unmount}
'';
formatMount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-format-mount" ''
export PATH=${lib.makeBinPath ((cfg.config._packages pkgs) ++ [ pkgs.bash ])}:$PATH
${cfg.config._formatMount}
'';
destroyFormatMount = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "/bin/disko-destroy-format-mount" ''
export PATH=${lib.makeBinPath ((cfg.config._packages pkgs) ++ [ pkgs.bash ] ++ destroyDependencies)}:$PATH
${cfg.config._destroyFormatMount}
'';
# These are useful to skip copying executables uploading a script to an in-memory installer
destroyNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-destroy" ''
${cfg.config._destroy}
'';
formatNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-format" ''
${cfg.config._create}
'';
mountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-mount" ''
${cfg.config._mount}
'';
unmountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-unmount" ''
${cfg.config._unmount}
'';
formatMountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-format-mount" ''
${cfg.config._formatMount}
'';
destroyFormatMountNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "/bin/disko-destroy-format-mount" ''
${cfg.config._destroyFormatMount}
'';
# Legacy scripts, to be removed in version 2.0.0
# They are generally less useful, because the scripts are directly written to their $out path instead of
# into the $out/bin directory, which makes them incompatible with `nix run`
# (see https://github.com/nix-community/disko/pull/78), `lib.buildEnv` and thus `environment.systemPackages`,
# `user.users.<name>.packages` and `home.packages`, see https://github.com/nix-community/disko/issues/454
destroyScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko-destroy" ''
export PATH=${lib.makeBinPath destroyDependencies}:$PATH
${cfg.config._legacyDestroy}
'';
formatScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko-format" ''
export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
${cfg.config._create}
'';
mountScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko-mount" ''
export PATH=${lib.makeBinPath (cfg.config._packages pkgs)}:$PATH
${cfg.config._mount}
'';
diskoScript = (diskoLib.writeCheckedBash { inherit pkgs checked; }) "disko" ''
export PATH=${lib.makeBinPath ((cfg.config._packages pkgs) ++ [ pkgs.bash ] ++ destroyDependencies)}:$PATH
${cfg.config._disko}
'';
# These are useful to skip copying executables uploading a script to an in-memory installer
destroyScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko-destroy" ''
${cfg.config._legacyDestroy}
'';
formatScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko-format" ''
${cfg.config._create}
'';
mountScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko-mount" ''
${cfg.config._mount}
'';
diskoScriptNoDeps = (diskoLib.writeCheckedBash { inherit pkgs checked; noDeps = true; }) "disko" ''
${cfg.config._disko}
'';
};
};
_legacyDestroy = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to unmount (& destroy) all devices defined by disko.devices
Does not ask for confirmation! Depracated in favor of _destroy
'';
default = ''
umount -Rv "${rootMountPoint}" || :
# shellcheck disable=SC2043
for dev in ${toString (lib.catAttrs "device" (lib.attrValues devices.disk))}; do
$BASH ${../disk-deactivate}/disk-deactivate "$dev"
done
'';
};
_destroy = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to unmount (& destroy) all devices defined by disko.devices
'';
default =
let
selectedDisks = lib.escapeShellArgs (lib.catAttrs "device" (lib.attrValues devices.disk));
in
''
if [ "$1" != "--yes-wipe-all-disks" ]; then
echo "WARNING: This will destroy all data on the disks defined in disko.devices, which are:"
echo
# shellcheck disable=SC2043,2041
for dev in ${selectedDisks}; do
echo " - $dev"
done
echo
echo " (If you want to skip this dialogue, pass --yes-wipe-all-disks)"
echo
echo "Are you sure you want to wipe the devices listed above?"
read -rp "Type 'yes' to continue, anything else to abort: " confirmation
if [ "$confirmation" != "yes" ]; then
echo "Aborted."
exit 1
fi
fi
umount -Rv "${rootMountPoint}" || :
# shellcheck disable=SC2043
for dev in ${selectedDisks}; do
$BASH ${../disk-deactivate}/disk-deactivate "$dev"
done
'';
};
_create = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to create all devices defined by disko.devices
'';
default =
with lib; let
sortedDeviceList = diskoLib.sortDevicesByDependencies (cfg.config._meta.deviceDependencies or { }) devices;
in
''
set -efux
disko_devices_dir=$(mktemp -d)
trap 'rm -rf "$disko_devices_dir"' EXIT
mkdir -p "$disko_devices_dir"
${concatMapStrings (dev: (attrByPath (dev ++ [ "_create" ]) {} devices)) sortedDeviceList}
'';
};
_mount = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to mount all devices defined by disko.devices
'';
default =
with lib; let
fsMounts = diskoLib.deepMergeMap (dev: dev._mount.fs or { }) (flatten (map attrValues (attrValues devices)));
sortedDeviceList = diskoLib.sortDevicesByDependencies (cfg.config._meta.deviceDependencies or { }) devices;
in
''
set -efux
# first create the necessary devices
${concatMapStrings (dev: (attrByPath (dev ++ [ "_mount" ]) {} devices).dev or "") sortedDeviceList}
# and then mount the filesystems in alphabetical order
${concatStrings (attrValues fsMounts)}
'';
};
_unmount = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to unmount all devices defined by disko.devices
'';
default =
with lib; let
fsMounts = diskoLib.deepMergeMap (dev: dev._unmount.fs or { }) (flatten (map attrValues (attrValues devices)));
sortedDeviceList = diskoLib.sortDevicesByDependencies (cfg.config._meta.deviceDependencies or { }) devices;
in
''
set -efux
# first unmount the filesystems in reverse alphabetical order
${concatStrings (lib.reverseList (attrValues fsMounts))}
# Than close the devices
${concatMapStrings (dev: (attrByPath (dev ++ [ "_unmount" ]) {} devices).dev or "") (lib.reverseList sortedDeviceList)}
'';
};
_disko = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to umount, create and mount all devices defined by disko.devices
Deprecated in favor of _destroyFormatMount
'';
default = ''
${cfg.config._legacyDestroy}
${cfg.config._create}
${cfg.config._mount}
'';
};
_destroyFormatMount = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to unmount, create and mount all devices defined by disko.devices
'';
default = ''
${cfg.config._destroy}
${cfg.config._create}
${cfg.config._mount}
'';
};
_formatMount = lib.mkOption {
internal = true;
type = lib.types.str;
description = ''
The script to create and mount all devices defined by disko.devices, without wiping the disks first
'';
default = ''
${cfg.config._create}
${cfg.config._mount}
'';
};
_config = lib.mkOption {
internal = true;
description = ''
The NixOS config generated by disko
'';
default =
with lib; let
configKeys = flatten (map attrNames (flatten (map (dev: dev._config) (flatten (map attrValues (attrValues devices))))));
collectedConfigs = flatten (map (dev: dev._config) (flatten (map attrValues (attrValues devices))));
in
genAttrs configKeys (key: mkMerge (catAttrs key collectedConfigs));
};
};
});
# import all the types from the types directory
types = lib.listToAttrs (
map
(file: lib.nameValuePair
(lib.removeSuffix ".nix" file)
(diskoLib.mkSubType (./types + "/${file}"))
)
(lib.attrNames (builtins.readDir ./types))
);
# render types into an json serializable format
serializeType = type:
let
options = lib.filter (x: !lib.hasPrefix "_" x) (lib.attrNames type.options);
in
lib.listToAttrs (
map
(option: lib.nameValuePair
option
type.options.${option}
)
options
);
typesSerializerLib = {
rootMountPoint = "";
options = null;
config = {
_module = {
args.name = "<self.name>";
args._parent.name = "<parent.name>";
args._parent.type = "<parent.type>";
};
name = "<config.name>";
};
parent = { };
device = "/dev/<device>";
# Spoof part of nixpkgs/lib to analyze the types
lib = lib // {
mkOption = option: {
inherit (option) type;
description = option.description or null;
default = option.defaultText or option.default or null;
};
types = {
attrsOf = subType: {
type = "attrsOf";
inherit subType;
};
listOf = subType: {
type = "listOf";
inherit subType;
};
nullOr = subType: {
type = "nullOr";
inherit subType;
};
oneOf = types: {
type = "oneOf";
inherit types;
};
either = t1: t2: {
type = "oneOf";
types = [ t1 t2 ];
};
enum = choices: {
type = "enum";
inherit choices;
};
anything = "anything";
nonEmptyStr = "str";
strMatching = _: "str";
str = "str";
bool = "bool";
int = "int";
submodule = x: x {
inherit (diskoLib.typesSerializerLib) lib config options;
name = "<self.name>";
};
};
};
diskoLib = {
optionTypes.absolute-pathname = "absolute-pathname";
# Spoof these types to avoid infinite recursion
deviceType = _: "<deviceType>";
partitionType = _: "<partitionType>";
subType = { types, ... }: {
type = "oneOf";
types = lib.attrNames types;
};
mkCreateOption = option: "_create";
};
};
jsonTypes = lib.listToAttrs
(
map
(file: lib.nameValuePair
(lib.removeSuffix ".nix" file)
(diskoLib.serializeType (import (./types + "/${file}") diskoLib.typesSerializerLib))
)
(lib.filter (name: lib.hasSuffix ".nix" name) (lib.attrNames (builtins.readDir ./types)))
) // {
partitionType = {
type = "oneOf";
types = lib.attrNames diskoLib._partitionTypes;
};
deviceType = {
type = "oneOf";
types = lib.attrNames diskoLib._deviceTypes;
};
};
} // outputs;
in
diskoLib

View File

@@ -0,0 +1,81 @@
{ diskoLib, modulesPath, config, pkgs, lib, ... }:
let
vm_disko = (diskoLib.testLib.prepareDiskoConfig config diskoLib.testLib.devices).disko;
cfg_ = (lib.evalModules {
modules = lib.singleton {
# _file = toString input;
imports = lib.singleton { disko.devices = vm_disko.devices; };
options = {
disko.devices = lib.mkOption {
type = diskoLib.toplevel;
};
disko.testMode = lib.mkOption {
type = lib.types.bool;
default = true;
};
};
};
}).config;
disks = lib.attrValues cfg_.disko.devices.disk;
rootDisk = {
name = "root";
file = ''"$tmp"/${lib.escapeShellArg (builtins.head disks).name}.qcow2'';
driveExtraOpts.cache = "writeback";
driveExtraOpts.werror = "report";
deviceExtraOpts.bootindex = "1";
deviceExtraOpts.serial = "root";
};
otherDisks = map
(disk: {
name = disk.name;
file = ''"$tmp"/${lib.escapeShellArg disk.name}.qcow2'';
driveExtraOpts.werror = "report";
})
(builtins.tail disks);
diskoBasedConfiguration = {
# generated from disko config
virtualisation.fileSystems = cfg_.disko.devices._config.fileSystems;
boot = cfg_.disko.devices._config.boot or { };
swapDevices = cfg_.disko.devices._config.swapDevices or [ ];
};
hostPkgs = config.virtualisation.host.pkgs;
in
{
imports = [
(modulesPath + "/virtualisation/qemu-vm.nix")
diskoBasedConfiguration
];
disko.testMode = true;
disko.imageBuilder.copyNixStore = false;
disko.imageBuilder.extraConfig = {
disko.devices = cfg_.disko.devices;
};
disko.imageBuilder.imageFormat = "qcow2";
virtualisation.useEFIBoot = config.disko.tests.efi;
virtualisation.memorySize = lib.mkDefault config.disko.memSize;
virtualisation.useDefaultFilesystems = false;
virtualisation.diskImage = null;
virtualisation.qemu.drives = [ rootDisk ] ++ otherDisks;
boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
boot.zfs.forceImportAll = true;
boot.zfs.forceImportRoot = lib.mkForce true;
system.build.vmWithDisko = hostPkgs.writers.writeDashBin "disko-vm" ''
set -efux
export tmp=$(${hostPkgs.coreutils}/bin/mktemp -d)
trap 'rm -rf "$tmp"' EXIT
${lib.concatMapStringsSep "\n" (disk: ''
${hostPkgs.qemu}/bin/qemu-img create -f qcow2 \
-b ${config.system.build.diskoImages}/${lib.escapeShellArg disk.name}.qcow2 \
-F qcow2 "$tmp"/${lib.escapeShellArg disk.name}.qcow2
'') disks}
set +f
${config.system.build.vm}/bin/run-*-vm
'';
}

View File

@@ -0,0 +1,228 @@
{ config
, diskoLib
, lib
, extendModules
, options
, ...
}:
let
diskoCfg = config.disko;
cfg = diskoCfg.imageBuilder;
inherit (cfg) pkgs imageFormat;
checked = diskoCfg.checkScripts;
configSupportsZfs = config.boot.supportedFilesystems.zfs or false;
vmTools = pkgs.vmTools.override
{
rootModules = [
"9p" "9pnet_virtio" # we can drop those in future if we stop supporting 24.11
"virtiofs"
"virtio_pci"
"virtio_blk"
"virtio_balloon"
"virtio_rng"
]
++ (lib.optional configSupportsZfs "zfs")
++ cfg.extraRootModules;
kernel = pkgs.aggregateModules
(with cfg.kernelPackages; [ kernel ]
++ lib.optional (lib.elem "zfs" cfg.extraRootModules || configSupportsZfs) zfs);
}
// lib.optionalAttrs (diskoLib.vmToolsSupportsCustomQemu lib)
{
customQemu = cfg.qemu;
};
cleanedConfig = diskoLib.testLib.prepareDiskoConfig config diskoLib.testLib.devices;
systemToInstall = extendModules {
modules = [
cfg.extraConfig
{
disko.testMode = true;
disko.devices = lib.mkForce cleanedConfig.disko.devices;
boot.loader.grub.devices = lib.mkForce cleanedConfig.boot.loader.grub.devices;
}
];
};
dependencies = with pkgs; [
bash
coreutils
gnused
parted # for partprobe
systemdMinimal
nix
util-linux
findutils
kmod
] ++ cfg.extraDependencies;
preVM = ''
# shellcheck disable=SC2154
mkdir -p "$out"
${lib.concatMapStringsSep "\n" (disk:
# shellcheck disable=SC2154
"${pkgs.qemu}/bin/qemu-img create -f ${imageFormat} \"$out/${disk.imageName}.${imageFormat}\" ${disk.imageSize}"
) (lib.attrValues diskoCfg.devices.disk)}
# This makes disko work, when canTouchEfiVariables is set to true.
# Technically these boot entries will no be persisted this way, but
# in most cases this is OK, because we can rely on the standard location for UEFI executables.
install -m600 ${pkgs.OVMF.variables} efivars.fd
'';
closureInfo = pkgs.closureInfo {
rootPaths = [ systemToInstall.config.system.build.toplevel ];
};
partitioner = ''
set -efux
# running udev, stolen from stage-1.sh
echo "running udev..."
ln -sfn /proc/self/fd /dev/fd
ln -sfn /proc/self/fd/0 /dev/stdin
ln -sfn /proc/self/fd/1 /dev/stdout
ln -sfn /proc/self/fd/2 /dev/stderr
mkdir -p /etc/udev
mount -t efivarfs none /sys/firmware/efi/efivars
ln -sfn ${systemToInstall.config.system.build.etc}/etc/udev/rules.d /etc/udev/rules.d
mkdir -p /dev/.mdadm
${pkgs.systemdMinimal}/lib/systemd/systemd-udevd --daemon
partprobe
udevadm trigger --action=add
udevadm settle
${lib.optionalString diskoCfg.testMode ''
export IN_DISKO_TEST=1
''}
${lib.getExe systemToInstall.config.system.build.destroyFormatMount} --yes-wipe-all-disks
'';
installer = lib.optionalString cfg.copyNixStore ''
# populate nix db, so nixos-install doesn't complain
export NIX_STATE_DIR=${systemToInstall.config.disko.rootMountPoint}/nix/var/nix
nix-store --load-db < "${closureInfo}/registration"
# We copy files with cp because `nix copy` seems to have a large memory leak
mkdir -p ${systemToInstall.config.disko.rootMountPoint}/nix/store
time xargs cp --recursive --target ${systemToInstall.config.disko.rootMountPoint}/nix/store < ${closureInfo}/store-paths
${systemToInstall.config.system.build.nixos-install}/bin/nixos-install --root ${systemToInstall.config.disko.rootMountPoint} --system ${systemToInstall.config.system.build.toplevel} --keep-going --no-channel-copy -v --no-root-password --option binary-caches ""
umount -Rv ${systemToInstall.config.disko.rootMountPoint}
'';
QEMU_OPTS = lib.concatStringsSep " " ([
"-drive if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}"
"-drive if=pflash,format=raw,unit=1,file=efivars.fd"
] ++ builtins.map
(disk:
"-drive file=\"$out\"/${disk.imageName}.${imageFormat},if=virtio,cache=unsafe,werror=report,format=${imageFormat}"
)
(lib.attrValues diskoCfg.devices.disk));
in
{
system.build.diskoImages = vmTools.runInLinuxVM (pkgs.runCommand cfg.name
{
buildInputs = dependencies;
inherit preVM QEMU_OPTS;
postVm = cfg.extraPostVM;
inherit (diskoCfg) memSize;
}
(partitioner + installer));
system.build.diskoImagesScript = diskoLib.writeCheckedBash { inherit checked pkgs; } cfg.name ''
set -efu
export PATH=${lib.makeBinPath dependencies}
showUsage() {
cat <<\USAGE
Usage: $script [options]
Options:
* --pre-format-files <src> <dst>
copies the src to the dst on the VM, before disko is run
This is useful to provide secrets like LUKS keys, or other files you need for formatting
* --post-format-files <src> <dst>
copies the src to the dst on the finished image
These end up in the images later and is useful if you want to add some extra stateful files
They will have the same permissions but will be owned by root:root
* --build-memory <amt>
specify the amount of memory in MiB that gets allocated to the build VM
This can be useful if you want to build images with a more involed NixOS config
The default is disko.memSize which defaults to ${builtins.toString options.disko.memSize.default} MiB
USAGE
}
export out=$PWD
TMPDIR=$(mktemp -d); export TMPDIR
trap 'rm -rf "$TMPDIR"' EXIT
cd "$TMPDIR"
mkdir copy_before_disko copy_after_disko
while [[ $# -gt 0 ]]; do
case "$1" in
--pre-format-files)
src=$2
dst=$3
cp --reflink=auto -r "$src" copy_before_disko/"$(echo "$dst" | base64)"
shift 2
;;
--post-format-files)
src=$2
dst=$3
cp --reflink=auto -r "$src" copy_after_disko/"$(echo "$dst" | base64)"
shift 2
;;
--build-memory)
regex="^[0-9]+$"
if ! [[ $2 =~ $regex ]]; then
echo "'$2' is not a number"
exit 1
fi
build_memory=$2
shift 1
;;
*)
showUsage
exit 1
;;
esac
shift
done
export preVM=${diskoLib.writeCheckedBash { inherit pkgs checked; } "preVM.sh" ''
set -efu
mv copy_before_disko copy_after_disko xchg/
origBuilder=${pkgs.writeScript "disko-builder" ''
set -eu
export PATH=${lib.makeBinPath dependencies}
for src in /tmp/xchg/copy_before_disko/*; do
[ -e "$src" ] || continue
dst=$(basename "$src" | base64 -d)
mkdir -p "$(dirname "$dst")"
cp -r "$src" "$dst"
done
set -f
${partitioner}
set +f
for src in /tmp/xchg/copy_after_disko/*; do
[ -e "$src" ] || continue
dst=/mnt/$(basename "$src" | base64 -d)
mkdir -p "$(dirname "$dst")"
cp -r "$src" "$dst"
done
${installer}
''}
echo "export origBuilder=$origBuilder" >> xchg/saved-env
${preVM}
''}
export postVM=${diskoLib.writeCheckedBash { inherit pkgs checked; } "postVM.sh" cfg.extraPostVM}
build_memory=''${build_memory:-${builtins.toString diskoCfg.memSize}}
# shellcheck disable=SC2016
QEMU_OPTS=${lib.escapeShellArg QEMU_OPTS}
# replace quoted $out with the actual path
QEUM_OPTS=''${QEMU_OPTS//\$out/$out}
QEMU_OPTS+=" -m $build_memory"
export QEMU_OPTS
${pkgs.bash}/bin/sh -e ${vmTools.vmRunCommand vmTools.qemuCommandLinux}
cd /
'';
}

337
pkgs/disko/lib/tests.nix Normal file
View File

@@ -0,0 +1,337 @@
{ lib
, makeTest
, eval-config
, ...
}:
let
testLib = {
# this takes a nixos config and changes the disk devices so we can run them inside the qemu test runner
# basically changes all the disk.*.devices to something like /dev/vda or /dev/vdb etc.
prepareDiskoConfig = cfg: devices:
let
cleanedTopLevel = lib.filterAttrsRecursive (n: _: !lib.hasPrefix "_" n) cfg;
preparedDisks = lib.foldlAttrs
(acc: n: v: {
devices = lib.tail acc.devices;
grub-devices = acc.grub-devices ++ (lib.optional (lib.any (part: (part.type or "") == "EF02") (lib.attrValues (v.content.partitions or { }))) (lib.head acc.devices));
disks = acc.disks // {
"${n}" = v // {
device = lib.head acc.devices;
content = v.content // { device = lib.head acc.devices; };
};
};
})
{
inherit devices;
grub-devices = [ ];
disks = { };
}
cleanedTopLevel.disko.devices.disk;
in
cleanedTopLevel // {
boot.loader.grub.devices = if (preparedDisks.grub-devices != [ ]) then preparedDisks.grub-devices else [ "nodev" ];
disko.devices = cleanedTopLevel.disko.devices // {
disk = preparedDisks.disks;
};
};
# list of devices generated inside qemu
devices = [
"/dev/vda"
"/dev/vdb"
"/dev/vdc"
"/dev/vdd"
"/dev/vde"
"/dev/vdf"
"/dev/vdg"
"/dev/vdh"
"/dev/vdi"
"/dev/vdj"
"/dev/vdk"
"/dev/vdl"
"/dev/vdm"
"/dev/vdn"
"/dev/vdo"
];
# This is the test generator for a disko test
makeDiskoTest =
{ name
, disko-config
, extendModules ? null
, pkgs ? import <nixpkgs> { }
, extraTestScript ? ""
, bootCommands ? ""
, extraInstallerConfig ? { }
, extraSystemConfig ? { }
, efi ? !pkgs.stdenv.hostPlatform.isRiscV64
, postDisko ? ""
, testMode ? "module" # can be one of direct module cli
, testBoot ? true # if we actually want to test booting or just create/mount
, enableOCR ? false
}:
let
makeTest' = args:
makeTest args {
inherit pkgs;
inherit (pkgs) system;
};
# for installation we skip /dev/vda because it is the test runner disk
importedDiskoConfig =
if builtins.isPath disko-config then
import disko-config
else
disko-config;
diskoConfigWithArgs =
if builtins.isFunction importedDiskoConfig then
importedDiskoConfig { inherit lib; }
else
importedDiskoConfig;
testConfigInstall = testLib.prepareDiskoConfig diskoConfigWithArgs (lib.tail testLib.devices);
# we need to shift the disks by one because the first disk is the /dev/vda of the test runner
# so /dev/vdb becomes /dev/vda etc.
testConfigBooted = testLib.prepareDiskoConfig diskoConfigWithArgs testLib.devices;
tsp-generator = pkgs.callPackage ../. { checked = true; };
tsp-format = (tsp-generator._cliFormat testConfigInstall) pkgs;
tsp-mount = (tsp-generator._cliMount testConfigInstall) pkgs;
tsp-unmount = (tsp-generator._cliUnmount testConfigInstall) pkgs;
tsp-disko = (tsp-generator._cliDestroyFormatMount testConfigInstall) pkgs;
tsp-config = tsp-generator.config testConfigBooted;
num-disks = builtins.length (lib.attrNames testConfigBooted.disko.devices.disk);
installed-system = { config, ... }: {
imports = [
(lib.optionalAttrs (testMode == "direct") tsp-config)
(lib.optionalAttrs (testMode == "module") {
disko.enableConfig = true;
imports = [
../module.nix
testConfigBooted
];
})
];
# config for tests to make them run faster or work at all
documentation.enable = false;
hardware.enableAllFirmware = lib.mkForce false;
# FIXME: we don't have an systemd in stage-1 equialvent for this
boot.initrd.preDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
echo -n 'secretsecret' > /tmp/secret.key
'';
boot.consoleLogLevel = lib.mkForce 100;
boot.loader.systemd-boot.enable = lib.mkDefault efi;
};
installed-system-eval = eval-config {
modules = [ installed-system ];
inherit (pkgs) system;
};
installedTopLevel = ((if extendModules != null then extendModules else installed-system-eval.extendModules) {
modules = [
({ config, ... }: {
imports = [
extraSystemConfig
({ modulesPath, ... }: {
imports = [
(modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests
(modulesPath + "/profiles/qemu-guest.nix")
];
disko.devices = lib.mkForce testConfigBooted.disko.devices;
})
];
# since we boot on a different machine, the efi payload needs to be portable
boot.loader.grub.efiInstallAsRemovable = efi;
boot.loader.grub.efiSupport = efi;
boot.loader.systemd-boot.graceful = true;
# we always want the bind-mounted nix store. otherwise tests take forever
fileSystems."/nix/store" = lib.mkForce {
device = "nix-store";
fsType = "9p";
neededForBoot = true;
options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
};
boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
# grub will install to these devices, we need to force those or we are offset by 1
# we use mkOveride 70, so that users can override this with mkForce in case they are testing grub mirrored boots
boot.loader.grub.devices = lib.mkOverride 70 testConfigInstall.boot.loader.grub.devices;
assertions = [
{
assertion = builtins.length config.boot.loader.grub.mirroredBoots > 1 -> config.boot.loader.grub.devices == [ ];
message = ''
When using `--vm-test` in combination with `mirroredBoots`,
it is necessary to configure `boot.loader.grub.devices` as an empty list by setting `boot.loader.grub.devices = lib.mkForce [];`.
This adjustment is crucial because the `--vm-test` mechanism automatically overrides the grub boot devices as part of the virtual machine test.
'';
}
];
})
];
}).config.system.build.toplevel;
in
makeTest' {
name = "disko-${name}";
meta.timeout = 600; # 10 minutes
inherit enableOCR;
nodes.machine = { pkgs, ... }: {
imports = [
(lib.optionalAttrs (testMode == "module") {
imports = [
../module.nix
];
disko = {
enableConfig = false;
checkScripts = true;
devices = testConfigInstall.disko.devices;
};
})
extraInstallerConfig
# from base.nix
({ config, ... }: {
boot.supportedFilesystems =
[ "btrfs" "cifs" "f2fs" "jfs" "ntfs" "reiserfs" "vfat" "xfs" ] ++
lib.optional (config.networking.hostId != null && lib.meta.availableOn pkgs.stdenv.hostPlatform config.boot.zfs.package) "zfs";
})
(if lib.versionAtLeast (lib.versions.majorMinor lib.version) "23.11" then {
boot.swraid.enable = true;
} else {
boot.initrd.services.swraid.enable = true;
})
];
systemd.services.mdmonitor.enable = false; # silence some weird warnings
environment.systemPackages = [
pkgs.jq
];
# speed-up eval
documentation.enable = false;
nix.settings = {
substituters = lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = 1;
};
networking.hostId = lib.mkIf
(
(testConfigInstall ? networking.hostId) && (testConfigInstall.networking.hostId != null)
)
testConfigInstall.networking.hostId;
virtualisation.emptyDiskImages = builtins.genList (_: 4096) num-disks;
# useful for debugging via repl
system.build.systemToInstall = installed-system-eval;
};
testScript = { nodes, ... }: ''
def disks(oldmachine, num_disks):
disk_flags = []
for i in range(num_disks):
disk_flags += [
'-drive',
f"file={oldmachine.state_dir}/empty{i}.qcow2,id=drive{i + 1},if=none,index={i + 1},werror=report",
'-device',
f"virtio-blk-pci,drive=drive{i + 1}"
]
return disk_flags
def create_test_machine(
oldmachine=None, **kwargs
): # taken from <nixpkgs/nixos/tests/installer.nix>
start_command = [
"${pkgs.qemu_test}/bin/qemu-kvm",
"-cpu",
"max",
"-m",
"1024",
"-virtfs",
"local,path=/nix/store,security_model=none,mount_tag=nix-store",
*disks(oldmachine, ${toString num-disks})
]
${lib.optionalString efi ''
start_command += ["-drive",
"if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}",
"-drive",
"if=pflash,format=raw,unit=1,readonly=on,file=${pkgs.OVMF.variables}"
]
''}
machine = create_machine(start_command=" ".join(start_command), **kwargs)
driver.machines.append(machine)
return machine
machine.start()
machine.succeed("echo -n 'additionalSecret' > /tmp/additionalSecret.key")
machine.succeed("echo -n 'secretsecret' > /tmp/secret.key")
${lib.optionalString (testMode == "direct") ''
# running direct mode
machine.succeed("${lib.getExe tsp-format}")
machine.succeed("${lib.getExe tsp-mount}")
machine.succeed("${lib.getExe tsp-mount}") # verify that mount is idempotent
machine.succeed("${lib.getExe tsp-unmount}")
machine.succeed("${lib.getExe tsp-unmount}") # verify that umount is idempotent
machine.succeed("${lib.getExe tsp-mount}") # verify that mount is idempotent
machine.succeed("${lib.getExe tsp-disko} --yes-wipe-all-disks") # verify that we can destroy and recreate
machine.succeed("mkdir -p /mnt/home")
machine.succeed("touch /mnt/home/testfile")
machine.succeed("${lib.getExe tsp-format}") # verify that format is idempotent
machine.succeed("test -e /mnt/home/testfile")
''}
${lib.optionalString (testMode == "module") ''
# running module mode
machine.succeed("${lib.getExe nodes.machine.system.build.format}")
machine.succeed("${lib.getExe nodes.machine.system.build.mount}")
machine.succeed("${lib.getExe nodes.machine.system.build.mount}") # verify that mount is idempotent
machine.succeed("${lib.getExe nodes.machine.system.build.destroyFormatMount} --yes-wipe-all-disks") # verify that we can destroy and recreate again
machine.succeed("mkdir -p /mnt/home")
machine.succeed("touch /mnt/home/testfile")
machine.succeed("${lib.getExe nodes.machine.system.build.format}") # verify that format is idempotent
machine.succeed("test -e /mnt/home/testfile")
''}
${postDisko}
${lib.optionalString testBoot ''
# mount nix-store in /mnt
machine.succeed("mkdir -p /mnt/nix/store")
machine.succeed("mount --bind /nix/store /mnt/nix/store")
machine.succeed("nix-store --load-db < ${pkgs.closureInfo {rootPaths = [installedTopLevel];}}/registration")
# fix "this is not a NixOS installation"
machine.succeed("mkdir -p /mnt/etc")
machine.succeed("touch /mnt/etc/NIXOS")
machine.succeed("mkdir -p /mnt/nix/var/nix/profiles")
machine.succeed("nix-env -p /mnt/nix/var/nix/profiles/system --set ${installedTopLevel}")
machine.succeed("NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root /mnt -- ${installedTopLevel}/bin/switch-to-configuration boot")
machine.succeed("sync")
machine.shutdown()
machine = create_test_machine(oldmachine=machine, name="booted_machine")
machine.start()
${bootCommands}
machine.wait_for_unit("local-fs.target")
''}
${extraTestScript}
'';
};
};
in
testLib

View File

@@ -0,0 +1,272 @@
{ config, options, diskoLib, lib, rootMountPoint, parent, device, ... }:
let
swapType = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
options = {
size = lib.mkOption {
type = lib.types.strMatching "^([0-9]+[KMGTP])?$";
description = "Size of the swap file (e.g. 2G)";
};
path = lib.mkOption {
type = lib.types.str;
default = name;
description = "Path to the swap file (relative to the mountpoint)";
};
priority = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Specify the priority of the swap file. Priority is a value between 0 and 32767.
Higher numbers indicate higher priority.
null lets the kernel choose a priority, which will show up as a negative value.
'';
};
options = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ "defaults" ];
example = [ "nofail" ];
description = "Options used to mount the swap.";
};
};
}));
default = { };
description = "Swap files";
};
swapConfig = { mountpoint, swap }:
{
swapDevices = builtins.map
(file: {
device = "${mountpoint}/${file.path}";
inherit (file) priority options;
})
(lib.attrValues swap);
};
swapCreate = mountpoint: swap:
lib.concatMapStringsSep
"\n"
(file: ''
if ! test -e "${mountpoint}/${file.path}"; then
btrfs filesystem mkswapfile --size ${file.size} "${mountpoint}/${file.path}"
fi
'')
(lib.attrValues swap);
in
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "btrfs" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
default = device;
description = "Device to use";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "A list of options to pass to mount.";
};
subvolumes = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = config._module.args.name;
description = "Name of the BTRFS subvolume.";
};
type = lib.mkOption {
type = lib.types.enum [ "btrfs_subvol" ];
default = "btrfs_subvol";
internal = true;
description = "Type";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "Options to pass to mount";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "Location to mount the subvolume to.";
};
swap = swapType;
};
}));
default = { };
description = "Subvolumes to define for BTRFS.";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "A path to mount the BTRFS filesystem to.";
};
swap = swapType;
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = _dev: { };
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
# create the filesystem only if the device seems empty
if ! (blkid "${config.device}" -o export | grep -q '^TYPE='); then
mkfs.btrfs "${config.device}" ${toString config.extraArgs}
fi
${lib.optionalString (config.swap != {} || config.subvolumes != {}) ''
if (blkid "${config.device}" -o export | grep -q '^TYPE=btrfs$'); then
${lib.optionalString (config.swap != {}) ''
(
MNTPOINT=$(mktemp -d)
mount ${device} "$MNTPOINT" -o subvol=/
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
${swapCreate "$MNTPOINT" config.swap}
)
''}
${lib.concatMapStrings (subvol: ''
(
MNTPOINT=$(mktemp -d)
mount "${config.device}" "$MNTPOINT" -o subvol=/
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
SUBVOL_ABS_PATH="$MNTPOINT/${subvol.name}"
mkdir -p "$(dirname "$SUBVOL_ABS_PATH")"
if ! btrfs subvolume show "$SUBVOL_ABS_PATH" > /dev/null 2>&1; then
btrfs subvolume create "$SUBVOL_ABS_PATH" ${toString subvol.extraArgs}
fi
${swapCreate "$SUBVOL_ABS_PATH" subvol.swap}
)
'') (lib.attrValues config.subvolumes)}
fi
''}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
let
subvolMounts = lib.concatMapAttrs
(_: subvol:
lib.warnIf (subvol.mountOptions != (options.subvolumes.type.getSubOptions [ ]).mountOptions.default && subvol.mountpoint == null)
"Subvolume ${subvol.name} has mountOptions but no mountpoint. See upgrade guide (2023-07-09 121df48)."
lib.optionalAttrs
(subvol.mountpoint != null)
{
${subvol.mountpoint} = ''
if ! findmnt "${config.device}" "${rootMountPoint}${subvol.mountpoint}" > /dev/null 2>&1; then
mount "${config.device}" "${rootMountPoint}${subvol.mountpoint}" \
${lib.concatMapStringsSep " " (opt: "-o ${opt}") (subvol.mountOptions ++ [ "subvol=${subvol.name}" ])} \
-o X-mount.mkdir
fi
'';
}
)
config.subvolumes;
in
{
fs = subvolMounts // lib.optionalAttrs (config.mountpoint != null) {
${config.mountpoint} = ''
if ! findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
mount "${config.device}" "${rootMountPoint}${config.mountpoint}" \
${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \
-o X-mount.mkdir
fi
'';
};
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default =
let
subvolMounts = lib.concatMapAttrs
(_: subvol:
lib.optionalAttrs
(subvol.mountpoint != null)
{
${subvol.mountpoint} = ''
if findmnt "${config.device}" "${rootMountPoint}${subvol.mountpoint}" > /dev/null 2>&1; then
umount "${rootMountPoint}${subvol.mountpoint}"
fi
'';
}
)
config.subvolumes;
in
{
fs = subvolMounts // lib.optionalAttrs (config.mountpoint != null) {
${config.mountpoint} = ''
if findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
umount "${rootMountPoint}${config.mountpoint}"
fi
'';
};
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [
(map
(subvol:
lib.optional (subvol.mountpoint != null) {
fileSystems.${subvol.mountpoint} = {
device = config.device;
fsType = "btrfs";
options = subvol.mountOptions ++ [ "subvol=${subvol.name}" ];
};
}
)
(lib.attrValues config.subvolumes))
(lib.optional (config.mountpoint != null) {
fileSystems.${config.mountpoint} = {
device = config.device;
fsType = "btrfs";
options = config.mountOptions;
};
})
(map
(subvol: swapConfig {
inherit (subvol) mountpoint swap;
})
(lib.attrValues config.subvolumes))
(swapConfig {
inherit (config) mountpoint swap;
})
];
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs:
[ pkgs.btrfs-progs pkgs.gnugrep ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,71 @@
{ config, options, lib, diskoLib, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = lib.replaceStrings [ "/" ] [ "_" ] config._module.args.name;
description = "Device name";
};
type = lib.mkOption {
type = lib.types.enum [ "disk" ];
default = "disk";
internal = true;
description = "Type";
};
device = lib.mkOption {
type = diskoLib.optionTypes.absolute-pathname; # TODO check if subpath of /dev ? - No! eg: /.swapfile
description = "Device path";
};
imageName = lib.mkOption {
type = lib.types.str;
default = config.name;
description = ''
name of the image when disko images are created
is used as an argument to "qemu-img create ..."
'';
};
imageSize = lib.mkOption {
type = lib.types.strMatching "[0-9]+[KMGTP]?";
description = ''
size of the image when disko images are created
is used as an argument to "qemu-img create ..."
'';
default = "2G";
};
content = diskoLib.deviceType { parent = config; device = config.device; };
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default =
lib.optionalAttrs (config.content != null) (config.content._meta [ "disk" config.device ]);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = config.content._create;
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = lib.optionalAttrs (config.content != null) config.content._mount;
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = lib.optionalAttrs (config.content != null) config.content._unmount;
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default =
lib.optional (config.content != null) config.content._config;
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.jq ] ++ lib.optionals (config.content != null) (config.content._pkgs pkgs);
description = "Packages";
};
};
}

View File

@@ -0,0 +1,109 @@
{ config, options, lib, diskoLib, rootMountPoint, parent, device, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "filesystem" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
default = device;
description = "Device to use";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "Options to pass to mount";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "Path to mount the filesystem to";
};
format = lib.mkOption {
type = lib.types.str;
description = "Format of the filesystem";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = _dev: { };
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! (blkid "${config.device}" | grep -q 'TYPE='); then
mkfs.${config.format} \
${lib.escapeShellArgs config.extraArgs} \
"${config.device}"
fi
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = lib.optionalAttrs (config.mountpoint != null) {
fs.${config.mountpoint} = ''
if ! findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
mount "${config.device}" "${rootMountPoint}${config.mountpoint}" \
-t "${config.format}" \
${lib.concatMapStringsSep " " (opt: "-o ${lib.escapeShellArg opt}") config.mountOptions} \
-o X-mount.mkdir
fi
'';
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = lib.optionalAttrs (config.mountpoint != null) {
fs.${config.mountpoint} = ''
if findmnt "${config.device}" "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
umount "${rootMountPoint}${config.mountpoint}"
fi
'';
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = lib.optional (config.mountpoint != null) {
fileSystems.${config.mountpoint} = {
device = config.device;
fsType = config.format;
options = config.mountOptions;
};
};
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
# type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs:
[ pkgs.util-linux pkgs.gnugrep ] ++ (
# TODO add many more
if (config.format == "xfs") then [ pkgs.xfsprogs ]
else if (config.format == "btrfs") then [ pkgs.btrfs-progs ]
else if (config.format == "vfat") then [ pkgs.dosfstools ]
else if (config.format == "ext2") then [ pkgs.e2fsprogs ]
else if (config.format == "ext3") then [ pkgs.e2fsprogs ]
else if (config.format == "ext4") then [ pkgs.e2fsprogs ]
else if (config.format == "bcachefs") then [ pkgs.bcachefs-tools ]
else if (config.format == "f2fs") then [ pkgs.f2fs-tools ]
else [ ]
);
description = "Packages";
};
};
}

View File

@@ -0,0 +1,297 @@
{ config, options, lib, diskoLib, parent, device, ... }:
let
sortedPartitions = lib.sort (x: y: x.priority < y.priority) (lib.attrValues config.partitions);
sortedHybridPartitions = lib.filter (p: p.hybrid != null) sortedPartitions;
in
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "gpt" ];
internal = true;
description = "Partition table";
};
device = lib.mkOption {
type = lib.types.str;
default = device;
description = "Device to use for the partition table";
};
partitions = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }@partition: {
options = {
type = lib.mkOption {
type =
let
hexPattern = len: "[A-Fa-f0-9]{${toString len}}";
in
lib.types.either
(lib.types.strMatching (hexPattern 4))
(lib.types.strMatching (lib.concatMapStringsSep "-" hexPattern [ 8 4 4 4 12 ]));
default = if partition.config.content != null && partition.config.content.type == "swap" then "8200" else "8300";
defaultText = ''8300 (Linux filesystem) normally, 8200 (Linux swap) if content.type is "swap"'';
description = ''
Filesystem type to use.
This can either be an sgdisk-specific short code (run sgdisk -L to see what is available),
or a fully specified GUID (see https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs).
'';
};
device = lib.mkOption {
type = lib.types.str;
default =
if config._parent.type == "mdadm" then
# workaround because mdadm partlabel do not appear in /dev/disk/by-partlabel
"/dev/disk/by-id/md-name-any:${config._parent.name}-part${toString partition.config._index}"
else
"/dev/disk/by-partlabel/${diskoLib.hexEscapeUdevSymlink partition.config.label}";
defaultText = ''
if the parent is an mdadm device:
/dev/disk/by-id/md-name-any:''${config._parent.name}-part''${toString partition.config._index}
otherwise:
/dev/disk/by-partlabel/''${diskoLib.hexEscapeUdevSymlink partition.config.label}
'';
description = "Device to use for the partition";
};
priority = lib.mkOption {
type = lib.types.int;
default =
if partition.config.size or "" == "100%" then
9001
else if partition.config.type == "EF02" then
# Boot partition should be created first, because some BIOS implementations require it.
# Priority defaults to 100 here to support any potential use-case for placing partitions prior to EF02
100
else
1000;
defaultText = ''
1000: normal partitions
9001: partitions with 100% size
100: boot partitions (EF02)
'';
description = "Priority of the partition, smaller values are created first";
};
name = lib.mkOption {
type = lib.types.str;
description = "Name of the partition";
default = name;
};
label = lib.mkOption {
type = lib.types.str;
default =
let
# 72 bytes is the maximum length of a GPT partition name
# the labels seem to be in UTF-16, so 2 bytes per character
limit = 36;
label = "${config._parent.type}-${config._parent.name}-${partition.config.name}";
in
if (lib.stringLength label) > limit then
builtins.substring 0 limit (builtins.hashString "sha256" label)
else
label;
defaultText = ''
''${config._parent.type}-''${config._parent.name}-''${partition.config.name}
or a truncated hash of the above if it is longer than 36 characters
'';
};
size = lib.mkOption {
type = lib.types.either (lib.types.enum [ "100%" ]) (lib.types.strMatching "[0-9]+[KMGTP]?");
default = "0";
description = ''
Size of the partition, in sgdisk format.
sets end automatically with the + prefix
can be 100% for the whole remaining disk, will be done last in that case.
'';
};
alignment = lib.mkOption {
type = lib.types.int;
default = if (builtins.substring (builtins.stringLength partition.config.start - 1) 1 partition.config.start == "s" || (builtins.substring (builtins.stringLength partition.config.end - 1) 1 partition.config.end == "s")) then 1 else 0;
defaultText = "1 if the unit of start or end is sectors, 0 otherwise";
description = "Alignment of the partition, if sectors are used as start or end it can be aligned to 1";
};
start = lib.mkOption {
type = lib.types.str;
default = "0";
description = "Start of the partition, in sgdisk format, use 0 for next available range";
};
end = lib.mkOption {
type = lib.types.str;
default = if partition.config.size == "100%" then "-0" else "+${partition.config.size}";
defaultText = ''
if partition.config.size == "100%" then "-0" else "+''${partition.config.size}";
'';
description = ''
End of the partition, in sgdisk format.
Use + for relative sizes from the partitions start
or - for relative sizes from the disks end
'';
};
content = diskoLib.partitionType { parent = config; device = partition.config.device; };
hybrid = lib.mkOption {
type = lib.types.nullOr (lib.types.submodule ({ ... } @ hp: {
options = {
mbrPartitionType = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "MBR type code";
};
mbrBootableFlag = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Set the bootable flag (aka the active flag) on any or all of your hybridized partitions";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
${lib.optionalString (hp.config.mbrPartitionType != null) ''
sfdisk --label-nested dos --part-type "${parent.device}" ${(toString partition.config._index)} ${hp.config.mbrPartitionType}
udevadm trigger --subsystem-match=block
udevadm settle
''}
${lib.optionalString hp.config.mbrBootableFlag ''
sfdisk --label-nested dos --activate "${parent.device}" ${(toString partition.config._index)}
''}
'';
};
};
}));
default = null;
description = "Entry to add to the Hybrid MBR table";
};
_index = lib.mkOption {
type = lib.types.int;
internal = true;
default = diskoLib.indexOf (x: x.name == partition.config.name) sortedPartitions 0;
defaultText = null;
};
};
}));
default = { };
description = "Attrs of partitions to add to the partition table";
};
efiGptPartitionFirst = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Place EFI GPT (0xEE) partition first in MBR (good for GRUB)";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev:
lib.foldr lib.recursiveUpdate { } (map
(partition:
lib.optionalAttrs (partition.content != null) (partition.content._meta dev)
)
(lib.attrValues config.partitions));
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! blkid "${config.device}" >&2; then
sgdisk --clear "${config.device}"
fi
${lib.concatStrings (map (partition: ''
# try to create the partition, if it fails, try to change the type and name
if ! sgdisk \
--align-end ${lib.optionalString (partition.alignment != 0) ''--set-alignment=${builtins.toString partition.alignment}''} \
--new=${toString partition._index}:${partition.start}:${partition.end} \
--change-name="${toString partition._index}:${partition.label}" \
--typecode=${toString partition._index}:${partition.type} \
"${config.device}"
then sgdisk \
--change-name="${toString partition._index}:${partition.label}" \
--typecode=${toString partition._index}:${partition.type} \
"${config.device}"
fi
# ensure /dev/disk/by-path/..-partN exists before continuing
partprobe "${config.device}" || : # sometimes partprobe fails, but the partitions are still up2date
udevadm trigger --subsystem-match=block
udevadm settle
'') sortedPartitions)}
${
lib.optionalString (sortedHybridPartitions != [])
("sgdisk -h "
+ (lib.concatStringsSep ":" (map (p: (toString p._index)) sortedHybridPartitions))
+ (
lib.optionalString (!config.efiGptPartitionFirst) ":EE "
)
+ ''"${parent.device}"'')
}
${lib.concatMapStrings (p:
p.hybrid._create
)
sortedHybridPartitions
}
${lib.concatStrings (map (partition: ''
${lib.optionalString (partition.content != null) partition.content._create}
'') sortedPartitions)}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
let
partMounts = lib.foldr lib.recursiveUpdate { } (map
(partition:
lib.optionalAttrs (partition.content != null) partition.content._mount
)
(lib.attrValues config.partitions));
in
{
dev = partMounts.dev or "";
fs = partMounts.fs or { };
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default =
let
partMounts = lib.foldr lib.recursiveUpdate { } (map
(partition:
lib.optionalAttrs (partition.content != null) partition.content._unmount
)
(lib.attrValues config.partitions));
in
{
dev = partMounts.dev or "";
fs = partMounts.fs or { };
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = (map
(partition:
lib.optional (partition.content != null) partition.content._config
)
(lib.attrValues config.partitions))
++ (lib.optional (lib.any (part: part.type == "EF02") (lib.attrValues config.partitions)) {
boot.loader.grub.devices = [ config.device ];
});
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs:
[
pkgs.gptfdisk
pkgs.systemdMinimal
pkgs.parted # for partprobe
] ++ lib.flatten (map
(partition:
lib.optional (partition.content != null) (partition.content._pkgs pkgs)
)
(lib.attrValues config.partitions));
description = "Packages";
};
};
}

View File

@@ -0,0 +1,210 @@
{ config, options, lib, diskoLib, parent, device, ... }:
let
keyFile =
if config.settings ? "keyFile"
then config.settings.keyFile
else if config.askPassword
then ''<(set +x; echo -n "$password"; set -x)''
else if config.passwordFile != null
# do not print the password to the console
then ''<(set +x; echo -n "$(cat ${config.passwordFile})"; set -x)''
else if config.keyFile != null
then
lib.warn
("The option `keyFile` is deprecated."
+ "Use passwordFile instead if you want to use interactive login or settings.keyFile if you want to use key file login")
config.keyFile
else null;
keyFileArgs = ''
${lib.optionalString (keyFile != null) "--key-file ${keyFile}"} \
${lib.optionalString (lib.hasAttr "keyFileSize" config.settings) "--keyfile-size ${builtins.toString config.settings.keyFileSize}"} \
${lib.optionalString (lib.hasAttr "keyFileOffset" config.settings) "--keyfile-offset ${builtins.toString config.settings.keyFileOffset}"} \
'';
cryptsetupOpen = ''
cryptsetup open "${config.device}" "${config.name}" \
${lib.optionalString (config.settings.allowDiscards or false) "--allow-discards"} \
${lib.optionalString (config.settings.bypassWorkqueues or false) "--perf-no_read_workqueue --perf-no_write_workqueue"} \
${toString config.extraOpenArgs} \
${keyFileArgs} \
'';
in
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "luks" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
description = "Device to encrypt";
default = device;
};
name = lib.mkOption {
type = lib.types.str;
description = "Name of the LUKS";
};
keyFile = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "DEPRECATED use passwordFile or settings.keyFile. Path to the key for encryption";
example = "/tmp/disk.key";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "Path to the file which contains the password for initial encryption";
example = "/tmp/disk.key";
};
askPassword = lib.mkOption {
type = lib.types.bool;
default = config.keyFile == null && config.passwordFile == null && (! config.settings ? "keyFile");
defaultText = "true if neither keyFile nor passwordFile are set";
description = "Whether to ask for a password for initial encryption";
};
settings = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = { };
description = "LUKS settings (as defined in configuration.nix in boot.initrd.luks.devices.<name>)";
example = ''{
keyFile = "/tmp/disk.key";
keyFileSize = 2048;
keyFileOffset = 1024;
fallbackToPassword = true;
allowDiscards = true;
};
'';
};
additionalKeyFiles = lib.mkOption {
type = lib.types.listOf diskoLib.optionTypes.absolute-pathname;
default = [ ];
description = "Path to additional key files for encryption";
example = [ "/tmp/disk2.key" ];
};
initrdUnlock = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to add a boot.initrd.luks.devices entry for the specified disk.";
};
extraFormatArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments to pass to `cryptsetup luksFormat` when formatting";
example = [ "--pbkdf argon2id" ];
};
extraOpenArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments to pass to `cryptsetup luksOpen` when opening";
example = [ "--timeout 10" ];
};
content = diskoLib.deviceType { parent = config; device = "/dev/mapper/${config.name}"; };
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev:
lib.optionalAttrs (config.content != null) (config.content._meta dev);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! blkid "${config.device}" >/dev/null || ! (blkid "${config.device}" -o export | grep -q '^TYPE='); then
${lib.optionalString config.askPassword ''
askPassword() {
if [ -z ''${IN_DISKO_TEST+x} ]; then
set +x
echo "Enter password for ${config.device}: "
IFS= read -r -s password
echo "Enter password for ${config.device} again to be safe: "
IFS= read -r -s password_check
export password
[ "$password" = "$password_check" ]
set -x
else
export password=disko
fi
}
until askPassword; do
echo "Passwords did not match, please try again."
done
''}
cryptsetup -q luksFormat "${config.device}" ${toString config.extraFormatArgs} ${keyFileArgs}
${cryptsetupOpen} --persistent
${toString (lib.forEach config.additionalKeyFiles (keyFile: ''
cryptsetup luksAddKey "${config.device}" ${keyFile} ${keyFileArgs}
''))}
fi
${lib.optionalString (config.content != null) config.content._create}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
let
contentMount = config.content._mount;
in
{
dev = ''
if ! cryptsetup status "${config.name}" >/dev/null 2>/dev/null; then
${lib.optionalString config.askPassword ''
if [ -z ''${IN_DISKO_TEST+x} ]; then
set +x
echo "Enter password for ${config.device}"
IFS= read -r -s password
export password
set -x
else
export password=disko
fi
''}
${cryptsetupOpen}
fi
${lib.optionalString (config.content != null) contentMount.dev or ""}
'';
fs = lib.optionalAttrs (config.content != null) contentMount.fs or { };
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default =
let
contentUnmount = config.content._unmount;
in
{
dev = ''
${lib.optionalString (config.content != null) contentUnmount.dev or ""}
if cryptsetup status "${config.name}" >/dev/null 2>/dev/null; then
cryptsetup close "${config.name}"
fi
'';
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [ ]
# If initrdUnlock is true, then add a device entry to the initrd.luks.devices config.
++ (lib.optional config.initrdUnlock [
{
boot.initrd.luks.devices.${config.name} = {
inherit (config) device;
} // config.settings;
}
]) ++ (lib.optional (config.content != null) config.content._config);
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.gnugrep pkgs.cryptsetup ] ++ (lib.optionals (config.content != null) (config.content._pkgs pkgs));
description = "Packages";
};
};
}

View File

@@ -0,0 +1,62 @@
{ config, options, lib, diskoLib, parent, device, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "lvm_pv" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
description = "Device";
default = device;
};
vg = lib.mkOption {
type = lib.types.str;
description = "Volume group";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev: {
deviceDependencies.lvm_vg.${config.vg} = [ dev ];
};
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! (blkid "${config.device}" | grep -q 'TYPE='); then
pvcreate "${config.device}"
fi
echo "${config.device}" >>"$disko_devices_dir"/lvm_${lib.escapeShellArg config.vg}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = { };
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = { };
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [ ];
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.gnugrep pkgs.lvm2 ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,183 @@
{ config, options, lib, diskoLib, ... }:
let
# Load kernel modules to ensure device mapper types are available
kernelModules =
[
# Prevent unbootable systems if LVM snapshots are present at boot time.
"dm-snapshot"
] ++
lib.filter (x: x != "") (map
(lv: lib.optionalString (lv.lvm_type != null && lv.lvm_type != "thinlv") "dm-${lv.lvm_type}")
(lib.attrValues config.lvs));
in
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = config._module.args.name;
description = "Name of the volume group";
};
type = lib.mkOption {
type = lib.types.enum [ "lvm_vg" ];
internal = true;
description = "Type";
};
lvs = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }@lv: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = "Name of the logical volume";
};
priority = lib.mkOption {
type = lib.types.int;
default = (if lv.config.lvm_type == "thin-pool" then 501 else 1000) + (if lib.hasInfix "100%" lv.config.size then 251 else 0);
defaultText = lib.literalExpression ''
if (lib.hasInfix "100%" lv.config.size) then 9001 else 1000
'';
description = "Priority of the logical volume, smaller values are created first";
};
size = lib.mkOption {
type = lib.types.str; # TODO lvm size type
description = "Size of the logical volume";
};
lvm_type = lib.mkOption {
# TODO: add raid10
type = lib.types.nullOr (lib.types.enum [ "mirror" "raid0" "raid1" "raid4" "raid5" "raid6" "thin-pool" "thinlv" ]); # TODO add all lib.types
default = null; # maybe there is always a default type?
description = "LVM type";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments";
};
pool = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Name of pool LV that this LV belongs to";
};
content = diskoLib.partitionType { parent = config; device = "/dev/${config.name}/${lv.config.name}"; };
};
}));
default = { };
description = "LVS for the volume group";
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default =
diskoLib.deepMergeMap
(lv:
lib.optionalAttrs (lv.content != null) (lv.content._meta [ "lvm_vg" config.name ])
)
(lib.attrValues config.lvs);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default =
let
sortedLvs = lib.sort (a: b: a.priority < b.priority) (lib.attrValues config.lvs);
in
''
${lib.concatMapStringsSep "\n" (k: ''modprobe "${k}"'') kernelModules}
readarray -t lvm_devices < <(cat "$disko_devices_dir"/lvm_${lib.escapeShellArg config.name})
if ! vgdisplay "${config.name}" >/dev/null; then
vgcreate "${config.name}" \
"''${lvm_devices[@]}"
fi
${lib.concatMapStrings (lv: ''
if ! lvdisplay "${config.name}/${lv.name}"; then
lvcreate \
--yes \
${if (lv.lvm_type == "thinlv") then "-V"
else if lib.hasInfix "%" lv.size then "-l" else "-L"} \
${if lib.hasSuffix "%" lv.size then "${lv.size}FREE" else lv.size} \
-n "${lv.name}" \
${lib.optionalString (lv.lvm_type == "thinlv") "--thinpool=${lv.pool}"} \
${lib.optionalString (lv.lvm_type != null && lv.lvm_type != "thinlv") "--type=${lv.lvm_type}"} \
${toString lv.extraArgs} \
"${config.name}"
fi
'') sortedLvs}
${lib.concatMapStrings (lv: ''
${lib.optionalString (lv.content != null) lv.content._create}
'') sortedLvs}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
let
lvMounts = diskoLib.deepMergeMap
(lv:
lib.optionalAttrs (lv.content != null) lv.content._mount
)
(lib.attrValues config.lvs);
in
{
dev = ''
vgchange -a y
${lib.concatMapStrings (x: x.dev or "") (lib.attrValues lvMounts)}
'';
fs = lvMounts.fs or { };
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default =
let
lvMounts = diskoLib.deepMergeMap
(lv:
lib.optionalAttrs (lv.content != null) lv.content._unmount
)
(lib.attrValues config.lvs);
in {
dev = ''
${lib.concatMapStrings (x: x.dev or "") (lib.attrValues lvMounts)}
vgchange -a n
'';
fs = lvMounts.fs or { };
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [{ boot.initrd.kernelModules = kernelModules; }] ++
map
(lv: [
(lib.optional (lv.content != null) lv.content._config)
(lib.optional (lv.lvm_type != null) {
boot.initrd.kernelModules = [
(if lv.lvm_type == "mirror" then "dm-mirror" else "dm-raid")
]
++ lib.optional (lv.lvm_type == "raid0") "raid0"
++ lib.optional (lv.lvm_type == "raid1") "raid1"
# ++ lib.optional (lv.lvm_type == "raid10") "raid10"
++ lib.optional
(lv.lvm_type == "raid4" ||
lv.lvm_type == "raid5" ||
lv.lvm_type == "raid6") "raid456";
})
])
(lib.attrValues config.lvs);
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: lib.flatten (map
(lv:
lib.optional (lv.content != null) (lv.content._pkgs pkgs)
)
(lib.attrValues config.lvs));
description = "Packages";
};
};
}

View File

@@ -0,0 +1,99 @@
{ config, options, lib, diskoLib, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = config._module.args.name;
description = "Name";
};
type = lib.mkOption {
type = lib.types.enum [ "mdadm" ];
default = "mdadm";
internal = true;
description = "Type";
};
level = lib.mkOption {
type = lib.types.int;
default = 1;
description = "mdadm level";
};
metadata = lib.mkOption {
type = lib.types.enum [ "1" "1.0" "1.1" "1.2" "default" "ddf" "imsm" ];
default = "default";
description = "Metadata";
};
content = diskoLib.deviceType { parent = config; device = "/dev/md/${config.name}"; };
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default =
lib.optionalAttrs (config.content != null) (config.content._meta [ "mdadm" config.name ]);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! test -e "/dev/md/${config.name}"; then
readarray -t disk_devices < <(cat "$disko_devices_dir"/raid_${lib.escapeShellArg config.name})
echo 'y' | mdadm --create "/dev/md/${config.name}" \
--level=${toString config.level} \
--raid-devices="$(wc -l "$disko_devices_dir"/raid_${lib.escapeShellArg config.name} | cut -f 1 -d " ")" \
--metadata=${config.metadata} \
--force \
--homehost=any \
"''${disk_devices[@]}"
partprobe "/dev/md/${config.name}"
udevadm trigger --subsystem-match=block
udevadm settle
# for some reason mdadm devices spawn with an existing partition table, so we need to wipe it
sgdisk --zap-all "/dev/md/${config.name}"
fi
${lib.optionalString (config.content != null) config.content._create}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
lib.optionalAttrs (config.content != null) config.content._mount;
# TODO we probably need to assemble the mdadm somehow
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = let
content = lib.optionalAttrs (config.content != null) config.content._unmount;
in {
fs = content.fs;
dev = ''
${content.dev or ""}
if [ -e "/dev/md/${config.name}" ]; then
mdadm --stop "/dev/md/${config.name}"
fi
'';
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default =
[
(if lib.versionAtLeast (lib.versions.majorMinor lib.version) "23.11" then {
boot.swraid.enable = true;
} else {
boot.initrd.services.swraid.enable = true;
})
] ++
lib.optional (config.content != null) config.content._config;
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [
pkgs.parted # for partprobe
] ++ (lib.optionals (config.content != null) (config.content._pkgs pkgs));
description = "Packages";
};
};
}

View File

@@ -0,0 +1,60 @@
{ config, options, lib, diskoLib, parent, device, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "mdraid" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
description = "Device";
default = device;
};
name = lib.mkOption {
type = lib.types.str;
description = "Name";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev: {
deviceDependencies.mdadm.${config.name} = [ dev ];
};
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
echo "${config.device}" >>"$disko_devices_dir"/raid_${lib.escapeShellArg config.name}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = { };
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = { };
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [ ];
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.mdadm ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,82 @@
{ lib, config, options, diskoLib, rootMountPoint, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "nodev" ];
default = "nodev";
internal = true;
description = "Device type";
};
fsType = lib.mkOption {
type = lib.types.str;
description = "File system type";
};
device = lib.mkOption {
type = lib.types.str;
default = "none";
description = "Device to use";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = config._module.args.name;
description = "Location to mount the file system at";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "Options to pass to mount";
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default = { };
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = "";
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = lib.optionalAttrs (config.mountpoint != null) {
fs.${config.mountpoint} = ''
if ! findmnt ${config.fsType} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
mount -t ${config.fsType} "${config.device}" "${rootMountPoint}${config.mountpoint}" \
${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \
-o X-mount.mkdir
fi
'';
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = lib.optionalAttrs (config.mountpoint != null) {
fs.${config.mountpoint} = ''
if findmnt ${config.fsType} "${rootMountPoint}${config.mountpoint}" > /dev/null 2>&1; then
umount "${rootMountPoint}${config.mountpoint}"
fi
'';
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = lib.optional (config.mountpoint != null) {
fileSystems.${config.mountpoint} = {
device = config.device;
fsType = config.fsType;
options = config.mountOptions;
};
};
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = _pkgs: [ ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,134 @@
{ diskoLib, config, options, lib, parent, device, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "swap" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
default = device;
description = "Device";
};
discardPolicy = lib.mkOption {
default = null;
example = "once";
type = lib.types.nullOr (lib.types.enum [ "once" "pages" "both" ]);
description = ''
Specify the discard policy for the swap device. If "once", then the
whole swap space is discarded at swapon invocation. If "pages",
asynchronous discard on freed pages is performed, before returning to
the available pages pool. With "both", both policies are activated.
See swapon(8) for more information.
'';
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ "defaults" ];
example = [ "nofail" ];
description = "Options used to mount the swap.";
};
priority = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Specify the priority of the swap device. Priority is a value between 0 and 32767.
Higher numbers indicate higher priority.
null lets the kernel choose a priority, which will show up as a negative value.
'';
};
randomEncryption = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to randomly encrypt the swap";
};
resumeDevice = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to use this as a boot.resumeDevice";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = _dev: { };
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
# TODO: we don't support encrypted swap yet
default = lib.optionalString (!config.randomEncryption) ''
if ! blkid "${config.device}" -o export | grep -q '^TYPE='; then
mkswap \
${toString config.extraArgs} \
"${config.device}"
fi
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
# TODO: we don't support encrypted swap yet
default = lib.optionalAttrs (!config.randomEncryption) {
fs.${config.device} = ''
if test "''${DISKO_SKIP_SWAP:-}" != 1 && ! swapon --show | grep -q "^$(readlink -f "${config.device}") "; then
swapon ${
lib.optionalString (config.discardPolicy != null)
"--discard${lib.optionalString (config.discardPolicy != "both")
"=${config.discardPolicy}"
}"} ${
lib.optionalString (config.priority != null)
"--priority=${toString config.priority}"
} \
--options=${lib.concatStringsSep "," config.mountOptions} \
"${config.device}"
fi
'';
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = lib.optionalAttrs (!config.randomEncryption) {
fs.${config.device} = ''
if swapon --show | grep -q "^$(readlink -f "${config.device}") "; then
swapoff "${config.device}"
fi
'';
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [{
swapDevices = [{
device = config.device;
inherit (config) discardPolicy priority;
randomEncryption = {
enable = config.randomEncryption;
# forward discard/TRIM attempts through dm-crypt
allowDiscards = config.discardPolicy != null;
};
options = config.mountOptions;
}];
boot.resumeDevice = lib.mkIf config.resumeDevice config.device;
}];
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.gnugrep pkgs.util-linux ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,179 @@
{ config, options, lib, diskoLib, parent, device, ... }:
{
options = lib.warn ''
The legacy table is outdated and should not be used. We recommend using the gpt type instead.
Please note that certain features, such as the test framework, may not function properly with the legacy table type.
If you encounter errors similar to:
"error: The option `disko.devices.disk.disk1.content.partitions."[definition 1-entry 1]".content._config` is read-only, but it's set multiple times,"
this is likely due to the use of the legacy table type.
for a migration you can follow the guide at https://github.com/nix-community/disko/blob/master/docs/table-to-gpt.md
''
{
type = lib.mkOption {
type = lib.types.enum [ "table" ];
internal = true;
description = "Partition table";
};
device = lib.mkOption {
type = lib.types.str;
default = device;
description = "Device to partition";
};
format = lib.mkOption {
type = lib.types.enum [ "gpt" "msdos" ];
default = "gpt";
description = "The kind of partition table";
};
partitions = lib.mkOption {
type = lib.types.listOf (lib.types.submodule ({ name, ... }@partition: {
options = {
part-type = lib.mkOption {
type = lib.types.enum [ "primary" "logical" "extended" ];
default = "primary";
description = "Partition type";
};
fs-type = lib.mkOption {
type = lib.types.nullOr (lib.types.enum [ "btrfs" "ext2" "ext3" "ext4" "fat16" "fat32" "hfs" "hfs+" "linux-swap" "ntfs" "reiserfs" "udf" "xfs" ]);
default = null;
description = "Filesystem type to use";
};
name = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Name of the partition";
};
start = lib.mkOption {
type = lib.types.str;
default = "0%";
description = "Start of the partition";
};
end = lib.mkOption {
type = lib.types.str;
default = "100%";
description = "End of the partition";
};
flags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Partition flags";
};
bootable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to make the partition bootable";
};
content = diskoLib.partitionType { parent = config; device = diskoLib.deviceNumbering config.device partition.config._index; };
_index = lib.mkOption {
type = lib.types.int;
internal = true;
default = lib.toInt (lib.head (builtins.match ".*entry ([[:digit:]]+)]" name));
defaultText = null;
};
};
}));
default = [ ];
description = "List of partitions to add to the partition table";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev:
lib.foldr lib.recursiveUpdate { } (lib.imap
(_index: partition:
lib.optionalAttrs (partition.content != null) (partition.content._meta dev)
)
config.partitions);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! blkid "${config.device}" >/dev/null; then
parted -s "${config.device}" -- mklabel ${config.format}
${lib.concatStrings (map (partition: ''
${lib.optionalString (config.format == "gpt") ''
parted -s "${config.device}" -- mkpart "${diskoLib.hexEscapeUdevSymlink partition.name}" ${diskoLib.maybeStr partition.fs-type} ${partition.start} ${partition.end}
''}
${lib.optionalString (config.format == "msdos") ''
parted -s "${config.device}" -- mkpart ${partition.part-type} ${diskoLib.maybeStr partition.fs-type} ${partition.start} ${partition.end}
''}
# ensure /dev/disk/by-path/..-partN exists before continuing
partprobe "${config.device}"
udevadm trigger --subsystem-match=block
udevadm settle
${lib.optionalString partition.bootable ''
parted -s "${config.device}" -- set ${toString partition._index} boot on
''}
${lib.concatMapStringsSep "" (flag: ''
parted -s "${config.device}" -- set ${toString partition._index} ${flag} on
'') partition.flags}
# ensure further operations can detect new partitions
partprobe "${config.device}"
udevadm trigger --subsystem-match=block
udevadm settle
'') config.partitions)}
fi
${lib.concatStrings (map (partition: ''
${lib.optionalString (partition.content != null) partition.content._create}
'') config.partitions)}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
let
partMounts = lib.foldr lib.recursiveUpdate { } (map
(partition:
lib.optionalAttrs (partition.content != null) partition.content._mount
)
config.partitions);
in
{
dev = partMounts.dev or "";
fs = partMounts.fs or { };
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default =
let
partMounts = lib.foldr lib.recursiveUpdate { } (map
(partition:
lib.optionalAttrs (partition.content != null) partition.content._unmount
)
config.partitions);
in
{
dev = partMounts.dev or "";
fs = partMounts.fs or { };
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default =
map
(partition:
lib.optional (partition.content != null) partition.content._config
)
config.partitions;
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs:
[ pkgs.parted pkgs.systemdMinimal ] ++ lib.flatten (map
(partition:
lib.optional (partition.content != null) (partition.content._pkgs pkgs)
)
config.partitions);
description = "Packages";
};
};
}

View File

@@ -0,0 +1,59 @@
{ config, options, lib, diskoLib, parent, device, ... }:
{
options = {
type = lib.mkOption {
type = lib.types.enum [ "zfs" ];
internal = true;
description = "Type";
};
device = lib.mkOption {
type = lib.types.str;
default = device;
description = "Device";
};
pool = lib.mkOption {
type = lib.types.str;
description = "Name of the ZFS pool";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev: {
deviceDependencies.zpool.${config.pool} = [ dev ];
};
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
echo "${config.device}" >>"$disko_devices_dir"/zfs_${lib.escapeShellArg config.pool}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = { };
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = { };
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = [ ];
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.zfs ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,160 @@
{ config, options, lib, diskoLib, rootMountPoint, parent, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = config._module.args.name;
description = "Name of the dataset";
};
_name = lib.mkOption {
type = lib.types.str;
default = "${config._parent.name}/${config.name}";
internal = true;
description = "Fully quantified name for dataset";
};
type = lib.mkOption {
type = lib.types.enum [ "zfs_fs" ];
default = "zfs_fs";
internal = true;
description = "Type";
};
options = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Options to set for the dataset";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "Mount options";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "Path to mount the dataset to";
};
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = _dev: { };
description = "Metadata";
};
_createFilesystem = lib.mkOption {
internal = true;
type = lib.types.bool;
default = true;
};
_create = diskoLib.mkCreateOption
{
inherit config options;
# -u prevents mounting newly created datasets, which is
# important to prevent accidental shadowing of mount points
# since (create order != mount order)
# -p creates parents automatically
default =
let
createOptions = (lib.optionalAttrs (config.mountpoint != null) { mountpoint = config.mountpoint; }) // config.options;
# All options defined as PROP_ONETIME or PROP_ONETIME_DEFAULT in https://github.com/openzfs/zfs/blob/master/module/zcommon/zfs_prop.c
onetimeProperties = [
"encryption"
"casesensitivity"
"utf8only"
"normalization"
"volblocksize"
"pbkdf2iters"
"pbkdf2salt"
"keyformat"
];
updateOptions = builtins.removeAttrs createOptions onetimeProperties;
in
''
if ! zfs get type ${config._name} >/dev/null 2>&1; then
${if config._createFilesystem then ''
zfs create -up ${config._name} \
${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") (createOptions))}
'' else ''
# don't create anything for root dataset of zpools
true
''}
${lib.optionalString (updateOptions != {}) ''
else
zfs set -u ${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${v}") updateOptions)} ${config._name}
''}
fi
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
(lib.optionalAttrs (config.options.keylocation or "none" != "none") {
dev = ''
if [ "$(zfs get keystatus ${config._name} -H -o value)" == "unavailable" ]; then
zfs load-key ${config._name}
fi
'';
}) // lib.optionalAttrs (config.options.mountpoint or "" != "none" && config.options.canmount or "" != "off") {
fs.${config.mountpoint} = ''
if ! findmnt ${config._name} "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
mount ${config._name} "${rootMountPoint}${config.mountpoint}" \
-o X-mount.mkdir \
${lib.concatMapStringsSep " " (opt: "-o ${opt}") config.mountOptions} \
${lib.optionalString ((config.options.mountpoint or "") != "legacy") "-o zfsutil"} \
-t zfs
fi
'';
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default =
(lib.optionalAttrs (config.options.keylocation or "none" != "none") {
dev = "zfs unload-key ${config.name}";
}) // lib.optionalAttrs (config.options.mountpoint or "" != "none" && config.options.canmount or "" != "off") {
fs.${config.mountpoint} = ''
if findmnt ${config._name} "${rootMountPoint}${config.mountpoint}" >/dev/null 2>&1; then
umount "${rootMountPoint}${config.mountpoint}"
fi
'';
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default =
lib.optional (config.options.mountpoint or "" != "none" && config.options.canmount or "" != "off") {
fileSystems.${config.mountpoint} = {
device = "${config._name}";
fsType = "zfs";
options = config.mountOptions ++ lib.optional ((config.options.mountpoint or "") != "legacy") "zfsutil";
};
};
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.util-linux ];
description = "Packages";
};
};
}

View File

@@ -0,0 +1,113 @@
{ config, options, lib, diskoLib, parent, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = config._module.args.name;
description = "Name of the dataset";
};
type = lib.mkOption {
type = lib.types.enum [ "zfs_volume" ];
default = "zfs_volume";
internal = true;
description = "Type";
};
options = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Options to set for the dataset";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments passed to `zfs create`";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "Mount options";
};
# volume options
size = lib.mkOption {
type = lib.types.nullOr lib.types.str; # TODO size
default = null;
description = "Size of the dataset";
};
content = diskoLib.partitionType { parent = config; device = "/dev/zvol/${config._parent.name}/${config.name}"; };
_parent = lib.mkOption {
internal = true;
default = parent;
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo diskoLib.jsonType;
default = dev:
lib.optionalAttrs (config.content != null) (config.content._meta dev);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default = ''
if ! zfs get type "${config._parent.name}/${config.name}" >/dev/null 2>&1; then
zfs create "${config._parent.name}/${config.name}" \
${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \
-V ${config.size} ${toString (builtins.map lib.escapeShellArg config.extraArgs)}
zvol_wait
partprobe "/dev/zvol/${config._parent.name}/${config.name}"
udevadm trigger --subsystem-match=block
udevadm settle
fi
${lib.optionalString (config.content != null) config.content._create}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default = {
dev = ''
${lib.optionalString (config.options.keylocation or "none" != "none") ''
if [ "$(zfs get keystatus ${config.name} -H -o value)" == "unavailable" ]; then
zfs load-key ${config.name}
fi
''}
${config.content._mount.dev or ""}
'';
fs = config.content._mount.fs or { };
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = {
dev = ''
${lib.optionalString (config.options.keylocation or "none" != "none") "zfs unload-key ${config.name}"}
${config.content._unmount.dev or ""}
'';
fs = config.content._unmount.fs;
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default =
lib.optional (config.content != null) config.content._config;
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [
pkgs.util-linux
pkgs.parted # for partprobe
] ++ lib.optionals (config.content != null) (config.content._pkgs pkgs);
description = "Packages";
};
};
}

View File

@@ -0,0 +1,390 @@
{ config, options, lib, diskoLib, rootMountPoint, ... }:
let
# TODO: Consider expanding to handle `file` and `draid` mode options.
modeOptions = [
""
"mirror"
"raidz"
"raidz1"
"raidz2"
"raidz3"
];
in
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = config._module.args.name;
description = "Name of the ZFS pool";
};
type = lib.mkOption {
type = lib.types.enum [ "zpool" ];
default = "zpool";
internal = true;
description = "Type";
};
mode = lib.mkOption {
default = "";
example = {
mode = {
topology = {
type = "topology";
vdev = [
{
# Members can be either specified by a full path or by a disk name
# This is example uses the full path
members = [ "/dev/disk/by-id/wwn-0x5000c500af8b2a14" ];
}
];
log = [
{
# Example using gpt partition labels
# This expects an disk called `ssd` with a gpt partition called `zfs`
# disko.devices.disk.ssd = {
# type = "disk";
# device = "/dev/nvme0n1";
# content = {
# type = "gpt";
# partitions = {
# zfs = {
# size = "100%";
# content = {
# type = "zfs";
# # use your own pool name here
# pool = "zroot";
# };
# };
# };
# };
# };
members = [ "ssd" ];
}
];
};
};
};
type = (lib.types.oneOf [
(lib.types.enum modeOptions)
(lib.types.attrsOf (diskoLib.subType {
types = {
topology =
let
vdev = lib.types.submodule ({ ... }: {
options = {
mode = lib.mkOption {
type = lib.types.enum modeOptions;
default = "";
description = "Mode of the zfs vdev";
};
members = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Members of the vdev";
};
};
});
in
lib.types.submodule
({ ... }: {
options = {
type = lib.mkOption {
type = lib.types.enum [ "topology" ];
default = "topology";
internal = true;
description = "Type";
};
# zfs device types
vdev = lib.mkOption {
type = lib.types.listOf vdev;
default = [ ];
description = ''
A list of storage vdevs. See
https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Virtual_Devices_(vdevs)
for details.
'';
example = [
{
mode = "mirror";
members = [ "x" "y" ];
}
{
members = [ "z" ];
}
];
};
spare = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
A list of devices to use as hot spares. See
https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Hot_Spares
for details.
'';
example = [ "x" "y" ];
};
log = lib.mkOption {
type = lib.types.listOf vdev;
default = [ ];
description = ''
A list of vdevs used for the zfs intent log (ZIL). See
https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Intent_Log
for details.
'';
example = [
{
mode = "mirror";
members = [ "x" "y" ];
}
{
members = [ "z" ];
}
];
};
dedup = lib.mkOption {
type = lib.types.listOf vdev;
default = [ ];
description = ''
A list of vdevs used for the deduplication table. See
https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#dedup
for details.
'';
example = [
{
mode = "mirror";
members = [ "x" "y" ];
}
{
members = [ "z" ];
}
];
};
special = lib.mkOption {
type = lib.types.either (lib.types.listOf vdev) (lib.types.nullOr vdev);
default = [ ];
description = ''
A list of vdevs used as special devices. See
https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#special
for details.
'';
example = [
{
mode = "mirror";
members = [ "x" "y" ];
}
{
members = [ "z" ];
}
];
};
cache = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
A dedicated zfs cache device (L2ARC). See
https://openzfs.github.io/openzfs-docs/man/master/7/zpoolconcepts.7.html#Cache_Devices
for details.
'';
example = [ "x" "y" ];
};
};
});
};
extraArgs.parent = config;
}))
]);
description = "Mode of the ZFS pool";
};
options = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Options for the ZFS pool";
};
rootFsOptions = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Options for the root filesystem";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr diskoLib.optionTypes.absolute-pathname;
default = null;
description = "The mountpoint of the pool";
};
mountOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "defaults" ];
description = "Options to pass to mount";
};
datasets = lib.mkOption {
type = lib.types.attrsOf (diskoLib.subType {
types = { inherit (diskoLib.types) zfs_fs zfs_volume; };
extraArgs.parent = config;
});
description = "List of datasets to define";
};
_meta = lib.mkOption {
internal = true;
readOnly = true;
type = diskoLib.jsonType;
default =
diskoLib.deepMergeMap (dataset: dataset._meta [ "zpool" config.name ]) (lib.attrValues config.datasets);
description = "Metadata";
};
_create = diskoLib.mkCreateOption {
inherit config options;
default =
let
formatOutput = type: mode: members: ''
entries+=("${type} ${mode}=${
lib.concatMapStringsSep " "
(d: if lib.strings.hasPrefix "/" d then d else "/dev/disk/by-partlabel/disk-${d}-zfs") members
}")
'';
formatVdev = type: vdev: formatOutput type vdev.mode vdev.members;
formatVdevList = type: vdevs: lib.concatMapStrings
(formatVdev type)
(builtins.sort (a: _: a.mode == "") vdevs);
hasTopology = !(builtins.isString config.mode);
mode = if hasTopology then "prescribed" else config.mode;
topology = lib.optionalAttrs hasTopology config.mode.topology;
in
''
readarray -t zfs_devices < <(cat "$disko_devices_dir/zfs_${config.name}")
if [ ''${#zfs_devices[@]} -eq 0 ]; then
echo "no devices found for zpool ${config.name}. Did you misspell the pool name?" >&2
exit 1
fi
# Try importing the pool without mounting anything if it exists.
# This allows us to set mounpoints.
if zpool import -N -f "${config.name}" || zpool list "${config.name}"; then
echo "not creating zpool ${config.name} as a pool with that name already exists" >&2
else
continue=1
for dev in "''${zfs_devices[@]}"; do
if ! blkid "$dev" >/dev/null; then
# blkid fails, so device seems empty
:
elif (blkid "$dev" -o export | grep '^PTUUID='); then
echo "device $dev already has a partuuid, skipping creating zpool ${config.name}" >&2
continue=0
elif (blkid "$dev" -o export | grep '^TYPE=zfs_member'); then
# zfs_member is a zfs partition, so we try to add the device to the pool
:
elif (blkid "$dev" -o export | grep '^TYPE='); then
echo "device $dev already has a partition, skipping creating zpool ${config.name}" >&2
continue=0
fi
done
if [ $continue -eq 1 ]; then
topology=""
# For shell check
mode="${mode}"
if [ "$mode" != "prescribed" ]; then
topology="${mode} ''${zfs_devices[*]}"
else
entries=()
${lib.optionalString (hasTopology && topology.vdev != null)
(formatVdevList "" topology.vdev)}
${lib.optionalString (hasTopology && topology.spare != [])
(formatOutput "spare" "" topology.spare)}
${lib.optionalString (hasTopology && topology.log != [])
(formatVdevList "log" topology.log)}
${lib.optionalString (hasTopology && topology.dedup != [])
(formatVdevList "dedup" topology.dedup)}
${lib.optionalString (hasTopology && topology.special != null && topology.special != [])
(formatVdevList "special" (lib.lists.toList topology.special))}
${lib.optionalString (hasTopology && topology.cache != [])
(formatOutput "cache" "" topology.cache)}
all_devices=()
last_type=
for line in "''${entries[@]}"; do
# lineformat is type mode=device1 device2 device3
mode="''${line%%=*}"
type="''${mode%% *}"
mode="''${mode#"$type "}"
devs="''${line#*=}"
IFS=' ' read -r -a devices <<< "$devs"
all_devices+=("''${devices[@]}")
if ! [ "$type" = "$last_type" ]; then
topology+=" $type"
last_type="$type"
fi
topology+=" ''${mode} ''${devices[*]}"
done
# all_devices sorted should equal zfs_devices sorted
all_devices_list=$(echo "''${all_devices[*]}" | tr ' ' '\n' | sort)
zfs_devices_list=$(echo "''${zfs_devices[*]}" | tr ' ' '\n' | sort)
if [[ "$all_devices_list" != "$zfs_devices_list" ]]; then
echo "not all disks accounted for, skipping creating zpool ${config.name}" >&2
diff <(echo "$all_devices_list" ) <(echo "$zfs_devices_list") >&2
continue=0
fi
fi
fi
if [ $continue -eq 1 ]; then
zpool create -f "${config.name}" \
-R ${rootMountPoint} \
${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-o ${n}=${v}") config.options)} \
${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "-O ${n}=${v}") config.rootFsOptions)} \
''${topology:+ $topology}
if [[ $(zfs get -H mounted "${config.name}" | cut -f3) == "yes" ]]; then
zfs unmount "${config.name}"
fi
fi
fi
${lib.concatMapStrings (dataset: dataset._create) (lib.attrValues config.datasets)}
'';
};
_mount = diskoLib.mkMountOption {
inherit config options;
default =
let
datasetFilesystemsMounts = diskoLib.deepMergeMap (dataset: dataset._mount.fs or {}) (lib.attrValues config.datasets);
in
{
dev = ''
zpool list "${config.name}" >/dev/null 2>/dev/null ||
zpool import -l -R ${rootMountPoint} "${config.name}"
${lib.concatMapStrings (x: x._mount.dev or "") (lib.attrValues config.datasets)}
'';
fs = datasetFilesystemsMounts;
};
};
_unmount = diskoLib.mkUnmountOption {
inherit config options;
default = {
dev = ''
${lib.concatMapStrings (dataset: dataset._unmount.dev or "") (lib.attrValues config.datasets)}
if zpool list "${config.name}" >/dev/null 2>/dev/null; then
zpool export "${config.name}"
fi
'';
fs = diskoLib.deepMergeMap (dataset: dataset._unmount.fs or {}) (lib.attrValues config.datasets);
};
};
_config = lib.mkOption {
internal = true;
readOnly = true;
default = map (dataset: dataset._config) (lib.attrValues config.datasets);
description = "NixOS configuration";
};
_pkgs = lib.mkOption {
internal = true;
readOnly = true;
type = lib.types.functionTo (lib.types.listOf lib.types.package);
default = pkgs: [ pkgs.gnugrep pkgs.util-linux ] ++ lib.flatten (map (dataset: dataset._pkgs pkgs) (lib.attrValues config.datasets));
description = "Packages";
};
};
config = {
datasets."__root" = {
_name = config.name;
_createFilesystem = false;
type = "zfs_fs";
mountpoint = config.mountpoint;
options = config.rootFsOptions;
mountOptions = config.mountOptions;
};
};
}