2023-01-28 18:19:13 +03:00
|
|
|
{ lib, rootMountPoint }:
|
|
|
|
with lib;
|
|
|
|
with builtins;
|
|
|
|
|
2023-05-16 14:40:03 +03:00
|
|
|
let
|
2023-01-28 18:19:13 +03:00
|
|
|
|
|
|
|
diskoLib = {
|
|
|
|
# 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 = typeAttr: lib.mkOptionType rec {
|
|
|
|
name = "subType";
|
|
|
|
description = "one of ${concatStringsSep "," (attrNames typeAttr)}";
|
|
|
|
check = x: if x ? type then typeAttr.${x.type}.check x else throw "No type option set in:\n${generators.toPretty {} x}";
|
2023-02-06 17:24:34 +03:00
|
|
|
merge = loc: foldl' (res: def: typeAttr.${def.value.type}.merge loc [ def ]) { };
|
2023-01-28 18:19:13 +03:00
|
|
|
nestedTypes = typeAttr;
|
|
|
|
};
|
|
|
|
|
|
|
|
# option for valid contents of partitions (basically like devices, but without tables)
|
|
|
|
partitionType = lib.mkOption {
|
2023-05-16 14:40:03 +03:00
|
|
|
type = lib.types.nullOr (diskoLib.subType { inherit (diskoLib.types) btrfs filesystem zfs mdraid luks lvm_pv swap; });
|
2023-01-28 18:19:13 +03:00
|
|
|
default = null;
|
|
|
|
description = "The type of partition";
|
|
|
|
};
|
|
|
|
|
|
|
|
# option for valid contents of devices
|
|
|
|
deviceType = lib.mkOption {
|
2023-05-16 14:40:03 +03:00
|
|
|
type = lib.types.nullOr (diskoLib.subType { inherit (diskoLib.types) table btrfs filesystem zfs mdraid luks lvm_pv swap; });
|
2023-01-28 18:19:13 +03:00
|
|
|
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"; }; }
|
|
|
|
*/
|
2023-02-06 17:24:34 +03:00
|
|
|
deepMergeMap = f: foldr (attr: acc: (recursiveUpdate acc (f attr))) { };
|
2023-01-28 18:19:13 +03:00
|
|
|
|
|
|
|
/* 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:
|
2023-06-01 23:42:14 +03:00
|
|
|
if match "/dev/([vs]|(xv)d).+" dev != null then
|
|
|
|
dev + toString index # /dev/{s,v,xv}da style
|
2023-02-02 16:11:49 +03:00
|
|
|
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
|
2023-02-09 02:37:37 +03:00
|
|
|
else if match "/dev/((nvme|mmcblk).+|md/.*[[:digit:]])" dev != null then
|
2023-01-28 18:19:13 +03:00
|
|
|
"${dev}p${toString index}" # /dev/nvme0n1p1 style
|
2023-02-09 02:37:37 +03:00
|
|
|
else if match "/dev/md/.+" dev != null then
|
|
|
|
"${dev}${toString index}" # /dev/md/raid1 style
|
2023-05-07 12:18:29 +03:00
|
|
|
else if match "/dev/mapper/.+" dev != null then
|
|
|
|
"${dev}${toString index}" # /dev/mapper/vg-lv1 style
|
2023-01-28 18:19:13 +03:00
|
|
|
else
|
2023-02-02 16:11:49 +03:00
|
|
|
abort ''
|
|
|
|
${dev} seems not to be a supported disk format. Please add this to disko in https://github.com/nix-community/disko/blob/master/types/default.nix
|
|
|
|
'';
|
2023-01-28 18:19:13 +03:00
|
|
|
|
|
|
|
/* 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:
|
|
|
|
elem a (attrByPath b [ ] deviceDependencies);
|
|
|
|
maybeSortedDevices = toposort dependsOn (diskoLib.deviceList devices);
|
|
|
|
in
|
|
|
|
if (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:
|
|
|
|
concatLists (mapAttrsToList (n: v: (map (x: [ n x ]) (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"
|
|
|
|
*/
|
2023-02-06 17:24:34 +03:00
|
|
|
maybeStr = x: optionalString (x != null) x;
|
2023-01-28 18:19:13 +03:00
|
|
|
|
|
|
|
/* Takes a Submodules config and options argument and returns a serializable
|
|
|
|
subset of config variables as a shell script snippet.
|
|
|
|
*/
|
|
|
|
defineHookVariables = { config, 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.
|
2023-04-07 19:29:48 +03:00
|
|
|
|| n == "content" || n == "partitions" || n == "datasets"
|
2023-01-28 18:19:13 +03:00
|
|
|
);
|
|
|
|
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.str;
|
|
|
|
default = "";
|
|
|
|
};
|
|
|
|
|
2023-02-06 17:24:34 +03:00
|
|
|
mkSubType = module: lib.types.submodule [
|
2023-01-28 18:19:13 +03:00
|
|
|
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";
|
|
|
|
};
|
|
|
|
config._module.args = {
|
2023-05-16 14:40:03 +03:00
|
|
|
inherit diskoLib rootMountPoint;
|
2023-01-28 18:19:13 +03:00
|
|
|
};
|
|
|
|
}
|
2023-02-06 17:24:34 +03:00
|
|
|
];
|
2023-01-28 18:19:13 +03:00
|
|
|
|
|
|
|
mkCreateOption = { config, options, default }@attrs:
|
|
|
|
lib.mkOption {
|
|
|
|
internal = true;
|
|
|
|
readOnly = true;
|
|
|
|
type = lib.types.functionTo lib.types.str;
|
|
|
|
default = args:
|
|
|
|
let
|
|
|
|
name = "format";
|
|
|
|
test = lib.optionalString (config ? name) "${config.${name}}";
|
|
|
|
in
|
|
|
|
''
|
|
|
|
( # ${config.type} ${concatMapStringsSep " " (n: toString (config.${n} or "")) ["name" "device" "format" "mountpoint"]}
|
|
|
|
${diskoLib.defineHookVariables { inherit config options; }}
|
|
|
|
${config.preCreateHook}
|
|
|
|
${attrs.default args}
|
|
|
|
${config.postCreateHook}
|
|
|
|
)
|
|
|
|
'';
|
|
|
|
description = "Creation script";
|
|
|
|
};
|
|
|
|
|
|
|
|
mkMountOption = { config, options, default }@attrs:
|
|
|
|
lib.mkOption {
|
|
|
|
internal = true;
|
|
|
|
readOnly = true;
|
|
|
|
type = lib.types.functionTo diskoLib.jsonType;
|
2023-02-07 18:37:12 +03:00
|
|
|
default = attrs.default;
|
2023-01-28 18:19:13 +03:00
|
|
|
description = "Mount script";
|
|
|
|
};
|
|
|
|
|
2023-01-30 23:36:51 +03:00
|
|
|
/* 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";
|
2023-02-02 20:35:00 +03:00
|
|
|
check = lib.optionalString checked "${pkgs.shellcheck}/bin/shellcheck -e SC2034";
|
2023-01-30 23:36:51 +03:00
|
|
|
};
|
|
|
|
|
2023-01-28 18:19:13 +03:00
|
|
|
|
|
|
|
/* Takes a disko device specification, returns an attrset with metadata
|
|
|
|
|
|
|
|
meta :: lib.types.devices -> AttrSet
|
|
|
|
*/
|
|
|
|
meta = devices: diskoLib.deepMergeMap (dev: dev._meta) (flatten (map attrValues (attrValues devices)));
|
|
|
|
|
|
|
|
/* Takes a disko device specification and returns a string which formats the disks
|
|
|
|
|
|
|
|
create :: lib.types.devices -> str
|
|
|
|
*/
|
|
|
|
create = devices:
|
|
|
|
let
|
|
|
|
sortedDeviceList = diskoLib.sortDevicesByDependencies ((diskoLib.meta devices).deviceDependencies or { }) devices;
|
|
|
|
in
|
|
|
|
''
|
|
|
|
set -efux
|
|
|
|
|
|
|
|
disko_devices_dir=$(mktemp -d)
|
|
|
|
trap 'rm -rf "$disko_devices_dir"' EXIT
|
|
|
|
mkdir -p "$disko_devices_dir"
|
|
|
|
|
2023-02-06 17:24:34 +03:00
|
|
|
${concatMapStrings (dev: (attrByPath (dev ++ [ "_create" ]) (_: {}) devices) {}) sortedDeviceList}
|
2023-01-28 18:19:13 +03:00
|
|
|
'';
|
|
|
|
/* Takes a disko device specification and returns a string which mounts the disks
|
|
|
|
|
|
|
|
mount :: lib.types.devices -> str
|
|
|
|
*/
|
|
|
|
mount = devices:
|
|
|
|
let
|
|
|
|
fsMounts = diskoLib.deepMergeMap (dev: (dev._mount { }).fs or { }) (flatten (map attrValues (attrValues devices)));
|
|
|
|
sortedDeviceList = diskoLib.sortDevicesByDependencies ((diskoLib.meta devices).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)}
|
|
|
|
'';
|
|
|
|
|
|
|
|
/* 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 = devices: ''
|
|
|
|
set -efux
|
|
|
|
umount -Rv "${rootMountPoint}" || :
|
|
|
|
|
2023-02-02 20:35:00 +03:00
|
|
|
# shellcheck disable=SC2043
|
2023-01-28 18:19:13 +03:00
|
|
|
for dev in ${toString (lib.catAttrs "device" (lib.attrValues devices.disk))}; do
|
|
|
|
${../disk-deactivate}/disk-deactivate "$dev" | bash -x
|
|
|
|
done
|
|
|
|
|
|
|
|
echo 'creating partitions...'
|
|
|
|
${diskoLib.create devices}
|
|
|
|
echo 'mounting partitions...'
|
|
|
|
${diskoLib.mount devices}
|
|
|
|
'';
|
|
|
|
/* Takes a disko device specification and returns a nixos configuration
|
|
|
|
|
|
|
|
config :: lib.types.devices -> nixosConfig
|
|
|
|
*/
|
|
|
|
config = devices: flatten (map (dev: dev._config) (flatten (map attrValues (attrValues devices))));
|
|
|
|
/* 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 = devices: pkgs: unique (flatten (map (dev: dev._pkgs pkgs) (flatten (map attrValues (attrValues devices)))));
|
|
|
|
|
2023-05-16 14:40:03 +03:00
|
|
|
optionTypes = rec {
|
|
|
|
filename = lib.mkOptionType {
|
|
|
|
name = "filename";
|
|
|
|
check = isString;
|
|
|
|
merge = mergeOneOption;
|
|
|
|
description = "A filename";
|
|
|
|
};
|
2023-01-28 18:19:13 +03:00
|
|
|
|
2023-05-16 14:40:03 +03:00
|
|
|
absolute-pathname = lib.mkOptionType {
|
|
|
|
name = "absolute pathname";
|
|
|
|
check = x: isString x && substring 0 1 x == "/" && pathname.check x;
|
|
|
|
merge = mergeOneOption;
|
|
|
|
description = "An absolute path";
|
|
|
|
};
|
2023-01-28 18:19:13 +03:00
|
|
|
|
2023-05-16 14:40:03 +03:00
|
|
|
pathname = lib.mkOptionType {
|
|
|
|
name = "pathname";
|
|
|
|
check = x:
|
|
|
|
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 = mergeOneOption;
|
|
|
|
description = "A path name";
|
|
|
|
};
|
2023-01-28 18:19:13 +03:00
|
|
|
};
|
|
|
|
|
2023-05-16 14:40:03 +03:00
|
|
|
/* topLevel type of the disko config, takes attrsets of disks, mdadms, zpools, nodevs, and lvm vgs.
|
|
|
|
*/
|
|
|
|
devices = lib.types.submodule {
|
|
|
|
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";
|
|
|
|
};
|
2023-01-28 18:19:13 +03:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-05-16 16:31:18 +03:00
|
|
|
# import all tge 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))
|
|
|
|
);
|
|
|
|
|
2023-05-30 16:28:36 +03:00
|
|
|
|
|
|
|
# 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";
|
|
|
|
lib = {
|
|
|
|
mkOption = option: {
|
|
|
|
inherit (option) type description;
|
|
|
|
default = option.default or null;
|
|
|
|
};
|
|
|
|
types = {
|
|
|
|
attrsOf = subType: {
|
|
|
|
type = "attrsOf";
|
|
|
|
inherit subType;
|
|
|
|
};
|
|
|
|
listOf = subType: {
|
|
|
|
type = "listOf";
|
|
|
|
inherit subType;
|
|
|
|
};
|
|
|
|
nullOr = subType: {
|
|
|
|
type = "nullOr";
|
|
|
|
inherit subType;
|
|
|
|
};
|
|
|
|
enum = choices: {
|
|
|
|
type = "enum";
|
|
|
|
inherit choices;
|
|
|
|
};
|
|
|
|
str = "str";
|
|
|
|
bool = "bool";
|
|
|
|
int = "int";
|
|
|
|
submodule = x: x { inherit (diskoLib.typesSerializerLib) lib config options; };
|
|
|
|
};
|
|
|
|
};
|
|
|
|
diskoLib = {
|
|
|
|
optionTypes.absolute-pathname = "absolute-pathname";
|
|
|
|
deviceType = "devicetype";
|
|
|
|
partitionType = "partitiontype";
|
|
|
|
subType = types: "onOf ${toString (lib.attrNames types)}";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
jsonTypes = lib.listToAttrs (
|
|
|
|
map
|
|
|
|
(file: lib.nameValuePair
|
|
|
|
(lib.removeSuffix ".nix" file)
|
|
|
|
(diskoLib.serializeType (import ./types/${file} diskoLib.typesSerializerLib))
|
|
|
|
)
|
|
|
|
(lib.attrNames (builtins.readDir ./types))
|
|
|
|
);
|
|
|
|
|
2023-01-28 18:19:13 +03:00
|
|
|
};
|
2023-05-16 14:40:03 +03:00
|
|
|
in
|
|
|
|
diskoLib
|