From da11e4ce5620042d700e2501dc00bd25a8cc1136 Mon Sep 17 00:00:00 2001 From: Richard Mauer Date: Sun, 29 Dec 2024 21:35:58 -0500 Subject: [PATCH] add diskco pkg --- pkgs/disko/.github/dependabot.yml | 6 + pkgs/disko/.github/workflows/publish.yml | 22 + .../.github/workflows/update-flake-lock.yml | 21 + pkgs/disko/.gitignore | 4 + pkgs/disko/.mergify.yml | 12 + pkgs/disko/CONTRIBUTING.md | 39 + pkgs/disko/LICENSE | 21 + pkgs/disko/README.md | 142 +++ pkgs/disko/cli.nix | 116 +++ pkgs/disko/default.nix | 65 ++ pkgs/disko/disk-deactivate/disk-deactivate | 8 + pkgs/disko/disk-deactivate/disk-deactivate.jq | 86 ++ pkgs/disko/disko | 193 ++++ pkgs/disko/disko-install | 273 ++++++ pkgs/disko/doc.nix | 43 + pkgs/disko/docs/HowTo.md | 178 ++++ pkgs/disko/docs/INDEX.md | 22 + pkgs/disko/docs/disko-images.md | 122 +++ pkgs/disko/docs/disko-install.md | 233 +++++ pkgs/disko/docs/interactive-vm.md | 26 + pkgs/disko/docs/logo.png | Bin 0 -> 67118 bytes pkgs/disko/docs/quickstart.md | 225 +++++ pkgs/disko/docs/reference.md | 47 + pkgs/disko/docs/requirements.md | 9 + pkgs/disko/docs/supportmatrix.md | 9 + pkgs/disko/docs/table-to-gpt.md | 137 +++ pkgs/disko/docs/testing.md | 160 ++++ pkgs/disko/docs/upgrade-guide.md | 173 ++++ pkgs/disko/example/bcachefs.nix | 34 + pkgs/disko/example/boot-raid1.nix | 89 ++ .../example/btrfs-only-root-subvolume.nix | 38 + pkgs/disko/example/btrfs-subvolumes.nix | 77 ++ pkgs/disko/example/complex.nix | 192 ++++ pkgs/disko/example/f2fs.nix | 41 + pkgs/disko/example/gpt-bios-compat.nix | 28 + .../example/gpt-name-with-whitespace.nix | 48 + pkgs/disko/example/gpt-unformatted.nix | 36 + pkgs/disko/example/hybrid-mbr.nix | 48 + pkgs/disko/example/hybrid-tmpfs-on-root.nix | 44 + pkgs/disko/example/hybrid.nix | 37 + .../example/legacy-table-with-whitespace.nix | 50 ++ pkgs/disko/example/legacy-table.nix | 40 + pkgs/disko/example/long-device-name.nix | 34 + pkgs/disko/example/luks-btrfs-raid.nix | 78 ++ pkgs/disko/example/luks-btrfs-subvolumes.nix | 61 ++ pkgs/disko/example/luks-interactive-login.nix | 39 + pkgs/disko/example/luks-lvm.nix | 73 ++ pkgs/disko/example/luks-on-mdadm.nix | 58 ++ pkgs/disko/example/lvm-raid.nix | 94 ++ pkgs/disko/example/lvm-sizes-sort.nix | 64 ++ pkgs/disko/example/lvm-thin.nix | 69 ++ pkgs/disko/example/mdadm-raid0.nix | 65 ++ pkgs/disko/example/mdadm.nix | 65 ++ pkgs/disko/example/multi-device-no-deps.nix | 40 + pkgs/disko/example/negative-size.nix | 23 + pkgs/disko/example/non-root-zfs.nix | 109 +++ pkgs/disko/example/simple-efi.nix | 33 + .../example/stand-alone/configuration.nix | 54 ++ pkgs/disko/example/swap.nix | 49 + pkgs/disko/example/tmpfs.nix | 41 + pkgs/disko/example/with-lib.nix | 27 + pkgs/disko/example/xfs-with-quota.nix | 34 + pkgs/disko/example/zfs-over-legacy.nix | 58 ++ pkgs/disko/example/zfs-with-vdevs.nix | 305 +++++++ pkgs/disko/example/zfs.nix | 119 +++ pkgs/disko/flake.lock | 27 + pkgs/disko/flake.nix | 155 ++++ pkgs/disko/install-cli.nix | 69 ++ pkgs/disko/lib/default.nix | 841 ++++++++++++++++++ pkgs/disko/lib/interactive-vm.nix | 81 ++ pkgs/disko/lib/make-disk-image.nix | 228 +++++ pkgs/disko/lib/tests.nix | 337 +++++++ pkgs/disko/lib/types/btrfs.nix | 272 ++++++ pkgs/disko/lib/types/disk.nix | 71 ++ pkgs/disko/lib/types/filesystem.nix | 109 +++ pkgs/disko/lib/types/gpt.nix | 297 +++++++ pkgs/disko/lib/types/luks.nix | 210 +++++ pkgs/disko/lib/types/lvm_pv.nix | 62 ++ pkgs/disko/lib/types/lvm_vg.nix | 183 ++++ pkgs/disko/lib/types/mdadm.nix | 99 +++ pkgs/disko/lib/types/mdraid.nix | 60 ++ pkgs/disko/lib/types/nodev.nix | 82 ++ pkgs/disko/lib/types/swap.nix | 134 +++ pkgs/disko/lib/types/table.nix | 179 ++++ pkgs/disko/lib/types/zfs.nix | 59 ++ pkgs/disko/lib/types/zfs_fs.nix | 160 ++++ pkgs/disko/lib/types/zfs_volume.nix | 113 +++ pkgs/disko/lib/types/zpool.nix | 390 ++++++++ pkgs/disko/module.nix | 266 ++++++ pkgs/disko/package.nix | 38 + pkgs/disko/scripts/create-release.nix | 18 + pkgs/disko/scripts/create-release.sh | 65 ++ pkgs/disko/statix.toml | 4 + pkgs/disko/tests/bcachefs.nix | 16 + pkgs/disko/tests/boot-raid1.nix | 16 + .../disko/tests/btrfs-only-root-subvolume.nix | 11 + pkgs/disko/tests/btrfs-subvolumes.nix | 20 + pkgs/disko/tests/cli.nix | 34 + pkgs/disko/tests/complex.nix | 29 + pkgs/disko/tests/default.nix | 20 + .../tests/disko-install/configuration.nix | 23 + pkgs/disko/tests/disko-install/default.nix | 66 ++ pkgs/disko/tests/f2fs.nix | 16 + pkgs/disko/tests/gpt-bios-compat.nix | 12 + .../tests/gpt-name-with-special-chars.nix | 13 + pkgs/disko/tests/gpt-unformatted.nix | 11 + pkgs/disko/tests/hybrid-mbr.nix | 11 + pkgs/disko/tests/hybrid-tmpfs-on-root.nix | 12 + pkgs/disko/tests/hybrid.nix | 11 + .../tests/legacy-table-with-whitespace.nix | 12 + pkgs/disko/tests/legacy-table.nix | 11 + pkgs/disko/tests/long-device-name.nix | 11 + pkgs/disko/tests/luks-btrfs-raid.nix | 14 + pkgs/disko/tests/luks-btrfs-subvolumes.nix | 14 + pkgs/disko/tests/luks-interactive-login.nix | 17 + pkgs/disko/tests/luks-lvm.nix | 12 + pkgs/disko/tests/luks-on-mdadm.nix | 16 + pkgs/disko/tests/lvm-raid.nix | 18 + pkgs/disko/tests/lvm-sizes-sort.nix | 11 + pkgs/disko/tests/lvm-thin.nix | 11 + pkgs/disko/tests/make-disk-image-impure.nix | 13 + pkgs/disko/tests/make-disk-image.nix | 14 + pkgs/disko/tests/mdadm-raid0.nix | 13 + pkgs/disko/tests/mdadm.nix | 13 + pkgs/disko/tests/module.nix | 26 + pkgs/disko/tests/multi-device-no-deps.nix | 14 + pkgs/disko/tests/negative-size.nix | 13 + pkgs/disko/tests/non-root-zfs.nix | 48 + pkgs/disko/tests/simple-efi.nix | 11 + pkgs/disko/tests/standalone.nix | 5 + pkgs/disko/tests/swap.nix | 31 + pkgs/disko/tests/tmpfs.nix | 12 + pkgs/disko/tests/with-lib.nix | 12 + pkgs/disko/tests/xfs.nix | 13 + pkgs/disko/tests/zfs-over-legacy.nix | 15 + pkgs/disko/tests/zfs-with-vdevs.nix | 77 ++ pkgs/disko/tests/zfs.nix | 41 + pkgs/disko/version.nix | 1 + size-test/.gitignore | 3 - size-test/base.nix | 25 - size-test/build.sh | 27 - size-test/incremental.nix | 4 - 142 files changed, 10235 insertions(+), 59 deletions(-) create mode 100644 pkgs/disko/.github/dependabot.yml create mode 100644 pkgs/disko/.github/workflows/publish.yml create mode 100644 pkgs/disko/.github/workflows/update-flake-lock.yml create mode 100644 pkgs/disko/.gitignore create mode 100644 pkgs/disko/.mergify.yml create mode 100644 pkgs/disko/CONTRIBUTING.md create mode 100644 pkgs/disko/LICENSE create mode 100644 pkgs/disko/README.md create mode 100644 pkgs/disko/cli.nix create mode 100644 pkgs/disko/default.nix create mode 100755 pkgs/disko/disk-deactivate/disk-deactivate create mode 100644 pkgs/disko/disk-deactivate/disk-deactivate.jq create mode 100755 pkgs/disko/disko create mode 100755 pkgs/disko/disko-install create mode 100644 pkgs/disko/doc.nix create mode 100644 pkgs/disko/docs/HowTo.md create mode 100644 pkgs/disko/docs/INDEX.md create mode 100644 pkgs/disko/docs/disko-images.md create mode 100644 pkgs/disko/docs/disko-install.md create mode 100644 pkgs/disko/docs/interactive-vm.md create mode 100644 pkgs/disko/docs/logo.png create mode 100644 pkgs/disko/docs/quickstart.md create mode 100644 pkgs/disko/docs/reference.md create mode 100644 pkgs/disko/docs/requirements.md create mode 100644 pkgs/disko/docs/supportmatrix.md create mode 100644 pkgs/disko/docs/table-to-gpt.md create mode 100644 pkgs/disko/docs/testing.md create mode 100644 pkgs/disko/docs/upgrade-guide.md create mode 100644 pkgs/disko/example/bcachefs.nix create mode 100644 pkgs/disko/example/boot-raid1.nix create mode 100644 pkgs/disko/example/btrfs-only-root-subvolume.nix create mode 100644 pkgs/disko/example/btrfs-subvolumes.nix create mode 100644 pkgs/disko/example/complex.nix create mode 100644 pkgs/disko/example/f2fs.nix create mode 100644 pkgs/disko/example/gpt-bios-compat.nix create mode 100644 pkgs/disko/example/gpt-name-with-whitespace.nix create mode 100644 pkgs/disko/example/gpt-unformatted.nix create mode 100644 pkgs/disko/example/hybrid-mbr.nix create mode 100644 pkgs/disko/example/hybrid-tmpfs-on-root.nix create mode 100644 pkgs/disko/example/hybrid.nix create mode 100644 pkgs/disko/example/legacy-table-with-whitespace.nix create mode 100644 pkgs/disko/example/legacy-table.nix create mode 100644 pkgs/disko/example/long-device-name.nix create mode 100644 pkgs/disko/example/luks-btrfs-raid.nix create mode 100644 pkgs/disko/example/luks-btrfs-subvolumes.nix create mode 100644 pkgs/disko/example/luks-interactive-login.nix create mode 100644 pkgs/disko/example/luks-lvm.nix create mode 100644 pkgs/disko/example/luks-on-mdadm.nix create mode 100644 pkgs/disko/example/lvm-raid.nix create mode 100644 pkgs/disko/example/lvm-sizes-sort.nix create mode 100644 pkgs/disko/example/lvm-thin.nix create mode 100644 pkgs/disko/example/mdadm-raid0.nix create mode 100644 pkgs/disko/example/mdadm.nix create mode 100644 pkgs/disko/example/multi-device-no-deps.nix create mode 100644 pkgs/disko/example/negative-size.nix create mode 100644 pkgs/disko/example/non-root-zfs.nix create mode 100644 pkgs/disko/example/simple-efi.nix create mode 100644 pkgs/disko/example/stand-alone/configuration.nix create mode 100644 pkgs/disko/example/swap.nix create mode 100644 pkgs/disko/example/tmpfs.nix create mode 100644 pkgs/disko/example/with-lib.nix create mode 100644 pkgs/disko/example/xfs-with-quota.nix create mode 100644 pkgs/disko/example/zfs-over-legacy.nix create mode 100644 pkgs/disko/example/zfs-with-vdevs.nix create mode 100644 pkgs/disko/example/zfs.nix create mode 100644 pkgs/disko/flake.lock create mode 100644 pkgs/disko/flake.nix create mode 100644 pkgs/disko/install-cli.nix create mode 100644 pkgs/disko/lib/default.nix create mode 100644 pkgs/disko/lib/interactive-vm.nix create mode 100644 pkgs/disko/lib/make-disk-image.nix create mode 100644 pkgs/disko/lib/tests.nix create mode 100644 pkgs/disko/lib/types/btrfs.nix create mode 100644 pkgs/disko/lib/types/disk.nix create mode 100644 pkgs/disko/lib/types/filesystem.nix create mode 100644 pkgs/disko/lib/types/gpt.nix create mode 100644 pkgs/disko/lib/types/luks.nix create mode 100644 pkgs/disko/lib/types/lvm_pv.nix create mode 100644 pkgs/disko/lib/types/lvm_vg.nix create mode 100644 pkgs/disko/lib/types/mdadm.nix create mode 100644 pkgs/disko/lib/types/mdraid.nix create mode 100644 pkgs/disko/lib/types/nodev.nix create mode 100644 pkgs/disko/lib/types/swap.nix create mode 100644 pkgs/disko/lib/types/table.nix create mode 100644 pkgs/disko/lib/types/zfs.nix create mode 100644 pkgs/disko/lib/types/zfs_fs.nix create mode 100644 pkgs/disko/lib/types/zfs_volume.nix create mode 100644 pkgs/disko/lib/types/zpool.nix create mode 100644 pkgs/disko/module.nix create mode 100644 pkgs/disko/package.nix create mode 100644 pkgs/disko/scripts/create-release.nix create mode 100755 pkgs/disko/scripts/create-release.sh create mode 100644 pkgs/disko/statix.toml create mode 100644 pkgs/disko/tests/bcachefs.nix create mode 100644 pkgs/disko/tests/boot-raid1.nix create mode 100644 pkgs/disko/tests/btrfs-only-root-subvolume.nix create mode 100644 pkgs/disko/tests/btrfs-subvolumes.nix create mode 100644 pkgs/disko/tests/cli.nix create mode 100644 pkgs/disko/tests/complex.nix create mode 100644 pkgs/disko/tests/default.nix create mode 100644 pkgs/disko/tests/disko-install/configuration.nix create mode 100644 pkgs/disko/tests/disko-install/default.nix create mode 100644 pkgs/disko/tests/f2fs.nix create mode 100644 pkgs/disko/tests/gpt-bios-compat.nix create mode 100644 pkgs/disko/tests/gpt-name-with-special-chars.nix create mode 100644 pkgs/disko/tests/gpt-unformatted.nix create mode 100644 pkgs/disko/tests/hybrid-mbr.nix create mode 100644 pkgs/disko/tests/hybrid-tmpfs-on-root.nix create mode 100644 pkgs/disko/tests/hybrid.nix create mode 100644 pkgs/disko/tests/legacy-table-with-whitespace.nix create mode 100644 pkgs/disko/tests/legacy-table.nix create mode 100644 pkgs/disko/tests/long-device-name.nix create mode 100644 pkgs/disko/tests/luks-btrfs-raid.nix create mode 100644 pkgs/disko/tests/luks-btrfs-subvolumes.nix create mode 100644 pkgs/disko/tests/luks-interactive-login.nix create mode 100644 pkgs/disko/tests/luks-lvm.nix create mode 100644 pkgs/disko/tests/luks-on-mdadm.nix create mode 100644 pkgs/disko/tests/lvm-raid.nix create mode 100644 pkgs/disko/tests/lvm-sizes-sort.nix create mode 100644 pkgs/disko/tests/lvm-thin.nix create mode 100644 pkgs/disko/tests/make-disk-image-impure.nix create mode 100644 pkgs/disko/tests/make-disk-image.nix create mode 100644 pkgs/disko/tests/mdadm-raid0.nix create mode 100644 pkgs/disko/tests/mdadm.nix create mode 100644 pkgs/disko/tests/module.nix create mode 100644 pkgs/disko/tests/multi-device-no-deps.nix create mode 100644 pkgs/disko/tests/negative-size.nix create mode 100644 pkgs/disko/tests/non-root-zfs.nix create mode 100644 pkgs/disko/tests/simple-efi.nix create mode 100644 pkgs/disko/tests/standalone.nix create mode 100644 pkgs/disko/tests/swap.nix create mode 100644 pkgs/disko/tests/tmpfs.nix create mode 100644 pkgs/disko/tests/with-lib.nix create mode 100644 pkgs/disko/tests/xfs.nix create mode 100644 pkgs/disko/tests/zfs-over-legacy.nix create mode 100644 pkgs/disko/tests/zfs-with-vdevs.nix create mode 100644 pkgs/disko/tests/zfs.nix create mode 100644 pkgs/disko/version.nix delete mode 100644 size-test/.gitignore delete mode 100644 size-test/base.nix delete mode 100755 size-test/build.sh delete mode 100644 size-test/incremental.nix 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 0000000000000000000000000000000000000000..56688e63fee69640291ba2d07bc09f8bfe262c86 GIT binary patch literal 67118 zcmXtg2RPO5|Nb#DkA2d?u}9LerDKomt?cZ*IrhxT&JH0vBRjG)4;_Uh;e+fX*&&<% zdhlqf{j_)M?#5^xtpsjNOGM&kUHKMHwnXn<1|LHs`>7Lg8TH;XrNx(oY^}XZxe~2k z#I%^hk)u4JcnAdUs@-Sv;{gA#-5*!m8zZ}fG|67!IL`mMgKZ)9R5A93KuBqBesCbU zui@ZLLLXI4MZ(|2G-P<(9x{EL;4L~IMI)a_9&T=S?mm!5UUpVKcD5}3PCkw-%Bq?= zh7sg}Dbswv@tVf>buYW z$Xp-D5J#S*T%V-y_gn~N59`8in|867%cXjZcm6~r+ZmTTQRDYDA{BK}$%L(YP>k<< zpJhW+llS>QH$`2g(zMNv6lSM_^^Fc`I8;QF2A>rr8W}LSKlNen_uk^-Vo&J#zdwJR zn@uJ~Mh`wL(H2;tk|#ENcIG>eEv}C&P{~b5_4J=7dV8UZ0x$Y3zi#*tM9GQJCn=R8 zhY7GslyWggFEC*!#6nNlwRYIq6G^7)pBdV97Ci;SYerdQl@>et> zl0_vQVPXC(gLyO|(lp~Lp8qRQ8dAf8+Z&`5V`pqoJ!$gv>4b=;zP=nKzMKjN zbxMz(+-vFv*SQwt@PXvmfdG`9$U4N$ZEoi%XmfM(7!1~H?eoftwNMU}`~^#7`W_wl z=GZ|-Muxh&diSnOz@DX@ot?jb>%@jp))8$p`NB{3K3}uB7Tw~cRaEk-jlkc~#g=U_ zz0D(yi5E(x8$RL)@(d0-(+-wM=;l4!!F?TWpXL5&um|6cFFXBr>uPFrV0Lr=qDk+p z?4vLOeY#<12!m-~jDSkIEj*@t1B*jSWmGy&ghk6ya;RFvxhe^xh#;GE5ms<4`ZQ$W zI5eTeG52a8%lx!<%27n#lQwaXDXcFtAU;TE`beu{!ns)}M*~S4{94D~%us*Q9xRk7 z_z}v1DGI98M}lW5s%EhefjQ!X$wsW6{Q2{zEBIKxm=cSI$0)K<<(5YYUyFo4pik16 zur)S-DJHLeyfcA*z3lnlh7Sk~LR3Ia`o`QTEAPo-0?T8Gc9&H z>l%HC*DFqIKQ5B=@dt8PzOkmZwpM{nZsPrVpb8tza!Rm?03%>hJN4y@R=X)5*h%aC zeXf{QR6+@Yk&zLB`0*i7IOCH_`r(zpPWNspZB6%u z{Cu)#c^u1I;YTI1jY_((yLa!R5frf)7Z(=>28I|b$J}2iEv^jWlNy_;sN8a}`cYAM z)4rt9@_gA0JrR{eHwSZksAXG_`MThZAyJi5scyx@+ev2$2??3Y!wC=|;NQQ0|DMF~ zkn8TJq#GricScD1pZWYCGDG9cWB!`E?Q!5`tlNc6J5_2S0uKR9ILTtVP}^A1|+2 znbG%ckoNH*%=v78HD0^IG;inI}NLG$JF2QY&kG_YS?g z(DnH3Pt=!h-yXj+`Ve&VXKU-`XyW4pB5pa}hD;6rG#+>Kad-dH;x^Y3cD7MbTg$7^ z+gX?Q$#&uSEG16HD(d$W)>wYC>G2PO7e6$XP&@l|G(pUJ7Fgph4#zivVPQR{d=5=B z*6&`al?4zwRiDDKeGmRD5hW*mm92!rUg7%6cQXG-BmA}!$`8@IGUG+Z7py5#=q;7eYuH!F<( zvOVr70*A(6ip!nKovNy4H+(2p2_Qpx0uc%&>3a@SoaxtD)R2LAm1?Fd4{*Vot0%}oFv z0+s_DtoDF?>tdZa1vbesLIPGC%X~6>L1=~s*Z3wcQGdRKfA{&0L_$eGL0HmRjiAT8 zdn*M61(<$Haxy^_j#@o9ZvT1W_PMjA$;uDBqZ3h0SMlK*_L5{u!LBZQ8l`i{Q5t0_ zE`C?HHBVkWJiqj6|IpK;O*!!Mr!8N028YvY2^QSyiH-MjZpK-hsw=4El1l$AUqzh< z2y&2-`}+FM&dvZZzST=mV0-AX@BzL0abyHa?Tvrd4Qcqo)CAK0Uj|CWG^3(+^cPRGnp4(<(VsdwP2Y?tB9NcM8O-6t>cA%0zVk=}bHFfX-p!2(T%cw8^2@}3E zX+$csQ6;ag><4gVsB%z)^pvj!-ce2em~%&SVu7<~thyUK zNl7&wpg{VYSPEt^y4>>kL*x39yEI}NPPsAPV?gMbfkXTBsXAB2!NI{$ft@KNIo2}O zF48GCZc@G>`c2;DGmGgX@NMak69E{>zGO=3efp$iVSW(;6d!{{F9~}`mq~4w8s}U~ z)65oNO5Cs@d8&kwi?2Hr`1!k!tpT$4EY|Ds0K^8rZT`m(Nl8g0jHEE^>U6T&aD9Cp zpc@K7PF)R__fC^ay21|u)%0?wot>S94&UmzZMJsI{iUX_!{J;eq7NVTobPm=AMc)b zUw^t=I5Ik_siDDAOOqftxYJQ*XB=b2LeEruTL_ntJofS#b0+UmP}jWs{e(z@9?Y`9 z=MUdDeAv=b?ZjvkIWts6e#LwnsmOdngm_wO&@}TiP?|O|TE6&>MV9})9$9bES_825 zy!`x&?cNT)z7H}!h3pUBNlZ+P`;v%V#jydWy&8j5O>dm3soP8_xfr{?7?Zi$Pcg(e zup(vi%K@Mu!IwcefOWUBTK>B>{N%}#{Q$&+!8GkJuj+zKe(wdC#cWvsV%0hC-fS`? zT|DpZ-#L%|#W^fA+z5_krW)A#v5^rLPsC=$N6E_ox7^JR5zXO7BvJplfTwUdO!wFM zgtm-Sz+Tbf?oGM@!@I+0$_*nSI*uCPfz%k%+mP~(9i&G80&A#4XH-A^P*n8U#Xfdz zZ4IFDxH5C--%oeJ&I1yltIJ&`{$*V1OH*n!amgQjeMLps+lK4^i#Z z)D$2Pqj?YOEqbJdg)`L3QfWmjYXI{j3#84R2T!zuE2X~yOZH-&OlnNMV!{DM8w>vd za;cBc`|j?m?bby=y#SK(U;pJV4Eg)1zaO_TP24G)?9%EbLKhZYehM zU~&G#A=Kg0($ZAz)493191X6FPaGMa97S?R?Wv+Q(%-D2;uTgB`MZm_ovp3qDW6x! z{I`0&R{QUZ@{P$KfedqIyX070A)STEf zwz#lx^{2WXWG2X0ktAn~@I5@u350N)%*7rCoO-xQN!8%cM=B`*!m7GHYQ^n ze>y_V(>bIH0-bWp0bk*c%b!_TSeTmwC4q9_a~2}77gjWi00E1g7!pdpUXZCN5=VNP z-5K|cv3ikN_xpZ|68m?~fR);3M?#l0oCd#lUaFO4W@dst1?LpEx1#V_g6sFxNSR6; zyxyRu6Qr%eF<8JWfDXkv-1JFo-_Ar*Hdbr*AHO}^TgHIn1m?NWhQ=FbtAt9XWG@r% zQ7cmECl6T`86o+Q$gS;dMkXeMYW&!V!%%qle@zP?KNm;m(evO@awbfiTbzQQX0Z-$MdpiH$j zz~UFft}m~yuKN4?S5{UCnL`8~KFkIoq8OTL1eTg$-`H_sZ?@TG-u)QtCqNWHe#aeO zuC`_Sy-wZxbzmRl8nl12LA9H^J2n1(tjxtS zh!}7hht|9*%@{^zJ}`d51~-pW1ZIHe->{jnu&aX;Wj_$#h@rz65ApUsLnbtKDU~E)*PGQc_ZtgQE^}0zHyq7~Z}qX4&(#VDT6BMyuaP zL2=jLM0hjRbYab1UE6_7$*V+Zy6;pe@u}aI{5&LvDUqPw>kzg}E3l=R^2M5x;mgU= z$gH9&R;V_HDRrBhnx51e+|MKt*MrEWzuBCwx9AG;4GI!ZQ~7{j23JV6>gt%KB$la#v57w6{gXR0lG1~@zO&?X8(ZRT1^ zxG~F=bexUI>diAtfdJEFij+@>Qe%%gLSBCR^5shj{{w^Ptm79(xnV6Ft?PkZ<{ox- zYoH*V0Gzyf67W9BvC{Unn^i7Nkp>S<34V49lqY#l;cO8n%`VffbLLNor_SmZ49W2@=|Bm${C<9!BdKNuI=!eSPQ3&%+ zzgilGXwwN15z_ddCws4~i{{;{tHj|@9oB!7h8BPTvC>j(2#nvQ!e`C+ z#L^`DhDzdoHLb|)hK`Q2b(w3r8J-a^pOFzHFYis#9~&#B`(>GEr$gQSAexnaNLJzw zJO<=Rx%0l{Yy46*PY;h9Hhg`8l?f>5@!O^s$_U#0_2pwGd&wVwCiT-3zKk2cZpnog zifsw98D&L#y%)6X8xy*G_pxMR<1nZzDk=(8J(riEi^mfiA|+j(2nRS<;TU^V?Yo~V z8afM&HQ*EVJy&PfSBuw)qWv+LgVP<~)}) z!IoubQPP{%4b+T2}-hBgY!#io*8=whxK2-D7{#lw^i%^YzRJ#gETBePmhU=f<*w~ zFx_)?#0++J)$w-Cx|o)<1lPISoYF`r0MqHsp?j`wZnZP6Uq(kkPLeO)0~l`B?!}os zd}~^i2xjX^oXk8_=ncXH)LEv+#>PfQH^7`KB4QMN!a%tC#N0$>mYzHoYc%Cj6N*Zshrp-$|Vm(5E!DsptN+LY-T5)KaU<-XIIT3Dnguo>2y=jY`C*4FoVQJ5l<<*PLlz6MB+kpK~8SmL90&pn0`KHUgW zrHOr@)C53kUVnVLUP$QHcfd!T#`yL7ci3Y^AMt!ZI2Vt7&3Zyky`&58;Ip=!fU13e zZg2@u*^2V=Nb9{1f~3ZSTAG@yd0#!cPYG(N1fiJYOCZnOs1|aPrEp1cae4J{khXQ8 z-7Au$Gf&wm&cWnELEcPWP2~V|y;z6nJ^9UZs}Di_?dIXJ*y&GkJ1U#cj3lAYFy7d4 z@aM&aWUlAMzc~+}da$Rdpb$z=yYa|x)HKL5^A{j^r>3V{TU*B)=0M#8ev$Qve6P>` zilWHk0hlrZNE7Yeu`F?h?X>?3zTdJ0Lb0*2Wsg6hU&|K@h>68Rx4~R)+t3a-f)fL9 z45nz;Gy`(}|K!E5JRUC(2%OtM*e=SN`;6Ly2%mOt2KXu_Mi*CW!EawoRjJ1V0Hu`s zbM4bsAYDQ6W-19t7H+;_P1*hcgd8ZJ?3=)mu>g^*SUR3D%T0(jh;SYLy5gR^iU4xb zdgpHUgb3N`1mI5o&!}7}aV(|SzovS>)$0Ig4=Cl@a5mtR$zS%CLIb)2Y3Wx!t$6`C z$E0?Cejaeu!yqZ!&<7F{!v`Tc4TRxG&u&LmRRQS=>@Fah&*h659=ap>__(O!!@Gjm znT*q4a=g{yHth~M1;o)K?0n9DF;rPsj~liUDDBA^+!}SV%6ttsjmBbHwu8E!U0+>- zZ39RM9uore2yl?(y!SNKo4Ux zBGV1D%kSUyKozv9)v&w-alHE>I9-NX(WgZVwpTF`|N&yG?1UiE~?@IxE~R$s)sXTjoL>yF_WEW#{@$b!F|XwD zR(Ptbs4F>oZ#%HNBqz9e;rQthGkI|JBRsrHZZ612p`ZA4*4%$NyFcuncdQ|rW6Oe= zA>|XxuBNoa7g8GBU#U0Vy~S&s`fp(Yh3J3E6le)lP5xQDzB&d4703$Yu}-<<2H!wbuy(qXP;VHGOjOVp6? z%?`ksyuH1Bef87dB;Mf%B%~m{>#tFMe0%u_2%7+l#{f9-~K`!_im^>4ehcq51g+5Yk+{~c>Xl6P+fVr@K8BFP

QOTuU5^;eFe_?O#9ZB*B41u^%#&u(5`u`%Rn;d1y zcqNBwg=W3TLBj*nYKrO(Mu)eH+DbC7Rkh0dH!5F|9tg%re%mVTF8?#7{z0o?ogu4q zE_wSL7J(K~eWIXq!eY2){Vrg+|CZTJ!TfnmXts?;avjIAOJXClgnQoIP+vcxL;;j& z`yvCeyHQRO+Yv2hyV)@WkO3nvCpMvc@>oT!U+}atBB%gl-b-)7Py3Y>h@aUnI>LGE zO&-Cj$!ebwSe=t#3L|(7ab)TzTTm+k0R^PBogF~myb*T;zJPh%!EJAONS_3#no_Bt zu(0=;9AfMkHi~Q|px9JX^fzUeM`kAMHTE`&e0ry6YHfpj(9l)qem~c#AeG-=%3`v1 zN@w@CXp{|cM6w!Ez0f)6%W>a92=<7Q?q61;m80!l>i9P|rOoWNACSK+1saf>p*t!c z)c3pniPK!pK{Q}uDQ+v?P$(ePu%`{FYx|x^zsj1Ruar2Fv$Or^WGy;ieqVuxHG+*8 zih(D*h;w~e>It3AouvO$Wu=Py2*KcvnBJ`ZsH^S5p!JgLo6sF9gOD}fqsS{5Fj8;hy~YP`Ia#j!^tpSjysU_TqG=t1^=VMvGUX)7Y4 z>{nx2e5-McTKXIAgip3Y8J|FsPyose)asG_ka!Lvg#Qm3 zo&LJ@6rXCruj09*OGb~2XoRp$Si2t=f_iiY!A08-&pMC#{DQyh@Qx8q9LbBEdbHec z5W(u8P7TN@*tdhZVbu7bG0|sPROQ^;S%_@?=364f{x<1cFtH7!{5%B!Pu zM}PhLW&c($H#fJ#b6H+IUpmw1`?2P*6UBQk?b^IAM{t?^)F)PHHerKtr8y215Zzs@ zq0XtaQOn$uLuasuelNI$Sozq{*c!r6X4mOSvd(J9$(FxM4F{`MyRyyVSq`Vt@p5qk z`q=8j@0{}*rz=l`9MZa3nNpNX$3fEjUn_@qn1o~ZE(ECL)F1X_D4=3*6 z+$3rWdQ7@V-;&XUS5U6q?2dXRW&zQ=g1TwttfDn1X17KE>d@k|LTYGijhfm2Tfl*| z1yCI-1_(d?ehh2)h>2SBI{p2dnpRvot4fRg z4O9#onv>d&ug+NM_p}){FT|KFrj$2!^dZe%zvIvN5Xln-=@C|0$uGfi<$=!gG*G; z(Sru5L3Jn)Hzy`0(o}wmsG~!eHRc#!jAU6uUF*4pHYo3+q5GSMgcyM=Hg-0KU)<2B zIxGK}7<(f&zr)uq zM%-R;(FL1cfs}yiKFD_}M|u?biL~W^?6r77Gvj0YZ4K~$Dz_t^p?}>pT~|>ZbwKko zn=Wh{B?^UNJj2L`8)1ravmYvOhL1xhMD+C;p#($NDKx_N{lCa{s8t4c0#TD=Zn9G8 zet;y9wJ}z@FeZ1BFLY){^RF{UHezpR4o@N0Lii%15Q;{H2aO{;P8^N?r?sLll>wis=LBh+7ze z1df2*H)vPObHW6tz>mHE@NpeHU-a&kaUZJ+O3P3ssvuN9%$riAe*g}Pi9<^PBDMZi z`OM8owN0j* z23SX|;N?y{2`eb_Gs$KX&58G*`X03(w~qVt=@Te$HMrQ*fO!Fnr5`W{qVJ6c4m3TW zI|5|`xDzT4ul=Mf9@HpVxNO=4!m0@O9R%^=>KW+x`_E7FBXp;0oDK8o=%}?Z@~vQ{=h~a|2wZBBF$aUW~?SAYRq~x%6oWQ<^js z6Ia4LEMy{dHqaECi~bz~I$+$wL2_#k_*5Qf0*N@wuD#AB*S>z*rP(A?%`WujCB>Ao zesBspZ0evG2OZ;lE#|O_i2z!M;9!YNwb8M$!e;?rkNX<)3reQXOvXIfU}@+v@~x!z@9XgN{Ud)#Vd-#ZHdKJOf%m`r5!{fNsNE>HM_~{Q&YmPC#|Qbp z;UQ;^D}mic$gA_7>+=atR<;J@Fp$AFeCU^9Bnc%2#<@v}$~V1V<(aRfkl`Eu3@;G! zhS8w5b)7TNZ=Toyf&!>q40tk~n}ar|YLAYNmObMRBd|X{y&zctjRU^yywP7k+yD8K zV>Z#7XVq_!VZ)&Iq4<^yJ2sC@@bne;0sl%ZF>dxPd!Bn^t;)v?pAD4r-N~B-RhJwI z?iLanv19&JWvv9j!*|c`eq*wP>%gce_6-x?%FFvnrZ{jLTi(hlQNhIutX%l-7R}RD zSslSbD|2Et-jrBN4jgRJsf%OZNCa^K^>C(R_Z&o#;?a^Lep%>$vyp@p#p96%gh+;t zN1mU*GC;;~sRtXzcrCVcs@Q9EDB%92gbjEAG$#Pr0@S+ML=hu$6mBA&TW2Rs@OXI+ zzIzAUn?87e+4%!j0+t*ooqAV8!xOp+PbeRHHF6F?#ZaZ_{K@=41*zL4(_d|P@T0~r z?@?EU@%4vPDzV(sd#Fua7br$2k#77>mffwX(w@K~IV8bDcUkdU85s>0F>AL2+K9U; zW?)EOd3W3ZCP#W(y7F>xsKoG*c=-FKz4RG7$Y4G*d7nH*-JgM1IgK%eH2An-J}ia% zeWT~9gxEFi$BdXh4+5a_dcIjL$j%{~HRVqz;ZRK| z;ZBI8m+teMT$fJcAYK+gjy!V5rKMe3qU>jFa6{GLD$1ehrO%;{?^(i^P^orHD9cyM zV$-DETkW6emjvg3<@pFQ`vTez*dTTM&x1e_#*YUVf}{TsSUM;{rrND|#h#15!$6>) z*eyB^v1r};C+pz?>Mp?ot>-7JMEru#Rbw7HS}vP@bBp@QDJ%c?l{tbrVWLGROXt!A zaB|A^%I70piq%F^V^4roGH$Wp;U*fRKY4}XH5*m*Lg$p-+d?Q**%aoeZ~)9Bi0X@n zkmgTS_U}pVqE!!!CNJvrvpIdi;K}sXnH}FIXS9@zHQ)^k+zO+xhwPC+NQo3=G^GERpeE_H(d3)Kuo!=T=rl zXlC@(x+{L~91*rEC95aI5BTZ$&rF*n3qEZ!y@rvd+qFziwHhnOkC1~)@t-q8j$ zm~Mhi<>FzlJUsUG&y*DO5PQvn{?7J;o>{ji`2PY8t{lasGvaMazKM-r*x2DI>bAI* zP8|p~iQ(&a)KSDf(%09VP`D>U@^ZJdUh?XvgY=7i@dG5Jh&xWn%9);zPp*~#N!kdN zq(Bf>ydntPg>062_oBT$f#DBWO|DMYK`R{ecwepkBn=eQet;Zo&>V9>Ypie=lqKh! zETOnOefYREW6wuB9y7sXx5pJUeu?EiD{dx@!Ab7h5d9E@H~C-vv7d8j?f}1r!rdp! zcY-ZhsXq(2&3j2Mt~;jNaVMN?_pwYLbUX?z{dT$0&{|>g(E4IB#8!}5osF6F!ZarQ zs{(7}ZOsWEvujPVrTPHXxgEbwAi@CgbH)z^SxbzL;hB@4bopi^rOCjFt9JoAt26 zLmH+l0l(V9qj5ft>xr|CR6L4(wCZWQEfPEE!_DKpF7?b+!o2k1nN{c2^ke1nxa(EC zKT}3TmG3h(^nSt<#2;Qu zzUL}^pfgRcrrIR@q8{F|dDe5<>WZ!PRkrZ>K9_rBCGCYdz86UuxE1(A=38!~OhUyEIyyO^^QmU){!--zLec3c5d0 zJ8wewgtZz2n7jhx1|g6D>7fR5MV5$X6%#kD|M~fwGB+njtW0;&W+QV#_fXByjn3~m zmlu^Z*FMxYG8Rhw^$Arm55)|u?w_8Xn(`fsOMRd$Y`YC23`^kfxxp zVn4C`Sh;ILN7SVB(=QgPl~ZrwdrD8FLnje4?yuR(`%Q%gQZ!#%>=8}CsHoaWH+1ZZ zn4zPcgk~a4FO$?I4>;`G3l^C-o=)yioX_W{MO!t%t-D`qnj!}tv7M8|R9np25|oKW zvJ{mF0plVP`KlaDC!_V>tyU74zidKjQ>e#agIu`E;sVx`YCZ6CG?7GC5ZNF$SDec}iPfQfPCq}zs z;o7z*$x@`s#)i&`OIm+qe^+agt{RKefvqJGzB-neQH-$5=HWgSB#oQ56!;ZtcDA z+-2Y`XTGah2Y@sR2TB0=RFF1Ppmewm#u2|(w2e=pUqFU}rowkH=vZA|Q5X0*JyA82 z&M-yR>zWCf`hwlOmg=W<+oy2Lf?XT9U+&)|mc7(o+Kj(?Pul0{xBZ*dp2n*o6e(ef zmX1Q}?|7&o!L_fV_mNUm8T@oD&f@ovmZLtV4hT*k*iaj8^ISi1Ui7F|r`nm=IC(5x z5U+|g41UcT;)%#s;Q-b>Lm+v1 zmO0^8;mbWi9HEf0=roc^o)c+~jFjhQ72bnESe)S-b3L%@`1$+al1t@?Uq2O|(OxfU z{?t(>E}ay$d0ugmfGSzA`4;lHis#q(P>9j?AV;#p24x>ELEXBHwWUC>;8wt!i9l=G z2z6ZH{N%QR6r2ApXP3;~isrPq2Brzl3=9(m2Og^x3@Zr<#@jevWO_8qa#|s2fkUAn zHy3DI{F!P%&)E;S@comwpu|HWHg*{F)%{Tg-Vwp!`B(S8U$j5ED4>^H@)Svk7G0^bB`% zv@kx>+NuXLo}yg)$jc7DJf5ovLbG{q-jO)VIfNt%0V4!0KUkxRs_otwpG4 z9CtX+($>@Zbrkegy5864{IjySR(|4?Tep0AMV)Pol+Vq>Il8?^nw{rc8n;;tb0^!_CTC>c17I#MF+G zBsb!~K@ctrsC+ABu9*7Vb8aR-K=mT_u9nO;G=o9-)g(<>FTgl0db|b{yl&hk6~)!2Fm*7Sx2O{ zs`Mu~1u5^zTi=UeLvI~5y20^of6O1R`0__`F1FZ zeDGeI50y3A5>Bc33MtD)+IaY)iqdKA9troKnk~l#bg%qGg`*BBcK;n_+)2@>pq~Dp zY^%@o9aTwAH2Tz*MvOn;S}Z0(-PGjQtCm@z*Z+pTLi5UMzvy^Wm;VW%%hCM&+S64# z&M@2K~Q;{za3N!`C&biHHJ!<^}~_h5Ww%b=`xzn1uYL z^W!^r0}HIhrRI1)k0|J+2ApY-FhlXudyR7k&<&<39|gwi@E%Rya4{kTPbOvUXKfy?F&(l(dPe>EE~g z=C)hPogCP(!N+)*aE|cu2s>F$7TPFzrUXS5u849Rti}Ed)+=`lxAzl!-^$dP!!Gtg z=NedAfYIC&CAs$TE;UvmIY(X1kp0I|yeA(Yu|um84lm^!VqA;>_E@=mUiZ+;yUl>% zyNCq;H}895RJ2BCnj7Bj1r@&yY>NIt8Ss$&f(K|_miVe)7oPtG>+I;wJsNFDYu?xl zf&zXPoCoBG(s4L>Y{cNX;8@5~z^hl`Ic#tOJ)W=5eU^}x%lnJthFSdA`zc`|>%>SP z&Xxk#t$6{& z<%Q{D5ByG@CMU_KCrPU}e2cE^qh)IaPOqyTce%V(T5$Z)VZrWZjF}rn*6=o=ras#~ zh0tFg)Aw$zA|lQ)Zh6TnR=AJ6psLgZo4CevE$5fdnoXooC>5@Z(-2_I&L=KDHRmu+ z#>22~WfvO|9=<-8UcyiPdH+kpLTIo2R;JtBwqLNFog!>dkH`13SO65mjhh|7MfYo! zRnF2WQW8TY|i0keXEZ|Nh)J7P*k5PXZU|-s*wI zQQ;U6&RU-t?M@cmt~y^@AB-NQMIdB*2+MEPo&UaOyLDkS8{*!2GQUgB#;H}TV`OCX zALwQR!^)2z;HH4%lbK=01~^2Onj^wn+V@u{FhRl-;^MXx;>cPjBUZP$%QsHhrz_Br z6~qO_TBBB_s+7!-)QP{tp?G!cWO}7PelN=W-XR+i%W2O_)F|tZ7hui|%&9n-yhC;6>K=FSEM3 z_w`Gfb`fM`unfySNumAIuvOZ^64ZeE?eY!9eT+c7f;AeA241u3`fd|rW8l9@Q^C*i z>~YnNBe?&C)V7rRVIIk7i_&pW&>|qS+|UGSsmxT4fTsdp zZos~}swGid?ml{H{q=9wPUBUC*i;bgUed}L2+j6xna26qp$n_}jZA25+qPEl zWf`7q<;`|^Zf(uSK-K<)`l5TgjU7Wz4_qtwd<`6-@|pxZ1f7p+zr$TcGP~CnR1y+t z(&Aqs8${%m@WYV}(Htk;W2q-v>c$?`1&eGzMcLPYI+$Zwe2}z>N~NHw4NL{Pu=n@) zirzh2)si5uHavphD6h2tO0*xJ39esBta@6scGm53RG(M;pP|bW`nAxL68f zjkZ3}zU{77r3JS~Y$|oQe*ni=QfArXdm<($1b+#jn)cS;M%!VUOXaJLFf{9JlM!qq zI;VVS;z!U+t0bRJX(7R0`_d3mw!OQrHT+EdE9uvMSvH9}Zu-4ub;zLh@?*-b#*a{! z6wZ%t5V?nbZ{Oa^K%%)MqPhgniX%c?{&{1Y^7MmCZVYmP3=@#gG~l6bMEm`Z=QY#I z;ol2Uc8|pIIEY2aV8pOn@bJxkOQK9>2!zdwC{~Wr(vP6~0*?q+Km%fQH`sjKuN;W$ z;2JD=0mG@36fkF}w8tLlKk-1(($1(q`4s=kwv$P7g*7cB@Yi8HaSTp`jc(d#Y}>c5 zDry>RK0;~uJOnO`YgT2QvwA9sa~5`fErnI9h~lsfJhpO*+YJS~$NEigjg=GjSIsB< zx=0JM1DoxiK{3xU%ix}XRt3=eyUY#sk6zL4?)rj@sXFz2hGDU5w=ErIYYNvv4E0>& zuDnwoCNZN>BGIc7fXi0k1qVYQtTi>~MZEBa4%8_du=0VkTwlrWh z$}I=i92Ip2|NK+pA;&*r$n~UwwAo zD-AIc;;HoTR3l&70rN?KF}#FGJre12M7pXD%+s`2u!wHx zjg>pO5Zw~0S71-l6Cp6F>~0)`17S&^jK>f6tr8Z^*8)#UN=hm%E!C<3hV;S(y5ruGDz?W`j4a}K~?T@aRF0~jy_6CiZ@|p!;rfxN@uOZ(%3RLMI%+%Deifg zpyOR4O8iti%I6jY#G%XqO!BOIlCq3O2$`o3E+qI6q2dnvlG?i9l15`!7jJ?!SksIc z53UT~x@_DqW99|rXeMO`o5wtCN*u|Tyk`i!Ucna}p14@ei5=*n)L>0zmdk&-1qpFT z^^A4R-ck=5-nTV+JW+itIU4ptbng#%RT>L^)}Do;P~r1t|II}?-|jGLsIf>|mE0x7 zy_b%kE_pDMf4pfaH{t@^D++9Nn{4rTxzS4Hq9a4B;PFheT%1~ zfR|%K)YUDj4iBOOdoB-}Cs;gH2taIkNrDX#j&w?LslTqM_3{4W`xyGW#y36>FJPkt zrdri>pagMH6SuvfF;fz$2rN;8%rb7DciTwJM=Xdig6^jmyGR;zpL*;=BnRoJ)6w~5 zP?t2gtNn??D}%k#FM4Gj``--xyu9hLilczB>o7TvuAznYvzwaFflTMtQ8;$F2Dn4auLhrtp>f@ zqM}PaW$h17<|Y~@0e0( z;LW&mx29$n06pn#;X{|EM_kL55?jv8sNzV&*9QY?szVN`*0?D$sJ3j8dSk=l_`97Y1P<(n%a z;KpO}Du_sBMTuc~E9$G`>&SMoI3-Ib;uSRIpt5!KyNnm>ps4*08yUllQ`OXC{*r(} zxVS6<4BC?v(CD?wpOJV3?zN-iTpnsR8B`ZDfeuoo8@TTs*ad@t|0V&}C-|~=9QlZ< zehU4uW!t?q*wa(zt={9e-+|eBY5kbg(h3ed4%Pw>ZcRK%)re$~q~>P%g!`CR=R3Rh zu_v63iYOX_Et-s+mx@!cWFsB_{iNClOkL3_{6GJKbzL%WQQxE%+|CA04}|iB56hUC z;Yhy#6#Gs%4<-Jo_BhmH;(bt8cWW#5e{Nu&Olti9N7Hr3Q{9Grdmfu(W*j3VDT83n$LsTPC*~?~$XE#kaUl%TZ z{fZQ4RE$+FDk;+Z7_@WBzl8r(X+^;JW|LHBMQg_wx9xpK7bdcKr7zs>mc(sA9b%%> znooZjrfJH0d)+>fm$cZI4=({Y3}0{{Jb48!Jc+KL!**p(w5F8=G9lO#xCCztNE!4%$&iBl-RNS`}ec1o4O{!qke_%H_BYf?!tdA?qe18_XxUSnQiu-plGw`oekoIT^o+ou?Roh^|acw@9 zHUs&TugwdhGG1~`#-~DTG@&gmEx`Bx(^~auFYo)i`~56mQ!yUi@~<^JSmeDjcsF z<$;A9K?L+@)#%Uz)vWTLHJ$`ma6`^3%gZXhyATJFuHbfbyj(ZzVSV-a`Rakkt8@b> z7PPnVX%kcPh5wvp|Jx7Uvzz5(_d9=T-T3Omj{XQDs`Rpja|u5mug==@i3mK&MKe8X zj-?^jBF>iW9eKRcTkKY{zP_F=wia+$ZeFS{reG%ir8@>grp6%g^^VG-S3*zYoUSBg zHqjMHO9{qE_x|1u`ZE7S5yo?(0M`sJMK;>znZhyoB5JmNtYnF<6u4a>=8=(+3o0rf z1CGJ1@pi-o8L9Iya0}T0zpmHeK1;bG7oF#KOaXs#a8veLL^s>)pSAsj&kb2k%Mubu zgUC(#vN!zgPdglkoX>;Tz2aJ)>EArD1HYdWu&_FvDnPfRX!gNc5>f&%qQ4dC-0E5Z zIBmo0;oZs952L*(2d;kb6Kq)AGAR{Er^E+^J~F(lnO~=o-QWK~ImC@vBB}+P2_w#x zA8p(ec|hFvFmI<)9NQpf0Rh#CFL1Se@0reUV>5A*MFd|(`5 zP2s^qcmOU%p4aAO*Q6C_i^%O|BqtvI)U|YT3iVYR$4VO-8-wqaoFphQktM1HmDrJL zI6D^hX$3d5;0Y!dQC1}tmFTu9`SiO!KHnW>BU>hw_lA@OB}7=X1Pn#WEE#Dg+_OfB zQ)uNWJ(X|`j7eF_85C(egID^Tr8HC|J5VSw-BC5rLzYZ6EuRGDSGfy{+Sqwq;WW&o~9VBXWc zCaG_Kh6g!2rpAk)$S}%3@g3g)kL-*Ga6A@84?q=~DaA;&dw8_Iey_&b|Mcp^$5%^- z&!lt_u7kg#}RzDKiJ0ljI+~Ok`?|F zKlIFaoTPe`bUMn)Rs&Lb78y0-qNbz-7JC3rVIPbv*B0%{zkwhYoSc36H}HP%?I}T5 zhr3xd3KGkVuk-pLR*p_MYm|kCqLSE=_~ugZ8V8xff=k> zIWF*T^2VKJq{;|QRAq0i=IjV zOz_b2@$iu0HPd^5FS}E2`MYnCzRc!Y3q6Gy@8d)RQg9OwF9CsKEy*6KWAyLqz&}6& z3FHIV!9@~pKYDa6r@w5(g#s_YNCn2cu9-jkhbN&Ov$Ox17O=urikv9XF2YJz*VZoW z?3;@Z4GcUTTrevzdnmOYMOi$-xFYX-UXBYZ%@3qr8yz}J?*g{Q+DM4mru1Pzz%iIv zmY2J+(klK3-<&Ey1_uEy!%N8q!ZqG$hX3iHWVm8NzCv=4w)1tJ?-Q=SmuGX>64;=@ zN>5J*{XT-i0nCf{?tvZsr&z`qF;bc%%QSQt-=y6>T{em>*AlPM>-Nd9e&FQ=gP>iS$vd{6%AWlv)rO31Z;L~D@JFq2GRpFcP>s?)A$?P9q z-Hg#aGdTml<~@jnKYjX)m4m_{7GpwKg3KX~3Ey~0+We-?biP|v zLYKYYspGk0x(sB%{uv@g9!8qdPZewXogapmguAy4^0a6;!CvMlo1n8=g8J>LbVEE@ zi6<6vNZyaR){gxKw-W~fxZRX4b5YiZE=}Cr*P%RktN43(ysfM>mq6Cl4-sD3zzz==Y@ z@?UOJ76|9AhHQWm3)=7|@W?K0fxZgrMe{6m6=fX3kN}>n1vqVu(PX`TW^^kvN0CmhFh><*@;sMr_OtVy8OFLa5aJxP#er zw}19KAU?_N9rZZ{8|6poprTqx!1HWgWDd$nuUbg5xK~xb5%%Hs$$<=|5R5S=##6wR z!5nba!h(mM(r#cZdx$(lB!=Si=e0?4|0D7Qs<#)UIk3{2`6fyMh9ZA1Ac#dsXDA*4Go3kJlUXS z#$)y87xH2oh87fk+*&VR**bHj(zI+er$BNrL8CUt5J`~s+G&{6ZlFwg928r~PHDNN zG;lklsoiOPUI003Qj~4ewkP7_xAurJvSGqnkND`umqLZCPDsWv6E}V1bCrIVw;Kcz z@JWGC^{+n$!*g3(n`8<^7_9&Gl~4Dpig9Z-S%k>|BRcI>+xz*m1hT8SnJn_Z)xO0> zBcG}$dnSGTZ$F?%s3jj9Oic^3phePFas>;x*C=ki8Vs^w$oNeXTAs>Yc?BvckESA` z1UWU8`a-8Jww=?c&bx*@iX2IsYf44N&f!=WZuP(fv$c0oCe0vkV8-Lb)R|$_Hn#y% z&hIPRu%yzjd;m`&AiNN%0%1=^`K2W#;5WV__Z(K#+ATUFadK7~T&3kT!XycTqHN^# zu$^?)n4V;~4`~PM6J4AfyKqYPN5*eie+W7x3kVkr-1)u*X&(DWoC|VZ+wm6#(?YE1 zD5feg6Q|uUS|v}~rra<^lE}h#s9x}PaB?TURhad}*<$pxv*T?_K~MScY3}e##QV_| zLy`Nwz6TJL;j4BGrsjI*%<6yHaszkN$BbP4nI;mk0hNg4S}F|qMK{_ z?VFB7m?4FO!>@H$3M>;!uP)Y_K2*g=b?t9F8nE~#LbsgwO;4rQX7_v%A}n6Y@4g#L zG-NUqx3{+(9r^P!a+sWaz|W_{pz^rL8yg!#5{qf?TdNX?M=*lAax>2WE|KQYD!(hV=YNWlG?*TPnaUiwM9K>W~mNfprz&D-oMZLwSKM` zPA3RD02|Uvs|OR92Wm1m${F%sNi)a^>I$`{ucJ2*4i<2H`+B&4bqck&kjZ0L zIf~A|=Ns)!f?^UmUH+a1Pi!rjY8cT{X4$tYn651sroouUjFWR$G;7_LNsrWE6MGY< zxRi|nbpGU2b9Mc#{n|Y9;O)F)D;#lZ8Ahe0r68hh^toDK25&Rgz^4VLtN5zJabRk_ zdhEB0JoJ(f6L2H@(Bo=Fq&-s$YEKnGSl0OTr)E>PjBq-p-Voz=7&}Ws*j;aMP1$4f+wgRjPeC5lpj{@u}KV^ zf)=sda(Q~XdBuy5j}P_+oVgb=j0((r6awyFi;^4p{E8}!U?0>&k$#vS5^1=C;eU|u z{$=KYp7JRT%lN6d-%`{PM@F+j8H9Tb8S2igoh= z5~}11)8_E(!@Vw)BQEG~H!31mI%W8kT&d9CBV=eH3!|Y#$II@)8KUK0Qy(PobkJF@ zEKZPzP>^EkiK{x6HV_gl?r};K4S|;jjS$3-Wf<+8R$O9F=YTE_+Vlx`G7`-KGibZ; zge)`hm|2$6T|vLwxFHw$#Exkgp|-cTJ36QnJFIPNUZ$nVa3BQj*YZ_--|kqQr0*a6A*=r7n!gk_WKphWuZ<~I88qAo3JK)W!eBm#ywZD=GCmg+6qJ|COtyH0UrS-S4pBi>qlcRd zgAkSg&CM?Ysg`|w*9^NkL1nV^v9RnzY(Ma47#8)1wP0qA99qQ z@`45r#t@65^Bf2&r?HXyHWF4a>;fSM2Wyv9J;2crD>pP`j(TMHy^{UUyNe@onZ2-A z^9OIkbRwI!?4@jC2%ZTQ-qlOdS^IvhPZU)eXGJE(sJ!Pre{JNL5@!TivGsGr#%ktd z+u%DQLdh+=+2^nI9&DZpArLhY?qK((PmQeZ6b*@wBtie^TBVGP>VaGfDXHF`9w1jj zwFpz|u8n8)LoPyN5!Eue@zIN87 zp}#Z}AL{Pz&e4y1VKhhnimfId{qQFIZ)pyd$J^aY09L;I{(oVKU;;N3*`IvcOd#6g z`i!E|>%#>#X-UL7)A7iMX}?nzO^!EoxXJY9V&~(nNy{8|CcA{<%0YNG|Kq`jjwKdd za~oilAu zkhZ_bWHu7}3P*J-pJ?&s$%bel!d5o~bEbcWXHAs+{RM-82bF8p{@NN$8IYD(I|dO< z=gyzMS#J5oxl)6_I_MsB>vJA8A-fPIz)C}&(gj9Z7y!9qpjg8|SYBSP+`X67AfDL^ z7CtCJpzqJg5rirIf763(y{}MbrQ$8KY}El_e2Y&X5@*y&^K+L3kd27vqY#JJETpdAS`=Cn5lOUaq`*2SUY0* z(d(W<5uP5|ftH9!Ap>D_bLom*FW@Z69geWB-@ktkxj%yoA7R}RF%pv&Eq6nzJ#11k%;a2ZUh@ylO1R@UrrGQ)+ zWuDkB?V^o|bLlV`-2&{j zvu*T6LrU&eGic=7Jzy&;GE>HSQc;q=8%5{+@|v2_pB<_^I-s4bQjO|7joX#Bm zPmb%dcrv7XK+24fvGJ1Uhfzfn!)=8E0@RcQGzdd%Kl#F_4edU7NE168%PpG^*XS#L zQWXQpkVdDXs(QFJYzu0Twz@i1B2aEI@TyosVN6YhC|djS-6uhT@ni4{{&gwPE`ui1 zQ)!as^Mw22*51g-HBnZ;#o(X%LJVGjM_mse1TICD`KYqeto?QOj_~Ex6HKBJWi9Q0 z@@1HtF4t61rFk&l`{<0-T?wg)e!=eU2K%ErYuWIo{-rI`f_K9(o~oc^A6yz=|N9h~ zje~lY*$a_1u)twqmsq5~cmXGcuUckqxb3d12wRa+{uRj-hw^Vw)<1uyw|iGGvY#-nUaP=86mm}#4foEjK6LtUK`Kz`{dD-H z&&zMb8&S3erkc_br;XPKrQ##*xv}m1u6yQkT6o)ob3)q7Yi*kP{lh*NyGPUkhqM+J zaAKeyIed(GVQE?}auk@U$$vS2Q2)HQ2LmlxFd)hGC)g|}LJxXB1O=ESSd~9nc5_8a zg4JDvzpS!yYk8z1CWbBn{qEg6Phg;+Aci;HyZ^m82L~xJkuCOspU552QQ-g8AEL$y zcVJ|kDxk>!%TDsISO31KF#iNKR%bywBu=Bt`bQLLQjj&08)1;s`-oI+t&Qj_x=1Ra zVV!z=$E3*4i7SrJ!I{S6Ld$#p?F;_uFV!B0THGy`m?{k*Ori`$I{#aHX#4V`mh7^} zh9EsN)8@Y4p9|w#x2caY{EYv)zq2V_kb;3Hd>b2s^8u)3o6`F=*1T~KbJ}lSJrL?} ze+*g7dlYx?Q&6|;mpH)vK*5_0PJ)2kmLW*6qPluRYKHigy zl2TDkOD#^Hq3pwxk!!Dquobp(_u8hujkN?Eoj%YkOSQZ~csOo~dvMPoez>|_@j8e_ zw|oF=*u{LjvymX5XeK_h@fSvC$rKjKuXDYbnJ-?z_{Ho9O+f;BGwBvIqgnl!4R$-f7(EJkEV4NzT-!T-q0+3$aPe)m%#2}M%fK`F46k=?l zi>t9#>lAvkQj`#~uuImPDafoT8|liP6=XyjPCCf3?3LPsDasz!&dv(4e6ylk8Q z+BDasenfjjsWcQu4WNnIEuJvpTA^hyr)!?##M{uqTu>fHa`q7yLG;E@Olmn!`2AY^3ajD&_ ziUNxHIOKTgIH-*hA@*3)UJXr=I(WNzs#F;-^yR~U_=AK%-Ri4BL`Fv0CJz3@Q&UL` zmN`M*Jo~N(n=Cf2*+Y$+tk0gU4GBs}_!{AKtwVRVxBYh~!XcP069R*Aa!*HE89Mme zznmzA3R7Y8-nw-^_T}M9z;hxBQWY3e4Vt9V znmefd52$+s$TQ>WkH{H#u`UR4;hUBL`~6RyJ0*Au1Zw3LgYadDuy=yUDSbizokcYUGAvN-Ri&H00BT-9CatROysf=BHOq_Vsb5 z)ImflHk&@~iqhxk(&?Pf=kdBoH`s?6L~8OK5ycvtl<=o(^9NefCwICjMTU^xOXJ&h zAc{gyc&jL?kc7eCAP;hjVlIF54$3(7!l&$QbepZi1GdCOOp(~cn_g6@fLca2W`3mN zR&S>IRZ&)`m)>i?2)n!0XrVopD?K@d$ZUS#q9S!juw>VPl^kEXuX7hXKx5{nYeB!cYW^ zdeQ){4{Mi(yRz5)yd(YTu72#hpTOu72!z&F7zA(F*!Umpu0s>0mAyNN56aEerFoZo z>Wyi^=Ozoa%u-mj&dEttXypShT|S^mkWr6RUP7}D-fdnU9>za=cuEH7jXy_)PM*6V z^j0MLiq?IB(6bO!!b7i}jgjUs%rk{w^Y8hgIB+1^$=Yw~j!a8$?~Ahj+InTynq?f* zBa?=a-5y5I|5E_3{ zz-(;fIS|mTv(V*Tw2gd6-`fNoZ?p~yrn8ffj&-XNJ;}xXlE+OsXSK{hxdjpIU^|_k zj}UG8=)L~Rw4fMb009N<>gs|{(VhJhBXMs(H=pixh;i{W5l-yzss+zGJbbyO%ZblB z)Qmy$CiW!}OGeNHQ49r&DR?=pVReM5=i`*9H54ck^k%bg{s+O!*<@Lg84QOCaKNP5)eV8?%EeE;~*2%%a z$1zvFi->@Zsc|=JGeGerQvex+Q0e%fP8iM$3dFPqYq2l5*qqx<%!|t3l9V7Lg?!b# zRnS7~%bb76ElZ?3`Sq|#X^tsWrfe#`rkm%QZZ|PJbQBKbEC<9S z-Y92~JdQS04dtOxnj)+QON;)&@gCBEjpoQaDr^H$-AiAVRj z*ejAnk!Le%2@pBTiYCkO*!xphocYfhQ=pNbGpb!0<;p%o^U6dhan0LALR(Be zPn<09^P~TyyCQ@Q*I0j^S_gD*Yv0d4L(2>)d0vkHgO!W?bqOR$-Rf+No!{A%DnNRJ zEa#+{vb?;LeEP)|V38osJyJ(YG)2ffH}iT5Q$RvmC#<4>$-vndMP=ptwPPT7B4Fb& zF%gl|-@b8_EAQD;o*qALy%=rSDgXmzKMwwRo$V=>OL=a&i1xYi`nbd^-gO-Gc2#!H zG@*&TX-Wmn)xsIjjiIkIdfFO?TMO(`pGt!%R?a*$qvR@5_iL023@F z^zsMc28ZkLjY_MtNLrKYb$YknkD89c38KSmBg*;|Iic&cC&bx3104pg= zBO|+tMwc)h5=jOGT(E)&Q&iFt0`=4MpT5^Js&qwc;%qu7_Zn>Z8*kC)f6L<%NeZe| zLe+e3Z0=*rv&^(g!!Z$F+zC|j3Np&tbn`elX%5CnIDNf&lpoJ;#~au^;$&gLh)eZh zr?d*M0>B6#`OrT`@0^T`rXy+U)qMeK7YOfyc+-B|ve)B0Q-SXUoreIBFv2Xhc$$P3dWMmTA~IWHmqed=p*~U$kuR721a`KgY@(q!->QkvhQ~vZoU- zruTTQ{#2E9)K=Qbq&u}KPWqxNCV+KUJ2IVw%TQn{Y;YLovYP*b6@H?5Kr0&Q?CYF zJH)n<-&sjWG)(A-;ITwJ^j{SwHgZOSF;ofjvqE72x%cDZMM1$)Z7o?bV(9(tYQUnN z50T>+sBEx{0cCYpgx(Z>z)?0g4#IN)ST3;;{M86z6!$am%30KPWVFPS|9GqXS?y4d zBw4&p4B7kdNAJoOUwT#OReJ+816xX9&sS0ch#yAKA!k61fJXNMJ*b!0R~LB}nRzLa zLi8h?S?j0rAE%JCG~^+?rS1Kj%?um#b zlNHpDf1g1!e}zH#!(X-WwGDawB(@H3pPJIfFid zli!y-_!@{~Y)TVzjrkelKhJ*qmTFZZ(kX4LW2P(QMGK@U2fYANTQcBm(c}AMDi@^% zmw^ohGrjG=T=bZ_VZ~-1R6kX zynk+Y|Cod~j!XQbaNt(#oV^Ubg(NW)OT?WHgxD(ZN@g?}^dje?DRzC$<|gURd^PzP zR%D?pBoY~-!5X7?ik`B-<_7EfVop|~=Q#Sgw84+hZPvMZmvD?AH`(&Z;J;KlEXaD_ zUS0cXn_iBl2UPt3VAU_G+Wz~tkKXWYGOWSnVBr@-(JBON2ZtPDq0FsiLdF~w^E{+R=kwc zdtSbH0ZB9lLQf$;6SPZdhF&d0XP(A_0LQ1!$dT8-mx4opU2G6CI8EvzTgFfv_#ey#Oy=A6t=M_dj7TII7|6XKgyts(`e0y zSli@`jU~)zlmzz>6_K|vCrdx5Md!JDhm2f9P~zT;W_;El^X1*NLg!VLgl!!uMwPv> zy6oZtws+6(S^d~@EL6?Eh5c)V!x|!tT{l?)=@VepDEW`;&DB58!T+|N4B8r1W#xUy z2fn!Le!hpWQ8d1qVjwKb)8$k_tq(e_Pi(a4v~88#*jMLF`C$pb!f?C~AmB;+WX^mJ znhCGkLklkyCl>Y-C`Fnk;%&&%K115AQ^mFX!GurKDVHqxN6#l{sKjakRoBSmT>CUP z$S52glBestf42GucR$_fTZwChVH!C>F7)*(LHk5wlE25o$0*Wco?6|mG7-1)LLw;U z5u5w3lbB)zILN|DbC@n&ibuZ*yxj3oNutWC&-s;Q<6!=`pFtW_Ty!PK8IsF6L3$Y` z@2uKqUK8g{IYf`H{}N!kWP5Z+q;gvab5N%ro?*pg&XL4cJ~D z2{_&f=yH4;a5x+w-0~kob7+F$5&9wEQ9CD8@wB^Q;gGlqp5Is36m#qsuiLH(obcxN z+j3lyzhG+4E_@m`m2&4mJDT29ta%bwWhbjq6ow9I=VYZROC_RfQY$nsc584B?a*O+ z@79RA9j`@Sd#XepC%3#hm6AX22}OFHJuZBL(LlJ{iDnijcOpo@l-MhVV5p$sH69qH zu`K0#LNYalt0H7I@ilZ%<*l8isYBUKvw;kP3mF4D>k=?37_9?83Jcy`;#L{Kad*_6Gcg0I-R7Lq8(+$5~YLlqs z@>JSY0qtOIc^#?JDNn9>&nG$^Q+S13_h-{J%+x-rg)thRW=qJ5C)_TXaK{r-{%bVaaQDL-S*peLMjt2t)*44p#cH%p#v`9bUhzy zXZH~Znu>*7n?eD6+_kC-CDjFvdwfSR6ibTlhy zG3>S+VVXzf&N;8murf?B zlV9+B{>?v$fl%KB;G!s}*=Yap(%rXER6w>JsRPsvd{DqrtgJkgG7R$mEI!^4+>CCq z2M;c&M{eJSB1}ydto(b1PW%Rw{zSfUd1cIuuAk6?|!{W~1o+9G&Q3U2Oi-^2QeDmb0aIiGKKKsOWEvE5XO zkIS%`c<^!^KUY>v$XfVmy)ItMhp+I~cTp;zL{*r?Fxwh=L(>BXar{ zx6R6L`&;btXDpt{T%a!5zT5(&H=bDBeAj~L?#g)Ay<7IF<$MpbLDVkDN zA6IcMr18s_i7#@6Y&@p~Ikd8gL|Cp|@!lm$lzg4^Mewolcf;RR2@BhN8EGJlaFOpw zx~RhY5Rm-*K}XpeekFcp20-fq|A35&5#oIFl6*T~-4`CS(h6p52Gu^-ds~Gz`YQDG zd?oUR4rYiykNwyh1Kq0nTuC)*L;a6MNRZcZAyxH?tXG{{uqJ$lL{0hd$9KR`YqVqa ze!_wTN$ok~5fOszOXxtchG2-2xq`=q?}f}u%Tw?&D5)}2hXi|<$2LKc`cAB|CvMW^P%^3OZlv5PB-M z_9(tX%z6KM-1_`p^Qibo1;&b9UyjYJL4IZdMh$7QdcNKO-4eY=nzo7JU&!wYim-TG zn4L3`Xygm*r9{$>@pg;MZYEx5qYIi4x+4CgP@|ZK6j#>PKJJ zWVv{IibMS7v5*>w9BfHhT9JfrSWVd}>g40iXq0O$ZS_@ROFw}&seD&e ziE+$TA+r~lNN`6Gfq($S#2z&VA7VL)i@_dPG_>pc32G2pCwVCgg=c7y0w-1sIL& z5KKhLT?7RnZaAz<^rE-Tkhza0JuaPMW;FFf7&*oj&8uA#z+O1ECFVc(gm8<(lf6FI?nU#fQn_{o zowirhITA?kfy4#gEm+rQN0if_>hy}HWy81?*BkOn^^6{9%?>WN1T6P?{CP#~^Vml}(%=lXqi zl!fPjFo^PY5&VeGgO@G#ej!Vx0Au4*OMkm7W{;)T#ah8$um2=H!!^L)f*)}sc- zSDH{Tzukd*a4d>SQ-13_`3lUlRS9!>;q|#tU=wYsfQ7j2{q8 zRZ%+eUD~CfEp4e~b3i}(WUW<17a3Cc_y;1nQ@x^{F;0(^o)X2x>VXzgRM9Gk_U5^B zC-r4;KRPeGinL6xUF-QnvO7x;%!+=wH?U_}hYd%yb9OuY+7J_nHKHs*o@d<)-}>yt zGDa_aUuJJ7N!x@+O)A(<(FId5E;Xp_=m;rkN*PYl9ALo6AEXX#TsrHPh_sVW@0r^G zYNo)SUym;rR5M z$V;i+iHR!93U3kf_d@jOcuk^B=+$PauGJmT1RDb+%<`b_!84muP_)XX8A7D#ix)zO z;_H-_AO;a-jg5)v`TQBYL>l~ntE$M$>+Nz@Nwss)P4e)-;{HL2tGsv2%d6hHtEls; z$_*I!-kEqqV(ocPh-sagcwZD2)43)T#Y_!a=UbH5N8{1hN)v%^E2I)1O}!o}W({94 z3-jda5Hb`0c9-!IMjU#>azz?)PbIgv8ojHjHVnz)Z?EY%Ln23{OjIs;h zn~_Dbm%Sy4no;Fag*_0#4@9(0=`FZQ$O3LRth2%K-{|tFF|>9EOcNApKj|AXDJ106 zOE99Kpc-6&mKNM1P8DLw2B0-K;}rd~S`Q;SIZdX16cIxg7`A_BcUH#9Hb$wQwhYZK zni-BpguWOUK2(`M74llQ?4oFhAx$1L1*u|+<->YrVs(*34;hfciwm&o$%eY|HBFF( z{o3ev&4E6`(ERPs%I|j_&ry+K+%$?B4|s}8`3pvM3qo9=3Io(G(TC$tslRhSehn}i zxQ)vH^jYUFUUghP~k*xPCb{CnPoU(>KNhR@L(5wx0WjCZyEPZ_;vxh4$Jj8a*r625b1}$27gef(1$7lx~T(0RI;Rup4-p_ zUA#pR5Lhc2ckE=-{wYpiNx}Uz7}0lCRi_g>VBhPYYR(7hPVRU^o27IrdKA_aJiy># zxhnx1K?wYAc6I53&(MMZQyeNc{1Rz#5y@lw)fa}1bJ}tgMQmQma$KV3W#6$?uST_n zf;ET=SZ8dHXJc=p(sZT~p(u|>(%1SH`4fYT^y^}AtdDDVx5y6!s6xvukEK6?@dGsG zps+iam{>G82r&8=m;NZ>FH7@VZ*F=7cVhueHWLRM4QPe*a+o4@svf=uodBQ;B^Ju( z&sUa}^|@AozzbY>aHo)0ZPo0Zcq3ccAWtroz@|@h+E0D%gO%&5JBn)~k|HeDlFjzw znCcd>iNDI?2@Tg+#f76OUJu2i8zw+GmrV1%YC=t;7H>g&e?xk{z#f?}`+CS7+47Uu z=40SKqn6sPZ-g#Z)}(+xEo7K7j{T5#TDDjMgayu9wM2K!>S!qNb*{rs<M;>5NrB7MVNx} z>c9dqbe?y3-1^_eWP_6T&kdamlGthb!QLFS4)V>^3_>$mQsCF*2`AAj-J^9J3U z1Z#?V9;78T@oS#3zg3E9n9!(vqQh+ba-!p0AMOPC2s&YWT<9rXRs1Myi9D5Z?Yj*@ z-NZ~yLk?Ybz>zT(S#<#4{R(NW>>(|2zpE!mGLW_Iip%TX0}GpQt&{NG%ce;%}{$>AglGo{%wB8M7(JU-v)BjOB<=_O;c^s>0rQ0(Uy+zq}W-t zv*SH?F6m6aGJ52K@u&aS0^AYI5UU#X1H&858H&Wne6xXh&*GA7Ov6iVsju0?n)#s^ z@8m!LMd5}u*vO8uz^j1Td}Dev^9+WS@*jT!HyTHLHD%I%4Sj!*yVR0bL={1-SBEqfYsif+0ka)DI9~Y^RPF{9tMHg zgftkvklF=^-$esA>f>A)NYq|T?fzCSeuFWqb9wd$v~Sra+NJr;2R~IKz3eU8`8mH1sw- zPa?afmvisIgG!7j+?ozbw8`7v+U7j33-5roh=*58yk!72J1eb5pR=CfLKtVSry*}< zLEp+64mv@zl^GI7ne5iRrMdW1h>0?f)3cs8q%{SBZpT+`^4>eE(l!cCM1(G5HFi`9<< z1%GAr>%)l^%?p#uE7_LK_B+ZbbLUBvd&YvWUX?l0cVzpBTwyl%v+y9}fMIA}a&6RM8L@ zf&z5Npz$>m=Z)JL`KAzU#?#u?2I@Mv4-1};j0Cnj^s~jpZUd>YZTEaZ$tTb4GzagZ zWLvQCY6%hqNLuLsq%UY*?28C_82Esjh;|oW?3{Z|JTZGP+cSrHES8Fw8H@U&-1uPL zvv282z_Wu!WiMuG8|dr6NYi2;qt^s7E{SKujR?WGKrikVW{(TarlQZv1Bej)zZ+Z{ zkGJcCTS%g|xFZR)XR+X2`3GHqsvIa)gS@Ib-Q8By?`^n54Xi?GhM<0DxvQ>}0qL@C zon*`xDX6cu&XMASs86@HWt;jn`~E;*QQ&HFr%I($Rwc6=e-R)VSNm|o8ND@4cKy2I z_7$OM7bYSO&m{5qt}K%)g@q}4f3QRSqxiouH`OujCu7LqwX`4#;h#m>OnpBjcin=*#N z>a29y>eVyC376!!{I-`*Jow-Zh2$GJ9{^;+8|A;wuRnSca1@B)C|7)XjjH;WN8e&# z7h#!o)+q}!DFJ+LnN6gut(;RrZ(7$3WNTVWo@VaI%)2FvCoo2}fUO)ZgyV{71!M> z7IE18%RFHzF0~DG9xe7@qX#G$B?~ej+@adQ?T@~Z$2;n4OB9{w5fmJV>?DWIP`y(G zX*qEqP8}OVSX9*BzqdW^r)>ew9C{JRi>n_$EjDq>qCx3cQt!tOs2toCtxHdCPK3y# zfq?-~-EZL*6P2_jDeL)or66L5$6YCF(9cs`-!Y+r^W;d&9uDsDiHn~e+xtNr4Li`ktHq_UbD4wuw?#D-9&rc9k4GmFg8UcY; zr!pjy!~tKH)oi+nMc$?bCpdh_)t$>CgO%KN*78o^GpVzwudYt*ae}MU;Z~f>pl*)T z0hK6F{vc;DpE_?+Os5a>?{-cedczv5w6ENw)IaN&1(Dk2RH2AVw5<2n?t3_WBrP_k zG74KQKjzJ#D=?!8Y@31>O)1csEkA#EE?_EU&g@=B8(~1@X!~mM8x7HB0&%P{rT~?x zT}MC2VJ*2B_vuF~Bj9Bn5!*ZDGv;*4l3E; z5bh2Gk%=d|)J*!05dVGez$upYT+7t(CgK_%K3(Tih&~ta zzqtC9ofu{-jT}tQkXBKs&1y+SC%pHLgX59My|eOzXK2^>=Uxz_n8_Lp#W6Mbhaayj z75w>ZwsO?*SMEHa;%?R0qN_=oJ%uI-KUHA+g>N~Nn-}beqJ2n%i2M(fkB(UE#Sj$nPuve_j<$|lORh_&hbxteo%TPVJn;Xv zf+@=CN7Vb*$LYs`6wpdHhd6vn_@aI32*ZGAg;fdIkfK_&7IURI%3jNWWEuQ6L(Xgs zaF1#II7H`y?|#S`9QJW$*x2Wq$Hekg7>UW*%fM~pd@G#dy%;Jufj0UnE-zo^!8Yy} z-a^h1!`CkBjj+t5wn|WpB0gyknWE@Qta`a-7#|;wF06Vb7)>0J9;_aRYjAJ`wlo$~ z9{)M*d#u_$J26l>5?yqrogJ=931#M@6Jq8-5cRp{ST!yBIaa*>+JJ3*WIr;0_Y8fx zWwB`iz&_wB=x{8Ssyh*k{`2g@EytJPe)q#8Fg%qGtH1x0`JB+l1ojSZs&LXM+OiS; z#JUm3vQgdgh$d|n|LB#w-YV7iPC*Bl)*2jE%3F4V@-Whvh)%9Mh_dzJ!rq)ORpDHb zM=>4josq9QVaFQOf{v#cs{ObvmfU%pu^J_a2llZRs?izI5hR7rS zuP+JCcrBS18udc)$w@)$WGEBQ4_G35;x{&}3JdCh8bs>HB;#r# zrvZwV3?GTk)17`>eN6O;bon=+dWIBf%PU;ksPB+5rGm`ve{Sr=p_7Y9)ydE9+bdKKNMw~zy3_r768 zOF9C}NJ%OEoS#`!JhTnM-*_ay4x5w+aD-^dSpRsmv9yVJwL;5gm$5TdVJV>nu^bxe zyOhW49mV45LA~Mo3}tgoG2GQ3NWw|~DBPydpWLleRq-XuWKeKLC{Xqqr*W&>a$@f( zVnly=f-$^6&iR4L<1i24o1AWyR})3exR^#tHd!N+#ZwLE&8mGlX=yP<#-uzq3C>@@ za)6l3&n#aG?V-qHRyzC63L+glNbo^zas95^5@8rBUIw@1R{!U?{k@lQAy?pD3z(mR z_ZimaWuB$DuWX<^z%sm#`s{lI@=AcDr91WW8U@KLv@M zws_Ncj;@ZEqT6po_i^;bdMiF%DxY`2Nx|0PNzs;NuZs(yhtDZyz`odKVOj+BWCrg|*PV%^v*5QPSrm62+xp zMrS@$de?xPAR3@h%VQ+oxj#IX$S0_@)gCux_*`Ql2tU3N=gt;-8ijke^DoN@qPn$@^*Fv>w9|n_`X|ZuTJQR1U7PnJdO-iN&emD$QQ2+RWgzOF$XE; z$eiOt)64js@gBbK!;5UC?+yMwZc-64F>!HSriF$jZaSm>UQG3<`rcJG;?9Evmo1M8 z3VyOa&FxUD$pu<;uyiAEc9lygcy z9U7Wi34s4=X}#X&z82i@wM0Qf(*`snW7YE3e6iZR>}tH!L5_C0D_ZH}`?4*6bk{2Q zR+V2M4|dia`VyC-w#09t5ST_su)K~Y-zHil^Tk@x%8a42v^mcNivU{w_)4yFoUeC$TkI499eFn5{g zWTvv;)}H2PL*Eyiw?TcB>YaUcxay_jEsuU95h_v)H=6l4)ukg`_w>y;nM@LxALC)F zwh!*0i2Qwp>ELJoM?ai2eWF8yLY>6eVEGHb%>QTevdMk>9U-L-Z z{bpi+B|=Wx4q@JNB*b$wl~bza$KHJLJ6uP=iM+*|Ch^Dj5>rI97lCt5hSdJI2m|jHl>OZ%)&r->+$FQ?V&OWfnNEYAC|X z9^%AhdbG;Iq$~s$gs)#)`lCf_W5<<~Mm`IQefmmuhxbnO+#KECzx%>dyNHu%MaOP2 zcg+A5AQELmT+T_lIp4X|MQCI@+}=R1E%G^y_+rlB?6#sbI=<&cbAMEC&1-kt7Qg_) zc_8rg!^8D9F*?fpYLbDrQ+O2P&OA{@qRpFIBXdy-0TTEBh&h`A9x?m8o0`EC~{gRs;U4~+EK91~rFL9>pVABa7!^2yQg_)pS^vnJ`N=5^pKL2hRj;v&!)$u#-zf8}~ zKw(id{P`?_(e03Fj#mcb?k&?)H!o+1Y6qUoW)eN&z3#C=GDKcw;aoE3HMDq|H=vI+ zoW=gQ@ez%hL07yrCY;|Gnol|NeS4-e=Ap6VLPHQwT*}Vcv>33Lc#}{|;Aa_a9DSdt z*CxXs`U^$^DkCkK$F9|lHddVbGd7(?Z$3SX>b4?OjaWB1r8l?^G?()_+Pbt7mnO<* zmUkNEfhHkISKjoiS13fh6=SVCEN^zgyp{7sFEwe)H19J-vAd~a`Ik&8Q! z6k+10A1~AsZk@(sEJQ-e;HxkkHe?<8$C01@Rfk??Oe6KhH)f28<2ZAn_qrsjon*@d zfxqJ1<&R6N{=U+W&WnW91u1N^l#0CX8<3m|;Esy=H2RFGMC{VO@S!I)8zMF~&ZA$h z-P^2PDehC-Y0i&|KRNsGu*OS>)~BncdmlduG^UOjFFx@^OeZ7Am^z4#jm=|~T>p^4 zWIVr8B;nLlsZ}X-zvHQWSS#vuoxSh)a`lN?y5KJ7DQ0d=6Qxz~#w%ws_pD#NG47r> zr7B4ieS;iEh&xKJjJh(ms{3XGxOvDAPG1ZZkzG_z^Gk)O1?-a!U37&{?Ve zxK2bUD99a~(ydY)dfIP%=)|dT5B4BJW`nV3C09rZlF72DfA7UgEoPU|?NS<{zqdh> z9v^Tp*Ic~HF|>}jfJL@{;=^qk+gE*7d3rfKoS_5J8qDs!&IEG4AN4=6c3cwP|1zW1 zKBZMfFx;7$+1!euKw|ZGq6KT8P*NuLU)nN`8xqp8Dw%gitk~p z;Q)Zdbih0j*%#};>HxUK?1Z{c&8qPbW$CGW1o<>roJ3ZL9}9cpDafe|Z@v2w{7!I( zn*)DsE3*f7PzIuB?q~B-_u$q>C zAzqifyADi|q>g@I#ZEA^nm5Dc6(+jX=z32z$A`8>!Df+1JgX>pKPk1*L|T7HLFTv zP$Ra^A7Zx^BiL~@5Xrxn`{Q*?B68x|+?r549r<9x`8RJZZy#`wdMZv%vf~)8a;R!z z!=Z`v9OfaOy8!X<_)Lr$`yJIeJrFS;mh&Y%tm zS@wq%235Aq-hI5~xUzCM)anzf!ogl;@vHF~&pXW>nIb1e$DhX>WHEQOjt$c{J_PjMEiY8GbuRt0KE;&A%Du)gPPtwzt|G8-$iX4R8Bva?Yw^+QjSQ3tRM8OpC+Sh>KW_uP@!U zd-v_#HIz}SYfYIUjltTaSyw$u%S-Onj|Mi%B}SO%amQ9W*8QOhJpINg#e^_4(vGUl zQ99|qW=ta=Q_z1DXDII`G%`qrFTEuS*)56MOw3-V-T!B{K#O?C#=#g~3(hQP9u-`@kb}XkXCMsIMnJq$3djNAx5QF@ z5!5W#rU~K{-cNMeVfK@KkiO?T*K#NWG0{s*G#XGW-y7-mm{aWUQ4ZqHEZyGTB;W5W z4`MX3@0oIClSVvA`D|}{pj`B2hRY_a;af{rS3A9v3q%`VCJ$9*LJHhub3W5xM`JO9 zqcyB1;k6(wygtmn=O^fsra2&(fXTQ#)dg71eu_Qo=L^oj@iGZ!0u}BSDKrD32SJ`vv;F1;(Bv{d;jg7Zd;ityIRY1U?mBvsWn;Q{SOV!0ihmitY6GWcY4Ck{9&U42qiKvwZ+Ayrix}L1JW~K^RIN$R9wDX|;3gdoKVcBYl{* zAlh*U8*cu~heHok!mJLhbR~k~($dUUI})M*43WZA4kS-YOP|Z2B+2b{StR9@RVfbl zpbCH)S|h#MN_ji8i!RE1SzbztXUBBc*oamPX?53E2VY_T>n%Jo2=k(3@FWy)0DP zQ;Fp2$Bu{9?)mvXtU*)b^X6bMQRgeZ=j)GPPA@fKUGeU>yO6`jR2`eKIHRT`_mG8T4-5;-JWvNgRoP8b#zVDe&~lM`5H=XW*v#H(=st>yj-%;rAbeFk!8rTl zc**7dP+p|XWYpT_|8laVMEvXP;y7E*TFmwVfmew&<-cy$U_y`TmMQ*4^5{Wy|JpH&Sgu8(Wt#si!*Q z%lEZHs}2h;p2%K2;6kxip8Pe&j)Jim13(F0X)c!BynCtfA>r!QN{1IH?FoKEG#go<|m`t{bUGi*QCNJYDx<9#Cx4^SFK zsv&vai6l$1GPek}8XJmqPgHS91+A{2n-kx?Yx9v;E zC?~hnI?m@A!T9T2;fF7E>(&W&8i@pLjF#Fa1P>m(z5{}RM)Jw+La2 zAoNDy&mAc=z`##1_GHqxtcy7iSs^XJP7*QbG_V%BO;E zjPiEObYF2v0~F^Hj)!c*7(ha_x^`e=+KHUKN<6@6#(Y%x(Ic^A^Y1I4CYzD{bSmye zqx`p5ron3yQ70!SE>2EBny@HzO@W(KJp{5i6jbJ7^`ovJQTeNW(UniuU2ebppFD7I z;1W%^LF*4UFxD#sQT{<^R_(y7sr``1;cYohsZt=Z`XNtHlOC@I3d;R^B01>Qk93VtFT{p}=7On7nQ_jApLnh9`bO60Phj#$2!_g@@QlCaG~0>sqDxTtn@P?>lvC=4*2^Mn&-8xR!!=#VO~-&qx7p0oJK`j{^lc5+RvK z_vXHO4CsSn@nxWrjoyIe0i4y{+F_jxWo3adl?Bspn`DG<)QH1KHX!*En{-(ge-v+# z=S^*sqH+`bDo`Il`pM1xWG&7m@4Y&^y)`@Fy+}>avwXYk!mn0NT}T`{?D4#V=FG`S zN)&**E`+wwD@E?RbtAi$FzkHt$xc#EnStmx^39yCvcf`Z zY}-VY>R0pCj9cax1qhie9w~C;ra?8Urm-^9clq*Vq*6`0$pFx7X{p3d-}Zuq#T%doILCKte=F7bTTAjl45#_P(ogDcXX3 z-`3-LWOyYTOu@ANwYUZ}XM?q!%R%}uc1}((Fy}mEcOiNh+xJcbdjnkfju_*Z@Ebcl zq{Dze(mVZk=fB~P(Ew~ax=uM;tgQI9DdU>)&x77YI_hKBov+{bpu>n7Bb<;>~2bg0eCO*5!{C&j`oeOKJLT`arB7lv%s65aO(fyI(iQ(=6MhPhuA>ui=a7! z;~V!mC@2X312CEBUic^$PP*Zk8m}x$n?kU;<)O0LP0UmoiOqY5lm^`ZN;BVzX3IQs z*i$|lXT#JfD=X{0@JlnNi;{$>$T(8Pf9V{YJYXDYsE}luDDo^ zllIyJ1@2hY(~G0xU#>2ZIhTdV8_tfJ@h7h?e&2g2=}%Va(W3;pJE7aWko>?-K@jd6 z9>scPCyd2G3PyJi*?v38U0TgcI{j=zNM3(gyl9(HdvU~a{x~c3F;)^IoYuJi+?38a zwyvBFz1OcxJIryB$2Pq6eDT#XJ-@qZ#&JyK-0@2I!BN1YL>rhr0f&28 zleIVqB0e+~R9`K{6LzJLn7M+6n$vaL!(-gF6rbzvbJ;bB9+Xn{k&^%hAR3R;GU}n7 zb!i4016c5_cI5sbh69bs6b?`}wj_XS?=pPjX zf3erp^NgVMz?l$B0^E)$QXGrhl%m0@4ZK@vj$gvLwW4CXt72;zNJ{zS=*Cf2s*v*X za=cFPf+Ngi~xGe z;ENASBu_^@9u5eO@t}wUahZBfIPSro^+Uf+)AjpGTu@5fzf62VP12PA6|V`fEkJbN zyt(YRHRcCv2SJ>cDud#K(^!4MZC=>CTF?#T4^|`89^>ACsKse1aGPWFMH|9NWzyff zo#@mCzp&xGPvd+>UQEE~9_Gqb&yy}(A?T(Wy`&d!>0OIYs>ah^r;3a2R z`tu&=N?^)6!?uFU$Avqtix%JRyp%Evp1fzdX@WK$NR(=*GZxo&H$&fRptT&*hSw*qS>>VQ5vy4-Pyy%r`}Yv)3AKW;4e5W@3k2^ ze8$x??Yss*os0^%e+!-KT8eJ#McrXD|2LPxV!1uLZh0)%57vNTnAffxpO6N^`=2@sBTBe+w5>LNe}jsa$lyFanaD;b!@vyCz;y3Xlm$-M6Nl5Rmk0mQ6pwP#b+O9K!dg0)*K9Z&zl zZ8~xi9K-~e`*uLnJ7QYDjXFP|zHrq*1mhlemIfWx#?}@)GT|a!NHP_0 zDZue>wOayCGA)RwZDobZ$iU+4ZCvUQI3~l*rEIa+^SFNBerYv?E z{(e>Z5Bw)g$$d6aSE;%FDT1Yi=Z~}>Co-Jp1P^WTGP#wZYpiv~b{8FlW1k-J-2qAt zjX16RYvPOnliMvK0Rx>jH_1ic7^NXsYquI%l9B0~}WD-2f{Z75HL;|=oIsW#VF@P;%*qR_zMbhcu9XXC(OA*$dBVeh*qHb4-xn01QRg7Vw&!lS?M|1Tp1Q4Sp#4rx?`grHrXw5E>ayMx zF$D4PE@L!rO#`Y4gDp$ck3C8;trs0?Xq;oDUH+%18d_!`44f3U9;jBG92^RbkA$hX z>ZhNim(i;fh|&1CK2rvx&^~ei82QOP%6b}h$o~C{kiSCIq-}1R4Pk^N^WUD z`6iF?X0l&R$NnF0?rRM^s6eyr_6Aj~X-e<(_XmTX`3v!i7f`%ke7U)~6H1uVb;@{< zIV@3!f&+MHfCpSlaVOC0p}anaGh$ra{^&X@sl@#hwe_D@Cn%OWc~J3>i)m>k$D_mwFM!=7kV4`G<*EIfU#J7BaUZ4 z=x#5y>Eg)VnSB|p7(2;l#;$^b;ry$+VLr*7UQ1qb*{P=9!Dk3v!f?`J59VPU{38Bw zoA_<^C&y5YCHc|ke8M-ULv_Fw- zCmzMegPYQQ<@?1`0&eDcfPhVnj{}ngs1{%!$-e*4@Wkar7;8cTOV4HRipQ$SfJ}lk z1EdPP&CFD0LCOr%Q};P1S63ffmZ+U+O${5$tWYXX)6Y!1cC5Qg@T_adRug+JTL* zxELE0J562De_f70ONLxa$z9Z%`h?tWw7W{NQXA`^l9dJX&@^WSTqfvKAAK~^ zaxYmFR6Y+?oZD)B-KCrgOIw~*n!epFP-<5$_1wH7w!FU37#Vfw&|W{_)qxyCLj%BU z{J<@b9Rwpi`{S*=IkO(B2Qgr4^K0Xt#W%vkl=|uw8rD4y?`~au!O+9B1LM)KX^|9M ze=YJ^ouN87cu)RGY!iI@iK6RnbNs%|FVy$*=MK&F#EBDEcIR+8CZ!7tJ(K*7a8Ui@ z#Fa#Uud}NQO)UVdDmV9h;A8yoq%p;aD3m&kgkld#;EJv=ov{EtY-=>M`%TY;x7f?8 zNB`8lWvH4}k768K7I^eN3%~YWe{cHHd+p!yl(oF(-9>lz20jMSp23M_FQi#3Y;V97 zB>12ojv<@!Z(ng7_=(o+RIYOB0Av-ETSV-fNi`A>KI<^*Z(RX<5z+pY?aJUsrZ@ zks759qC*Ezq&3<5iVB63Cp9VKj-D{V8-v8VvBh&^-$q@pOZ9GSYyhaM!fPSMe6UYm zhp9E=6)EoyVHYH+q$Gi>PvJK^`&5N@?Uj!V%ax*Dg6H?EM*~f_w=%O#X^U8+tmbBh zZv*QeAC(X}&nEU+sv)R@{Jc7Z{g6%DKYX15aPmq?9->pKK zuuIr63#HKp%PT0T1{6HF>gUg&_$a113YqFYK3_SqPvCTb6@IZLN5b_U6)UQv-f4J< zaQhZ3D+$*EAIQ&qjStpp*WkyM+Cuh;_#OixM*6TjI`#EgBmBlOeB^7}N#8$4yBBDF zdtX$f8o*lP;A?=<2wc4eO67!v+ncv+ccvhBs(<{f!8Fxzc=CexI<<|hg{1F&`g(uupsUcIbFB0wE@&BJwJ52lm9#HAG!xWa2Ud@E`#cdEo3AHV%~ zf|^79X{9I=M8sJhV(l8xO;ihG4kPJ&HWbR0TOJ;F76C37qr4Bwt z!BlAue;PW_n5St}E(OS+LCrVh+;Xp?akQtqd)})YFI2jxcad=t_6NDiE|DTNnRT?) zjHe|!9++LQ(&(JUK^PRqji?6400$-A^4CBe7>OW>hc2jLWF9pXHJByrzH@_w-Ng@X zjf8LCLraQuuKhpxzyj1oSY`5)c+_n$lbSD_r6NU?Wyj+rRo*al-RJi)w=g0~Cz=(t zonF=x;obMC{IpjdXTIn0SP0Bg=RVW| zvR$}!sW5n$CDENe-s|L?N|~QJ8XqSIT^p7d&6dCRiMNM0N#|nL1*t?i zyqfJD>h)Fpt@}Jbpd8($;A1obN~qtquV8G-P1UkKM@d#)(!Wp1jD8)`h+5VbT&xy6 z>m#Vjbi5BP!IAkpN-56LA25m6&m2F_l`VvRmxB}{Zpz-hLqJHT7S8RM4%dy~8sYKX zbxrEt>`rC1w^sD}oMKym_EwV((4v@vqX9NJh-rjRJBw&Iuv@T4S}v{sSP5zhIZazW zll0@(y=o0Xm%+Mc2ZD)qYM)cirQGp8gs1l>vzxbSrVkR(gr;^)A)8$OIok?lve1h{ z__XZEj<81Jp}6kgAnUaRgOyO+7ZgEQ81M|l#Q*jz?8(Y5`l$8wMX+^oFQ|~u_ny5ig&AQ<#({y;ynW68&DXPB zNNvUbx=ZyZ4)AZA;9p($cw9TAC>RPIGUut=a~#}mcN+KG6*=i%xe2ra2&qsX6$MRo z&28Q?GV(Xw)=6Qu`XrO{0gc=Lp}kL?I)xT6ys6)s>NY-|??1pYiwXt%<4#v@?Hlk4 zpLayMq6aRu=^4w=9UEbtTxL_g_X>6ELxvcg3GOY%5J;4 zEg=&}kz27Q6wur!PcUha`Ljo<%E#xTbXlwJw zYBGf*kY_j3gw6BFhYwtSO-i~5c{;Qc#F%M-?2>H5*nw7cB9WAR3=Ga?&d&bE)JZgx zHw#ZWou_WK7K{D-O|0p72(-4!QOY_ zfdv14-A^;c0d3iu0&D>)k zP=W5I*0LM>2zC-XNd%Y{?$=Q1Kvta_wL7k zo>Jal-Yz*W9Sc%#!fu=%>`#%hZ)j;~ge3nQ35j;vR8kc3*P(4A@i1Yefby%AAdgvp zVv5MHB3g$mo!;`;DlvJm`LXKhkXjfM%pP6%Ra+g$FlSM_ImYDBhZWGgZ+Z zns0!SB93o(b$0pvPjOd|k`W1dQJWE|N!MLXoKg6q&l=FzFpwS!-pc3#QYP?qbTX{+ zjHKD0J$a%p9LGfq4ztj&+)G35;0>m#i|Kx{^S7h4f2xE)zvA*;BTqFA|deSbGXQ#j0swd=zH^`NY^Zgi1 zw`F}V%h`>IIMTG&l#)rBDbGp!UM}-ptyOO}a?naLfauf6yn(}z z*8gh(gh&A}#+2zUB1&N#8 zI3>q6FovsAOmXfVz$Jx|8fOSlXwi=1r^leU!yW<|f(-Uy^~{HurtSuH;)7%xW-{9i(P#sR5cUgf+6(Sl~UZxE_~&k$LqbY}8GDTbU% zO!9gTbKg?KY(g{)_w5f%O3x2CmUsSIA{adookb>Ui~iKD`c zDUzPhcwzZUQ`xSR2HVfpRK?cRSh8PKt)&XTi!?hXQ!h?7^lmahFLO7}IQz4?*yOTI zk?&G0`r@3?&@4`zm*iypXk?9HdPZC6hYx5zLpdbm3c!titrnj#z>&M_k3FH&h{49pLHJk7hN!n zWI`|Nq_~MDNq!q%8-o$=yG^I`b^c6GZz3lE{nk52;ofzSI(qbI+xzWPou>-3Aqc`8 z0MnYK>ehDe!fVn`u19*VKQa2y%}+B|fU&soWHSzUp7O5A9cLk`}n)=->c)^VY#DY$|+F zB{$xyqEk_B3K0+gEC#iVvoG5wwx$QRraworrDb3;jy)RjXu@$=s0gEGb3}4LA{Vb8 zYn@OO3GAjSCk!ljeDwRvCrwawrqjD44VEVn2Jsj>6p$Utbghc#mQ$DWXzv(eg$G~N zY-`jEz0!eM#A9JXiSpFCdP$Up;o-yLyu6{6^)}3xybylk}#D2*TT`c4z8XRB?76cK;FHGg`dGkA_NdFA#87 zL@$alqYV62Cwbz8dg`lUwJf|9$R$OzD#qJ6m`33WW7CB+=3%y?0?h>@Z+Sn-vv8~) zEj7kEp_WRBNj+Y#Y(uuar%m5JMlnMXJd6euqnT2L_>=GYh2QTw^ZzZ!pkl?Gw^c9-qL^&FLA5~A)C#)0|5IFsER%~RjUelNags*kNtv_#-_v52`-aQ%D zihJEe(Kx-#7S{6T5Y|ESVKgkt`hG)is~~&w0on?DjSuH#h8hPB(S$&GdJ&|=z>d1J{m`Mf@i!Xj-=QXePAi7|xZ$kP{n!^te&Umdw0~t9o zzlswWSQF2Xa;lSo^aoF?`L&G$CwtD*r_A5r9M|A~fMX`kn>&4+CBs_FbNuw>;@?A_ zXyu=LxfrXJj{AU|T{oG+;Ha^LHyCm5go0ibem?WF^u| z;l$VkJzlpP=N+3y?*_QFSmCZ0gG8#S&?DvpOz2? zSOHE4Hd^j|+Go$Au?<1ggwmU`k7>N&6hr?TDp3S)_$nOvR3ZC4MC%}ZFul%oN5_BO zACY{9HsbjM|E3IFkTr-?xnWjA=@Onv&gjrT#DsTiEm0~J8RPVTh0F-yD|V8m-XY}Ia_C*s^ueVUqmmFYRZqe?-WaTd zgt9w)KYo1L3K-Xhr2Tz;V1A(shGhci z+iTaZ+1uMceCo|J7V>e?T6|)0jZ?u_JAn~FRv$eVUqZ7#wcOqt)#cbf%44OF?~JRA zuqgiX+Xs}Vb?LFi-P1~ea^1s&c}IJR&5Zw|IAU*mUUIXdvSc8lcc2TF@ftz-n%R#y zGu{~PzRW(ygPC9UFc`t9OZSJ%+=A1FawkqRr?(}~2q;BK%O2vK1O0M$_@t?nGWK9k zw96OucZ0U8+%y`8vlnD)k88&kQ811+!mG1zv|=Ow-8;if0~%UdC~K2W-TLWxqLlu8 z=;}OWHAH^}1&g>vZ*y`am?JJ1JNA9>ykTSWyIeAR#MXni+aXxVqPQ2s9KM+Pt*wu3 z6Y#d6naRDc<#y||ySmj!%yla&xX4NF7Uvs@?Bvo_RS`89qE0_~_xlxbOH#GwaM|BN zWKa{$&di{xg4ABj;{aA>&Lt%->=}1M(nb?$^vY*nDiJ0|ijuz7zieU&KY9XS*d!G1 z9gcu)lHl4PsSb8K`Rn}kRVW>x6IlQnS4#kd^z;F}ozQ(d3i`1~i8|)+&w-k9N z{&6!nl&={6a^;Cp%nH^6({fEVkc@ey+PsABtf5(s{E=)lpi}g?PcrurI_u5qwxnf!pR9W#{b&U5P`=x zuKPc}RnUO5t$k4sTJ}4C(Rh@G3)Sr~GdsK8g|G&MIs2QAZ-9OQ;e6dr2qTyLGlr8< z56Uq&mQOy&Cqe(KO|4_p719J;6zBwF8%VY5dCl!5o#~aw1dzjYDc1%zf$tEW}; zIBE3{X{*^u{-7NeyOYXSVsc+Ar-E>UfZ#Arz6GFO;pFE#bzDR4q^bg}VyJ7~WUi5{ z_6}^Ju7{)uF|nm4ZeAX$EBns9Md%U~xT5hO{=k;9EB3{KEu_*p{VXJgb6(}>+GFbp zQ1t&{#IGvYrUDItdl|ItyXPge^fC>6SKHM>qG~?NNJ~S>UGd?=o31GbsdZhye{CR2 z`w%8KuWYUi%uY{(&T9XPOz?W#^@Sm->^xA^(f1kGQB5zY?>n}{Yq zdxkXuS<2gwnrtvu>~`p!aKl5u8{=(Y)p0%>0!%1>yK>AFFs;7Rf=6{~IVvLJ2c|ZC z@+VIwXr)6o4M8oCj}6wud^yggEq2b^fT_Y`^ixSF!jKJiR2i@dfnNm%F`i5I3Rf73 z9&yb?)=Mo(;^yii=mE?nKFq)1wX8(@xM~)mmcVfs(Ot6a!7D)JfXGIvdVe zYM$DR=Xhiim3OXwlst6Mb!eRYa47Fu+-t?pS#j?ia=SszVLL4wrA?Y~ z?G9=SZYE{kHLj;`bTX!LwJGyYw-u2}AaXLC_p$u1e(e1=&-FVP=QA}eQ+ zoJ;S?axG;KuSJ5hX@zf}#1-zA4}Es3`8OWO`9_6IAgqF&hHP&a-%c_pvM(PRuD?tx zwGH2xF}p`TKjHQdz6KmuG0n#W7TG9fKFb|=|BOUvNP`d$!GAuZi|6#O%F5F_iK0Q} zLJNdHiGP(nZ&eh*_#%U$l|frakv~w2I+HO)3<-d*uXuYlmKGvXd%UpH=GFnz4&h(o z8tL_;xdoI-oN#@0CLaqg?2My$-QlQnJiB3r#vICJ-f)DsxJGPz>b~dD4;5}p%l+mt zm=elMN<5Y)5d@y!JFTC&M7i*999iK_XuM(1K}6YGdNZj5BVJ1T4>iNGxb_m6R#=pD zNQo;&kN~!;u99ShXuhw%AM(I*Au*Z#*Q62)S2og_0{tVtIyCV8+Srh#4};zSS%?w7 zm1}Ej=w6~=(Hc%kR`fXQg=A1isYFcJu_7bZx|@Q==AhzdQ1VoG!R1LDst7fvJ`hjN zX?_#nkjb05-GeT8bnDhVYo=QES`kN*BC_X~-Mf|vWasgQDG{av%O&DA=%ys}$2iW^ z1PPPkd-IqbPiVh%404|$44=uy>$G!Dc!VM)wTyQ5?L-_#*e^3O3cCM%|BhjnW=7kP zGg6FjlsM|j7L$xrW%vN_>?7AyIO+}yzW zM=&lrI*%VSv$35Qj>9@9QtYLno}-E6y~nEWxn8tWEtK3hKO-2V0wzR82CBjBEd`If zyT-&KOeO3{qqv9bQoUy?W9V(48i0RZd(-xjfc+$L-iOrsZK)XT)1pHMS@ z3}&e^%2>MXDZiVg%ePXhUyR`kKGYIGtt7@Q&%+v8vxK6%$%c(81S#JiKKv|j=DCPG z?zeWW&rqR7ML}@CxF(x)PEAQ%X^q(1e7V7b%KtUHeGPdeg+lgh*EDEGL;|lX z9T7=Q@|P|=wJpRj{6A;nmHoM1Pf2-8y@{x!pNJUWj=e?i4^30Q!m9Px7ujb)VpdQX z=jEo**SjozYiaI&Bd~)yUdhbA?t*#>z;Za}fn;frR9S!C31-?;) zDXZJT)WifLOLW*cqiu_f$*Lw0h=u29+8r|rgj=8>QRGI;Na%7I-b3CP93#Z#Q@>vS zccz>r1MTI*aZsk?cSBJz>KbF+XDuFMALy{{n^nsvj`dPbS{m`@H%m>@R#`r<%fu@S zMU-E$rW_~Wm47Jy>Y-kiMG>MCHI_)tkAE5iGF*l_jF|}uB?k891$vp#s2WibR!miF zR4dGNRSZi~7c<;gId+UN>*nBtYTxKPyIa!(tCfuT3@;t0U$@V!;1X!fp0}%2)LRWu zNd9;^)8NJ6z=5NtvWP&X58GH&*j}u!zYDjseNpne{|??sk9~tmrQl|T{0_tuUwmk2 z2=lzjNtl4q;)PrXZcGdznW=nN`ffcub*B2<_9czN26P6|fx^+|MXdXCukvU53MiKf zx<$%AGg7F|l8!H5!ilAE)Jv9=V95D#r|B=U^_gND-C-H@N&1h&+lhnneaBDOv4WN8 zKo|JT&7{QduR%_8K!2}?$lcO%qYK-O@KW$MFKT}?i(d?2j>A3)2W&f+9{QU3d=g!m9 zwwClaHt`g`xG{sF8x`pZDJdaNl#@@i1@U?`91N-vY}*MIR4|J5$ z^`S%9+F0Ak+d|m_xt8C*3_mZpz$(o(GTQhxFPWJg%(&5Gy}aGnCfZ;p=_UINVnib4 z9^nTA&)~6;lM@(aIx=|eW9m9Y68FF3q=|VpbR5`!qwBClRhoO`7zM6U<4ChK1}Bz^ zHpo2}l=(C?-68zeXJsh>7^+ZO#A-k7^$;dsT7^Jy&(B?^=QXrEuT zcp%Y`!y5TuNdZ*>7gvQBRP|XGF_o$t*->Su>mLyjdEE57IU%V3p5UKg1v7?1-pl{c z(11|a*T?5dy8c;R-J>ER$Z&0;xou}xKje%VEBtQ}QBml*9gVBsycU!KBT4VHOp@r1 zdga{r7HwVKgY@2JDL>)J7ndg_d-AP_m^}ULKPvc)NsDLWRf1#-NQ~{dh$D zx()|TU3n7c?5(*pV)cMQnwv%!T^K6D zF2VHg;)2X&2!7$7q7Q84eI)+pl~?FMIdLV1894HwbuPkmRlK{sol$@oT-D~Z*0wVr zi>vX*chz(zwcKmh!Lc571S}3X0z;SmYe9G)dC&CUni>`85q|zeS=DOi=Iw2g(MD}e zGnv56@DGy<{L$X>IXmu1Cy+4Ww5!d5>P6d(qq{?&hCPv0azXXK@8XSFLL59W@uaHSVycyd;f)oHdq z@hl?+re_B}wbBi1j^BLztmYR9r>uYP9x1kYY11RaqlZ00EX06-mm!wt65cO(aBz{@ zgwJP$c4DALMClIJ`x~V{!3Kc&)3p>^F&za2x>qpI$Vy1o5wc~I zBqT|SkPt%VV6B$V*R4!z^rcUVy$(8gGyN{9gI(r81RX?HDAKQ!?w5{?gF% z?!eKQe|axi#VoR}q0P1wxv1NzRHl0@=ekUUK3WaT09I9Q`bo33c~QfTAx=U$VohN+ zpnJOU^T!{wP2BI^-BgTdi#DNYVu&1LpuX9D=t|^(jC@9k`Jtq}-3l|eySe?3`ASAY z8i+dQkf{_g5wQHY=w_Qenq(LY=jUPgZZ{;pk22IG#q@I6pG5|J9NN#pawcosI63FVs-rFqc zJ#)S?^{cAt_`_~xR+oyJ7kArFX=FpYHzNF2tkJ=4lM|UsznW}G6Qa}@V<=FR9SqU9 zuEy9#kY_h5a#v)C;kgo9CIzrLG2Dt?)W>jiWwdz|U0?EI0a!WydtG0Tq|gipfv_M& zB*>1Ad1>m|l%15UEEM)26OV7EZp4@ZNJmx#&X%s=tzSFJ@brv~rL_Rlf~C~JeG=M& z7ADy}I0jr~j}0w2j|7jpJWoty^qhwpHd_e(38RttCTsYf@v!X^2C^IKcAF71j=QNH zuJz%0V92&6=0?5_!Wl6sxAg>|U>cpB8TwvJPkiVfO*w|0O864KO``glo#ct40}2?| zl_svRJA*Etu)K`XfveP1%D#1(YmX}rmW=>g?&JH@rK5m+`6pivHGDX)39l$jxPLwz`hX4IST)cqD%uIT%kxBB@)?Gn8f8umV8tn&20{1`vr@>^q z3NT*b@**uJe`cHnTe1+1Jpj1i;LI$d;|2(JWSjsPh-(3)0ce6d<^#kYy*ePl!3cCZ zByF7qYq32GsCdj3>}JDf-rhc0KjOH%{3bj%u+vRWB2+}2WzR0r1I{w9UUeg1qxn1A z-T1ju>jFy71HzP+2G@NTmj)L$Y^1$g5clx&Dm(j!VX1%>Iri`#N1NB7C+Qcac57)_ ziys$2j$~=oB^t9LOs6c+lxP3f$2k8tFORq@vo*X5(2sa{AR6HHz1pt0+1eOmx499W zqd@Udyid$?adF9|>2axjod37xlIK#i2E_QNbiRCnpBiu?ez|cSkdOH$&xU`XIsm=i zW)GWEye|k3U4CwjKCW%tLtAsgmgQ18@!z;v<^J@W(H|P3CjOH4 z^^FA&lk=9cXz^ax0=RJ}v1^hWQ|ui6%jA6l)TTGmf1xee2h69ks%nB(_OutaB3|m- zj*g+(<`COWRUt;MCfitZy4q6FJWa-J>JKAzL{Afhl2{$RQ#B;{fo<= zngi3E?J%QeJ@Q;2rHG#okWl0X4bHfI>-(8@z=oR()|mgXgo}%dfDI9UaZ*dmwOmU1 zsuWZ|lDzRq#At0T7aMexMj|>^F=E2dc4MX}v(?L253=oXYh{e&=~qdc!|{M^3JxbA z06|f0|3zLGr3yJ05F|&JXLwvV!i%kkrJOCGBE&jmU6xXRE#Mva~PRhpR@E1O{(U0g*zyu5ozI8oly)@$Vk?W@UM^Rcw z;#$@p<0aPT+9Fp$9MMF;pM{t%4Ei2+sqeAs()fhx4Idl#F&`!W*kjWIcx!Xx0)m1J zHmN?oWKx3cpi4;+(BqCn9-~D-yMV9)8Gq}25{o1E<%H;WLJ6ZHUG4 z>0DPsV2I}F*R?(rmHFSyVsG<;b%%xN+@_vOB)V|ef%F7~_QU!|tJ}Ou2^`3&V#I%C z4)8C=Lo81BL#Qy7=7{P^Uqy0t_=ih?5+6=jW4Wlr!bzpu~P$e# zp~F>dEq-$=O|9Cg?SgDNlv?|b>IsEm3S{Cy$MUl5h0U7^v)snQUgn5AT;nFFiOjum zG(UPT)9&IfvGX>n6cMzaCF`$-tNz)YlvH9?b7YO)X@Z^3U0yuB(bmrQ@hMkpk{R#o z;mKd%d5Ue}S+DoWODw_U`LQE9O>TXYBGVg?^}K%ldgAZa3X!M`v6G2#%+@&XHQeR} z59)4xLMH&#EhN=gzEGLFR>o;$CD53yJ(H(Ne0*uB``5w@hR#ijhksT5xFqo8iq}uy z@(?#py8trNnUmU=1^GKBL#*f@r2lF0GONsQ%lg+%%=1YX2Q%e6CW~H2>2XQf-3T-Z zoY>q9kWJ@5sK(}xt?VE3;Jm#E=w>@RJHV`G@?lN{#F3(VRcZXV$U^YA9MNOngEJ?j zV+ZyZ7V;!UnouMhOyIQfBr|RAs-Nc?f2oth5Ql+dr_LKwmAL-ir+N6~pUNxqUW$nE z10GADnVp*p`qe3i_>{B%c*?E+gxCsA)uJ!jWS&HM5ZMXb$dEA<6g;=}o$jijpR2R8 zaE5_u@K%76Qw(8Mqn0Z`)Ur1I%LtNNF!_j%b*6 zIO@qvJab)ugD24wHc)X-c+Xt>PDkWe*cOv)9>B8SKOnXUh~F0Wbd}Nz!PCDgfztSIdJZ?#8X+b zweW?l1wa#oCg9@5lf{F^_L1;$>)D;koj2cWX~06eI>sCDOGY3_tss3!cO@`)E+k(? zpZv0?1w;g(mOd>a+OA1(Y+as(X(Iq58PoKTg#~H8jpbS8)TL`=$-19kzNFwciBfxr z{v3$Pj+LF{sZtbq1X)rN19i8H&FlEd$@DnWKjb2kY&oAc<$jT}npO8+LZ~6Yj)DOm zB6W+^*vEDQCRN0vj@CB>{kdx^1BNW+nhfhIFstL-+$p-Si^9HyTcNl27L? z1I)}gF@JsS8l9ER?E3m^jXCaf?3ZU&zN>&Cw=M9HuME$66Rk20@}Q~2B=z6F5o*Xm z9qyB1!^gyCshTXMDP%K6rftj^^=rl|@He>|D}CHIl$?Ol(JaNv5HF2Ok~bB~)h5&~ z99iKD!o{+%;Eqw?81{AY_4>1C_I~^jBcO5-PJx>eW=mL{;|_>1#Zc(T$6Ose<-|to z5GZo-N#BXGm@~UmQvu)?gm#8%Q^c?Y1g)IxDTX3RFx=30^v4CqOe|O&5*Hpwg|zgi z%wCq?A+kW@qjZC#4X@?r_052Do^I7WaIeQ}iKYU+hVBqaU9g}vPeLE&?(XjAXD zBpMfA`{(4Zes)i-Yva{OTN!`2e43)jVH)AU7%OLPrqE8(ICJrkR>Z|8VOAZ*HP^bK z*&!QTJ^1Yr5^{*YBfF)4U~eRDAc$euZ$M24`Cvl>6mqIZ$G_;`ES z9|*4hm>^WG1;*gn-@gI3;qkBi*@E}La?hKMsOAN+R^^Y9slioh8^d@+pey|No~Ln!JZn^ zvE& zJQ*VmaP#q>_lUl6W4l)+4K@R#eH zm!T+TO21=XUU_I_=ghP+AJ(N_eZ7VM+ZA_(NpJPyC_{|Z8a_2tI5ghWwi^RS7uha4 z5#k*6Ubr^UOiTrlo0bhrr+3!KIgWX94{B(w=Pf zt%$Z|T!KZg%JTes;ez6qMp@S(+g0SRJGN_9Lm7yva{pW&|2qyYTGT#6LoAe|s2>e) zU;q@q4mL&s0-h2u> z#QHco zTSXhz97PWxke>;zM@WciBh-MFGsmHMxz#j`tOsI z)+)Udr~SadKe$GnD-gei=*z9GAecWD6dsB|KdG<7%-01k4b*`sNI*AOilWlNz&2Q0 zV|^fIE+^DVoD6LU%xPTLsfT;I>+iWM+IKEULmDznJFVh*b@PuuiF`w2^a0Z&6lCS2 zmv_SpZ1|6lPVT-0EDD1>e}^W*SKjSX zlTjZti{g1We+HF~&Z6-B{aYFzN-}L}X$ckqJpp8ek}CNwSR)UjZyF{T?^^vd7ZRL@ zJc*97>5%j#3*n5~Hrh!_+LoF*9`yGYh|G)#<&d^;lMz=c_KjY&t=s@(6%pmf6a%aTvRIXC1S8F+IjMk=8g4_TG=NyTGRdO zb^JJX26J=JMg3hLYyg&k8pcxeV%hL!&|e5Z4Dxu-!x30dBuAO@#T#b~M8~by4RxIMcBsaGWEmc+ppG-8~5r11Y))a*>n66$uU# zC5r+(T~Ps^zgSWm(uI(;mh2K+_jx-^(ALGJ!?^;y)Lm9ho}Q!cn=vvg`k|>h&xz9L z9DeC^$BGY2Y7f78zSrczAbafx0~pHE7TwJ7?!2877}r&@~@~lY8jQ{reSxt?(1C)xm%voPwP!NrQGmoJE?2)@wvF1j13FCpFsvUTsN9```ew zxL9qeqCFTYR<8G`&>^X<93)!70FrgOySqV24>;$%w!lwFN~(D+V}YG3f|p3TqjD`I zy&d4J|H^{kjRRsO)nQ3ygJgyxb6NE1dsVn}`2lyMdjQx%KLZ^ZQ?e2dM$!^66qT zo{#UhvM!DxBvB4>2#5NNjI@Y*92!$s!jibz*60w?Lmih>5X?-2$YO4!>sQy+ z&f1pN+*>`8w0ngdUPL?m5ciUMy zvwo|@Wdue6!uESOCSx9kRpclQ;L!B&qqoyt%?Ls8yH||m_gKSZ!?ZxtdZhg%w?0{1 zORsYUg*P}QN9h_%(ezF&?+m#=i}f?v6aI0{p(Zq(^e8OYO@vpsgs#;_w|Jys@>A-* zva&lFvmZyIELtY0x5sJQk6~FjhWQ_9j+z*?NxXM6+9+^OaPG|Jc!9p{>Gf0pT3W(@ zi}E@+#oc7Z`8cDCN?Sq~zqHX&Jb>w3n#_ zJ@EH!3tsQMhI$(;R~ujwre1syvcs$871{xN1`r{}7L? zmo|UbCLY>>Ruv;(zF2X6GUQNhOyFqs7z1d${6X4D>Db9gJc^16!=3E;rVCs zF+#a4jWu%|%xkBy4}Q8-U2@p3C(%OkPM@sse#^J?ci9~&HnBofB~OJMmbRqG|JnW~ zvoZd=A*9LjWF=?zYKGJL)kQ19m9bqwDOS@Lj1la5OTq8I|r{PLc;O_RdR z@87#)J00+`O3mKydq?E`5Vhq$q9bvR3V+=t(gGMeb#uU|1M`+h(S=@Xf~?SrS4(0g zbNauZRz3%0(~mMx!&RpC@PYMyjMBCsP`nw#LqLs(n9<`25-JyC^AFOSg%gzPaLS?A zSy)(rB1{#K#{^@DK?oQ*G=^#H0g~2B<@Lm#`LpQ)qN$evi895YW^A*^GC`;I+nw%^V13jTXo`4q(_!bN<2F|*>&#Kf2lu2?v_VTN~i zx2_z;3otL5yKp9uV2*F#<>#9KUA398P-nM6u?_L(4&5o1+11cYj5AEQB@bb6`{$bA zzgs(DZul(WMI_p*{}!2A6m46)N#cZZpZZ4)Y~}kk6LS24clgYgfT$28z$OQt39h9lQrcnwo#f<-mgtZqYAS5GO-pFDND%jxlf`l3|O^dz( z5ZtiP;!J#HE0NlsGdP2&i{soRhN7vE980DF7M?O`S}3_=`~5B%v*u5u+lR4tZpp>9 zz^sf5DBq-hBe}#<6fc)X52AP&G=O{>rS3I*`Qx*y78(*pQU0f|#n0EBz>N=qO~f-N3DwF6U-z=QtdhX_R8 zzpk=LvF2jM{cGHaF&H~$On4^XXjjYb`Tbn2TqAh?@4xvbU>y-FgQST8;T%Dv)g)YA z1oVWM;kAH?z!k`f$6vqJR?J90&?HMjLSCh#al(X?l@)nnsF8|H_e7|TNHA(I-Tck5 zQGrZbL4wxFlSl;+B!H)fIC5ulH>MFpsF8ASXAUX=A+0cnxC=%m80wjSVR`G5B7|Jk zccF3C8>TaORsYFvH$r4%xjP-oNYEq#{m{*UEGr-Y8c&=|xy?8>W~kAM zT0BHmuNK$x0<$J$m($oJz@0EqgZu4t&6G`ppmUE z$6+%8xpPi!&{-zdmhVokCTFwQh1mQ{z*)oB*`I$+%_94~zO4~IlaFB`;1raUFdjzC zst#5M0Lmz4krgoM4&f3TlWGb7qjM?L`*;!`w?=(iE0pv(XskCz(zt%lxpLHA0A~a6 z>jS%O_!W=wCqQkDEj-|?FJO#^s+Ela8_i$709*)I_|VkRaUCSQHXm0lZ{Gd5Y9jUo z9Vs(M^XFezM{JO#idC;!v!4tB!0Od3(44x+B12jw7{zb!#wf_+^>sg}jVnj-9x;1? z`Nz(AyD^IU27XR3K}wultgH`>>%^itz@(nJp`)bTo(tjv9<=ArL7WSv#GOl$Y9WxD z%{vw24A_|xG3|xmdRP@LDP!^v?|u|{D&GXTTzRK6UGe4Sc3MvlTB8WHCh@#Gwz!gf zD$b%-fpVdF66g+sRW4rSpxb3sTEE%~EjgeI4AZdnz}lxE126>dds17nZqCf7p%#L% zPxVu$!Q+wVaHz1*!uEqbzXNS!r1^mUMXyNMH$c4hIQBbN;NHb(oo2mjLb-Qr6-7Yp zOFF?_j5*KvR68|)z>CnaJpE`S+lnyzD00MTyl_mxRX1HS9sxrtc0Ih_B1ezHU0|yz zl=9|uc6IO%K^!=!uD9!Jc|5e?Dyf!!nf$->Qcp!&PP8{XA;Fi9rh&#&HS25t2b<5A AT>t<8 literal 0 HcmV?d00001 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; -}