diff options
54 files changed, 3714 insertions, 380 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2be92b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Nix community projects + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. @@ -1,71 +1,188 @@ -disko -===== +# disko - declarative disk partitioning -nix-powered automatic disk partitioning +Disko takes the NixOS module system and makes it work for disk partitioning +as well. -Usage -===== +I wanted to write a curses NixOS installer, and that was the first step that I +hit; the disk formatting is a manual process. Once that's done, the NixOS +system itself is declarative, but the actual formatting of disks is manual. -Master Boot Record ------------------- -This is how your iso configuation may look like +## Features -```nix -{ pkgs, ... }: -let - disko = (builtins.fetchGit { - url = https://cgit.lassul.us/disko/; - rev = "88f56a0b644dd7bfa8438409bea5377adef6aef4"; - }) + "/lib"; - cfg = builtins.fromJSON ./tsp-disk.json; -in { - imports = [ - <nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix> - ]; - environment.systemPackages = with pkgs;[ - (pkgs.writeScriptBin "tsp-create" (disko.mount cfg)) - (pkgs.writeScriptBin "tsp-mount" (disko.mount cfg)) - ]; - # Optional: Automatically creates a service which runs at startup to perform the partitioning - systemd.services.install-to-hd = { - enable = true; - wantedBy = ["multi-user.target"]; - after = ["getty@tty1.service" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = [ (disko.create cfg) (disk.mount cfg) (]; - StandardInput = "null"; - StandardOutput = "journal+console"; - StandardError = "inherit"; +* supports LVM, ZFS, btrfs, GPT, mdadm, ext4, ... +* supports recursive layouts +* outputs a NixOS-compatible module +* CLI + +## How-to guides + +### NixOS installation + +During the NixOS installation process, replace the [Partitioning and +formatting](https://nixos.org/manual/nixos/stable/index.html#sec-installation-partitioning) +steps with the following: + +1. Find a disk layout in ./examples that you like. +2. Write the config based on the example and your disk layout. +4. Run the CLI (`nix run github:nix-community/disko`) to apply the changes. +5. FIXME: Copy the disko module and disk layout around. +6. Continue the NixOS installation. + +### Using without NixOS + +## Reference + +### Module options + +TODO: link to generated module options + +### Examples + +./examples + +### CLI + +TODO: output of the cli --help + +## Installing NixOS module + +You can use the NixOS module in one of the following ways: + +<details> + <summary>Flakes (Current recommendation)</summary> + +If you use nix flakes support: + +``` nix +{ + inputs.disko.url = "github:nix-community/disko"; + inputs.disko.inputs.nixpkgs.follows = "nixpkgs"; + + outputs = { self, nixpkgs, disko }: { + # change `yourhostname` to your actual hostname + nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem { + # change to your system: + system = "x86_64-linux"; + modules = [ + ./configuration.nix + disko.nixosModules.disko + ]; }; }; } ``` -tsp-disk.json (TODO: find the correct disk) -```json +</details> +<details> + <summary>niv</summary> + + First add it to [niv](https://github.com/nmattia/niv): + +```console +$ niv add nix-community/disko +``` + + Then add the following to your configuration.nix in the `imports` list: + +```nix { - "type": "devices", - "content": { - "sda": { - "type": "table", - "format": "msdos", - "partitions": [ - { "type": "partition", - "start": "1M", - "end": "100%", - "bootable": true, - "content": { - "type": "filesystem", - "format": "ext4", - "mountpoint": "/" + imports = [ "${(import ./nix/sources.nix).disko}/modules/disko.nix" ]; +} +``` +</details> +<details> + <summary>nix-channel</summary> + + As root run: + +```console +$ nix-channel --add https://github.com/nix-community/disko/archive/main.tar.gz disko +$ nix-channel --update +``` + + Then add the following to your configuration.nix in the `imports` list: + +```nix +{ + imports = [ <disko/modules/disko.nix> ]; +} +``` +</details> +<details> + <summary>fetchTarball</summary> + + Add the following to your configuration.nix: + +``` nix +{ + imports = [ "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix" ]; +} +``` + + or with pinning: + +```nix +{ + imports = let + # replace this with an actual commit id or tag + commit = "f2783a8ef91624b375a3cf665c3af4ac60b7c278"; + in [ + "${builtins.fetchTarball { + url = "https://github.com/nix-community/disko/archive/${commit}.tar.gz"; + # replace this with an actual hash + sha256 = "0000000000000000000000000000000000000000000000000000"; + }}/module.nix" + ]; +} +``` +</details> + +## Using the NixOS module + +```nix +{ + # checkout the example folder for how to configure different diska layouts + disko.devices = { + disk.sda = { + device = "/dev/sda"; + type = "disk"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "ESP"; + start = "1MiB"; + end = "100MiB"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; } - } - ] - } - } + { + name = "root"; + type = "partition"; + start = "100MiB"; + end = "100%"; + part-type = "primary"; + bootable = true; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + } + ]; + }; + }; + }; } ``` -GUID Partition Table, LVM and dm-crypt --------------------------------------- -See `examples/` +this will configure `fileSystems` and other required NixOS options to boot the specified configuration. + +If you are on an installer, you probably want to disable `enableConfig`. + +disko will create the scripts `disko-create` and `disko-mount` which can be used to create/mount the configured disk layout. @@ -3,6 +3,6 @@ let in { test = pkgs.writeScript "test" '' #!/bin/sh - nix-build "${toString ./tests/test.nix}"; + nix-build "${toString ./tests}"; ''; } @@ -0,0 +1,37 @@ +{ pkgs ? import <nixpkgs> {} +, mode ? "mount" +, flake ? null +, flakeAttr ? null +, diskoFile ? null +, noDeps ? false +, ... }@args: +let + disko = import ./. { + lib = pkgs.lib; + }; + + diskFormat = if flake != null then + (pkgs.lib.attrByPath [ "diskoConfigurations" flakeAttr ] (builtins.abort "${flakeAttr} does not exist") (builtins.getFlake flake)) args + else + import diskoFile args; + + diskoEval = if noDeps then + if (mode == "create") then + disko.createScriptNoDeps diskFormat pkgs + else if (mode == "mount") then + disko.mountScriptNoDeps diskFormat pkgs + else if (mode == "zap_create_mount") then + disko.zapCreateMountScriptNoDeps diskFormat pkgs + else + builtins.abort "invalid mode" + else + if (mode == "create") then + disko.createScript diskFormat pkgs + else if (mode == "mount") then + disko.mountScript diskFormat pkgs + else if (mode == "zap_create_mount") then + disko.zapCreateMount diskFormat pkgs + else + builtins.abort "invalid mode" + ; +in diskoEval diff --git a/default.nix b/default.nix index 6d08b8d..ada33e4 100644 --- a/default.nix +++ b/default.nix @@ -1,3 +1,49 @@ -{ - inherit (import ./lib) config create mount; +{ lib ? import <nixpkgs/lib> }: +let + types = import ./types.nix { inherit lib; }; + eval = cfg: lib.evalModules { + modules = lib.singleton { + # _file = toString input; + imports = lib.singleton { devices = cfg; }; + options = { + devices = lib.mkOption { + type = types.devices; + }; + }; + }; + }; +in { + types = types; + create = cfg: types.diskoLib.create (eval cfg).config.devices; + createScript = cfg: pkgs: pkgs.writeScript "disko-create" '' + #!/usr/bin/env bash + export PATH=${lib.makeBinPath (types.diskoLib.packages (eval cfg).config.devices pkgs)}:$PATH + ${types.diskoLib.create (eval cfg).config.devices} + ''; + createScriptNoDeps = cfg: pkgs: pkgs.writeScript "disko-create" '' + #!/usr/bin/env bash + ${types.diskoLib.create (eval cfg).config.devices} + ''; + mount = cfg: types.diskoLib.mount (eval cfg).config.devices; + mountScript = cfg: pkgs: pkgs.writeScript "disko-mount" '' + #!/usr/bin/env bash + export PATH=${lib.makeBinPath (types.diskoLib.packages (eval cfg).config.devices pkgs)}:$PATH + ${types.diskoLib.mount (eval cfg).config.devices} + ''; + mountScriptNoDeps = cfg: pkgs: pkgs.writeScript "disko-mount" '' + #!/usr/bin/env bash + ${types.diskoLib.mount (eval cfg).config.devices} + ''; + zapCreateMount = cfg: types.diskoLib.zapCreateMount (eval cfg).config.devices; + zapCreateMountScript = cfg: pkgs: pkgs.writeScript "disko-zap-create-mount" '' + #!/usr/bin/env bash + export PATH=${lib.makeBinPath (types.diskoLib.packages (eval cfg).config.devices pkgs)}:$PATH + ${types.diskoLib.zapCreateMount (eval cfg).config.devices} + ''; + zapCreateMountScriptNoDeps = cfg: pkgs: pkgs.writeScript "disko-zap-create-mount" '' + #!/usr/bin/env bash + ${types.diskoLib.zapCreateMount (eval cfg).config.devices} + ''; + config = cfg: { imports = types.diskoLib.config (eval cfg).config.devices; }; + packages = cfg: types.diskoLib.packages (eval cfg).config.devices; } diff --git a/disk-deactivate/disk-deactivate b/disk-deactivate/disk-deactivate new file mode 100755 index 0000000..7dcd753 --- /dev/null +++ b/disk-deactivate/disk-deactivate @@ -0,0 +1,6 @@ +#!/bin/sh +set -efux +# dependencies: jq util-linux lvm2 mdadm zfs +disk=$1 + +lsblk --output-all --json | jq -r --arg disk_to_clear "$disk" -f "$(dirname $0)/disk-deactivate.jq" diff --git a/disk-deactivate/disk-deactivate.jq b/disk-deactivate/disk-deactivate.jq new file mode 100644 index 0000000..54d98f7 --- /dev/null +++ b/disk-deactivate/disk-deactivate.jq @@ -0,0 +1,77 @@ +# since lsblk lacks zfs support, we have to do it this way +def remove: + if .fstype == "zfs_member" then + "zpool destroy -f \(.label)" + elif .fstype == "LVM2_member" then + [ + "vg=$(pvs \(.path) --noheadings --options vg_name | grep -o '[a-zA-Z0-9-]*')", + "vgchange -a n \"$vg\"", + "vgremove -f \"$vg\"" + ] + elif .fstype == "swap" then + "swapoff \(.path)" + elif .fstype == null then + # maybe its zfs + [ + # the next line has some horrible escaping + "zpool=$(zdb -l \(.path) | sed -nr $'s/ +name: \\'(.*)\\'/\\\\1/p')", + "if [[ -n \"${zpool}\" ]]; then zpool destroy -f \"$zpool\"; fi", + "unset zpool" + ] + else + [] + end +; + +def deactivate: + if .type == "disk" then + [ + "wipefs --all -f \(.path)" + ] + elif .type == "part" then + [ + "wipefs --all -f \(.path)" + ] + elif .type == "crypt" then + [ + "cryptsetup luksClose \(.path)", + "wipefs --all -f \(.path)" + ] + elif .type == "lvm" then + (.name | split("-")[0]) as $vgname | + (.name | split("-")[1]) as $lvname | + [ + "lvremove -fy \($vgname)/\($lvname)" + ] + elif .type == "raid1" then + [ + "mdadm --stop \(.name)" + ] + else + [] + end +; + +def walk: + [ + (.mountpoints[] | "umount -R \(.)"), + ((.children // []) | map(walk)), + remove, + deactivate + ] +; + +def init: + "/dev/\(.name)" as $disk | + if $disk == $disk_to_clear then + [ + "set -fu", + walk + ] + else + [] + end +; + +.blockdevices | map(init) | flatten | join("\n") + @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly libexec_dir="${0%/*}" + +# a file with the disko config +declare disko_config + +# a flake uri, if present disko config is relative to the flake root +declare from_flake + +# mount was chosen as the default mode because it's less destructive +mode=mount +nix_args=() + +showUsage() { + cat <<USAGE +Usage: $0 [options] disk-config.nix +or $0 [options] --flake github:somebody/somewhere + +Options: + +* -m, --mode mode + set the mode, either create or mount +* -f, --flake uri + fetch the disko config relative to this flake's root +* --arg name value + pass value to nix-build. can be used to set disk-names for example +* --argstr name value + pass value to nix-build as string +* --dry-run + just show the path to the script instead of running it +* --debug + run with set -x +USAGE +} + +abort() { + echo "aborted: $*" >&2 + exit 1 +} + +## Main ## + +[[ $# -eq 0 ]] && { + showUsage + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --debug) + set -x + ;; + -m | --mode) + mode=$2 + shift + ;; + -f | --flake) + flake="$2" + shift + ;; + --argstr | --arg) + nix_args+=("$1" "$2" "$3") + shift + shift + ;; + --help) + showUsage + exit 0 + ;; + --dry-run) + dry_run=y + ;; + --no-deps) + nix_args+=(--arg noDeps true) + ;; + --show-trace) + nix_args+=("$1") + ;; + *) + if [ -z ${disko_config+x} ]; then + disko_config=$1 + else + showUsage + exit 1 + fi + ;; + esac + shift +done + +if ! ([[ $mode = "create" ]] || [[ $mode = "mount" ]] || [[ $mode = "zap_create_mount" ]]); then + abort "mode must be either create, mount or zap_create_mount" +fi + +if [[ ! -z "${flake+x}" ]]; then + if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then + flake="${BASH_REMATCH[1]}" + flakeAttr="${BASH_REMATCH[2]}" + fi + if [[ -z "$flakeAttr" ]]; then + echo "Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri." + echo "For example, to use the output diskoConfigurations.foo from the flake.nix, append \"#foo\" to the flake-uri." + exit 1 + fi + nix_args+=("--arg" "flake" "$flake") + nix_args+=("--argstr" "flakeAttr" "$flakeAttr") + nix_args+=(--extra-experimental-features flakes) +elif [[ ! -z "${disko_config+x}" ]] && [[ -e "$disko_config" ]]; then + nix_args+=("--arg" "diskoFile" "$disko_config") +else + abort "disko config must be an existing file or flake must be set" +fi + +script=$(nix-build "${libexec_dir}"/cli.nix \ + --no-out-link \ + --argstr mode "$mode" \ + "${nix_args[@]}" +) +if [[ ! -z "${dry_run+x}" ]]; then + echo "$script" +else + exec "$script" +fi diff --git a/example/boot-raid1.nix b/example/boot-raid1.nix new file mode 100644 index 0000000..dfd2e6c --- /dev/null +++ b/example/boot-raid1.nix @@ -0,0 +1,117 @@ +{ disks ? [ "/dev/vdb" "/dev/vdc" ], ... }: { + disk = { + one = { + type = "disk"; + device = builtins.elemAt disks 0; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + name = "boot"; + type = "partition"; + start = "0"; + end = "1M"; + part-type = "primary"; + flags = ["bios_grub"]; + } + { + type = "partition"; + name = "ESP"; + start = "1MiB"; + end = "128MiB"; + fs-type = "fat32"; + bootable = true; + content = { + type = "mdraid"; + name = "boot"; + }; + } + { + type = "partition"; + name = "mdadm"; + start = "128MiB"; + end = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + } + ]; + }; + }; + two = { + type = "disk"; + device = builtins.elemAt disks 1; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + name = "boot"; + type = "partition"; + start = "0"; + end = "1M"; + part-type = "primary"; + flags = ["bios_grub"]; + } + { + type = "partition"; + name = "ESP"; + start = "1MiB"; + end = "128MiB"; + fs-type = "fat32"; + bootable = true; + content = { + type = "mdraid"; + name = "boot"; + }; + } + { + type = "partition"; + name = "mdadm"; + start = "128MiB"; + end = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + } + ]; + }; + }; + }; + mdadm = { + boot = { + type = "mdadm"; + level = 1; + metadata = "1.0"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + raid1 = { + type = "mdadm"; + level = 1; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "primary"; + start = "1MiB"; + end = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + } + ]; + }; + }; + }; +} diff --git a/example/btrfs-subvolumes.nix b/example/btrfs-subvolumes.nix new file mode 100644 index 0000000..25994df --- /dev/null +++ b/example/btrfs-subvolumes.nix @@ -0,0 +1,52 @@ +{ disks ? [ "/dev/vdb" ], ... }: { + disk = { + vdb = { + type = "disk"; + device = builtins.elemAt disks 0; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "ESP"; + start = "1MiB"; + end = "128MiB"; + fs-type = "fat32"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + } + { + name = "root"; + type = "partition"; + start = "128MiB"; + end = "100%"; + content = { + type = "btrfs"; + extraArgs = "-f"; # Override existing partition + subvolumes = { + # Subvolume name is different from mountpoint + "/rootfs" = { + mountpoint = "/"; + }; + # Mountpoints inferred from subvolume name + "/home" = { + mountOptions = ["compress=zstd"]; + }; + "/nix" = { + mountOptions = ["compress=zstd" "noatime"]; + }; + "/test" = {}; + }; + }; + } + ]; + }; + }; + }; +} + diff --git a/example/complex.nix b/example/complex.nix new file mode 100644 index 0000000..939f71e --- /dev/null +++ b/example/complex.nix @@ -0,0 +1,199 @@ +{ disks ? [ "/dev/vdb" "/dev/vdc" "/dev/vdd" ], ... }: { + disk = { + disk0 = { + type = "disk"; + device = builtins.elemAt disks 0; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + type = "partition"; + name = "ESP"; + start = "1MiB"; + end = "128MiB"; + fs-type = "fat32"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat |