diff --git a/pkgs/disko/.github/dependabot.yml b/pkgs/disko/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/pkgs/disko/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/pkgs/disko/.github/workflows/publish.yml b/pkgs/disko/.github/workflows/publish.yml new file mode 100644 index 0000000..58660af --- /dev/null +++ b/pkgs/disko/.github/workflows/publish.yml @@ -0,0 +1,22 @@ +name: "Publish a flake to flakestry" +on: + push: + tags: + - "v?[0-9]+.[0-9]+.[0-9]+" + - "v?[0-9]+.[0-9]+" + workflow_dispatch: + inputs: + tag: + description: "The existing tag to publish" + type: "string" + required: true +jobs: + publish-flake: + runs-on: ubuntu-latest + permissions: + id-token: "write" + contents: "read" + steps: + - uses: flakestry/flakestry-publish@main + with: + version: "${{ inputs.tag || github.ref_name }}" diff --git a/pkgs/disko/.github/workflows/update-flake-lock.yml b/pkgs/disko/.github/workflows/update-flake-lock.yml new file mode 100644 index 0000000..046db72 --- /dev/null +++ b/pkgs/disko/.github/workflows/update-flake-lock.yml @@ -0,0 +1,21 @@ +name: update-flake-lock +on: + workflow_dispatch: # allows manual triggering + schedule: + - cron: '0 0 * * 1,4' # Run twice a week +permissions: + pull-requests: write + contents: write +jobs: + lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v30 + - name: Update flake.lock + uses: DeterminateSystems/update-flake-lock@v24 + with: + pr-labels: | + merge-queue diff --git a/pkgs/disko/.gitignore b/pkgs/disko/.gitignore new file mode 100644 index 0000000..31b0a4b --- /dev/null +++ b/pkgs/disko/.gitignore @@ -0,0 +1,4 @@ +result + +# Created by the NixOS interactive test driver +.nixos-test-history \ No newline at end of file diff --git a/pkgs/disko/.mergify.yml b/pkgs/disko/.mergify.yml new file mode 100644 index 0000000..8c1155e --- /dev/null +++ b/pkgs/disko/.mergify.yml @@ -0,0 +1,12 @@ +queue_rules: + - name: default + merge_conditions: + - check-success=buildbot/nix-build + merge_method: rebase +pull_request_rules: + - name: merge using the merge queue + conditions: + - base=master + - label=merge-queue + actions: + queue: {} diff --git a/pkgs/disko/CONTRIBUTING.md b/pkgs/disko/CONTRIBUTING.md new file mode 100644 index 0000000..e8540b5 --- /dev/null +++ b/pkgs/disko/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing + +We welcome contributions of all kinds, be it in terms of bug fixes, +reproductions, features or documentation. + +In general, PRs are more likely to be merged quickly if they contain tests which +prove that a feature is working as intended or that a bug was indeed present and +has now been fixed. Creating a draft PR that reproduces a bug is also a great +way to help us fix issues quickly. Check out +[this PR](https://github.com/nix-community/disko/pull/330) as an example. + +For more information on how to run and debug tests, check out +[Running and debugging tests](./docs/testing.md). + +## How to find issues to work on + +If you're looking for a low-hanging fruit, check out +[this list of `good first issue`s](https://github.com/nix-community/disko/labels/good%20first%20issue). +These are issues that we have confirmed to be real and which have a strategy for +a fix already lined out in the comments. All you need to do is implement. + +If you're looking for something more challenging, check out +[this list of issues tagged `contributions welcome`](https://github.com/nix-community/disko/labels/contributions%20welcome). +These are issues that we have confirmed to be real and we know we want to be +fixed. + +For the real though nuts, we also have +[the `help wanted` label](https://github.com/nix-community/disko/labels/help%20wanted) +for issues that we feel like we need external help with. If you want a real +challenge, take a look there! + +If you're looking for bugs that still need to be reproduced, have a look at +[this list of non-`confirmed` bugs](https://github.com/nix-community/disko/issues?q=is%3Aissue+is%3Aopen+label%3Abug+-label%3Aconfirmed+). +These are things that look like bugs but that we haven't reproduced yet. + +If you're looking to contribute to the documentation, check out +[the `documentation` tag](https://github.com/nix-community/disko/issues?q=is%3Aissue+is%3Aopen+label%3Adocumentation) +or just read through [our docs](./docs/INDEX.md) and see if you can find any +issues. diff --git a/pkgs/disko/LICENSE b/pkgs/disko/LICENSE new file mode 100644 index 0000000..f099180 --- /dev/null +++ b/pkgs/disko/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018, 2019, 2022–2024 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. diff --git a/pkgs/disko/README.md b/pkgs/disko/README.md new file mode 100644 index 0000000..88a4f66 --- /dev/null +++ b/pkgs/disko/README.md @@ -0,0 +1,142 @@ +# disko - Declarative disk partitioning + +Project logo + +[Documentation Index](./docs/INDEX.md) + +NixOS is a Linux distribution where everything is described as code, with one +exception: during installation, the disk partitioning and formatting are manual +steps. **disko** aims to correct this sad 🤡 omission. + +This is especially useful for unattended installations, re-installation after a +system crash or for setting up more than one identical server. + +## Overview + +**disko** can either be used after booting from a NixOS installer, or in +conjunction with [nixos-anywhere](https://github.com/numtide/nixos-anywhere) if +you're installing remotely. + +Before using **disko**, the specifications of the disks, partitions, type of +formatting and the mount points must be defined in a Nix configuration. You can +find [examples](./example) of typical configurations in the Nix community +repository, and use one of these as the basis of your own configuration. + +You can keep your configuration and re-use it for other installations, or for a +system rebuild. + +**disko** is flexible, in that it supports most of the common formatting and +partitioning options, including: + +- Disk layouts: GPT, MBR, and mixed. +- Partition tools: LVM, mdadm, LUKS, and more. +- Filesystems: ext4, btrfs, ZFS, bcachefs, tmpfs, and others. + +It can work with these in various configurations and orders, and supports +recursive layouts. + +## How to use disko + +Disko doesn't require installation: it can be run directly from nix-community +repository. The [Quickstart Guide](./docs/quickstart.md) documents how to run +Disko in its simplest form when installing NixOS. Alternatively, you can also +use the new [disko-install](./docs/disko-install.md) tool, which combines +`disko` and `nixos-install` into one step. + +For information on other use cases, including upgrading from an older version of +**disko**, using **disko** without NixOS and downloading the module, see the +[How To Guide](./docs/HowTo.md) + +For more detailed options, such as command line switches, see the +[Reference Guide](./docs/reference.md) + +To access sample configurations for commonly-used disk layouts, refer to the +[examples](./example) provided. + +Disko can be also used to create [disk images](./docs/disko-images.md). + +## Sample Configuration and CLI command + +A simple disko configuration may look like this: + +```nix +{ + disko.devices = { + disk = { + my-disk = { + device = "/dev/sda"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} +``` + +If you'd saved this configuration in /tmp/disk-config.nix, and wanted to create +a disk named /dev/sda, you would run the following command to partition, format +and mount the disk. + +```console +sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode destroy,format,mount /tmp/disk-config.nix +``` + +## Related Tools + +This tool is used by +[nixos-anywhere](https://github.com/numtide/nixos-anywhere), which carries out a +fully-automated remote install of NixOS. + +We also acknowledge https://github.com/NixOS/nixpart, the conceptual ancestor of +this project. + +## Licensing and Contribution details + +This software is provided free under the +[MIT Licence](https://opensource.org/licenses/MIT). + +If you want to contribute, check out [CONTRIBUTING.md](./CONTRIBUTING.md). + +## Get in touch + +We have a public matrix channel at +[disko](https://matrix.to/#/#disko:nixos.org). + +--- + +This project is supported by [Numtide](https://numtide.com/). +![Untitledpng](https://codahosted.io/docs/6FCIMTRM0p/blobs/bl-sgSunaXYWX/077f3f9d7d76d6a228a937afa0658292584dedb5b852a8ca370b6c61dabb7872b7f617e603f1793928dc5410c74b3e77af21a89e435fa71a681a868d21fd1f599dd10a647dd855e14043979f1df7956f67c3260c0442e24b34662307204b83ea34de929d) + +We are a team of independent freelancers that love open source.  We help our +customers make their project lifecycles more efficient by: + +- Providing and supporting useful tools such as this one +- Building and deploying infrastructure, and offering dedicated DevOps support +- Building their in-house Nix skills, and integrating Nix with their workflows +- Developing additional features and tools +- Carrying out custom research and development. + +[Contact us](https://numtide.com/contact) if you have a project in mind, or if +you need help with any of our supported tools, including this one. We'd love to +hear from you. diff --git a/pkgs/disko/cli.nix b/pkgs/disko/cli.nix new file mode 100644 index 0000000..d32e492 --- /dev/null +++ b/pkgs/disko/cli.nix @@ -0,0 +1,116 @@ +{ pkgs ? import { } +, lib ? pkgs.lib +, mode ? "mount" +, flake ? null +, flakeAttr ? null +, diskoFile ? null +, rootMountPoint ? "/mnt" +, noDeps ? false +, ... +}@args: +let + disko = import ./. { + inherit rootMountPoint; + inherit lib; + }; + + hasDiskoFile = diskoFile != null; + + diskoAttr = + (if noDeps then + (if hasDiskoFile then + { + destroy = "_cliDestroyNoDeps"; + format = "_cliFormatNoDeps"; + mount = "_cliMountNoDeps"; + unmount = "_cliUnmountNoDeps"; + + "format,mount" = "_cliFormatMountNoDeps"; + "destroy,format,mount" = "_cliDestroyFormatMountNoDeps"; + } + else + { + destroy = "destroyNoDeps"; + format = "formatNoDeps"; + mount = "mountNoDeps"; + unmount = "unmountNoDeps"; + + "format,mount" = "formatMountNoDeps"; + "destroy,format,mount" = "destroyFormatMountNoDeps"; + }) // { + # legacy aliases + disko = "diskoScriptNoDeps"; + create = "createScriptNoDeps"; + zap_create_mount = "diskoScriptNoDeps"; + } + else + (if hasDiskoFile then + { + destroy = "_cliDestroy"; + format = "_cliFormat"; + mount = "_cliMount"; + unmount = "_cliUnmount"; + + "format,mount" = "_cliFormatMount"; + "destroy,format,mount" = "_cliDestroyFormatMount"; + } + else + { + destroy = "destroy"; + format = "format"; + mount = "mount"; + unmount = "unmount"; + + "format,mount" = "formatMount"; + "destroy,format,mount" = "destroyFormatMount"; + }) // { + # legacy aliases + disko = "diskoScript"; + create = "createScript"; + zap_create_mount = "diskoScript"; + } + ).${mode}; + + hasDiskoConfigFlake = + hasDiskoFile || lib.hasAttrByPath [ "diskoConfigurations" flakeAttr ] (builtins.getFlake flake); + + hasDiskoModuleFlake = + lib.hasAttrByPath [ "nixosConfigurations" flakeAttr "config" "disko" "devices" ] (builtins.getFlake flake); + + + diskFormat = + let + diskoConfig = + if hasDiskoFile then + import diskoFile + else + (builtins.getFlake flake).diskoConfigurations.${flakeAttr}; + in + if builtins.isFunction diskoConfig then + diskoConfig ({ inherit lib; } // args) + else + diskoConfig; + + diskoEval = + disko.${diskoAttr} diskFormat pkgs; + + diskoScript = + if hasDiskoConfigFlake then + diskoEval + else if hasDiskoModuleFlake then + (builtins.getFlake flake).nixosConfigurations.${flakeAttr}.config.system.build.${diskoAttr} or ( + pkgs.writeShellScriptBin "disko-compat-error" '' + echo 'Error: Attribute `nixosConfigurations.${flakeAttr}.config.system.build.${diskoAttr}` >&2 + echo ' not found in flake `${flake}`!' >&2 + echo ' This is probably caused by the locked version of disko in the flake' >&2 + echo ' being different from the version of disko you executed.' >&2 + echo 'EITHER set the `disko` input of your flake to `github:nix-community/disko/latest`,' >&2 + echo ' run `nix flake update disko` in the flake directory and then try again,' >&2 + echo 'OR run `nix run github:nix-community/disko/v1.9.0 -- --help` and use one of its modes.' >&2 + exit 1;'' + ) + else + (builtins.abort "couldn't find `diskoConfigurations.${flakeAttr}` or `nixosConfigurations.${flakeAttr}.config.disko.devices`"); + +in +diskoScript diff --git a/pkgs/disko/default.nix b/pkgs/disko/default.nix new file mode 100644 index 0000000..02f429c --- /dev/null +++ b/pkgs/disko/default.nix @@ -0,0 +1,65 @@ +{ lib ? import +, rootMountPoint ? "/mnt" +, checked ? false +, diskoLib ? import ./lib { inherit lib rootMountPoint; } +}: +let + eval = cfg: lib.evalModules { + modules = lib.singleton { + # _file = toString input; + imports = lib.singleton { disko.devices = cfg.disko.devices; }; + options = { + disko.devices = lib.mkOption { + type = diskoLib.toplevel; + }; + }; + }; + }; + # We might instead reuse some of the deprecated output names to refer to the values the _cli* outputs currently do, + # but this warning allows us to get feedback from users early in case they have a use case we haven't considered. + warnDeprecated = name: lib.warn "the ${name} output is deprecated and will be removed, please open an issue if you're using it!"; +in +{ + lib = warnDeprecated ".lib.lib" diskoLib; + + _cliDestroy = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroy; + _cliDestroyNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroyNoDeps; + + _cliFormat = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).format; + _cliFormatNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatNoDeps; + + _cliMount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mount; + _cliMountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mountNoDeps; + + _cliUnmount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).unmount; + _cliUnmountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).unmountNoDeps; + + _cliFormatMount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatMount; + _cliFormatMountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatMountNoDeps; + + _cliDestroyFormatMount = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroyFormatMount; + _cliDestroyFormatMountNoDeps = cfg: pkgs: ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).destroyFormatMountNoDeps; + + # legacy aliases + create = cfg: warnDeprecated "create" (eval cfg).config.disko.devices._create; + createScript = cfg: pkgs: warnDeprecated "createScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScript; + createScriptNoDeps = cfg: pkgs: warnDeprecated "createScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScriptNoDeps; + + format = cfg: warnDeprecated "format" (eval cfg).config.disko.devices._create; + formatScript = cfg: pkgs: warnDeprecated "formatScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScript; + formatScriptNoDeps = cfg: pkgs: warnDeprecated "formatScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).formatScriptNoDeps; + + mount = cfg: warnDeprecated "mount" (eval cfg).config.disko.devices._mount; + mountScript = cfg: pkgs: warnDeprecated "mountScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mountScript; + mountScriptNoDeps = cfg: pkgs: warnDeprecated "mountScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).mountScriptNoDeps; + + disko = cfg: warnDeprecated "disko" (eval cfg).config.disko.devices._disko; + diskoScript = cfg: pkgs: warnDeprecated "diskoScript" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).diskoScript; + diskoScriptNoDeps = cfg: pkgs: warnDeprecated "diskoScriptNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).diskoScriptNoDeps; + + # we keep this old output for backwards compatibility + diskoNoDeps = cfg: pkgs: warnDeprecated "diskoNoDeps" ((eval cfg).config.disko.devices._scripts { inherit pkgs checked; }).diskoScriptNoDeps; + + config = cfg: (eval cfg).config.disko.devices._config; + packages = cfg: (eval cfg).config.disko.devices._packages; +} diff --git a/pkgs/disko/disk-deactivate/disk-deactivate b/pkgs/disko/disk-deactivate/disk-deactivate new file mode 100755 index 0000000..9ddb2de --- /dev/null +++ b/pkgs/disko/disk-deactivate/disk-deactivate @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -efux -o pipefail +# dependencies: bash jq util-linux lvm2 mdadm zfs gnugrep sed +disk=$(realpath "$1") + +lsblk -a -f >&2 +lsblk --output-all --json | jq -r --arg disk_to_clear "$disk" -f "$(dirname "$0")/disk-deactivate.jq" | bash -x +lsblk -a -f >&2 diff --git a/pkgs/disko/disk-deactivate/disk-deactivate.jq b/pkgs/disko/disk-deactivate/disk-deactivate.jq new file mode 100644 index 0000000..cf6e943 --- /dev/null +++ b/pkgs/disko/disk-deactivate/disk-deactivate.jq @@ -0,0 +1,86 @@ +# since lsblk lacks zfs support, we have to do it this way +def remove: + if .fstype == "zfs_member" then + "if type zpool >/dev/null; then zpool destroy -f \(.label); zpool labelclear -f \(.label); fi" + 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=$(if type zdb >/dev/null; then zdb -l \(.path) | sed -nr $'s/ +name: \\'(.*)\\'/\\\\1/p'; fi)", + "if [[ -n \"${zpool}\" ]]; then zpool destroy -f \"$zpool\"; zpool labelclear -f \"$zpool\"; fi", + "unset zpool" + ] + else + [] + end +; + +def deactivate: + if .type == "disk" or .type == "loop" then + [ + # If this disk is a member of raid, stop that raid + "md_dev=$(lsblk \(.path) -l -p -o type,name | awk 'match($1,\"raid.*\") {print $2}')", + "if [[ -n \"${md_dev}\" ]]; then umount \"$md_dev\"; mdadm --stop \"$md_dev\"; fi", + # Remove all file-systems and other magic strings + "wipefs --all -f \(.path)", + # Remove the MBR bootstrap code + "dd if=/dev/zero of=\(.path) bs=440 count=1" + ] + 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 | contains("raid")) then + [ + "mdadm --stop \(.name)" + ] + else + ["echo Warning: unknown type '\(.type)'. Consider handling this in https://github.com/nix-community/disko/blob/master/disk-deactivate/disk-deactivate.jq"] + end +; + +def walk: + [ + (.mountpoints[] | select(. != null) | "umount -R \(.)"), + ((.children // []) | map(walk)), + remove, + deactivate + ] +; + +def init: + "/dev/\(.name)" as $disk | + "/dev/disk/by-id/\(."id-link")" as $disk_by_id | + "/dev/disk/by-id/\(.tran)-\(.id)" as $disk_by_id2 | + "/dev/disk/by-id/\(.tran)-\(.wwn)" as $disk_by_wwn | + if $disk == $disk_to_clear or $disk_by_id == $disk_to_clear or $disk_by_id2 == $disk_to_clear or $disk_by_wwn == $disk_to_clear then + [ + "set -fu", + walk + ] + else + [] + end +; + +.blockdevices | map(init) | flatten | join("\n") + diff --git a/pkgs/disko/disko b/pkgs/disko/disko new file mode 100755 index 0000000..3dd784c --- /dev/null +++ b/pkgs/disko/disko @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly libexec_dir="${0%/*}" + +# a file with the disko config +declare disko_config + +# mount was chosen as the default mode because it's less destructive +mode=mount +nix_args=() +skip_destroy_safety_check=false + +# DISKO_VERSION is set by the wrapper in package.nix +DISKO_VERSION="${DISKO_VERSION:="unknown! This is a bug, please report it!"}" +onlyPrintVersion=false + +showUsage() { + cat <&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 + ;; + -h | --help) + showUsage + exit 0 + ;; + --dry-run) + dry_run=y + ;; + --root-mountpoint) + nix_args+=(--argstr rootMountPoint "$2") + shift + ;; + --no-deps) + nix_args+=(--arg noDeps true) + ;; + --yes-wipe-all-disks) + skip_destroy_safety_check=true + ;; + --show-trace) + nix_args+=("$1") + ;; + --version) + onlyPrintVersion=true + ;; + *) + if [ -z ${disko_config+x} ]; then + disko_config=$1 + else + showUsage + exit 1 + fi + ;; + esac + shift +done + +if [[ "$onlyPrintVersion" = true ]]; then + echo "$DISKO_VERSION" + exit 0 +fi +# Always print version information to help with debugging +echo "disko version $DISKO_VERSION" + +nixBuild() { + if command -v nom-build > /dev/null; then + nom-build "$@" + else + nix-build "$@" + fi +} + +if ! { + # Base modes + [[ $mode = "destroy" ]] || [[ $mode = "format" ]] || [[ $mode = "mount" ]] || [[ $mode = "unmount" ]] || + # Combined modes + [[ $mode = "format,mount" ]] || + [[ $mode = "destroy,format,mount" ]] || # Replaces --mode disko + # Legacy modes, will be removed in next major version + [[ $mode = "disko" ]] || [[ $mode = "create" ]] || [[ $mode = "zap_create_mount" ]] ; +}; then + abort 'mode must be one of "destroy", "format", "mount", "destroy,format,mount" or "format,mount"' +fi + +if [[ -n "${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 + if [[ -e "$flake" ]]; then + flake="$(realpath "$flake")" + fi + nix_args+=("--arg" "flake" "\"$flake\"") + nix_args+=("--argstr" "flakeAttr" "$flakeAttr") + nix_args+=(--extra-experimental-features flakes) +elif [[ -n "${disko_config+x}" ]] && [[ -e "$disko_config" ]]; then + nix_args+=("--arg" "diskoFile" "$(realpath "$disko_config")") +else + abort "disko config must be an existing file or flake must be set" +fi + +# The "--impure" is still pure, as the path is within the nix store. +script=$(nixBuild "${libexec_dir}"/cli.nix \ + --no-out-link \ + --impure \ + --argstr mode "$mode" \ + "${nix_args[@]}" +) + +command=("$(echo "$script"/bin/*)") +if [[ $mode = "destroy,format,mount" && $skip_destroy_safety_check = true ]]; then + command+=("--yes-wipe-all-disks") +fi + +# Legacy modes don't support --yes-wipe-all-disks and are not in `$script/bin` +if [[ $mode = "disko" ]] || [[ $mode = "create" ]] || [[ $mode = "zap_create_mount" ]] ; then + command=("$script") +fi + +if [[ -n "${dry_run+x}" ]]; then + echo "${command[@]}" +else + exec "${command[@]}" +fi diff --git a/pkgs/disko/disko-install b/pkgs/disko/disko-install new file mode 100755 index 0000000..2c5596e --- /dev/null +++ b/pkgs/disko/disko-install @@ -0,0 +1,273 @@ +#!/usr/bin/env bash + +set -euo pipefail + +showUsage() { + cat <&2 + exit 1 + fi + case $2 in + format) + diskoAttr=diskoScript + ;; + mount) + diskoAttr=mountScript + ;; + *) + echo "Invalid mode: $2" >&2 + echo "Valid modes are: format, mount" >&2 + exit 1 + ;; + esac + shift + ;; + --system-config) + if [[ $# -lt 2 ]]; then + echo "Option $1 requires one JSON argument." >&2 + exit 1 + fi + # shellcheck disable=SC2034 + extraSystemConfig="$2" + shift + ;; + --extra-files) + if [[ $# -lt 3 ]]; then + echo "Option $1 requires two arguments: source, destination" >&2 + exit 1 + fi + extraFiles[$2]=$3 + shift + shift + ;; + --option) + if [[ $# -lt 3 ]]; then + echo "Option $1 requires two arguments: key, value" >&2 + exit 1 + fi + nix_args+=(--option "$2" "$3") + shift + shift + ;; + --disk) + if [[ $# -lt 3 ]]; then + echo "Option $1 requires two arguments: disk_name, device_path" >&2 + exit 1 + fi + # shellcheck disable=SC2034 + diskMappings[$2]=$3 + shift + shift + ;; + --mount-point) + if [[ $# -lt 2 ]]; then + echo "Option $1 requires an argument" >&2 + exit 1 + fi + mountPoint=$2 + shift + ;; + *) + echo "Unknown option: $1" >&2 + showUsage + exit 1 + ;; + esac + shift + done +} + +cleanupMountPoint() { + if mountpoint -q "${mountPoint}"; then + umount -R "${mountPoint}" + fi + rmdir "${mountPoint}" +} + +nixBuild() { + if command -v nom-build > /dev/null; then + nom-build "$@" + else + nix-build "$@" + fi +} + +maybeRun () { + if [[ -z ${dry_run-} ]]; then + "$@" + else + echo "Would run: $*" + fi +} + +main() { + parseArgs "$@" + + if [[ -z ${flake-} ]]; then + echo "Please specify the flake-uri with --flake to use for installation." >&2 + exit 1 + fi + + # check if we are root + if [[ "$EUID" -ne 0 ]] && [[ -z ${dry_run-} ]]; then + echo "This script must be run as root" >&2 + exit 1 + fi + + if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then + flake="${BASH_REMATCH[1]}" + flakeAttr="${BASH_REMATCH[2]}" + fi + + if [[ -e "$flake" ]]; then + flake=$(realpath "$flake") + 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." >&2 + echo 'For example, to use the output nixosConfigurations.foo from the flake.nix, append "#foo" to the flake-uri.' >&2 + exit 1 + fi + + maybeRun mkdir -p "${mountPoint}" + maybeRun chmod 755 "${mountPoint}" # bcachefs wants 755 + # shellcheck disable=SC2064 + if [[ -z ${dry_run-} ]]; then + trap cleanupMountPoint EXIT + fi + + outputs=$(nixBuild "${libexec_dir}"/install-cli.nix \ + "${nix_args[@]}" \ + --no-out-link \ + --impure \ + --argstr flake "$flake" \ + --argstr flakeAttr "$flakeAttr" \ + --argstr rootMountPoint "$mountPoint" \ + --arg writeEfiBootEntries "$writeEfiBootEntries" \ + --arg diskMappings "$(serialiaseArrayToNix diskMappings)" \ + --argstr extraSystemConfig "$extraSystemConfig" \ + -A installToplevel \ + -A closureInfo \ + -A "$diskoAttr") + + IFS=$'\n' mapfile -t artifacts <<<"$outputs" + nixos_system=${artifacts[0]} + closure_info=${artifacts[1]} + disko_script=${artifacts[2]} + + if [[ -n ${dry_run-} ]]; then + echo "Would run: $disko_script" + echo "Would run: nixos-install --system '$nixos_system' --root '$mountPoint'" + exit 0 + fi + + # We don't want swap as can break your running system in weird ways if you eject the disk + # Hopefully disko-install has enough RAM to run without swap, otherwise we can make this configurable in future. + DISKO_SKIP_SWAP=1 "$disko_script" + + for source in "${!extraFiles[@]}"; do + destination=${extraFiles[$source]} + mkdir -p "$mountPoint/$(dirname "$destination")" + cp -ar "$source" "$mountPoint/$destination" + done + + # nix copy uses up a lot of memory and we work around issues with incorrect checksums in our store + # that can be caused by using closureInfo in combination with multiple builders and non-deterministic builds. + # Therefore if we have a blank store, we copy the store paths and registration from the closureInfo. + if [[ ! -f "${mountPoint}/nix/var/nix/db/db.sqlite" ]]; then + echo "Copying store paths" >&2 + mkdir -p "${mountPoint}/nix/store" + xargs cp --recursive --target "${mountPoint}/nix/store" < "${closure_info}/store-paths" + echo "Loading nix database" >&2 + NIX_STATE_DIR=${mountPoint}/nix/var/nix nix-store --load-db < "${closure_info}/registration" + fi + + nixos-install --no-channel-copy --no-root-password --system "$nixos_system" --root "$mountPoint" +} + +if main "$@"; then + echo "disko-install succeeded" + exit 0 +else + echo "disko-install failed" >&2 + exit 1 +fi diff --git a/pkgs/disko/doc.nix b/pkgs/disko/doc.nix new file mode 100644 index 0000000..d07f8f3 --- /dev/null +++ b/pkgs/disko/doc.nix @@ -0,0 +1,43 @@ +{ lib, nixosOptionsDoc, runCommand, fetchurl, pandoc }: + +let + diskoLib = import ./lib { + inherit lib; + rootMountPoint = "/mnt"; + }; + eval = lib.evalModules { + modules = [ + { + options.disko = { + devices = lib.mkOption { + type = diskoLib.toplevel; + default = { }; + description = "The devices to set up"; + }; + }; + } + ]; + }; + options = nixosOptionsDoc { + options = eval.options; + }; + md = (runCommand "disko-options.md" { } '' + cat >$out <>$out + '').overrideAttrs (_o: { + # Work around https://github.com/hercules-ci/hercules-ci-agent/issues/168 + allowSubstitutes = true; + }); + css = fetchurl { + url = "https://gist.githubusercontent.com/killercup/5917178/raw/40840de5352083adb2693dc742e9f75dbb18650f/pandoc.css"; + sha256 = "sha256-SzSvxBIrylxBF6B/mOImLlZ+GvCfpWNLzGFViLyOeTk="; + }; +in +runCommand "disko.html" { nativeBuildInputs = [ pandoc ]; } '' + mkdir $out + cp ${css} $out/pandoc.css + pandoc --css="pandoc.css" ${md} --to=html5 -s -f markdown+smart --metadata pagetitle="Disko options" -o $out/index.html +'' diff --git a/pkgs/disko/docs/HowTo.md b/pkgs/disko/docs/HowTo.md new file mode 100644 index 0000000..e57d73a --- /dev/null +++ b/pkgs/disko/docs/HowTo.md @@ -0,0 +1,178 @@ +# How-to Guide: Disko + +## How to use Disko without NixOS + +TODO: Still to be documented + +## Upgrading From Older disko versions + +TODO: Include documentation here. + +For now, see the +[upgrade guide](https://github.com/JillThornhill/disko/blob/master/docs/upgrade-guide.md) + +## Installing NixOS module + +You can use the NixOS module in one of the following ways: + +
+ Flakes (Current recommendation) + +If you use nix flakes support: + +```nix +{ + inputs.disko.url = "github:nix-community/disko/latest"; + 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 + ]; + }; + }; +} +``` + +
+
+ niv + +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 +{ + imports = [ "${(import ./nix/sources.nix).disko}/module.nix" ]; +} +``` + +
+
+ npins + +First add it to [npins](https://github.com/andir/npins): + +```console +npins add github nix-community disko +``` + +Then add the following to your configuration.nix in the `imports` list: + +```nix +let + sources = import ./npins; + disko = import sources.disko {}; +in +{ + imports = [ "${disko}/module.nix" ]; + … +} +``` + +
+
+ nix-channel + +As root run: + +```console +nix-channel --add https://github.com/nix-community/disko/archive/master.tar.gz disko +nix-channel --update +``` + +Then add the following to your configuration.nix in the `imports` list: + +```nix +{ + imports = [ ]; +} +``` + +
+
+ fetchTarball + +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" + ]; +} +``` + +
+ +## Using the NixOS module + +```nix +{ + # checkout the example folder for how to configure different disko layouts + disko.devices = { + disk = { + vdb = { + device = "/dev/disk/by-id/some-disk-id"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "100M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} +``` + +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. diff --git a/pkgs/disko/docs/INDEX.md b/pkgs/disko/docs/INDEX.md new file mode 100644 index 0000000..2c1b027 --- /dev/null +++ b/pkgs/disko/docs/INDEX.md @@ -0,0 +1,22 @@ +# disko - Declarative disk partitioning + + + +## Table of Contents + +### For users + +- [README](../README.md) +- [Quickstart](./quickstart.md) +- [System Requirements](./requirements.md) +- [How to Guide](./HowTo.md) +- [Disko-Install](./disko-install.md) +- [Disko-Images](./disko-images.md) +- [Support Matrix](./supportmatrix.md) +- [Reference](./reference.md) +- [Upgrade Guide](./upgrade-guide.md) + - [Migrating to the new GPT layout](./table-to-gpt.md) + +### For contributors + +- [Running and debugging tests](./testing.md) diff --git a/pkgs/disko/docs/disko-images.md b/pkgs/disko/docs/disko-images.md new file mode 100644 index 0000000..51b2e86 --- /dev/null +++ b/pkgs/disko/docs/disko-images.md @@ -0,0 +1,122 @@ +# Generating Disk Images with Secrets Included using Disko + +Using Disko on NixOS allows you to efficiently create `.raw` VM images from a +system configuration. The generated image can be used as a VM or directly +written to a physical drive to create a bootable disk. Follow the steps below to +generate disk images: + +## Generating the `.raw` VM Image + +1. **Create a NixOS configuration that includes the disko and the disk + configuration of your choice** + +In the this example we create a flake containing a nixos configuration for +`myhost`. + +```nix +# save this as flake.nix +{ + description = "A disko images example"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + disko.url = "github:nix-community/disko/latest"; + disko.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, disko, nixpkgs }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + # You can get this file from here: https://github.com/nix-community/disko/blob/master/example/simple-efi.nix + ./simple-efi.nix + disko.nixosModules.disko + ({ config, ... }: { + # shut up state version warning + system.stateVersion = config.system.nixos.version; + # Adjust this to your liking. + # WARNING: if you set a too low value the image might be not big enough to contain the nixos installation + disko.devices.disk.main.imageSize = "10G"; + }) + ]; + }; + }; +} +``` + +2. **Build the disko image script:** Replace `myhost` in the command below with + your specific system configuration name: + + ```console + nix build .#nixosConfigurations.myhost.config.system.build.diskoImagesScript + ``` + +3. **Execute the disko image script:** Execute the generated disko image script. + Running `./result --help` will output the available options: + + ```console + ./result --help + Usage: $script [options] + + Options: + * --pre-format-files + 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 + 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 + 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 1024 MiB + ``` + + An example run may look like this: + + ``` + sudo ./result --build-memory 2048 + ``` + + The script will generate the actual image outside of the nix store in the + current working directory. The create image names depend on the names used in + `disko.devices.disk` attrset in the NixOS configuration. In our code example + it will produce the following image: + + ``` + $ ls -la main.raw + .rw-r--r-- root root 10 GB 2 minutes ago main.raw + ``` + +## Additional Configuration + +- For custom image name output, define the image name in your Disko configuration: + + ```console + disko.devices.disk..imageName = "nixos-x86_64-linux-generic-btrfs"; # Set your preferred name + ``` + + The image scirpt will produce `nixos-x86_64-linux-generic-btrfs.raw` instead of `.raw`. + +- For virtual drive use, define the image size in your Disko configuration: + + ```console + disko.devices.disk..imageSize = "32G"; # Set your preferred size + ``` + +## Understanding the Image Generation Process + +1. Files specified in `--pre-format-files` and `--post-format-files` are + temporarily copied to `/tmp`. +2. Files are then moved to their respective locations in the VM both before and + after the Disko partitioning script runs. +3. The NixOS installer is executed, having access only to `--post-format-files`. +4. Upon installer completion, the VM is shutdown, and the `.raw` disk files are + moved to the local directory. + +> **Note**: The auto-resizing feature is currently not available in Disko. +> Contributions for this feature are welcomed. Adjust the `imageSize` +> configuration to prevent issues related to file size and padding. + +By following these instructions and understanding the process, you can smoothly +generate disk images with Disko for your NixOS system configurations. diff --git a/pkgs/disko/docs/disko-install.md b/pkgs/disko/docs/disko-install.md new file mode 100644 index 0000000..7abaff9 --- /dev/null +++ b/pkgs/disko/docs/disko-install.md @@ -0,0 +1,233 @@ +# disko-install + +**disko-install** enhances the normal `nixos-install` with disko's partitioning +feature. It can be started from the NixOS installer but it can also be used to +create bootable USB-Sticks from your normal workstation. Furthermore +`disko-install` has a mount mode that will only mount but not destroy existing +partitions. The mount mode can be used to mount and repair existing NixOS +installations. This document provides a comprehensive guide on how to use +**disko-install**, including examples for typical usage scenarios. + +## Requirements + +- a Linux system with Nix installed. +- a target disk or partition for the NixOS installation. +- a Nix flake that defines your desired NixOS configuration. + +## Usage + +### Fresh Installation + +For a fresh installation, where **disko-install** will handle partitioning and +setting up the disk, use the following syntax: + +```console +sudo nix run 'github:nix-community/disko/latest#disko-install' -- --flake # --disk +``` + +Example: + +First run `nixos-generate-config --root /tmp/config --no-filesystems` and edit +`configuration.nix` to your liking. + +Then add the following `flake.nix` inside `/tmp/config/etc/nixos`. In this +example we assume a system that has been booted with EFI: + +```nix +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + inputs.disko.url = "github:nix-community/disko/latest"; + inputs.disko.inputs.nixpkgs.follows = "nixpkgs"; + + outputs = { self, disko, nixpkgs }: { + nixosConfigurations.mymachine = nixpkgs.legacyPackages.x86_64-linux.nixos [ + ./configuration.nix + disko.nixosModules.disko + { + disko.devices = { + disk = { + main = { + # When using disko-install, we will overwrite this value from the commandline + device = "/dev/disk/by-id/some-disk-id"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + MBR = { + type = "EF02"; # for grub MBR + size = "1M"; + priority = 1; # Needs to be first partition + }; + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; + } + ]; + }; +} +``` + +Identify the device name that you want to install NixOS to: + +```console +$ lsblk +NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS +sda 8:0 1 14.9G 0 disk +└─sda1 8:1 1 14.9G 0 part +zd0 230:0 0 10G 0 disk +├─zd0p1 230:1 0 500M 0 part +└─zd0p2 230:2 0 9.5G 0 part /mnt +nvme0n1 259:0 0 1.8T 0 disk +├─nvme0n1p1 259:1 0 1G 0 part /boot +├─nvme0n1p2 259:2 0 16M 0 part +├─nvme0n1p3 259:3 0 250G 0 part +└─nvme0n1p4 259:4 0 1.6T 0 part +``` + +In our example, we want to install to a USB-stick (/dev/sda): + +```console +$ sudo nix run 'github:nix-community/disko/latest#disko-install' -- --flake '/tmp/config/etc/nixos#mymachine' --disk main /dev/sda +``` + +Afterwards you can test your USB-stick by either selecting during the boot or +attaching it to a qemu-vm: + +``` +$ sudo qemu-kvm -enable-kvm -hda /dev/sda +``` + +### Persisting boot entries to EFI vars flash + +**disko-install** is designed for NixOS installations on portable storage or +disks that may be transferred between computers. As such, it does not modify the +host's NVRAM by default. To ensure your NixOS installation boots seamlessly on +new hardware or to prioritize it in your current machine's boot order, use the +--write-efi-boot-entries option: + +```console +$ sudo nix run 'github:nix-community/disko/latest#disko-install' -- --write-efi-boot-entries --flake '/tmp/config/etc/nixos#mymachine' --disk main /dev/sda +``` + +This command installs NixOS with **disko-install** and sets the newly installed +system as the default boot option, without affecting the flexibility to boot +from other devices if needed. + +### Using disko-install in an offline installer + +If you want to use **disko-install** from a custom installer without internet, +you need to make sure that in addition to the toplevel of your NixOS closure +that you plan to install, it also needs to contain **diskoScript** and all the +flake inputs for evaluation. + +#### Example configuration to install + +Add this to your flake.nix output: + +```nix +{ + nixosConfigurations.your-machine = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + # to pass this flake into your configuration (see the example below) + specialArgs = {inherit self;}; + modules = [ + { + # TODO: add your NixOS configuration here, don't forget your hardware-configuration.nix as well! + boot.loader.systemd-boot.enable = true; + imports = [ self.inputs.disko.nixosModules.disko ]; + disko.devices = { + disk = { + vdb = { + device = "/dev/disk/by-id/some-disk-id"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; + } + ]; + }; +} +``` + +#### Example for a NixOS installer + +```nix +# `self` here is referring to the flake `self`, you may need to pass it using `specialArgs` or define your NixOS installer configuration +# in the flake.nix itself to get direct access to the `self` flake variable. +{ pkgs, self, ... }: +let + dependencies = [ + self.nixosConfigurations.your-machine.config.system.build.toplevel + self.nixosConfigurations.your-machine.config.system.build.diskoScript + self.nixosConfigurations.your-machine.config.system.build.diskoScript.drvPath + self.nixosConfigurations.your-machine.pkgs.stdenv.drvPath + + # https://github.com/NixOS/nixpkgs/blob/f2fd33a198a58c4f3d53213f01432e4d88474956/nixos/modules/system/activation/top-level.nix#L342 + self.nixosConfigurations.your-machine.pkgs.perlPackages.ConfigIniFiles + self.nixosConfigurations.your-machine.pkgs.perlPackages.FileSlurp + + (self.nixosConfigurations.your-machine.pkgs.closureInfo { rootPaths = [ ]; }).drvPath + ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); + + closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; +in +# Now add `closureInfo` to your NixOS installer +{ + environment.etc."install-closure".source = "${closureInfo}/store-paths"; + + environment.systemPackages = [ + (pkgs.writeShellScriptBin "install-nixos-unattended" '' + set -eux + # Replace "/dev/disk/by-id/some-disk-id" with your actual disk ID + exec ${pkgs.disko}/bin/disko-install --flake "${self}#your-machine" --disk vdb "/dev/disk/by-id/some-disk-id" + '') + ]; +} +``` + +Also see the +[NixOS test of disko-install](https://github.com/nix-community/disko/blob/master/tests/disko-install/default.nix) +that also runs without internet. diff --git a/pkgs/disko/docs/interactive-vm.md b/pkgs/disko/docs/interactive-vm.md new file mode 100644 index 0000000..02d1244 --- /dev/null +++ b/pkgs/disko/docs/interactive-vm.md @@ -0,0 +1,26 @@ +# Running Interactive VMs with disko + +disko now exports its own flavor of interactive VMs (similiar to +config.system.build.vm). Simply import the disko module and build the vm runner +with: + +``` +nix run -L '.#nixosConfigurations.mymachine.config.system.build.vmWithDisko' +``` + +You can configure the VM using the `virtualisation.vmVariantWithDisko` NixOS +option: + +```nix +{ + virtualisation.vmVariantWithDisko = { + virtualisation.fileSystems."/persist".neededForBoot = true; + # For running VM on macos: https://www.tweag.io/blog/2023-02-09-nixos-vm-on-macos/ + # virtualisation.host.pkgs = inputs.nixpkgs.legacyPackages.aarch64-darwin; + }; +} +``` + +extraConfig that is set in disko.tests.extraConfig is also applied to the +interactive VMs. imageSize of the VMs will be determined by the imageSize in the +disk type in your disko config. memorySize is set by disko.memSize diff --git a/pkgs/disko/docs/logo.png b/pkgs/disko/docs/logo.png new file mode 100644 index 0000000..56688e6 Binary files /dev/null and b/pkgs/disko/docs/logo.png differ diff --git a/pkgs/disko/docs/quickstart.md b/pkgs/disko/docs/quickstart.md new file mode 100644 index 0000000..b0f3bb8 --- /dev/null +++ b/pkgs/disko/docs/quickstart.md @@ -0,0 +1,225 @@ +# disko - Declarative disk partitioning + +Project logo + +[Documentation Index](./INDEX.md) + +## Quickstart Guide + +This tutorial describes how to install NixOS on a single disk system using +`disko`. You will also need to refer to the NixOS manual, which is available +[here.](https://nixos.org/manual/nixos/stable/index.html#ex-config) + +Please note that `disko` will reformat the entire disk and overwrite any +existing partitions. Dual booting with other operating systems is not supported. + +### Step 1: Choose a Disk Configuration + +Real-world templates are provided in this +[repository](https://github.com/nix-community/disko-templates). + +More disk layouts for all filesystems can be also found in the +[example](https://github.com/nix-community/disko/tree/master/example) directory +of disko. However these examples are also used for regression tests in disko and +may have uncommon options in them to fully exercise all features of disko, that +you may need to change or remove. + +Decide which of these layouts best suits your requirements. If you're not sure +which layout to pick, use the +[single-disk-ext4](https://github.com/nix-community/disko-templates/blob/main/single-disk-ext4/disko-config.nix) +configuration. This layout is compatible with both BIOS and EFI systems. + +Refer to the [reference manual](./reference.md) for more information about the +sample layouts and how to build your own configuration. + +To copy a template use this command in your nixos configuration directory: + +``` +nix flake init --template github:nix-community/disko-templates#single-disk-ext4 +``` + +This will write a file called `disko-config.nix` into the current directory. +Import this file in your NixOS configuration: + +```nix +{ + imports = [ ./disko-config.nix ]; +} +``` + +If you want to choose a layout from the disko example directory instead, you'll +need to make a note of the URL to the raw file. To do this, open the file in +Github. Immediately below the list of contributors, you will see a button +labelled 'RAW' near the right hand side. Click this. The URL of the raw file +will appear in the search bar of your browser. It will look something like this: + +``` +https://raw.githubusercontent.com/nix-community/disko/master/example/hybrid.nix +``` + +### Step 2: Boot the installer + +Download the NixOS ISO image from the NixOS +[download page](https://nixos.org/download.html#nixos-iso), and create a +bootable USB drive following the instructions +in [Section 2.4.1 "Booting from a USB flash drive"](https://nixos.org/manual/nixos/stable/index.html#sec-booting-from-usb) of +the NixOS manual. Boot the machine from this USB drive. + +### Step 3: Retrieve the disk name + +Identify the name of your system disk by using the `lsblk` command as follows: + +```console +lsblk +``` + +The output from this command will look something like this: + +``` +NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS +nvme0n1     259:0    0   1,8T  0 disk +``` + +In this example, an empty NVME SSD with 2TB space is shown with the disk name +"nvme0n1". Make a note of the disk name as you will need it later. + +### Step 4: Copy the disk configuration to your machine + +In Step 1, you chose a disk layout configuration from the +[examples directory](https://github.com/nix-community/disko/tree/master/example), +and made a note of its URL. + +Your configuration needs to be saved on the new machine for example +as /tmp/disk-config.nix. You can do this using the `curl` command to download +from the url you noted above, using the `-o` option to save the file as +disk-config.nix. Your commands would look like this if you had chosen the hybrid +layout: + +```console +cd /tmp +curl https://raw.githubusercontent.com/nix-community/disko/master/example/hybrid.nix -o /tmp/disk-config.nix +``` + +### Step 5: Adjust the device in the disk configuration + +Inside the disk-config.nix the device needs to point to the correct disk name. + +Open the configuration in your favorite editor i.e.: + +```console +nano /tmp/disk-config.nix +``` + +Replace `` with the name of your disk obtained in Step 1. + +```nix +# ... +main = { + type = "disk"; + device = ""; + content = { + type = "gpt"; +# ... +``` + +### Step 6: Run disko to partition, format and mount your disks + +The following step will partition and format your disk, and mount it to `/mnt`. + +**Please note: This will erase any existing data on your disk.** + +```console +sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode destroy,format,mount /tmp/disk-config.nix +``` + +After the command has run, your file system should have been formatted and +mounted. You can verify this by running the following command: + +```console +mount | grep /mnt +``` + +The output should look like this if your disk name is `nvme0n1`. + +``` +/dev/nvme0n1p1 on /mnt type ext4 (rw,relatime,stripe=2) +/dev/nvme0n1p2 on /mnt/boot type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro) +``` + +### Step 7: Complete the NixOS installation. + +Your disks have now been formatted and mounted, and you are ready to complete +the NixOS installation as described in the +[NixOS manual](https://nixos.org/manual/nixos/stable/index.html#sec-installation) - +see the section headed "**Installing**", Steps 3 onwards. However, you will need +to include the partitioning and formatting configurations that you copied into +`/tmp/disk-config.nix` in your configuration, rather than allowing NixOS to +generate information about your file systems. When you are configuring the +system as per Step 4 of the manual, you should: + +a) Include the `no-filesystems` switch when using the `nixos-generate-config` +command to generate an initial `configuration.nix`. You will be supplying the +file system configuration details from `disk-config.nix`. Your CLI command to +generate the configuration will be: + +```console +nixos-generate-config --no-filesystems --root /mnt +``` + +This will create the file `configuration.nix` in `/mnt/etc/nixos`. + +b) Move the `disko` configuration to /etc/nixos + +```console +mv /tmp/disk-config.nix /mnt/etc/nixos +``` + +c) You can now edit `configuration.nix` as per your requirements. This is +described in Step 4 of the manual. For more information about configuring your +system, refer to the NixOS manual. +[Chapter 6, Configuration Syntax](https://nixos.org/manual/nixos/stable/index.html#sec-configuration-syntax) +describes the NixOS configuration syntax, and +[Appendix A, Configuration Options](https://nixos.org/manual/nixos/stable/options.html) +gives a list of available options. You can find also find a minimal example of a +NixOS configuration in the manual: +[Example: NixOS Configuration](https://nixos.org/manual/nixos/stable/index.html#ex-config). + +d) When editing `configuration.nix`, you will need to add the `disko` NixOS +module and `disk-config.nix` to the imports section. This section will already +include the file `./hardware-configuration.nix`, and you can add the new entries +just below this. This section will now include: + +```nix +imports = + [ # Include the results of the hardware scan. + ./hardware-configuration.nix + "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix" + ./disk-config.nix + ]; +``` + +e) If you chose the hybrid-partition scheme, then choose `grub` as a bootloader, +otherwise follow the recommendations in Step 4 of the **Installation** section +of the NixOS manual. The following configuration for `grub` works for both EFI +and BIOS systems. Add this to your configuration.nix, commenting out the +existing lines that configure `systemd-boot`. The entries will look like this: + +**Note:** Its not necessary to set `boot.loader.grub.device` here, since Disko +will take care of that automatically. + +```nix +# ... + #boot.loader.systemd-boot.enable = true; + #boot.loader.efi.canTouchEfiVariables = true; + boot.loader.grub.enable = true; + boot.loader.grub.efiSupport = true; + boot.loader.grub.efiInstallAsRemovable = true; +# ... +``` + +f) Finish the installation and reboot your machine, + +```console +nixos-install +reboot +``` diff --git a/pkgs/disko/docs/reference.md b/pkgs/disko/docs/reference.md new file mode 100644 index 0000000..3d390b9 --- /dev/null +++ b/pkgs/disko/docs/reference.md @@ -0,0 +1,47 @@ +# Reference Manual: disko + +## Module Options + +We are currently having issues being able to generate proper module option +documentation for our recursive disko types. However you can read the available +options [here](https://github.com/nix-community/disko/tree/master/lib/types). +Combined with the +[examples](https://github.com/nix-community/disko/tree/master/example) this +hopefully gives you an overview. + +## Command Line Options + +``` +Usage: ./disko [options] disk-config.nix +or ./disko [options] --flake github:somebody/somewhere#disk-config + +With flakes, disk-config is discovered first under the .diskoConfigurations top level attribute +or else from the disko module of a NixOS configuration of that name under .nixosConfigurations. + +Options: + +* -m, --mode mode + set the mode, either distroy, format, mount, format,mount or destroy,format,mount + destroy: unmount filesystems and destroy partition tables of the selected disks + format: create partition tables, zpools, lvms, raids and filesystems if they don't exist yet + mount: mount the partitions at the specified root-mountpoint + format,mount: run format and mount in sequence + destroy,format,mount: run all three modes in sequence. Previously known as --mode disko +* -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 +* --root-mountpoint /some/other/mnt + where to mount the device tree (default: /mnt) +* --dry-run + just show the path to the script instead of running it +* --no-deps + avoid adding another dependency closure to an in-memory installer + requires all necessary dependencies to be available in the environment +* --debug + run with set -x +* --yes-wipe-all-disks + skip the safety check for destroying partitions, useful for automation +``` diff --git a/pkgs/disko/docs/requirements.md b/pkgs/disko/docs/requirements.md new file mode 100644 index 0000000..93d8398 --- /dev/null +++ b/pkgs/disko/docs/requirements.md @@ -0,0 +1,9 @@ +# disko - Declarative disk partitioning + + + +[Documentation Index](./INDEX.md) + +## System Requirements + +TODO: Populate this diff --git a/pkgs/disko/docs/supportmatrix.md b/pkgs/disko/docs/supportmatrix.md new file mode 100644 index 0000000..53ee715 --- /dev/null +++ b/pkgs/disko/docs/supportmatrix.md @@ -0,0 +1,9 @@ +# disko - Declarative disk partitioning + + + +[Documentation Index](./INDEX.md) + +## Support Matrix + +TODO: Populate this diff --git a/pkgs/disko/docs/table-to-gpt.md b/pkgs/disko/docs/table-to-gpt.md new file mode 100644 index 0000000..7ca5d80 --- /dev/null +++ b/pkgs/disko/docs/table-to-gpt.md @@ -0,0 +1,137 @@ +# Migrating to the new GPT layout + +## Situation + +When evaluating your NixOS system closure the following trace appears: + +``` +trace: warning: 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. +``` + +The solution is to migrate to the new `gpt` layout type. + +## Precondition + +Disko was set up with + +- `type = "table"` and +- `format = "gpt"`, + +for example like this: + +```nix +{ + disko.devices.disk.example = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + name = "ESP"; + start = "0"; + end = "512MiB"; + fs-type = "fat32"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + } + { + name = "root"; + start = "512MiB"; + end = "100%"; + content.format = "ext4"; + } + ]; + }; + }; +} +``` + +## Remediation + +The new GPT layout (`type = "gpt"`) uses partlabels to realize the partiton +numbering. For this reason you have to manually set up partition labels, if you +want to resolve this issue. + +### Create GPT partition labels + +For each partition involved, create the partition label from these components: + +- The partition number (e.g. /dev/nvme0n**1**, or /dev/sda**1**) +- The parent type in your disko config (value of + `disko.device.disk.example.type = "disk";`) +- The parent name in your disko config (attribute name of + `disko.devices.disk.example`, so `example` in this example) +- The partition name in your disko config (attribute name of + `disko.devices.disk.content.partitions.*.name`) + +```bash +# sgdisk -c 1:disk-example-ESP /dev/nvme0n1 +# sgdisk -c 2:disk-example-zfs /dev/nvme0n1 +Warning: The kernel is still using the old partition table. +The new table will be used at the next reboot or after you +run partprobe(8) or kpartx(8) +The operation has completed successfully. +``` + +### Update disko configuration + +Make the following changes to your disko configuration: + +1. Set `disko.devices.disk.example.content.type = "gpt"` +1. Remove `disko.devices.disk.example.content.format` +1. Convert `disko.devices.disk.example.content.partitions` to an attribute set and + promote the `name` field to the key for its partition +1. Add a `priority` field to each partition, to reflect the intended partition + number + +Then rebuild your system and reboot. + +### Recovering from mistake + +If you made a mistake here, your system will be waiting for devices to appear, +and then run into timeouts. You can easily recover from this, since rebooting +into an old generation will still use the legacy way of numbering of partitions. + +## Result + +The fixed disko configuration would look like this: + +```nix +{ + disko.devices.disk.example = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512MiB"; + type = "EF00"; + priority = 1; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + priority = 2; + content.format = "ext4"; + }; + }; + }; + }; +} +``` diff --git a/pkgs/disko/docs/testing.md b/pkgs/disko/docs/testing.md new file mode 100644 index 0000000..769b1a3 --- /dev/null +++ b/pkgs/disko/docs/testing.md @@ -0,0 +1,160 @@ +# Running and debugging tests + +Disko makes extensive use of VM tests. All examples you can find in +[the example directory](../example) have a respective test suite that verifies +the example is working in [the tests directory](../tests/). They utilize the +[NixOS test functionality](https://nixos.org/manual/nixos/stable/#sec-nixos-tests). + +We use a wrapper around this called `makeDiskoTest`. There is currently (as of +2024-10-16) no documentation for all its arguments, but you can have a look at +[its current code](https://github.com/nix-community/disko/blob/master/lib/tests.nix#L44C5-L58C10), +that should already be helpful. + +However, you don't need to know about all of the inner workings to interact with +the tests effectively. For some of the most common operations, see the sections +below. + +## Run just one of the tests + +```sh +nix build --no-link .#checks.x86_64-linux.simple-efi +``` + +This will run the test in [`tests/simple-efi.nix`](../tests/simple-efi.nix), +which builds a VM with all disks specified in the +[`example/simple-efi.nix`](../example/simple-efi.nix) config connected as +virtual devices, run disko to format them, reboot, verify the VM boots properly, +and then run the code specified in `extraTestScript` to validate that the +partitions have been created and were mounted as expected. + +### How `extraTestScript` works + +This is written in Python. The most common lines you'll see look something like +this: + +```python +machine.succeed("test -b /dev/md/raid1"); +machine.succeed("mountpoint /"); +``` + +The `machine` in these is a machine object, which defines +[a multitude of functions to interact with and test](https://nixos.org/manual/nixos/stable/#ssec-machine-objects), +assumptions about the state of the VM after formatting and rebooting. + +Disko currently (as of 2024-10-16) doesn't have any tests that utilize multiple +VMs at once, so the only machine available in these scripts is always just the +default `machine`. + +## Debugging tests + +If you make changes to disko, you might break a test, or you may want to modify +a test to prevent regressions. In these cases, running the full test with +`nix build` every time is time-consuming and tedious. + +Instead, you can build and then run the VM for a test in interactive mode. This +will create the VM and all virtual disks as required by the test's config, but +allow you to interact with the machine on a terminal afterwards. + +First, build the interactive test driver and run it: + +``` +nix build .#checks.x86_64-linux.simple-efi.driverInteractive +result/bin/nixos-test-driver --keep-vm-state +``` + +This will open an IPython prompt in which you can use th same objects and +functions as in `extraTestScript`. In there, you can run + +``` +machine.shell_interact() +``` + +to start the VM and attach the terminal to it. This will also open a QEMU +window, in which you can log in as `root` with no password, but that makes it +more difficult to paste input and output. Instead, wait for the systemd messages +to settle down, and then **simply start typing**. This should make a `$` prompt +appear, indicating that the machine is ready to take commands. The NixOS manual +calls out a few special messages to look for, but these are buried underneath +the systemd logs. + +Once you are in this terminal, you're running commands on the VM. The only thing +that doesn't work here is the `exit` command. Instead, you need to press Ctrl+D +and wait for a second to return to the IPython prompt. + +In summary, a full session looks something like this: + +``` +# nix build .#checks.x86_64-linux.simple-efi.driverInteractive +# result/bin/nixos-test-driver --keep-vm-state +start all VLans +start vlan +running vlan (pid 146244; ctl /tmp/vde1.ctl) +(finished: start all VLans, in 0.00 seconds) +additionally exposed symbols: + machine, + vlan1, + start_all, test_script, machines, vlans, driver, log, os, create_machine, subtest, run_tests, join_all, retry, serial_stdout_off, serial_stdout_on, polling_condition, Machine +>>> machine.shell_interact() +machine: waiting for the VM to finish booting +machine: starting vm +machine: QEMU running (pid 146286) +machine # [ 0.000000] Linux version 6.6.48 (nixbld@localhost) (gcc (GCC) 13.3.0, GNU ld (GNU Binutils) 2.42) #1-NixOS SMP PREEMPT_DYNAMIC Thu Aug 29 15:33:59 UTC 2024 +machine # [ 0.000000] Command line: console=ttyS0 panic=1 boot.panic_on_fail clocksource=acpi_pm loglevel=7 net.ifnames=0 init=/nix/store/0a52bbvxr5p7xijbbk17qqlk8xm4790y-nixos-system-machine-test/init regInfo=/nix/store/3sh5nl75bnj1jg87p5gcrdzs0lk154ma-closure-info/registration console=ttyS0 +machine # [ 0.000000] BIOS-provided physical RAM map: +... +... more systemd messages +... +machine # [ 6.135577] dhcpcd[679]: DUID 00:01:00:01:2e:a2:74:e6:52:54:00:12:34:56 +machine # [ 6.142785] systemd[1]: Finished Kernel Auditing. +machine: Guest shell says: b'Spawning backdoor root shell...\n' +machine: connected to guest root shell +machine: (connecting took 6.61 seconds) +(finished: waiting for the VM to finish booting, in 6.99 seconds) +machine: Terminal is ready (there is no initial prompt): +machine # [ 6.265451] 8021q: 802.1Q VLAN Support v1.8 +machine # [ 6.186797] nsncd[669]: Oct 16 13:11:55.010 INFO started, config: Config { ignored_request_types: {}, worker_count: 8, handoff_timeout: 3s }, path: "/var/run/nscd/socket" +... +... more systemd messages +... +machine # [ 12.376900] systemd[1]: Reached target Host and Network Name Lookups. +machine # [ 12.379265] systemd[1]: Reached target User and Group Name Lookups. +$ lsblk +NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS +fd0 2:0 1 4K 0 disk +sr0 11:0 1 1024M 0 rom +vda 253:0 0 1G 0 disk / +vdb 253:16 0 4G 0 disk +├─vdb1 253:17 0 500M 0 part +└─vdb2 253:18 0 3.5G 0 part +``` + +You can find some additional details in +[the NixOS manual's section on interactive testing](https://nixos.org/manual/nixos/stable/#sec-running-nixos-tests-interactively). + +## Running all tests at once + +If you have a bit of experience, you might be inclined to run `nix flake check` +to run all tests at once. However, we instead recommend using +[nix-fast-build](https://github.com/Mic92/nix-fast-build). The reason for this +is that each individual test takes a while to run, but only uses <=4GiB of RAM +and a limited amount of CPU resources. This means they can easily be evaluated +and run in parallel to save time, but `nix` doesn't to that, so a full test run +takes >40 minutes on a mid-range system. With `nix-fast-build` you can scale up +the number of workers depending on your system's capabilities. It also utilizes +[`nix-output-monitor`](https://github.com/maralorn/nix-output-monitor) to give +you a progress indicator during the build process as well. For example, on a +machine with 16GB of RAM, this gives you a 2x speed up without clogging your +system: + +```sh +nix shell nixpkgs#nix-fast-build +nix-fast-build --no-link -j 2 --eval-workers 2 --flake .#checks +``` + +You can try higher numbers if you want to. Be careful with scaling up +`--eval-workers`, each of these will use 100% of a CPU core and they don't leave +any time for hyperthreading, so 4 workers will max out a a CPU with 4 cores and +8 threads, potentially rendering your system unresponsive! `-j` is less +dangerous to scale up, but you probably don't want to go higher than +`( - 4GB)/4GB` to prevent excessive swap usage, which will +would slow down the test VMs to a crawl. diff --git a/pkgs/disko/docs/upgrade-guide.md b/pkgs/disko/docs/upgrade-guide.md new file mode 100644 index 0000000..e6aa835 --- /dev/null +++ b/pkgs/disko/docs/upgrade-guide.md @@ -0,0 +1,173 @@ +# 2023-07-09 121df48 + +Changes: + +- BTRFS subvolumes are mounted if and only their `mountpoint` is set. + +Especially, if you have specific mount options for a subvolume (through +`mountOptions`), make sure to set `mountpoint` otherwise the subvolume will not +be mounted. + +This change allows more flexibility when using BTRFS, giving access to its +volume management functionality. + +It allows layouts as the following: + +```nix +content = { + type = "btrfs"; + # BTRFS partition is not mounted as it doesn't set a mountpoint explicitly + subvolumes = { + # This subvolume will not be mounted + "SYSTEM" = { }; + # mounted as "/" + "SYSTEM/rootfs" = { + mountpoint = "/"; + }; + # mounted as "/nix" + "SYSTEM/nix" = { + mountOptions = [ "compress=zstd" "noatime" ]; + mountpoint = "/nix"; + }; + # This subvolume will not be mounted + "DATA" = { }; + # mounted as "/home" + "DATA/home" = { + mountOptions = [ "compress=zstd" ]; + mountpoint = "/home"; + }; + # mounted as "/var/www" + "DATA/www" = { + mountpoint = "/var/www"; + }; + }; +}; +``` + +corresponding to the following BTRFS layout: + +``` +BTRFS partition # not mounted + | + |-SYSTEM # not mounted + | |-rootfs # mounted as "/" + | |-nix # mounted as "/nix" + | + |-DATA # not mounted + |-home # mounted as "/home" + |-www # mounted as "/var/www" +``` + +# 2023-04-07 7d70009 + +Changes: + +- ZFS datasets have been split into two types: `zfs_fs` and `zfs_volume`. +- The `zfs_type` attribute has been removed. +- The size attribute is now only available for `zfs_volume`. + +Updated example/zfs.nix file: + +```nix +{ +datasets = { + zfs_fs = { + type = "zfs_fs"; + mountpoint = "/zfs_fs"; + options."com.sun:auto-snapshot" = "true"; + }; + zfs_unmounted_fs = { + type = "zfs_fs"; + options.mountpoint = "none"; + }; + zfs_legacy_fs = { + type = "zfs_fs"; + options.mountpoint = "legacy"; + mountpoint = "/zfs_legacy_fs"; + }; + zfs_testvolume = { + type = "zfs_volume"; + size = "10M"; + content = { + type = "filesystem"; + # ... +} +``` + +Note: The `zfs_type` attribute has been replaced with a type attribute for each +dataset, and the `size` attribute is only available for `zfs_volume`. These +changes have been reflected in the `example/zfs.nix` file. + +# 2023-04-07 654ecb3 + +The `lvm_lv` type is always part of an `lvm_vg` and it is no longer necessary to +specify the type. + +This means that if you were using the `lvm_lv` type in your code, you should +remove it. For example, if you were defining an `lvm_lv` type like this: + +```nix +{ + type = "lvm_lv"; + size = "10G"; + # ... +} +``` + +You should now define it like this: + +```nix +{ + size = "10G"; + # ... +} +``` + +Note that the `type` field is no longer necessary and should be removed from +your code. + +# 2023-04-07 d6f062e + +Partition types are now always part of a table and cannot be specified +individually anymore. This change makes the library more consistent and easier +to use. + +Example of how to change code: + +Before: + +```nix +{ + type = "partition"; + name = "ESP"; + start = "1MiB"; + end = "100MiB"; + part-type = "primary"; +} +``` + +After: + +```nix +{ + name = "ESP"; + start = "1MiB"; + end = "100MiB"; + part-type = "primary"; +} +``` + +Note that the `type` field is no longer necessary and should be removed from +your code. + +# 2023-03-22 2624af6 + +disk config now needs to be inside a disko.devices attrset always + +# 2023-03-22 0577409 + +the extraArgs option in the luks type was renamed to extraFormatArgs + +# 2023-02-14 6d630b8 + +btrfs, `btrfs_subvol` filesystem and `lvm_lv` extraArgs are now lists diff --git a/pkgs/disko/example/bcachefs.nix b/pkgs/disko/example/bcachefs.nix new file mode 100644 index 0000000..c7e3bf3 --- /dev/null +++ b/pkgs/disko/example/bcachefs.nix @@ -0,0 +1,34 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/disk/by-path/pci-0000:02:00.0-nvme-1"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + end = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + name = "root"; + end = "-0"; + content = { + type = "filesystem"; + format = "bcachefs"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/boot-raid1.nix b/pkgs/disko/example/boot-raid1.nix new file mode 100644 index 0000000..39569c1 --- /dev/null +++ b/pkgs/disko/example/boot-raid1.nix @@ -0,0 +1,89 @@ +{ + disko.devices = { + disk = { + one = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + BOOT = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + mdadm = { + size = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + }; + }; + }; + two = { + type = "disk"; + device = "/dev/sdb"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + mdadm = { + size = "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 = "gpt"; + partitions.primary = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/btrfs-only-root-subvolume.nix b/pkgs/disko/example/btrfs-only-root-subvolume.nix new file mode 100644 index 0000000..46e7334 --- /dev/null +++ b/pkgs/disko/example/btrfs-only-root-subvolume.nix @@ -0,0 +1,38 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/disk/by-diskseq/1"; + content = { + type = "gpt"; + partitions = { + ESP = { + priority = 1; + name = "ESP"; + start = "1M"; + end = "128M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; # Override existing partition + mountpoint = "/"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + }; + }; + }; + }; + }; + }; +} + diff --git a/pkgs/disko/example/btrfs-subvolumes.nix b/pkgs/disko/example/btrfs-subvolumes.nix new file mode 100644 index 0000000..8f36443 --- /dev/null +++ b/pkgs/disko/example/btrfs-subvolumes.nix @@ -0,0 +1,77 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/disk/by-diskseq/1"; + content = { + type = "gpt"; + partitions = { + ESP = { + priority = 1; + name = "ESP"; + start = "1M"; + end = "128M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; # Override existing partition + # Subvolumes must set a mountpoint in order to be mounted, + # unless their parent is mounted + subvolumes = { + # Subvolume name is different from mountpoint + "/rootfs" = { + mountpoint = "/"; + }; + # Subvolume name is the same as the mountpoint + "/home" = { + mountOptions = [ "compress=zstd" ]; + mountpoint = "/home"; + }; + # Sub(sub)volume doesn't need a mountpoint as its parent is mounted + "/home/user" = { }; + # Parent is not mounted so the mountpoint must be set + "/nix" = { + mountOptions = [ "compress=zstd" "noatime" ]; + mountpoint = "/nix"; + }; + # This subvolume will be created but not mounted + "/test" = { }; + # Subvolume for the swapfile + "/swap" = { + mountpoint = "/.swapvol"; + swap = { + swapfile.size = "20M"; + swapfile2.size = "20M"; + swapfile2.path = "rel-path"; + }; + }; + }; + + mountpoint = "/partition-root"; + swap = { + swapfile = { + size = "20M"; + }; + swapfile1 = { + size = "20M"; + }; + }; + }; + }; + }; + }; + }; + }; + }; +} + diff --git a/pkgs/disko/example/complex.nix b/pkgs/disko/example/complex.nix new file mode 100644 index 0000000..210105e --- /dev/null +++ b/pkgs/disko/example/complex.nix @@ -0,0 +1,192 @@ +{ + disko.devices = { + disk = { + disk0 = { + type = "disk"; + device = "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001"; + content = { + type = "gpt"; + partitions = { + ESP = { + start = "1M"; + end = "128M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + }; + }; + }; + disk1 = { + type = "disk"; + device = "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00002"; + content = { + type = "gpt"; + partitions = { + luks = { + start = "1M"; + size = "100%"; + content = { + type = "luks"; + name = "crypted1"; + settings.keyFile = "/tmp/secret.key"; + additionalKeyFiles = [ "/tmp/additionalSecret.key" ]; + extraFormatArgs = [ + "--iter-time 1" # insecure but fast for tests + ]; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + disk2 = { + type = "disk"; + device = "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00003"; + content = { + type = "gpt"; + partitions = { + luks = { + start = "1M"; + size = "100%"; + content = { + type = "luks"; + name = "crypted2"; + settings = { + keyFile = "/tmp/secret.key"; + keyFileSize = 8; + keyFileOffset = 2; + }; + extraFormatArgs = [ + "--iter-time 1" # insecure but fast for tests + ]; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + }; + mdadm = { + raid1 = { + type = "mdadm"; + level = 1; + content = { + type = "gpt"; + partitions = { + bla = { + start = "1M"; + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4_mdadm_lvm"; + }; + }; + }; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + root = { + size = "10M"; + lvm_type = "mirror"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4_on_lvm"; + mountOptions = [ + "defaults" + ]; + postMountHook = '' + touch /mnt/ext4_on_lvm/file-from-postMountHook + ''; + }; + }; + raid1 = { + size = "30M"; + lvm_type = "raid0"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + raid2 = { + size = "30M"; + lvm_type = "raid0"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + zfs1 = { + size = "128M"; + lvm_type = "raid0"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + zfs2 = { + size = "128M"; + lvm_type = "raid0"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + zpool = { + zroot = { + type = "zpool"; + mode = "mirror"; + rootFsOptions = { + compression = "zstd"; + "com.sun:auto-snapshot" = "false"; + }; + mountpoint = "/"; + + datasets = { + zfs_fs = { + type = "zfs_fs"; + mountpoint = "/zfs_fs"; + options."com.sun:auto-snapshot" = "true"; + }; + zfs_unmounted_fs = { + type = "zfs_fs"; + options.mountpoint = "none"; + }; + zfs_legacy_fs = { + type = "zfs_fs"; + options.mountpoint = "legacy"; + mountpoint = "/zfs_legacy_fs"; + }; + zfs_testvolume = { + type = "zfs_volume"; + size = "10M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4onzfs"; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/f2fs.nix b/pkgs/disko/example/f2fs.nix new file mode 100644 index 0000000..baf8852 --- /dev/null +++ b/pkgs/disko/example/f2fs.nix @@ -0,0 +1,41 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/disk/by-path/pci-0000:02:00.0-nvme-1"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + end = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + name = "root"; + end = "-0"; + content = { + type = "filesystem"; + format = "f2fs"; + mountpoint = "/"; + extraArgs = [ + "-O" + "extra_attr,inode_checksum,sb_checksum,compression" + ]; + mountOptions = [ + "compress_algorithm=zstd:6,compress_chksum,atgc,gc_merge,lazytime,nodiscard" + ]; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/gpt-bios-compat.nix b/pkgs/disko/example/gpt-bios-compat.nix new file mode 100644 index 0000000..4be1942 --- /dev/null +++ b/pkgs/disko/example/gpt-bios-compat.nix @@ -0,0 +1,28 @@ +# Example to create a bios compatible gpt partition +{ + disko.devices = { + disk = { + main = { + device = "/dev/vdb"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/gpt-name-with-whitespace.nix b/pkgs/disko/example/gpt-name-with-whitespace.nix new file mode 100644 index 0000000..22a74b5 --- /dev/null +++ b/pkgs/disko/example/gpt-name-with-whitespace.nix @@ -0,0 +1,48 @@ +{ + disko.devices = { + disk = { + vdb = { + device = "/dev/disk/by-id/some-disk-id"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "100M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + "name with spaces" = { + size = "100M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/name with spaces"; + }; + }; + "name^with\\some@special#chars" = { + size = "100M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/name^with\\some@special#chars"; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/gpt-unformatted.nix b/pkgs/disko/example/gpt-unformatted.nix new file mode 100644 index 0000000..871e972 --- /dev/null +++ b/pkgs/disko/example/gpt-unformatted.nix @@ -0,0 +1,36 @@ +# Example to create a GPT partition but doesn't format it +{ + disko.devices = { + disk = { + main = { + device = "/dev/vdb"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "100M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + empty = { + size = "1G"; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/hybrid-mbr.nix b/pkgs/disko/example/hybrid-mbr.nix new file mode 100644 index 0000000..d96c898 --- /dev/null +++ b/pkgs/disko/example/hybrid-mbr.nix @@ -0,0 +1,48 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/sdb"; + content = { + type = "gpt"; + efiGptPartitionFirst = false; + partitions = { + TOW-BOOT-FI = { + priority = 1; + type = "EF00"; + size = "32M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = null; + }; + hybrid = { + mbrPartitionType = "0x0c"; + mbrBootableFlag = false; + }; + }; + ESP = { + type = "EF00"; + size = "512M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/hybrid-tmpfs-on-root.nix b/pkgs/disko/example/hybrid-tmpfs-on-root.nix new file mode 100644 index 0000000..55dbbf5 --- /dev/null +++ b/pkgs/disko/example/hybrid-tmpfs-on-root.nix @@ -0,0 +1,44 @@ +{ + disko.devices = { + disk.main = { + device = "/dev/disk/by-id/ata-Samsung_SSD_860_EVO_500GB_S3Z1NB0K303456L"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + ESP = { + name = "ESP"; + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + nix = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/nix"; + }; + }; + }; + }; + }; + nodev."/" = { + fsType = "tmpfs"; + mountOptions = [ + "size=2G" + "defaults" + "mode=755" + ]; + }; + }; +} diff --git a/pkgs/disko/example/hybrid.nix b/pkgs/disko/example/hybrid.nix new file mode 100644 index 0000000..9825e85 --- /dev/null +++ b/pkgs/disko/example/hybrid.nix @@ -0,0 +1,37 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/disk/by-id/ata-Samsung_SSD_850_EVO_250GB_S21PNXAGB12345"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/legacy-table-with-whitespace.nix b/pkgs/disko/example/legacy-table-with-whitespace.nix new file mode 100644 index 0000000..5eb28ab --- /dev/null +++ b/pkgs/disko/example/legacy-table-with-whitespace.nix @@ -0,0 +1,50 @@ +{ + disko.devices = { + disk = { + vdb = { + device = "/dev/sda"; + type = "disk"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + name = "ESP"; + start = "1MiB"; + end = "100MiB"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + } + { + name = "name with spaces"; + start = "100MiB"; + end = "200MiB"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/name_with_spaces"; + }; + } + { + name = "root"; + start = "200MiB"; + end = "100%"; + part-type = "primary"; + bootable = true; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + } + ]; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/legacy-table.nix b/pkgs/disko/example/legacy-table.nix new file mode 100644 index 0000000..b022e8f --- /dev/null +++ b/pkgs/disko/example/legacy-table.nix @@ -0,0 +1,40 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/sda"; + type = "disk"; + content = { + type = "table"; + format = "gpt"; + partitions = [ + { + name = "ESP"; + start = "1M"; + end = "500M"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + } + { + name = "root"; + start = "500M"; + end = "100%"; + part-type = "primary"; + bootable = true; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + } + ]; + }; + }; + }; + }; +} + diff --git a/pkgs/disko/example/long-device-name.nix b/pkgs/disko/example/long-device-name.nix new file mode 100644 index 0000000..ab43696 --- /dev/null +++ b/pkgs/disko/example/long-device-name.nix @@ -0,0 +1,34 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/disk/by-id/some-disk-id"; + name = "this-is-some-super-long-name-to-test-what-happens-when-the-name-is-too-long"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/luks-btrfs-raid.nix b/pkgs/disko/example/luks-btrfs-raid.nix new file mode 100644 index 0000000..13ac197 --- /dev/null +++ b/pkgs/disko/example/luks-btrfs-raid.nix @@ -0,0 +1,78 @@ +{ + disko.devices = { + disk = { + # Devices will be mounted and formatted in alphabetical order, and btrfs can only mount raids + # when all devices are present. So we define an "empty" luks device on the first disk, + # and the actual btrfs raid on the second disk, and the name of these entries matters! + disk1 = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + crypt_p1 = { + size = "100%"; + content = { + type = "luks"; + name = "p1"; # device-mapper name when decrypted + # Remove settings.keyFile if you want to use interactive password entry + settings = { + allowDiscards = true; + keyFile = "/tmp/secret.key"; + }; + }; + }; + }; + }; + }; + disk2 = { + type = "disk"; + device = "/dev/sdb"; + content = { + type = "gpt"; + partitions = { + crypt_p2 = { + size = "100%"; + content = { + type = "luks"; + name = "p2"; + # Remove settings.keyFile if you want to use interactive password entry + settings = { + allowDiscards = true; + keyFile = "/tmp/secret.key"; # Same key for both devices + }; + content = { + type = "btrfs"; + extraArgs = [ + "-d raid1" + "/dev/mapper/p1" # Use decrypted mapped device, same name as defined in disk1 + ]; + subvolumes = { + "/root" = { + mountpoint = "/"; + mountOptions = [ + "rw" + "relatime" + "ssd" + ]; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/luks-btrfs-subvolumes.nix b/pkgs/disko/example/luks-btrfs-subvolumes.nix new file mode 100644 index 0000000..eb1b6c0 --- /dev/null +++ b/pkgs/disko/example/luks-btrfs-subvolumes.nix @@ -0,0 +1,61 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted"; + # disable settings.keyFile if you want to use interactive password entry + #passwordFile = "/tmp/secret.key"; # Interactive + settings = { + allowDiscards = true; + keyFile = "/tmp/secret.key"; + }; + additionalKeyFiles = [ "/tmp/additionalSecret.key" ]; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; + subvolumes = { + "/root" = { + mountpoint = "/"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "/home" = { + mountpoint = "/home"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "/nix" = { + mountpoint = "/nix"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; + "/swap" = { + mountpoint = "/.swapvol"; + swap.swapfile.size = "20M"; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/luks-interactive-login.nix b/pkgs/disko/example/luks-interactive-login.nix new file mode 100644 index 0000000..0aaf776 --- /dev/null +++ b/pkgs/disko/example/luks-interactive-login.nix @@ -0,0 +1,39 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted"; + settings.allowDiscards = true; + passwordFile = "/tmp/secret.key"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/luks-lvm.nix b/pkgs/disko/example/luks-lvm.nix new file mode 100644 index 0000000..1bede94 --- /dev/null +++ b/pkgs/disko/example/luks-lvm.nix @@ -0,0 +1,73 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted"; + extraOpenArgs = [ ]; + settings = { + # if you want to use the key for interactive login be sure there is no trailing newline + # for example use `echo -n "password" > /tmp/secret.key` + keyFile = "/tmp/secret.key"; + allowDiscards = true; + }; + additionalKeyFiles = [ "/tmp/additionalSecret.key" ]; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ + "defaults" + ]; + }; + }; + home = { + size = "10M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + raw = { + size = "10M"; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/luks-on-mdadm.nix b/pkgs/disko/example/luks-on-mdadm.nix new file mode 100644 index 0000000..6a33208 --- /dev/null +++ b/pkgs/disko/example/luks-on-mdadm.nix @@ -0,0 +1,58 @@ +{ lib, ... }: +{ + disko.devices.disk = lib.genAttrs [ "a" "b" ] (name: { + type = "disk"; + device = "/dev/sd${name}"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + mdadm = { + size = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + }; + }; + }); + disko.devices.mdadm = { + boot = { + type = "mdadm"; + level = 1; + metadata = "1.0"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + raid1 = { + type = "mdadm"; + level = 1; + content = { + type = "luks"; + name = "crypted"; + settings.keyFile = "/tmp/secret.key"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; +} + diff --git a/pkgs/disko/example/lvm-raid.nix b/pkgs/disko/example/lvm-raid.nix new file mode 100644 index 0000000..2e36fff --- /dev/null +++ b/pkgs/disko/example/lvm-raid.nix @@ -0,0 +1,94 @@ +{ + disko.devices = { + disk = { + one = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "500M"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + primary = { + size = "100%"; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + two = { + type = "disk"; + device = "/dev/sdb"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "500M"; + type = "EF00"; + content = { + type = "mdraid"; + name = "boot"; + }; + }; + primary = { + size = "100%"; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + mdadm = { + boot = { + type = "mdadm"; + level = 1; + metadata = "1.0"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + root = { + size = "100M"; + lvm_type = "mirror"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ + "defaults" + ]; + }; + }; + home = { + size = "10M"; + lvm_type = "raid0"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/lvm-sizes-sort.nix b/pkgs/disko/example/lvm-sizes-sort.nix new file mode 100644 index 0000000..1309e3b --- /dev/null +++ b/pkgs/disko/example/lvm-sizes-sort.nix @@ -0,0 +1,64 @@ +{ + disko.devices = { + disk = { + one = { + type = "disk"; + device = "/dev/disk/by-id/ata-VMware_Virtual_SATA_CDRW_Drive_00000000000000000001"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + primary = { + size = "100%"; + content = { + type = "lvm_pv"; + vg = "pool"; + }; + }; + }; + }; + }; + }; + lvm_vg = { + pool = { + type = "lvm_vg"; + lvs = { + aaa = { + size = "1M"; + }; + zzz = { + size = "1M"; + }; + root = { + size = "100M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ + "defaults" + ]; + }; + }; + home = { + size = "100%FREE"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/lvm-thin.nix b/pkgs/disko/example/lvm-thin.nix new file mode 100644 index 0000000..054f5a3 --- /dev/null +++ b/pkgs/disko/example/lvm-thin.nix @@ -0,0 +1,69 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + primary = { + size = "100%"; + content = { + type = "lvm_pv"; + vg = "mainpool"; + }; + }; + }; + }; + }; + }; + lvm_vg = { + mainpool = { + type = "lvm_vg"; + lvs = { + thinpool = { + size = "100M"; + lvm_type = "thin-pool"; + }; + root = { + size = "10M"; + lvm_type = "thinlv"; + pool = "thinpool"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ + "defaults" + ]; + }; + }; + home = { + size = "10M"; + lvm_type = "thinlv"; + pool = "thinpool"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/home"; + }; + }; + raw = { + size = "10M"; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/mdadm-raid0.nix b/pkgs/disko/example/mdadm-raid0.nix new file mode 100644 index 0000000..dcdebc5 --- /dev/null +++ b/pkgs/disko/example/mdadm-raid0.nix @@ -0,0 +1,65 @@ +{ + disko.devices = { + disk = { + disk1 = { + type = "disk"; + device = "/dev/my-disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + mdadm = { + size = "100%"; + content = { + type = "mdraid"; + name = "raid0"; + }; + }; + }; + }; + }; + disk2 = { + type = "disk"; + device = "/dev/my-disk2"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + mdadm = { + size = "100%"; + content = { + type = "mdraid"; + name = "raid0"; + }; + }; + }; + }; + }; + }; + mdadm = { + raid0 = { + type = "mdadm"; + level = 0; + content = { + type = "gpt"; + partitions = { + primary = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/mdadm.nix b/pkgs/disko/example/mdadm.nix new file mode 100644 index 0000000..b6c7883 --- /dev/null +++ b/pkgs/disko/example/mdadm.nix @@ -0,0 +1,65 @@ +{ + disko.devices = { + disk = { + disk1 = { + type = "disk"; + device = "/dev/my-disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + mdadm = { + size = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + }; + }; + }; + disk2 = { + type = "disk"; + device = "/dev/my-disk2"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + mdadm = { + size = "100%"; + content = { + type = "mdraid"; + name = "raid1"; + }; + }; + }; + }; + }; + }; + mdadm = { + raid1 = { + type = "mdadm"; + level = 1; + content = { + type = "gpt"; + partitions = { + primary = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/multi-device-no-deps.nix b/pkgs/disko/example/multi-device-no-deps.nix new file mode 100644 index 0000000..dda6782 --- /dev/null +++ b/pkgs/disko/example/multi-device-no-deps.nix @@ -0,0 +1,40 @@ +{ + disko.devices = { + disk = { + disk0 = { + device = "/dev/vda"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + nix = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/a"; + }; + }; + }; + }; + }; + disk1 = { + device = "/dev/vdb"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/b"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/negative-size.nix b/pkgs/disko/example/negative-size.nix new file mode 100644 index 0000000..d4d4753 --- /dev/null +++ b/pkgs/disko/example/negative-size.nix @@ -0,0 +1,23 @@ +{ + disko.devices = { + disk = { + disk0 = { + device = "/dev/disk/by-id/ata-disk0"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + nix = { + end = "-10M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/non-root-zfs.nix b/pkgs/disko/example/non-root-zfs.nix new file mode 100644 index 0000000..2e3f4ac --- /dev/null +++ b/pkgs/disko/example/non-root-zfs.nix @@ -0,0 +1,109 @@ +{ + disko.devices = { + disk = { + x = { + type = "disk"; + device = "/dev/sdx"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "64M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + y = { + type = "disk"; + device = "/dev/sdy"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "storage"; + }; + }; + }; + }; + }; + z = { + type = "disk"; + device = "/dev/sdz"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "storage"; + }; + }; + }; + }; + }; + a = { + type = "disk"; + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "storage2"; + }; + }; + }; + }; + }; + }; + zpool = { + storage = { + type = "zpool"; + mode = "mirror"; + mountpoint = "/storage"; + + datasets = { + dataset = { + type = "zfs_fs"; + mountpoint = "/storage/dataset"; + }; + }; + }; + storage2 = { + type = "zpool"; + mountpoint = "/storage2"; + rootFsOptions = { + canmount = "off"; + }; + + datasets = { + dataset = { + type = "zfs_fs"; + mountpoint = "/storage2/dataset"; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/simple-efi.nix b/pkgs/disko/example/simple-efi.nix new file mode 100644 index 0000000..435596b --- /dev/null +++ b/pkgs/disko/example/simple-efi.nix @@ -0,0 +1,33 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/disk/by-id/some-disk-id"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/stand-alone/configuration.nix b/pkgs/disko/example/stand-alone/configuration.nix new file mode 100644 index 0000000..6161cea --- /dev/null +++ b/pkgs/disko/example/stand-alone/configuration.nix @@ -0,0 +1,54 @@ +{ pkgs +, lib +, ... +}: +let + # We just import from the repository for testing here: + disko = import ../../. { + inherit lib; + }; + # In your own system use something like this: + #import (builtins.fetchGit { + # url = "https://github.com/nix-community/disko"; + # ref = "master"; + #}) { + # inherit lib; + #}; + cfg.disko.devices = { + disk = { + sda = { + device = "/dev/sda"; + type = "disk"; + content = { + type = "table"; + format = "msdos"; + partitions = [ + { + name = "root"; + part-type = "primary"; + start = "1M"; + end = "100%"; + bootable = true; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + } + ]; + }; + }; + }; + }; +in +{ + imports = [ + (disko.config cfg) + ]; + boot.loader.grub.devices = [ "/dev/sda" ]; + system.stateVersion = "22.05"; + environment.systemPackages = with pkgs; [ + (pkgs.writeScriptBin "tsp-create" (disko.create cfg)) + (pkgs.writeScriptBin "tsp-mount" (disko.mount cfg)) + ]; +} diff --git a/pkgs/disko/example/swap.nix b/pkgs/disko/example/swap.nix new file mode 100644 index 0000000..b5a61b7 --- /dev/null +++ b/pkgs/disko/example/swap.nix @@ -0,0 +1,49 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/vdb"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + end = "-1G"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + encryptedSwap = { + size = "10M"; + content = { + type = "swap"; + randomEncryption = true; + priority = 100; # prefer to encrypt as long as we have space for it + }; + }; + plainSwap = { + size = "100%"; + content = { + type = "swap"; + discardPolicy = "both"; + resumeDevice = true; # resume from hiberation from this device + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/tmpfs.nix b/pkgs/disko/example/tmpfs.nix new file mode 100644 index 0000000..4f0c3a3 --- /dev/null +++ b/pkgs/disko/example/tmpfs.nix @@ -0,0 +1,41 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/vdb"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + nodev = { + "/tmp" = { + fsType = "tmpfs"; + mountOptions = [ + "size=200M" + ]; + }; + }; + }; +} diff --git a/pkgs/disko/example/with-lib.nix b/pkgs/disko/example/with-lib.nix new file mode 100644 index 0000000..5558fac --- /dev/null +++ b/pkgs/disko/example/with-lib.nix @@ -0,0 +1,27 @@ +# Example to create a bios compatible gpt partition +{ disks ? [ "/dev/vdb" ], lib, ... }: { + disko.devices = { + disk = lib.genAttrs disks (device: { + name = lib.replaceStrings [ "/" ] [ "_" ] device; + device = device; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }); + }; +} diff --git a/pkgs/disko/example/xfs-with-quota.nix b/pkgs/disko/example/xfs-with-quota.nix new file mode 100644 index 0000000..f4b0fab --- /dev/null +++ b/pkgs/disko/example/xfs-with-quota.nix @@ -0,0 +1,34 @@ +{ + disko.devices = { + disk = { + main = { + device = "/dev/disk/by-id/some-disk-id"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "xfs"; + mountpoint = "/"; + mountOptions = [ "defaults" "pquota" ]; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/zfs-over-legacy.nix b/pkgs/disko/example/zfs-over-legacy.nix new file mode 100644 index 0000000..273975b --- /dev/null +++ b/pkgs/disko/example/zfs-over-legacy.nix @@ -0,0 +1,58 @@ +# systemd will mount an ext4 filesystem at / and zfs will mount the dataset underneath it +{ + disko.devices = { + disk = { + disk1 = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "500M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + primary = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + disk2 = { + type = "disk"; + device = "/dev/vdc"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + zpool = { + zroot = { + type = "zpool"; + datasets = { + "root" = { + type = "zfs_fs"; + options.mountpoint = "none"; + }; + "root/zfs_fs" = { + type = "zfs_fs"; + mountpoint = "/zfs_fs"; + options."com.sun:auto-snapshot" = "true"; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/zfs-with-vdevs.nix b/pkgs/disko/example/zfs-with-vdevs.nix new file mode 100644 index 0000000..effe53e --- /dev/null +++ b/pkgs/disko/example/zfs-with-vdevs.nix @@ -0,0 +1,305 @@ +{ + disko.devices = { + disk = { + data1 = { + type = "disk"; + device = "/dev/vda"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "64M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + data2 = { + type = "disk"; + device = "/dev/vdb"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + data3 = { + type = "disk"; + device = "/dev/vdc"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + spare = { + type = "disk"; + device = "/dev/vdd"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + log1 = { + type = "disk"; + device = "/dev/vde"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + log2 = { + type = "disk"; + device = "/dev/vdf"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + log3 = { + type = "disk"; + device = "/dev/vdg"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + dedup1 = { + type = "disk"; + device = "/dev/vdh"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + dedup2 = { + type = "disk"; + device = "/dev/vdi"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + dedup3 = { + type = "disk"; + device = "/dev/vdj"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + special1 = { + type = "disk"; + device = "/dev/vdk"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + special2 = { + type = "disk"; + device = "/dev/vdl"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + special3 = { + type = "disk"; + device = "/dev/vdm"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + cache = { + type = "disk"; + device = "/dev/vdn"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + }; + zpool = { + zroot = { + type = "zpool"; + mode = { + topology = { + type = "topology"; + vdev = [ + { + # This syntax expects a disk called 'data3' with a gpt partition called 'zfs'. + members = [ "data1" ]; + # It's also possible to use the full path of the device or partition + # members = [ "/dev/disk/by-id/wwn-0x5000c500af8b2a14" ]; + } + { + mode = "mirror"; + members = [ "data2" "data3" ]; + } + ]; + spare = [ "spare" ]; + log = [ + { + mode = "mirror"; + members = [ "log1" "log2" ]; + } + { + members = [ "log3" ]; + } + ]; + dedup = [ + { + mode = "mirror"; + members = [ "dedup1" "dedup2" ]; + } + { + members = [ "dedup3" ]; + } + ]; + special = [ + { + mode = "mirror"; + members = [ "special1" "special2" ]; + } + { + members = [ "special3" ]; + } + ]; + cache = [ "cache" ]; + }; + }; + + rootFsOptions = { + compression = "zstd"; + "com.sun:auto-snapshot" = "false"; + }; + mountpoint = "/"; + datasets = { + # See examples/zfs.nix for more comprehensive usage. + zfs_fs = { + type = "zfs_fs"; + mountpoint = "/zfs_fs"; + options."com.sun:auto-snapshot" = "true"; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/example/zfs.nix b/pkgs/disko/example/zfs.nix new file mode 100644 index 0000000..5ec99b4 --- /dev/null +++ b/pkgs/disko/example/zfs.nix @@ -0,0 +1,119 @@ +{ + disko.devices = { + disk = { + x = { + type = "disk"; + device = "/dev/sdx"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "64M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + y = { + type = "disk"; + device = "/dev/sdy"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "zroot"; + }; + }; + }; + }; + }; + }; + zpool = { + zroot = { + type = "zpool"; + mode = "mirror"; + # Workaround: cannot import 'zroot': I/O error in disko tests + options.cachefile = "none"; + rootFsOptions = { + compression = "zstd"; + "com.sun:auto-snapshot" = "false"; + }; + mountpoint = "/"; + postCreateHook = "zfs list -t snapshot -H -o name | grep -E '^zroot@blank$' || zfs snapshot zroot@blank"; + + datasets = { + zfs_fs = { + type = "zfs_fs"; + mountpoint = "/zfs_fs"; + options."com.sun:auto-snapshot" = "true"; + }; + zfs_unmounted_fs = { + type = "zfs_fs"; + options.mountpoint = "none"; + }; + zfs_legacy_fs = { + type = "zfs_fs"; + options.mountpoint = "legacy"; + mountpoint = "/zfs_legacy_fs"; + }; + zfs_volume = { + type = "zfs_volume"; + size = "10M"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4onzfs"; + }; + }; + zfs_encryptedvolume = { + type = "zfs_volume"; + size = "10M"; + options = { + encryption = "aes-256-gcm"; + keyformat = "passphrase"; + keylocation = "file:///tmp/secret.key"; + }; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/ext4onzfsencrypted"; + }; + }; + encrypted = { + type = "zfs_fs"; + options = { + mountpoint = "none"; + encryption = "aes-256-gcm"; + keyformat = "passphrase"; + keylocation = "file:///tmp/secret.key"; + }; + # use this to read the key during boot + # postCreateHook = '' + # zfs set keylocation="prompt" "zroot/$name"; + # ''; + }; + "encrypted/test" = { + type = "zfs_fs"; + mountpoint = "/zfs_crypted"; + }; + }; + }; + }; + }; +} diff --git a/pkgs/disko/flake.lock b/pkgs/disko/flake.lock new file mode 100644 index 0000000..ca4a6b9 --- /dev/null +++ b/pkgs/disko/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1734435836, + "narHash": "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4989a246d7a390a859852baddb1013f825435cee", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/pkgs/disko/flake.nix b/pkgs/disko/flake.nix new file mode 100644 index 0000000..da3bfcb --- /dev/null +++ b/pkgs/disko/flake.nix @@ -0,0 +1,155 @@ +{ + description = "Disko - declarative disk partitioning"; + + # FIXME: in future we don't want lock here to give precedence to a USB live-installer's registry, + # but garnix currently does not allow this. + #inputs.nixpkgs.url = "nixpkgs"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { self, nixpkgs, ... }: + let + lib = nixpkgs.lib; + supportedSystems = [ + "x86_64-linux" + "i686-linux" + "aarch64-linux" + "riscv64-linux" + ]; + forAllSystems = lib.genAttrs supportedSystems; + + versionInfo = import ./version.nix; + version = versionInfo.version + (lib.optionalString (!versionInfo.released) "-dirty"); + + diskoLib = import ./lib { + inherit (nixpkgs) lib; + }; + in + { + nixosModules.default = self.nixosModules.disko; # convention + nixosModules.disko.imports = [ ./module.nix ]; + lib = diskoLib; + packages = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + disko = pkgs.callPackage ./package.nix { diskoVersion = version; }; + # alias to make `nix run` more convenient + disko-install = self.packages.${system}.disko.overrideAttrs (_old: { + name = "disko-install"; + }); + default = self.packages.${system}.disko; + + create-release = pkgs.callPackage ./scripts/create-release.nix { }; + } // pkgs.lib.optionalAttrs (!pkgs.stdenv.buildPlatform.isRiscV64) { + disko-doc = pkgs.callPackage ./doc.nix { }; + }); + # TODO: disable bios-related tests on aarch64... + # Run checks: nix flake check -L + checks = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + # FIXME: aarch64-linux seems to hang on boot + nixosTests = lib.optionalAttrs pkgs.stdenv.hostPlatform.isx86_64 (import ./tests { + inherit pkgs; + makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix"); + eval-config = import (pkgs.path + "/nixos/lib/eval-config.nix"); + }); + + disko-install = pkgs.callPackage ./tests/disko-install { + inherit self; + diskoVersion = version; + }; + + shellcheck = pkgs.runCommand "shellcheck" { nativeBuildInputs = [ pkgs.shellcheck ]; } '' + cd ${./.} + shellcheck disk-deactivate/disk-deactivate disko + touch $out + ''; + + jsonTypes = pkgs.writeTextFile { name = "jsonTypes"; text = (builtins.toJSON diskoLib.jsonTypes); }; + in + # FIXME: aarch64-linux seems to hang on boot + lib.optionalAttrs pkgs.stdenv.hostPlatform.isx86_64 (nixosTests // { inherit disko-install; }) // + pkgs.lib.optionalAttrs (!pkgs.stdenv.buildPlatform.isRiscV64 && !pkgs.stdenv.hostPlatform.isx86_32) { + inherit shellcheck jsonTypes; + inherit (self.packages.${system}) disko-doc; + }); + + nixosConfigurations.testmachine = lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ./tests/disko-install/configuration.nix + ./example/hybrid.nix + ./module.nix + ]; + }; + formatter = forAllSystems (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + pkgs.writeShellApplication { + name = "format"; + runtimeInputs = with pkgs; [ + nixpkgs-fmt + deno + deadnix + ]; + text = '' + showUsage() { + cat < +, rootMountPoint ? "/mnt" +, makeTest ? import +, eval-config ? import +}: +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..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 = ""; + args._parent.name = ""; + args._parent.type = ""; + }; + name = ""; + }; + parent = { }; + device = "/dev/"; + # 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 = ""; + }; + }; + }; + diskoLib = { + optionTypes.absolute-pathname = "absolute-pathname"; + # Spoof these types to avoid infinite recursion + deviceType = _: ""; + 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 diff --git a/pkgs/disko/lib/interactive-vm.nix b/pkgs/disko/lib/interactive-vm.nix new file mode 100644 index 0000000..67d5eb8 --- /dev/null +++ b/pkgs/disko/lib/interactive-vm.nix @@ -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 + ''; +} diff --git a/pkgs/disko/lib/make-disk-image.nix b/pkgs/disko/lib/make-disk-image.nix new file mode 100644 index 0000000..39b55f0 --- /dev/null +++ b/pkgs/disko/lib/make-disk-image.nix @@ -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 + 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 + 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 + 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 / + ''; +} diff --git a/pkgs/disko/lib/tests.nix b/pkgs/disko/lib/tests.nix new file mode 100644 index 0000000..3cef657 --- /dev/null +++ b/pkgs/disko/lib/tests.nix @@ -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 { } + , 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 + 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 diff --git a/pkgs/disko/lib/types/btrfs.nix b/pkgs/disko/lib/types/btrfs.nix new file mode 100644 index 0000000..d9cff2c --- /dev/null +++ b/pkgs/disko/lib/types/btrfs.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/disk.nix b/pkgs/disko/lib/types/disk.nix new file mode 100644 index 0000000..343f968 --- /dev/null +++ b/pkgs/disko/lib/types/disk.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/filesystem.nix b/pkgs/disko/lib/types/filesystem.nix new file mode 100644 index 0000000..e59e186 --- /dev/null +++ b/pkgs/disko/lib/types/filesystem.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/gpt.nix b/pkgs/disko/lib/types/gpt.nix new file mode 100644 index 0000000..27a87fc --- /dev/null +++ b/pkgs/disko/lib/types/gpt.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/luks.nix b/pkgs/disko/lib/types/luks.nix new file mode 100644 index 0000000..738acd1 --- /dev/null +++ b/pkgs/disko/lib/types/luks.nix @@ -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.)"; + 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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/lvm_pv.nix b/pkgs/disko/lib/types/lvm_pv.nix new file mode 100644 index 0000000..6b0ae41 --- /dev/null +++ b/pkgs/disko/lib/types/lvm_pv.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/lvm_vg.nix b/pkgs/disko/lib/types/lvm_vg.nix new file mode 100644 index 0000000..b396fee --- /dev/null +++ b/pkgs/disko/lib/types/lvm_vg.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/mdadm.nix b/pkgs/disko/lib/types/mdadm.nix new file mode 100644 index 0000000..f29d6a1 --- /dev/null +++ b/pkgs/disko/lib/types/mdadm.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/mdraid.nix b/pkgs/disko/lib/types/mdraid.nix new file mode 100644 index 0000000..b00626a --- /dev/null +++ b/pkgs/disko/lib/types/mdraid.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/nodev.nix b/pkgs/disko/lib/types/nodev.nix new file mode 100644 index 0000000..6a45865 --- /dev/null +++ b/pkgs/disko/lib/types/nodev.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/swap.nix b/pkgs/disko/lib/types/swap.nix new file mode 100644 index 0000000..83a6a74 --- /dev/null +++ b/pkgs/disko/lib/types/swap.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/table.nix b/pkgs/disko/lib/types/table.nix new file mode 100644 index 0000000..6640da5 --- /dev/null +++ b/pkgs/disko/lib/types/table.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/zfs.nix b/pkgs/disko/lib/types/zfs.nix new file mode 100644 index 0000000..f9e9778 --- /dev/null +++ b/pkgs/disko/lib/types/zfs.nix @@ -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"; + }; + }; +} diff --git a/pkgs/disko/lib/types/zfs_fs.nix b/pkgs/disko/lib/types/zfs_fs.nix new file mode 100644 index 0000000..1399ce9 --- /dev/null +++ b/pkgs/disko/lib/types/zfs_fs.nix @@ -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"; + }; + }; +} + diff --git a/pkgs/disko/lib/types/zfs_volume.nix b/pkgs/disko/lib/types/zfs_volume.nix new file mode 100644 index 0000000..e4c1e90 --- /dev/null +++ b/pkgs/disko/lib/types/zfs_volume.nix @@ -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"; + }; + }; +} + diff --git a/pkgs/disko/lib/types/zpool.nix b/pkgs/disko/lib/types/zpool.nix new file mode 100644 index 0000000..8034048 --- /dev/null +++ b/pkgs/disko/lib/types/zpool.nix @@ -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; + }; + }; +} diff --git a/pkgs/disko/module.nix b/pkgs/disko/module.nix new file mode 100644 index 0000000..6543e11 --- /dev/null +++ b/pkgs/disko/module.nix @@ -0,0 +1,266 @@ +{ config, lib, pkgs, extendModules, diskoLib, ... }: +let + cfg = config.disko; + + vmVariantWithDisko = extendModules { + modules = [ + ./lib/interactive-vm.nix + config.disko.tests.extraConfig + ]; + }; +in +{ + imports = [ ./lib/make-disk-image.nix ]; + + options.disko = { + imageBuilder = { + qemu = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + the qemu emulator string used when building disk images via make-disk-image.nix. + Useful when using binfmt on your build host, and wanting to build disk + images for a foreign architecture + ''; + default = null; + example = lib.literalExpression "\${pkgs.qemu_kvm}/bin/qemu-system-aarch64"; + }; + + pkgs = lib.mkOption { + type = lib.types.attrs; + description = '' + the pkgs instance used when building disk images via make-disk-image.nix. + Useful when the config's kernel won't boot in the image-builder. + ''; + default = pkgs; + defaultText = lib.literalExpression "pkgs"; + example = lib.literalExpression "pkgs"; + }; + + kernelPackages = lib.mkOption { + type = lib.types.attrs; + description = '' + the kernel used when building disk images via make-disk-image.nix. + Useful when the config's kernel won't boot in the image-builder. + ''; + default = config.boot.kernelPackages; + defaultText = lib.literalExpression "config.boot.kernelPackages"; + example = lib.literalExpression "pkgs.linuxPackages_testing"; + }; + + extraRootModules = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + extra kernel modules to pass to the vmTools.runCommand invocation in the make-disk-image.nix builder + ''; + default = [ ]; + example = [ "bcachefs" ]; + }; + + extraPostVM = lib.mkOption { + type = lib.types.lines; + description = '' + extra shell code to execute once the disk image(s) have been succesfully created and moved to $out + ''; + default = ""; + example = lib.literalExpression '' + ''${pkgs.zstd}/bin/zstd --compress $out/*raw + rm $out/*raw + ''; + }; + + extraDependencies = lib.mkOption { + type = lib.types.listOf lib.types.package; + description = '' + list of extra packages to make available in the make-disk-image.nix VM builder, an example might be f2fs-tools + ''; + default = [ ]; + }; + + name = lib.mkOption { + type = lib.types.str; + description = "name for the disk images"; + default = "${config.networking.hostName}-disko-images"; + defaultText = "\${config.networking.hostName}-disko-images"; + }; + + copyNixStore = lib.mkOption { + type = lib.types.bool; + description = "whether to copy the nix store into the disk images we just created"; + default = true; + }; + + extraConfig = lib.mkOption { + description = '' + Extra NixOS config for your test. Can be used to specify a different luks key for tests. + A dummy key is in /tmp/secret.key + ''; + default = { }; + }; + + imageFormat = lib.mkOption { + type = lib.types.enum [ "raw" "qcow2" ]; + description = "QEMU image format to use for the disk images"; + default = "raw"; + }; + }; + + memSize = lib.mkOption { + type = lib.types.int; + description = '' + size of the memory passed to runInLinuxVM, in megabytes + ''; + default = 1024; + }; + + devices = lib.mkOption { + type = diskoLib.toplevel; + default = { }; + description = "The devices to set up"; + }; + + rootMountPoint = lib.mkOption { + type = lib.types.str; + default = "/mnt"; + description = "Where the device tree should be mounted by the mountScript"; + }; + + enableConfig = lib.mkOption { + description = '' + configure nixos with the specified devices + should be true if the system is booted with those devices + should be false on an installer image etc. + ''; + type = lib.types.bool; + default = true; + }; + + checkScripts = lib.mkOption { + description = '' + Whether to run shellcheck on script outputs + ''; + type = lib.types.bool; + default = false; + }; + + testMode = lib.mkOption { + internal = true; + description = '' + this is true if the system is being run in test mode. + like a vm test or an interactive vm + ''; + type = lib.types.bool; + default = false; + }; + + tests = { + bootCommands = lib.mkOption { + description = '' + NixOS test script commands to run after the machine has started. Can + be used to enter an interactive password. + ''; + type = lib.types.lines; + default = ""; + }; + + efi = lib.mkOption { + description = '' + Whether efi is enabled for the `system.build.installTest`. + We try to automatically detect efi based on the configured bootloader. + ''; + type = lib.types.bool; + defaultText = "config.boot.loader.systemd-boot.enable || config.boot.loader.grub.efiSupport"; + default = config.boot.loader.systemd-boot.enable || config.boot.loader.grub.efiSupport; + }; + + enableOCR = lib.mkOption { + description = '' + Sets the enableOCR option in the NixOS VM test driver. + ''; + type = lib.types.bool; + default = false; + }; + + extraChecks = lib.mkOption { + description = '' + extra checks to run in the `system.build.installTest`. + ''; + type = lib.types.lines; + default = ""; + example = '' + machine.succeed("test -e /var/secrets/my.secret") + ''; + }; + + extraConfig = lib.mkOption { + description = '' + Extra NixOS config for your test. Can be used to specify a different luks key for tests. + A dummy key is in /tmp/secret.key + ''; + default = { }; + }; + }; + }; + + options.virtualisation.vmVariantWithDisko = lib.mkOption { + description = '' + Machine configuration to be added for the vm script available at `.system.build.vmWithDisko`. + ''; + inherit (vmVariantWithDisko) type; + default = { }; + visible = "shallow"; + }; + + config = { + assertions = [ + { + assertion = config.disko.imageBuilder.qemu != null -> diskoLib.vmToolsSupportsCustomQemu lib; + message = '' + You have set config.disko.imageBuild.qemu, but vmTools in your nixpkgs version "${lib.version}" + does not support overriding the qemu package with the customQemu option yet. + Please upgrade nixpkgs so that `lib.version` is at least "24.11.20240709". + ''; + } + ]; + + _module.args.diskoLib = import ./lib { + inherit lib; + rootMountPoint = config.disko.rootMountPoint; + makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix"); + eval-config = import (pkgs.path + "/nixos/lib/eval-config.nix"); + }; + + system.build = (cfg.devices._scripts { inherit pkgs; checked = cfg.checkScripts; }) // ( + let + throwIfNoDisksDetected = _: v: if cfg.devices.disk == { } then throw "No disks defined, did you forget to import your disko config?" else v; + in + lib.mapAttrs throwIfNoDisksDetected { + # we keep these old outputs for compatibility + disko = builtins.trace "the .disko output is deprecated, please use .diskoScript instead" (cfg.devices._scripts { inherit pkgs; }).diskoScript; + diskoNoDeps = builtins.trace "the .diskoNoDeps output is deprecated, please use .diskoScriptNoDeps instead" (cfg.devices._scripts { inherit pkgs; }).diskoScriptNoDeps; + + installTest = diskoLib.testLib.makeDiskoTest { + inherit extendModules pkgs; + name = "${config.networking.hostName}-disko"; + disko-config = builtins.removeAttrs config [ "_module" ]; + testMode = "direct"; + bootCommands = cfg.tests.bootCommands; + efi = cfg.tests.efi; + enableOCR = cfg.tests.enableOCR; + extraSystemConfig = cfg.tests.extraConfig; + extraTestScript = cfg.tests.extraChecks; + }; + + vmWithDisko = lib.mkDefault config.virtualisation.vmVariantWithDisko.system.build.vmWithDisko; + } + ); + + # we need to specify the keys here, so we don't get an infinite recursion error + # Remember to add config keys here if they are added to types + fileSystems = lib.mkIf + cfg.enableConfig cfg.devices._config.fileSystems or { }; + boot = lib.mkIf + cfg.enableConfig cfg.devices._config.boot or { }; + swapDevices = lib.mkIf + cfg.enableConfig cfg.devices._config.swapDevices or [ ]; + }; +} diff --git a/pkgs/disko/package.nix b/pkgs/disko/package.nix new file mode 100644 index 0000000..3464854 --- /dev/null +++ b/pkgs/disko/package.nix @@ -0,0 +1,38 @@ +{ stdenvNoCC, makeWrapper, lib, path, nix, coreutils, nixos-install-tools, binlore, diskoVersion }: + +let + self = stdenvNoCC.mkDerivation (finalAttrs: { + name = "disko"; + src = ./.; + nativeBuildInputs = [ + makeWrapper + ]; + installPhase = '' + mkdir -p $out/bin $out/share/disko + cp -r install-cli.nix cli.nix default.nix disk-deactivate lib $out/share/disko + + for i in disko disko-install; do + sed -e "s|libexec_dir=\".*\"|libexec_dir=\"$out/share/disko\"|" "$i" > "$out/bin/$i" + chmod 755 "$out/bin/$i" + wrapProgram "$out/bin/$i" \ + --set DISKO_VERSION "${diskoVersion}" \ + --prefix PATH : ${lib.makeBinPath [ nix coreutils nixos-install-tools ]} \ + --prefix NIX_PATH : "nixpkgs=${path}" + done + ''; + # Otherwise resholve thinks that disko and disko-install might be able to execute their arguments + passthru.binlore.out = binlore.synthesize self '' + execer cannot bin/.disko-wrapped + execer cannot bin/.disko-install-wrapped + ''; + meta = with lib; { + description = "Format disks with nix-config"; + homepage = "https://github.com/nix-community/disko"; + license = licenses.mit; + maintainers = with maintainers; [ lassulus ]; + platforms = platforms.linux; + mainProgram = finalAttrs.name; + }; + }); +in +self diff --git a/pkgs/disko/scripts/create-release.nix b/pkgs/disko/scripts/create-release.nix new file mode 100644 index 0000000..42f3319 --- /dev/null +++ b/pkgs/disko/scripts/create-release.nix @@ -0,0 +1,18 @@ +{ + lib, + writeShellApplication, + bash, + coreutils, + git, + nix-fast-build, +}: +writeShellApplication { + name = "create-release"; + runtimeInputs = [ + bash + git + coreutils + nix-fast-build + ]; + text = lib.readFile ./create-release.sh; +} diff --git a/pkgs/disko/scripts/create-release.sh b/pkgs/disko/scripts/create-release.sh new file mode 100755 index 0000000..9ea37dc --- /dev/null +++ b/pkgs/disko/scripts/create-release.sh @@ -0,0 +1,65 @@ +# Don't run directly! Instead, use +# nix run .#create-release + +version=${1:-} +if [[ -z "$version" ]]; then + echo "USAGE: nix run .#create-release -- " >&2 + exit 1 +fi + +# Check if we're running from the root of the repository +if [[ ! -f "flake.nix" || ! -f "version.nix" ]]; then + echo "This script must be run from the root of the repository" >&2 + exit 1 +fi + +# Check if the version matches the semver pattern (without suffixes) +semver_regex="^([0-9]+)\.([0-9]+)\.([0-9]+)$" +if [[ ! "$version" =~ $semver_regex ]]; then + echo "Version must match the semver pattern (e.g., 1.0.0, 2.3.4)" >&2 + exit 1 +fi + +if [[ "$(git symbolic-ref --short HEAD)" != "master" ]]; then + echo "must be on master branch" >&2 + exit 1 +fi + +# Ensure there are no uncommitted or unpushed changes +uncommited_changes=$(git diff --compact-summary) +if [[ -n "$uncommited_changes" ]]; then + echo -e "There are uncommited changes, exiting:\n${uncommited_changes}" >&2 + exit 1 +fi +git pull git@github.com:nix-community/disko master +unpushed_commits=$(git log --format=oneline origin/master..master) +if [[ "$unpushed_commits" != "" ]]; then + echo -e "\nThere are unpushed changes, exiting:\n$unpushed_commits" >&2 + exit 1 +fi + +# Run all tests to ensure we don't release a broken version +# Two workers are safe on systems with at least 16GB of RAM +nix-fast-build --no-link -j 2 --eval-workers 2 --flake .#checks + +# Update the version file +echo "{ version = \"$version\"; released = true; }" > version.nix + +# Commit and tag the release +git commit -am "release: v$version" +git tag -a "v$version" -m "release: v$version" +git tag -d "latest" +git tag -a "latest" -m "release: v$version" + +# a revsion suffix when run from the tagged release commit +echo "{ version = \"$version\"; released = false; }" > version.nix +git commit -am "release: reset released flag" + +echo "Release was prepared successfully!" +echo "To push the release, run the following command:" +echo +echo " git push origin master v$version && git push --force origin latest" +echo +echo "After that, create a release on GitHub:" +echo +echo " https://github.com/nix-community/disko/releases/new" diff --git a/pkgs/disko/statix.toml b/pkgs/disko/statix.toml new file mode 100644 index 0000000..c46ef0a --- /dev/null +++ b/pkgs/disko/statix.toml @@ -0,0 +1,4 @@ +disabled = [ + "manual_inherit", # Prefer `inherit types;` instead of `types = types;` + "manual_inherit_from", # Prefer `inherit (eval) options;` instead of `options = eval.options`. +] diff --git a/pkgs/disko/tests/bcachefs.nix b/pkgs/disko/tests/bcachefs.nix new file mode 100644 index 0000000..f8806d5 --- /dev/null +++ b/pkgs/disko/tests/bcachefs.nix @@ -0,0 +1,16 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "bcachefs"; + disko-config = ../example/bcachefs.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + machine.succeed("lsblk >&2"); + ''; + # so that the installer boots with a bcachefs enabled kernel + extraInstallerConfig = { + boot.supportedFilesystems = [ "bcachefs" ]; + }; +} diff --git a/pkgs/disko/tests/boot-raid1.nix b/pkgs/disko/tests/boot-raid1.nix new file mode 100644 index 0000000..e020fdd --- /dev/null +++ b/pkgs/disko/tests/boot-raid1.nix @@ -0,0 +1,16 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "boot-raid1"; + disko-config = ../example/boot-raid1.nix; + extraTestScript = '' + machine.succeed("test -b /dev/md/boot"); + machine.succeed("mountpoint /boot"); + ''; + extraSystemConfig = { + # sadly systemd-boot fails to install to a raid /boot device + boot.loader.systemd-boot.enable = false; + }; +} diff --git a/pkgs/disko/tests/btrfs-only-root-subvolume.nix b/pkgs/disko/tests/btrfs-only-root-subvolume.nix new file mode 100644 index 0000000..818aec7 --- /dev/null +++ b/pkgs/disko/tests/btrfs-only-root-subvolume.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "btrfs-only-root-subvolume"; + disko-config = ../example/btrfs-only-root-subvolume.nix; + extraTestScript = '' + machine.succeed("btrfs subvolume list /"); + ''; +} diff --git a/pkgs/disko/tests/btrfs-subvolumes.nix b/pkgs/disko/tests/btrfs-subvolumes.nix new file mode 100644 index 0000000..7059c1a --- /dev/null +++ b/pkgs/disko/tests/btrfs-subvolumes.nix @@ -0,0 +1,20 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "btrfs-subvolumes"; + disko-config = ../example/btrfs-subvolumes.nix; + extraTestScript = '' + machine.succeed("test ! -e /test"); + machine.succeed("test -e /home/user"); + machine.succeed("btrfs subvolume list / | grep -qs 'path test$'"); + machine.succeed("btrfs subvolume list / | grep -qs 'path nix$'"); + machine.succeed("btrfs subvolume list / | grep -qs 'path home$'"); + machine.succeed("test -e /.swapvol/swapfile"); + machine.succeed("test -e /.swapvol/rel-path"); + machine.succeed("test -e /partition-root/swapfile"); + machine.succeed("test -e /partition-root/swapfile1"); + ''; +} + diff --git a/pkgs/disko/tests/cli.nix b/pkgs/disko/tests/cli.nix new file mode 100644 index 0000000..76cc2b1 --- /dev/null +++ b/pkgs/disko/tests/cli.nix @@ -0,0 +1,34 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "cli"; + disko-config = ../example/complex.nix; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig = { + networking.hostId = "8425e349"; + fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this! + fileSystems."/zfs_fs".options = [ "nofail" ]; # TODO find out why we need this! + }; + testMode = "direct"; + extraTestScript = '' + machine.succeed("test -b /dev/md/raid1p1"); + + machine.succeed("mountpoint /zfs_fs"); + machine.succeed("mountpoint /zfs_legacy_fs"); + machine.succeed("mountpoint /ext4onzfs"); + machine.succeed("mountpoint /ext4_on_lvm"); + ''; + extraSystemConfig = { + imports = [ + ../module.nix + ]; + }; + extraInstallerConfig = { + boot.kernelModules = [ "dm-raid" "dm-mirror" ]; + imports = [ + ../module.nix + ]; + }; +} diff --git a/pkgs/disko/tests/complex.nix b/pkgs/disko/tests/complex.nix new file mode 100644 index 0000000..26cc5e9 --- /dev/null +++ b/pkgs/disko/tests/complex.nix @@ -0,0 +1,29 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "complex"; + disko-config = ../example/complex.nix; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig = { + networking.hostId = "8425e349"; + fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this! + fileSystems."/zfs_fs".options = [ "nofail" ]; # TODO find out why we need this! + }; + extraTestScript = '' + machine.succeed("test -b /dev/md/raid1p1"); + + + machine.succeed("mountpoint /zfs_fs"); + machine.succeed("mountpoint /zfs_legacy_fs"); + machine.succeed("mountpoint /ext4onzfs"); + machine.succeed("mountpoint /ext4_on_lvm"); + + + machine.succeed("test -e /ext4_on_lvm/file-from-postMountHook"); + ''; + extraInstallerConfig = { + boot.kernelModules = [ "dm-raid" "dm-mirror" ]; + }; +} diff --git a/pkgs/disko/tests/default.nix b/pkgs/disko/tests/default.nix new file mode 100644 index 0000000..1ea3143 --- /dev/null +++ b/pkgs/disko/tests/default.nix @@ -0,0 +1,20 @@ +{ makeTest ? import +, eval-config ? import +, pkgs ? import { } +}: +let + lib = pkgs.lib; + diskoLib = import ../lib { inherit lib makeTest eval-config; }; + + allTestFilenames = + builtins.map (lib.removeSuffix ".nix") ( + builtins.filter + (x: lib.hasSuffix ".nix" x && x != "default.nix") + (lib.attrNames (builtins.readDir ./.)) + ); + incompatibleTests = lib.optionals pkgs.stdenv.buildPlatform.isRiscV64 [ "zfs" "zfs-over-legacy" "cli" "module" "complex" ]; + allCompatibleFilenames = lib.subtractLists incompatibleTests allTestFilenames; + + allTests = lib.genAttrs allCompatibleFilenames (test: import (./. + "/${test}.nix") { inherit diskoLib pkgs; }); +in +allTests diff --git a/pkgs/disko/tests/disko-install/configuration.nix b/pkgs/disko/tests/disko-install/configuration.nix new file mode 100644 index 0000000..41e33e0 --- /dev/null +++ b/pkgs/disko/tests/disko-install/configuration.nix @@ -0,0 +1,23 @@ +{ lib, pkgs, modulesPath, ... }: { + imports = [ + (modulesPath + "/testing/test-instrumentation.nix") + (modulesPath + "/profiles/qemu-guest.nix") + (modulesPath + "/profiles/minimal.nix") + ]; + + networking.hostName = "disko-machine"; + + # do not try to fetch stuff from the internet + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 3; + flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; + experimental-features = [ "nix-command" "flakes" ]; + }; + services.openssh.enable = true; + boot.kernelParams = [ "console=tty0" ] ++ + (lib.optional (pkgs.stdenv.hostPlatform.isAarch) "ttyAMA0,115200") ++ + (lib.optional (pkgs.stdenv.hostPlatform.isRiscV64) "ttySIF0,115200") ++ + [ "console=ttyS0,115200" ]; +} diff --git a/pkgs/disko/tests/disko-install/default.nix b/pkgs/disko/tests/disko-install/default.nix new file mode 100644 index 0000000..af3a660 --- /dev/null +++ b/pkgs/disko/tests/disko-install/default.nix @@ -0,0 +1,66 @@ +{ pkgs ? import { }, self, diskoVersion }: +let + disko = pkgs.callPackage ../../package.nix { inherit diskoVersion; }; + + dependencies = [ + self.nixosConfigurations.testmachine.pkgs.stdenv.drvPath + (self.nixosConfigurations.testmachine.pkgs.closureInfo { rootPaths = [ ]; }).drvPath + + # https://github.com/NixOS/nixpkgs/blob/f2fd33a198a58c4f3d53213f01432e4d88474956/nixos/modules/system/activation/top-level.nix#L342 + self.nixosConfigurations.testmachine.pkgs.perlPackages.ConfigIniFiles + self.nixosConfigurations.testmachine.pkgs.perlPackages.FileSlurp + + self.nixosConfigurations.testmachine.config.system.build.toplevel + self.nixosConfigurations.testmachine.config.system.build.diskoScript + ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); + + closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; +in +pkgs.nixosTest { + name = "disko-test"; + nodes.machine = { + virtualisation.emptyDiskImages = [ 4096 ]; + virtualisation.memorySize = 3000; + environment.etc."install-closure".source = "${closureInfo}/store-paths"; + }; + + testScript = '' + def create_test_machine( + oldmachine=None, **kwargs + ): # taken from + start_command = [ + "${pkgs.qemu_test}/bin/qemu-kvm", + "-cpu", + "max", + "-m", + "1024", + "-virtfs", + "local,path=/nix/store,security_model=none,mount_tag=nix-store", + "-drive", + f"file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report", + "-device", + "virtio-blk-pci,drive=drive1", + ] + machine = create_machine(start_command=" ".join(start_command), **kwargs) + driver.machines.append(machine) + return machine + machine.succeed("lsblk >&2") + + print(machine.succeed("tty")) + machine.succeed("umask 066; echo > /tmp/age.key") + permission = machine.succeed("stat -c %a /tmp/age.key").strip() + assert permission == "600", f"expected permission 600 on /tmp/age.key, got {permission}" + + machine.succeed("${disko}/bin/disko-install --disk main /dev/vdb --extra-files /tmp/age.key /var/lib/secrets/age.key --flake ${../..}#testmachine") + # test idempotency + machine.succeed("${disko}/bin/disko-install --mode mount --disk main /dev/vdb --flake ${../..}#testmachine") + machine.shutdown() + + new_machine = create_test_machine(oldmachine=machine, name="after_install") + new_machine.start() + name = new_machine.succeed("hostname").strip() + assert name == "disko-machine", f"expected hostname 'disko-machine', got {name}" + permission = new_machine.succeed("stat -c %a /var/lib/secrets/age.key").strip() + assert permission == "600", f"expected permission 600 on /var/lib/secrets/age.key, got {permission}" + ''; +} diff --git a/pkgs/disko/tests/f2fs.nix b/pkgs/disko/tests/f2fs.nix new file mode 100644 index 0000000..ebcf8d8 --- /dev/null +++ b/pkgs/disko/tests/f2fs.nix @@ -0,0 +1,16 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "f2fs"; + disko-config = ../example/f2fs.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + machine.succeed("lsblk --fs >&2"); + ''; + # so that the installer boots with a f2fs enabled kernel + extraInstallerConfig = { + boot.supportedFilesystems = [ "f2fs" ]; + }; +} diff --git a/pkgs/disko/tests/gpt-bios-compat.nix b/pkgs/disko/tests/gpt-bios-compat.nix new file mode 100644 index 0000000..de45d3b --- /dev/null +++ b/pkgs/disko/tests/gpt-bios-compat.nix @@ -0,0 +1,12 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "gpt-bios-compat"; + disko-config = ../example/gpt-bios-compat.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; + efi = false; +} diff --git a/pkgs/disko/tests/gpt-name-with-special-chars.nix b/pkgs/disko/tests/gpt-name-with-special-chars.nix new file mode 100644 index 0000000..48b4304 --- /dev/null +++ b/pkgs/disko/tests/gpt-name-with-special-chars.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "gpt-name-with-whitespace"; + disko-config = ../example/gpt-name-with-whitespace.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + machine.succeed("mountpoint '/name with spaces'"); + machine.succeed("mountpoint '/name^with\\some@special#chars'"); + ''; +} diff --git a/pkgs/disko/tests/gpt-unformatted.nix b/pkgs/disko/tests/gpt-unformatted.nix new file mode 100644 index 0000000..9c75f46 --- /dev/null +++ b/pkgs/disko/tests/gpt-unformatted.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "gpt-unformatted"; + disko-config = ../example/gpt-unformatted.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; +} diff --git a/pkgs/disko/tests/hybrid-mbr.nix b/pkgs/disko/tests/hybrid-mbr.nix new file mode 100644 index 0000000..de68b26 --- /dev/null +++ b/pkgs/disko/tests/hybrid-mbr.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "hybrid-mbr"; + disko-config = ../example/hybrid-mbr.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; +} diff --git a/pkgs/disko/tests/hybrid-tmpfs-on-root.nix b/pkgs/disko/tests/hybrid-tmpfs-on-root.nix new file mode 100644 index 0000000..09d15d6 --- /dev/null +++ b/pkgs/disko/tests/hybrid-tmpfs-on-root.nix @@ -0,0 +1,12 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "hybrid-tmpfs-on-root"; + disko-config = ../example/hybrid-tmpfs-on-root.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + machine.succeed("findmnt / --types tmpfs"); + ''; +} diff --git a/pkgs/disko/tests/hybrid.nix b/pkgs/disko/tests/hybrid.nix new file mode 100644 index 0000000..adf7ccd --- /dev/null +++ b/pkgs/disko/tests/hybrid.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "hybrid"; + disko-config = ../example/hybrid.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; +} diff --git a/pkgs/disko/tests/legacy-table-with-whitespace.nix b/pkgs/disko/tests/legacy-table-with-whitespace.nix new file mode 100644 index 0000000..300c641 --- /dev/null +++ b/pkgs/disko/tests/legacy-table-with-whitespace.nix @@ -0,0 +1,12 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "legacy-table-with-whitespace"; + disko-config = ../example/legacy-table-with-whitespace.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + machine.succeed("mountpoint /name_with_spaces"); + ''; +} diff --git a/pkgs/disko/tests/legacy-table.nix b/pkgs/disko/tests/legacy-table.nix new file mode 100644 index 0000000..2c7b646 --- /dev/null +++ b/pkgs/disko/tests/legacy-table.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "legacy-table"; + disko-config = ../example/legacy-table.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; +} diff --git a/pkgs/disko/tests/long-device-name.nix b/pkgs/disko/tests/long-device-name.nix new file mode 100644 index 0000000..e328a9f --- /dev/null +++ b/pkgs/disko/tests/long-device-name.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "long-device-name"; + disko-config = ../example/long-device-name.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; +} diff --git a/pkgs/disko/tests/luks-btrfs-raid.nix b/pkgs/disko/tests/luks-btrfs-raid.nix new file mode 100644 index 0000000..6e12749 --- /dev/null +++ b/pkgs/disko/tests/luks-btrfs-raid.nix @@ -0,0 +1,14 @@ +{ + pkgs ? import { }, + diskoLib ? pkgs.callPackage ../lib { }, +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-btrfs-raid"; + disko-config = ../example/luks-btrfs-raid.nix; + extraTestScript = '' + machine.succeed("cryptsetup isLuks /dev/vda2"); + machine.succeed("cryptsetup isLuks /dev/vdb1"); + machine.succeed("btrfs subvolume list /"); + ''; +} diff --git a/pkgs/disko/tests/luks-btrfs-subvolumes.nix b/pkgs/disko/tests/luks-btrfs-subvolumes.nix new file mode 100644 index 0000000..6a4e64b --- /dev/null +++ b/pkgs/disko/tests/luks-btrfs-subvolumes.nix @@ -0,0 +1,14 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-btrfs-subvolumes"; + disko-config = ../example/luks-btrfs-subvolumes.nix; + extraTestScript = '' + machine.succeed("cryptsetup isLuks /dev/vda2"); + machine.succeed("btrfs subvolume list / | grep -qs 'path nix$'"); + machine.succeed("btrfs subvolume list / | grep -qs 'path home$'"); + machine.succeed("test -e /.swapvol/swapfile"); + ''; +} diff --git a/pkgs/disko/tests/luks-interactive-login.nix b/pkgs/disko/tests/luks-interactive-login.nix new file mode 100644 index 0000000..a1225d0 --- /dev/null +++ b/pkgs/disko/tests/luks-interactive-login.nix @@ -0,0 +1,17 @@ +{ + pkgs ? import { }, + diskoLib ? pkgs.callPackage ../lib { }, +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-interactive-login"; + disko-config = ../example/luks-interactive-login.nix; + enableOCR = true; + extraTestScript = '' + machine.succeed("cryptsetup isLuks /dev/vda2"); + ''; + bootCommands = '' + machine.wait_for_text("[Pp]assphrase for") + machine.send_chars("secretsecret\n") + ''; +} diff --git a/pkgs/disko/tests/luks-lvm.nix b/pkgs/disko/tests/luks-lvm.nix new file mode 100644 index 0000000..848a5b7 --- /dev/null +++ b/pkgs/disko/tests/luks-lvm.nix @@ -0,0 +1,12 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-lvm"; + disko-config = ../example/luks-lvm.nix; + extraTestScript = '' + machine.succeed("cryptsetup isLuks /dev/vda2"); + machine.succeed("mountpoint /home"); + ''; +} diff --git a/pkgs/disko/tests/luks-on-mdadm.nix b/pkgs/disko/tests/luks-on-mdadm.nix new file mode 100644 index 0000000..bd49762 --- /dev/null +++ b/pkgs/disko/tests/luks-on-mdadm.nix @@ -0,0 +1,16 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-on-mdadm"; + disko-config = ../example/luks-on-mdadm.nix; + extraTestScript = '' + machine.succeed("test -b /dev/md/raid1"); + machine.succeed("mountpoint /"); + ''; + extraSystemConfig = { + # sadly systemd-boot fails to install to a raid /boot device + boot.loader.systemd-boot.enable = false; + }; +} diff --git a/pkgs/disko/tests/lvm-raid.nix b/pkgs/disko/tests/lvm-raid.nix new file mode 100644 index 0000000..b30332a --- /dev/null +++ b/pkgs/disko/tests/lvm-raid.nix @@ -0,0 +1,18 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "lvm-raid"; + disko-config = ../example/lvm-raid.nix; + extraTestScript = '' + machine.succeed("mountpoint /home"); + ''; + extraInstallerConfig = { + boot.kernelModules = [ "dm-raid" "raid0" "dm-mirror" ]; + }; + extraSystemConfig = { + # sadly systemd-boot fails to install to a raid /boot device + boot.loader.systemd-boot.enable = false; + }; +} diff --git a/pkgs/disko/tests/lvm-sizes-sort.nix b/pkgs/disko/tests/lvm-sizes-sort.nix new file mode 100644 index 0000000..83ed472 --- /dev/null +++ b/pkgs/disko/tests/lvm-sizes-sort.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "lvm-sizes-sort"; + disko-config = ../example/lvm-sizes-sort.nix; + extraTestScript = '' + machine.succeed("mountpoint /home"); + ''; +} diff --git a/pkgs/disko/tests/lvm-thin.nix b/pkgs/disko/tests/lvm-thin.nix new file mode 100644 index 0000000..bfbcfc1 --- /dev/null +++ b/pkgs/disko/tests/lvm-thin.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "lvm-thin"; + disko-config = ../example/lvm-thin.nix; + extraTestScript = '' + machine.succeed("mountpoint /home"); + ''; +} diff --git a/pkgs/disko/tests/make-disk-image-impure.nix b/pkgs/disko/tests/make-disk-image-impure.nix new file mode 100644 index 0000000..1f96a3e --- /dev/null +++ b/pkgs/disko/tests/make-disk-image-impure.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { } +, ... +}: + +(pkgs.nixos [ + ../module.nix + ../example/simple-efi.nix + ({ config, ... }: { + documentation.enable = false; + system.stateVersion = config.system.nixos.version; + disko.checkScripts = true; + }) +]).config.system.build.diskoImagesScript diff --git a/pkgs/disko/tests/make-disk-image.nix b/pkgs/disko/tests/make-disk-image.nix new file mode 100644 index 0000000..75647d1 --- /dev/null +++ b/pkgs/disko/tests/make-disk-image.nix @@ -0,0 +1,14 @@ +{ pkgs ? import { } +, ... +}: + +(pkgs.nixos [ + ../module.nix + ../example/simple-efi.nix + ({ config, ... }: { + documentation.enable = false; + system.stateVersion = config.system.nixos.version; + disko.memSize = 2048; + disko.checkScripts = true; + }) +]).config.system.build.diskoImages diff --git a/pkgs/disko/tests/mdadm-raid0.nix b/pkgs/disko/tests/mdadm-raid0.nix new file mode 100644 index 0000000..7d40109 --- /dev/null +++ b/pkgs/disko/tests/mdadm-raid0.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "mdadm-raid0"; + disko-config = ../example/mdadm-raid0.nix; + extraTestScript = '' + machine.succeed("test -b /dev/md/raid0"); + machine.succeed("mountpoint /"); + ''; + efi = false; +} diff --git a/pkgs/disko/tests/mdadm.nix b/pkgs/disko/tests/mdadm.nix new file mode 100644 index 0000000..3bd9037 --- /dev/null +++ b/pkgs/disko/tests/mdadm.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "mdadm"; + disko-config = ../example/mdadm.nix; + extraTestScript = '' + machine.succeed("test -b /dev/md/raid1"); + machine.succeed("mountpoint /"); + ''; + efi = false; +} diff --git a/pkgs/disko/tests/module.nix b/pkgs/disko/tests/module.nix new file mode 100644 index 0000000..c6eb092 --- /dev/null +++ b/pkgs/disko/tests/module.nix @@ -0,0 +1,26 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "module"; + disko-config = ../example/complex.nix; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig = { + networking.hostId = "8425e349"; + fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this! + }; + testMode = "module"; + extraTestScript = '' + machine.succeed("test -b /dev/md/raid1p1"); + + + machine.succeed("mountpoint /zfs_fs"); + machine.succeed("mountpoint /zfs_legacy_fs"); + machine.succeed("mountpoint /ext4onzfs"); + machine.succeed("mountpoint /ext4_on_lvm"); + ''; + extraInstallerConfig = { + boot.kernelModules = [ "dm-raid" "dm-mirror" ]; + }; +} diff --git a/pkgs/disko/tests/multi-device-no-deps.nix b/pkgs/disko/tests/multi-device-no-deps.nix new file mode 100644 index 0000000..77e3477 --- /dev/null +++ b/pkgs/disko/tests/multi-device-no-deps.nix @@ -0,0 +1,14 @@ +# this is a regression test for https://github.com/nix-community/disko/issues/52 +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "multi-device-no-deps"; + disko-config = ../example/multi-device-no-deps.nix; + testBoot = false; + extraTestScript = '' + machine.succeed("mountpoint /mnt/a"); + machine.succeed("mountpoint /mnt/b"); + ''; +} diff --git a/pkgs/disko/tests/negative-size.nix b/pkgs/disko/tests/negative-size.nix new file mode 100644 index 0000000..b891b08 --- /dev/null +++ b/pkgs/disko/tests/negative-size.nix @@ -0,0 +1,13 @@ +# this is a regression test for https://github.com/nix-community/disko/issues/52 +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "negative-size"; + disko-config = ../example/negative-size.nix; + testBoot = false; + extraTestScript = '' + machine.succeed("mountpoint /mnt"); + ''; +} diff --git a/pkgs/disko/tests/non-root-zfs.nix b/pkgs/disko/tests/non-root-zfs.nix new file mode 100644 index 0000000..2338483 --- /dev/null +++ b/pkgs/disko/tests/non-root-zfs.nix @@ -0,0 +1,48 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "non-root-zfs"; + disko-config = ../example/non-root-zfs.nix; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig.networking.hostId = "8425e349"; + postDisko = '' + machine.succeed("mountpoint /mnt/storage") + machine.succeed("mountpoint /mnt/storage/dataset") + + filesystem = machine.execute("stat --file-system --format=%T /mnt/storage")[1].rstrip() + print(f"/mnt/storage {filesystem=}") + assert filesystem == "zfs", "/mnt/storage is not ZFS" + + machine.fail("mountpoint /mnt/storage2") + machine.succeed("mountpoint /mnt/storage2/dataset") + + filesystem = machine.execute("stat --file-system --format=%T /mnt/storage2")[1].rstrip() + print(f"/mnt/storage2 {filesystem=}") + assert filesystem != "zfs", "/mnt/storage should not be ZFS" + + filesystem = machine.execute("stat --file-system --format=%T /mnt/storage2/dataset")[1].rstrip() + print(f"/mnt/storage2/dataset {filesystem=}") + assert filesystem == "zfs", "/mnt/storage/dataset is not ZFS" + ''; + extraTestScript = '' + machine.succeed("mountpoint /storage") + machine.succeed("mountpoint /storage/dataset") + + filesystem = machine.execute("stat --file-system --format=%T /storage")[1].rstrip() + print(f"/storage {filesystem=}") + assert filesystem == "zfs", "/storage is not ZFS" + + machine.fail("mountpoint /storage2") + machine.succeed("mountpoint /storage2/dataset") + + filesystem = machine.execute("stat --file-system --format=%T /storage2")[1].rstrip() + print(f"/storage2 {filesystem=}") + assert filesystem != "zfs", "/storage should not be ZFS" + + filesystem = machine.execute("stat --file-system --format=%T /storage2/dataset")[1].rstrip() + print(f"/storage2/dataset {filesystem=}") + assert filesystem == "zfs", "/storage/dataset is not ZFS" + ''; +} diff --git a/pkgs/disko/tests/simple-efi.nix b/pkgs/disko/tests/simple-efi.nix new file mode 100644 index 0000000..f3b9071 --- /dev/null +++ b/pkgs/disko/tests/simple-efi.nix @@ -0,0 +1,11 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "simple-efi"; + disko-config = ../example/simple-efi.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; +} diff --git a/pkgs/disko/tests/standalone.nix b/pkgs/disko/tests/standalone.nix new file mode 100644 index 0000000..5da5cc0 --- /dev/null +++ b/pkgs/disko/tests/standalone.nix @@ -0,0 +1,5 @@ +{ pkgs ? import { }, ... }: +(pkgs.nixos [ + ../example/stand-alone/configuration.nix + { documentation.enable = false; } +]).config.system.build.toplevel diff --git a/pkgs/disko/tests/swap.nix b/pkgs/disko/tests/swap.nix new file mode 100644 index 0000000..7d1678b --- /dev/null +++ b/pkgs/disko/tests/swap.nix @@ -0,0 +1,31 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "swap"; + disko-config = ../example/swap.nix; + extraTestScript = '' + import json + machine.succeed("mountpoint /"); + machine.succeed("swapon --show >&2"); + machine.succeed("lsblk -o +PARTTYPENAME --json /dev/vda >&2"); + out = json.loads(machine.succeed("lsblk -o +PARTTYPENAME --json /dev/vda")) + + encrypted_swap_crypt = out["blockdevices"][0]["children"][1] + mnt_point = encrypted_swap_crypt["children"][0]["mountpoints"][0] + assert mnt_point == "[SWAP]", f"Expected encrypted swap partition to be mounted as [SWAP], got {mnt_point}" + part_type = encrypted_swap_crypt["parttypename"] + # The dm-crypt partition should be labelled as swap, not dm-crypt, see https://github.com/util-linux/util-linux/issues/3238 + assert part_type == "Linux swap", f"Expected encrypted swap container to be of type Linux swap, got {part_type}" + + plain_swap_part = out["blockdevices"][0]["children"][3] + mnt_point = plain_swap_part["mountpoints"][0] + assert mnt_point == "[SWAP]", f"Expected swap partition to be mounted as [SWAP], got {mnt_point}" + part_type = plain_swap_part["parttypename"] + assert part_type == "Linux swap", f"Expected plain swap partition to be of type Linux swap, got {part_type}" + ''; + extraSystemConfig = { + environment.systemPackages = [ pkgs.jq ]; + }; +} diff --git a/pkgs/disko/tests/tmpfs.nix b/pkgs/disko/tests/tmpfs.nix new file mode 100644 index 0000000..21cb217 --- /dev/null +++ b/pkgs/disko/tests/tmpfs.nix @@ -0,0 +1,12 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "tmpfs"; + disko-config = ../example/tmpfs.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + machine.succeed("mountpoint /tmp"); + ''; +} diff --git a/pkgs/disko/tests/with-lib.nix b/pkgs/disko/tests/with-lib.nix new file mode 100644 index 0000000..d8274b7 --- /dev/null +++ b/pkgs/disko/tests/with-lib.nix @@ -0,0 +1,12 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "with-lib"; + disko-config = ../example/with-lib.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + ''; + efi = false; +} diff --git a/pkgs/disko/tests/xfs.nix b/pkgs/disko/tests/xfs.nix new file mode 100644 index 0000000..3c7a123 --- /dev/null +++ b/pkgs/disko/tests/xfs.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "xfs"; + disko-config = ../example/xfs-with-quota.nix; + extraTestScript = '' + machine.succeed("mountpoint /"); + + machine.succeed("xfs_quota -c 'print' / | grep -q '(pquota)'") + ''; +} diff --git a/pkgs/disko/tests/zfs-over-legacy.nix b/pkgs/disko/tests/zfs-over-legacy.nix new file mode 100644 index 0000000..af78060 --- /dev/null +++ b/pkgs/disko/tests/zfs-over-legacy.nix @@ -0,0 +1,15 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "zfs-over-legacy"; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig.networking.hostId = "8425e349"; + disko-config = ../example/zfs-over-legacy.nix; + extraTestScript = '' + machine.succeed("test -e /zfs_fs"); + machine.succeed("mountpoint /zfs_fs"); + ''; +} + diff --git a/pkgs/disko/tests/zfs-with-vdevs.nix b/pkgs/disko/tests/zfs-with-vdevs.nix new file mode 100644 index 0000000..6b9e2d5 --- /dev/null +++ b/pkgs/disko/tests/zfs-with-vdevs.nix @@ -0,0 +1,77 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "zfs-with-vdevs"; + disko-config = ../example/zfs-with-vdevs.nix; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig = { + networking.hostId = "8425e349"; + # It looks like the 60s of NixOS is sometimes not enough for our virtio-based zpool. + # This fixes the flakeiness of the test. + boot.initrd.postResumeCommands = '' + for i in $(seq 1 120); do + if zpool list | grep -q zroot || zpool import -N zroot; then + break + fi + done + ''; + }; + extraTestScript = '' + def assert_property(ds, property, expected_value): + out = machine.succeed(f"zfs get -H {property} {ds} -o value").rstrip() + assert ( + out == expected_value + ), f"Expected {property}={expected_value} on {ds}, got: {out}" + + # These fields are 0 if l2arc is disabled + assert ( + machine.succeed( + "cat /proc/spl/kstat/zfs/arcstats" + " | grep '^l2_' | tr -s ' '" + " | cut -s -d ' ' -f3 | uniq" + ).strip() != "0" + ), "Excepted cache to be utilized." + + assert_property("zroot", "compression", "zstd") + assert_property("zroot/zfs_fs", "com.sun:auto-snapshot", "true") + assert_property("zroot/zfs_fs", "compression", "zstd") + machine.succeed("mountpoint /zfs_fs"); + + # Take the status output and flatten it so that each device is on a single line prefixed with with the group (either + # the pool name or a designation like log/cache/spare/dedup/special) and first portion of the vdev name (empty for a + # disk from a single vdev, mirror for devices in a mirror. This makes it easy to verify that the layout is as + # expected. + group = "" + vdev = "" + actual = [] + for line in machine.succeed("zpool status -P zroot").split("\n"): + first_word = line.strip().split(" ", 1)[0] + if line.startswith("\t ") and first_word.startswith("/"): + actual.append(f"{group}{vdev}{first_word}") + elif line.startswith("\t "): + vdev = f"{first_word.split('-', 1)[0]} " + elif line.startswith("\t"): + group = f"{first_word} " + vdev = "" + actual.sort() + expected=sorted([ + 'zroot /dev/disk/by-partlabel/disk-data1-zfs', + 'zroot mirror /dev/disk/by-partlabel/disk-data2-zfs', + 'zroot mirror /dev/disk/by-partlabel/disk-data3-zfs', + 'dedup /dev/disk/by-partlabel/disk-dedup3-zfs', + 'dedup mirror /dev/disk/by-partlabel/disk-dedup1-zfs', + 'dedup mirror /dev/disk/by-partlabel/disk-dedup2-zfs', + 'special /dev/disk/by-partlabel/disk-special3-zfs', + 'special mirror /dev/disk/by-partlabel/disk-special1-zfs', + 'special mirror /dev/disk/by-partlabel/disk-special2-zfs', + 'logs /dev/disk/by-partlabel/disk-log3-zfs', + 'logs mirror /dev/disk/by-partlabel/disk-log1-zfs', + 'logs mirror /dev/disk/by-partlabel/disk-log2-zfs', + 'cache /dev/disk/by-partlabel/disk-cache-zfs', + 'spares /dev/disk/by-partlabel/disk-spare-zfs', + ]) + assert actual == expected, f"Incorrect pool layout. Expected:\n\t{'\n\t'.join(expected)}\nActual:\n\t{'\n\t'.join(actual)}" + ''; +} diff --git a/pkgs/disko/tests/zfs.nix b/pkgs/disko/tests/zfs.nix new file mode 100644 index 0000000..806d623 --- /dev/null +++ b/pkgs/disko/tests/zfs.nix @@ -0,0 +1,41 @@ +{ pkgs ? import { } +, diskoLib ? pkgs.callPackage ../lib { } +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "zfs"; + disko-config = ../example/zfs.nix; + extraInstallerConfig.networking.hostId = "8425e349"; + extraSystemConfig = { + networking.hostId = "8425e349"; + fileSystems."/zfs_legacy_fs".options = [ "nofail" ]; # TODO find out why we need this! + }; + extraTestScript = '' + machine.succeed("test -b /dev/zvol/zroot/zfs_volume"); + machine.succeed("test -b /dev/zvol/zroot/zfs_encryptedvolume"); + + def assert_property(ds, property, expected_value): + out = machine.succeed(f"zfs get -H {property} {ds} -o value").rstrip() + assert ( + out == expected_value + ), f"Expected {property}={expected_value} on {ds}, got: {out}" + + assert_property("zroot", "compression", "zstd") + assert_property("zroot/zfs_fs", "compression", "zstd") + assert_property("zroot", "com.sun:auto-snapshot", "false") + assert_property("zroot/zfs_fs", "com.sun:auto-snapshot", "true") + assert_property("zroot/zfs_volume", "volsize", "10M") + assert_property("zroot/zfs_encryptedvolume", "volsize", "10M") + assert_property("zroot/zfs_unmounted_fs", "mountpoint", "none") + + machine.succeed("zfs get name zroot@blank") + + machine.succeed("mountpoint /zfs_fs"); + machine.succeed("mountpoint /zfs_legacy_fs"); + machine.succeed("mountpoint /ext4onzfs"); + machine.succeed("mountpoint /ext4onzfsencrypted"); + machine.succeed("mountpoint /zfs_crypted"); + machine.succeed("zfs get keystatus zroot/encrypted"); + machine.succeed("zfs get keystatus zroot/encrypted/test"); + ''; +} diff --git a/pkgs/disko/version.nix b/pkgs/disko/version.nix new file mode 100644 index 0000000..fc21dc8 --- /dev/null +++ b/pkgs/disko/version.nix @@ -0,0 +1 @@ +{ version = "1.10.0"; released = false; } diff --git a/size-test/.gitignore b/size-test/.gitignore deleted file mode 100644 index 91ce5dd..0000000 --- a/size-test/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -netboot-base -netboot-incremental -netboot-incremental.drv diff --git a/size-test/base.nix b/size-test/base.nix deleted file mode 100644 index d0418c3..0000000 --- a/size-test/base.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ config, pkgs, lib, ... }: - -{ - imports = [ - ../quickly.nix - ../installer/installer.nix - ]; - config = { - users.users.nixos = { - isNormalUser = true; - password = "password123"; - extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user. - packages = with pkgs; [ - tmux - htop - tree - ]; - }; - environment.systemPackages = with pkgs; [ - vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default. - wget - ]; - - }; -} \ No newline at end of file diff --git a/size-test/build.sh b/size-test/build.sh deleted file mode 100755 index ca5426c..0000000 --- a/size-test/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -set -eux - -cd "$(dirname "$0")" - -nix-build '' \ - -I nixos-config=./base.nix \ - -A config.system.build.ipxeBootDir \ - --out-link ./netboot-base - -nix-instantiate '' \ - -I nixos-config=./incremental.nix \ - -A config.system.build.ipxeBootDir \ - --add-root ./netboot-incremental.drv --indirect - -time nix-build ./netboot-incremental.drv \ - --out-link ./netboot-incremental - -ensureSame() ( - test "$(realpath "./netboot-base/$1")" = "$(realpath "./netboot-incremental/$1")" -) - -ensureSame bzImage -ensureSame initrd - -echo "ok!" diff --git a/size-test/incremental.nix b/size-test/incremental.nix deleted file mode 100644 index e58ac57..0000000 --- a/size-test/incremental.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ - imports = [ ./base.nix ]; - services.nginx.enable = true; -}