├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── auto-merge.yaml │ ├── gh-pages.yml │ └── update-flake-lock.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── INDEX.md ├── SUMMARY.md ├── book.toml ├── cli.md ├── flake-module.nix ├── howtos.md ├── howtos │ ├── INDEX.md │ ├── custom-kexec.md │ ├── disko-modes.md │ ├── extra-files.md │ ├── ipv6.md │ ├── limited-ram.md │ ├── nix-path.md │ ├── no-os.md │ ├── secrets.md │ ├── terraform.md │ └── use-without-flakes.md ├── logo.png ├── logo.svg ├── quickstart.md ├── reference.md └── requirements.md ├── flake.lock ├── flake.nix ├── scripts └── create-release.sh ├── src ├── default.nix ├── flake-module.nix ├── get-facts.sh └── nixos-anywhere.sh ├── terraform ├── .envrc ├── .terraform-docs.yml ├── README.md ├── all-in-one.md ├── all-in-one │ ├── main.tf │ └── variables.tf ├── flake-module.nix ├── install.md ├── install │ ├── main.tf │ ├── providers.tf │ ├── run-nixos-anywhere.sh │ └── variables.tf ├── nix-build.md ├── nix-build │ ├── main.tf │ ├── nix-build.sh │ └── variables.tf ├── nixos-rebuild.md ├── nixos-rebuild │ ├── deploy.sh │ ├── main.tf │ └── variables.tf ├── tests │ ├── README.md │ ├── digitalocean-deployment.tftest.hcl │ ├── digitalocean │ │ └── main.tf │ ├── hcloud-deployment.tftest.hcl │ ├── hcloud │ │ └── main.tf │ ├── main.tf │ ├── nix-build-special-args.tftest.hcl │ ├── nixos-anywhere.tftest.hcl │ └── terraform.tfvars.example └── update-docs.sh ├── tests ├── flake-module.nix ├── from-nixos-build-on-remote.nix ├── from-nixos-generate-config.nix ├── from-nixos-separated-phases.nix ├── from-nixos-with-sudo.nix ├── from-nixos.nix ├── lib │ └── test-base.nix ├── linux-kexec-test.nix └── modules │ ├── installer.nix │ ├── ssh-keys │ ├── ssh │ └── ssh.pub │ └── system-to-install.nix └── treefmt └── flake-module.nix /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report to help us improve nixos-anywhere 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! Please make sure you've followed the steps below before submitting. 9 | 10 | **Before submitting:** 11 | 1. Run `nix run --refresh github:nix-community/nixos-anywhere` to ensure you're using the latest version 12 | 2. Reproduce the issue with the `--debug` flag to get detailed logs 13 | 14 | - type: checkboxes 15 | id: prerequisites 16 | attributes: 17 | label: Prerequisites 18 | description: Please confirm you've completed these steps 19 | options: 20 | - label: I have updated to the latest version using `nix run --refresh github:nix-community/nixos-anywhere` 21 | required: true 22 | - label: I have reproduced the issue with the `--debug` flag 23 | required: true 24 | - label: I have searched existing issues to make sure this isn't a duplicate 25 | required: true 26 | 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: Bug Description 31 | description: A clear and concise description of what the bug is 32 | placeholder: Describe what happened and what you expected to happen 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | id: reproduction 38 | attributes: 39 | label: Steps to Reproduce 40 | description: Steps to reproduce the behavior 41 | placeholder: | 42 | 1. Run command '...' 43 | 2. See error 44 | value: | 45 | 1. 46 | 2. 47 | 3. 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | id: logs 53 | attributes: 54 | label: Debug Logs 55 | description: Please paste the full output from running nixos-anywhere with the `--debug` flag 56 | placeholder: Paste the complete debug output here 57 | render: shell 58 | validations: 59 | required: true 60 | 61 | - type: input 62 | id: command 63 | attributes: 64 | label: Command Used 65 | description: The exact nixos-anywhere command you ran 66 | placeholder: "nix run github:nix-community/nixos-anywhere -- --debug [your options]" 67 | validations: 68 | required: true 69 | 70 | - type: dropdown 71 | id: target-system 72 | attributes: 73 | label: Target System 74 | description: What type of system are you installing NixOS on? 75 | options: 76 | - Cloud server (AWS, DigitalOcean, etc.) 77 | - Bare metal server 78 | - Virtual machine 79 | - Hetzner dedicated server 80 | - Local machine 81 | - Other 82 | validations: 83 | required: true 84 | 85 | - type: input 86 | id: nixos-version 87 | attributes: 88 | label: NixOS Version 89 | description: What version of NixOS are you trying to install? 90 | placeholder: "e.g., 23.11, unstable" 91 | 92 | - type: textarea 93 | id: environment 94 | attributes: 95 | label: Environment Information 96 | description: Please provide information about your environment 97 | placeholder: | 98 | - Host OS: (e.g., NixOS 23.11, Ubuntu 22.04) 99 | - Nix version: (output of `nix --version`) 100 | - Target architecture: (e.g., x86_64-linux, aarch64-linux) 101 | value: | 102 | - Host OS: 103 | - Nix version: 104 | - Target architecture: 105 | 106 | - type: textarea 107 | id: configuration 108 | attributes: 109 | label: Configuration Files 110 | description: Please share relevant parts of your NixOS configuration or disko configuration 111 | placeholder: Share your flake.nix, disko configuration, or other relevant config files 112 | render: nix 113 | 114 | - type: textarea 115 | id: additional-context 116 | attributes: 117 | label: Additional Context 118 | description: Add any other context about the problem here 119 | placeholder: Any additional information that might help understand the issue 120 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Documentation 4 | url: https://github.com/nix-community/nixos-anywhere/tree/main/docs 5 | about: Check the documentation for usage guides and examples 6 | - name: Community Discussion 7 | url: https://discourse.nixos.org/ 8 | about: Ask questions and discuss with the NixOS community 9 | - name: Matrix Chat 10 | url: https://matrix.to/#/#nixos-anywhere:matrix.org 11 | about: Real-time chat support for nixos-anywhere 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea or enhancement for nixos-anywhere 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to suggest a new feature! Please provide as much detail as possible to help us understand your request. 9 | 10 | - type: checkboxes 11 | id: prerequisites 12 | attributes: 13 | label: Prerequisites 14 | description: Please confirm you've completed these steps 15 | options: 16 | - label: I have searched existing issues to make sure this isn't a duplicate 17 | required: true 18 | - label: I have checked the documentation to see if this feature already exists 19 | required: true 20 | 21 | - type: textarea 22 | id: problem 23 | attributes: 24 | label: Problem Description 25 | description: Is your feature request related to a problem? Please describe the problem you're trying to solve. 26 | placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: solution 32 | attributes: 33 | label: Proposed Solution 34 | description: Describe the solution you'd like to see implemented 35 | placeholder: A clear and concise description of what you want to happen 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: alternatives 41 | attributes: 42 | label: Alternatives Considered 43 | description: Describe any alternative solutions or features you've considered 44 | placeholder: A clear and concise description of any alternative solutions or features you've considered 45 | 46 | - type: dropdown 47 | id: feature-type 48 | attributes: 49 | label: Feature Type 50 | description: What type of feature is this? 51 | options: 52 | - New command-line option 53 | - New installation target support 54 | - Performance improvement 55 | - Documentation improvement 56 | - Developer experience improvement 57 | - Integration with other tools 58 | - Other 59 | validations: 60 | required: true 61 | 62 | - type: textarea 63 | id: use-case 64 | attributes: 65 | label: Use Case 66 | description: Describe your specific use case and how this feature would help 67 | placeholder: | 68 | - What are you trying to accomplish? 69 | - How would this feature fit into your workflow? 70 | - Who else might benefit from this feature? 71 | 72 | - type: textarea 73 | id: implementation 74 | attributes: 75 | label: Implementation Ideas 76 | description: Do you have any ideas about how this could be implemented? 77 | placeholder: If you have thoughts on the technical implementation, please share them here 78 | 79 | - type: textarea 80 | id: additional-context 81 | attributes: 82 | label: Additional Context 83 | description: Add any other context, screenshots, or examples about the feature request here 84 | placeholder: Any additional information that might help understand the request 85 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Merge Dependency Updates 2 | on: 3 | - pull_request_target 4 | jobs: 5 | auto-merge-dependency-updates: 6 | runs-on: ubuntu-latest 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | concurrency: 11 | group: "auto-merge:${{ github.head_ref }}" 12 | cancel-in-progress: true 13 | steps: 14 | - uses: Mic92/auto-merge@main 15 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - staging 8 | pull_request: 9 | merge_group: 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: cachix/install-nix-action@v18 20 | 21 | - run: nix build .#docs 22 | 23 | - name: Deploy 24 | uses: peaceiris/actions-gh-pages@v3 25 | if: ${{ github.ref == 'refs/heads/main' }} 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: ./result 29 | -------------------------------------------------------------------------------- /.github/workflows/update-flake-lock.yml: -------------------------------------------------------------------------------- 1 | name: "Update flakes" 2 | on: 3 | repository_dispatch: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "51 2 * * 0" 7 | 8 | jobs: 9 | createPullRequest: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Install Nix 14 | uses: cachix/install-nix-action@v31 15 | with: 16 | extra_nix_config: | 17 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 18 | - uses: actions/create-github-app-token@v1 19 | id: app-token 20 | with: 21 | app-id: ${{ vars.CI_APP_ID }} 22 | private-key: ${{ secrets.CI_APP_PRIVATE_KEY }} 23 | - name: Update flakes 24 | run: nix flake update 25 | - name: Create Pull Request 26 | uses: peter-evans/create-pull-request@v7 27 | with: 28 | title: Update flakes 29 | token: ${{ steps.app-token.outputs.token }} 30 | labels: | 31 | auto-merge 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result* 2 | .direnv 3 | /docs/book 4 | 5 | # terraform 6 | .terraform.lock.hcl 7 | test_key 8 | test_key.pub 9 | errored_test.tfstate 10 | errored_test.tfstate.backup 11 | terraform.tfstate 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | To run `nixos-anywhere` from the repo: 2 | 3 | ```console 4 | nix run . -- --help 5 | ``` 6 | 7 | To format the code: 8 | 9 | ```console 10 | nix fmt 11 | ``` 12 | 13 | To run all tests: 14 | 15 | ```console 16 | nix flake check -vL 17 | ``` 18 | 19 | To run an individual test: 20 | 21 | ``` 22 | nix build .#checks.x86_64-linux.from-nixos -vL 23 | ``` 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Numtide 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | [Documentation Index](docs/INDEX.md) 8 | 9 | ## README 10 | 11 | Setting up a new machine is time-consuming, and becomes complicated when it 12 | needs to be done remotely. If you're installing NixOS, the **nixos-anywhere** 13 | tool allows you to pre-configure the whole process including: 14 | 15 | - Disk partitioning and formatting 16 | - Configuring and installing NixOS 17 | - Installing additional files and software 18 | 19 | You can then initiate an unattended installation with a single CLI command. 20 | Since **nixos-anywhere** can access the new machine using SSH, it's ideal for 21 | remote installations. 22 | 23 | Once you have initiated the command, there is no need to 'babysit' the 24 | installation. It all happens automatically. 25 | 26 | You can use the stored configuration to repeat the same installation if you need 27 | to. 28 | 29 | ## Overview 30 | 31 | If you have machines on a mix of platforms, you'll need a common installation 32 | solution that works anywhere. **nixos-anywhere** is ideal in this situation. 33 | 34 | **nixos-anywhere** can be used equally well for cloud servers, bare metal 35 | servers such as Hetzner, and local servers accessible via a LAN. You can create 36 | standard configurations, and use the same configuration to create identical 37 | servers anywhere. 38 | 39 | You first create Nix configurations to specify partitioning, formatting and 40 | NixOS configurations. Further options can be controlled by a flake and by 41 | run-time switches. 42 | 43 | Once the configuration has been created, a single command will: 44 | 45 | - Connect to the remote server via SSH 46 | - Detect whether a NixOS installer is present; if not, it will use the Linux 47 | `kexec` tool to boot into a Nixos installer. 48 | - Use the [disko](https://github.com/nix-community/disko) tool to partition and 49 | format the hard drive 50 | - Install NixOS 51 | - Optionally install any Nix packages and other software required. 52 | - Optionally copy additional files to the new machine 53 | 54 | It's also possible to use **nixos-anywhere** to simplify the installation on a 55 | machine that has no current operating system, first booting from a NixOS 56 | installer image. This feature is described in the 57 | [how-to guide](./docs/howtos/no-os.md#installing-on-a-machine-with-no-operating-system). 58 | It's useful because you can pre-configure your required software and 59 | preferences, and build the new machine with a single command. 60 | 61 | **Important Note:** Never use a production server as the target. It will be 62 | completely overwritten and all data lost. This tool should only be used for 63 | commissioning a new computer or repurposing an old machine once all important 64 | data has been migrated. 65 | 66 | ## Prerequisites 67 | 68 | - Source Machine: 69 | 70 | - Can be any machine with Nix installed, e.g. a NixOS machine. 71 | 72 | - Target Machine: 73 | 74 | - Unless you're using the option to boot from a NixOS installer image, or 75 | providing your own `kexec` image, it must be running x86-64 Linux with kexec 76 | support. Most `x86_64` Linux systems do have kexec support. By providing 77 | your own [image](./docs/howtos/custom-kexec.md#using-your-own-kexec-image) 78 | you can also perform kexec for other architectures eg aarch64 79 | - The machine must be reachable over the public internet or local network. 80 | Nixos-anywhere does not support wifi networks. If a VPN is needed, define a 81 | custom installer via the --kexec flag which connects to your VPN. 82 | - When `kexec` is used the target must have at least 1 GB of RAM, excluding 83 | swap. 84 | 85 | ## How to use nixos-anywhere 86 | 87 | The [Quickstart Guide](./docs/quickstart.md) gives more information on how to 88 | run **nixos-anywhere** in its simplest form. For more specific instructions to 89 | suit individual requirements, see the [How To Guide](./docs/howtos/INDEX.md). 90 | 91 | ## Related Tools 92 | 93 | **nixos-anywhere** makes use of the 94 | [disko](https://github.com/nix-community/disko) tool to handle the partitioning 95 | and formatting of the disks. 96 | 97 | ## Contact 98 | 99 | For questions, come join us in the 100 | [nixos-anywhere](https://matrix.to/#/#nixos-anywhere:nixos.org) matrix room. 101 | 102 | ## Licensing and Contribution details 103 | 104 | This software is provided free under the 105 | [MIT License](https://opensource.org/licenses/MIT). 106 | 107 | --- 108 | 109 | This project is supported by [Numtide](https://numtide.com/). 110 | ![Untitledpng](https://codahosted.io/docs/6FCIMTRM0p/blobs/bl-sgSunaXYWX/077f3f9d7d76d6a228a937afa0658292584dedb5b852a8ca370b6c61dabb7872b7f617e603f1793928dc5410c74b3e77af21a89e435fa71a681a868d21fd1f599dd10a647dd855e14043979f1df7956f67c3260c0442e24b34662307204b83ea34de929d) 111 | 112 | We are a team of independent freelancers that love open source.  We help our 113 | customers make their project lifecycles more efficient by: 114 | 115 | - Providing and supporting useful tools such as this one 116 | - Building and deploying infrastructure, and offering dedicated DevOps support 117 | - Building their in-house Nix skills, and integrating Nix with their workflows 118 | - Developing additional features and tools 119 | - Carrying out custom research and development. 120 | 121 | [Contact us](https://numtide.com/contact) if you have a project in mind, or if 122 | you need help with any of our supported tools, including this one. We'd love to 123 | hear from you. 124 | -------------------------------------------------------------------------------- /docs/INDEX.md: -------------------------------------------------------------------------------- 1 | # Table of Content: - nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | - [README](../README.md) 8 | - [Quickstart](./quickstart.md) 9 | - [System Requirements](./requirements.md) 10 | - [How to Guide](./howtos/INDEX.md) 11 | - [Reference](./reference.md) 12 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary: - nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | The **nixos-anywhere** tool allows you to pre-configure the whole process of 8 | installing NixOS, and run the install remotely with a single CLI command. 9 | 10 | Refer to the following documentation for more information. 11 | 12 | [System Requirements](./requirements.md): CPU and memory requirements 13 | 14 | [Quickstart](./quickstart.md): Instructions for a typical installation 15 | 16 | [How to Guide](./howtos/INDEX.md): Instructions for non-typical use cases 17 | 18 | - [Installing on a machine with no operating system](./howtos/no-os.md) 19 | - [Using your own kexec image](./howtos/custom-kexec.md) 20 | - [Secrets and full disk encryption](./howtos/secrets.md) 21 | - [Use without flakes](./howtos/use-without-flakes.md) 22 | - [Terraform](./howtos/terraform.md) 23 | - [Nix-channels / `NIX_PATH`](./howtos/nix-path.md) 24 | - [IPv6-only targets](./howtos/ipv6.md) 25 | 26 | [Reference](./reference.md): Reference Guide 27 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = [ ] 3 | language = "en" 4 | multilingual = false 5 | src = "." 6 | title = "nixos-anywhere - install NixOS everywhere" 7 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | ``` 4 | Usage: nixos-anywhere [options] [] 5 | 6 | Options: 7 | 8 | * -f, --flake 9 | set the flake to install the system from. 10 | * --target-host 11 | specified the SSH target host to deploy onto. 12 | * -i 13 | selects which SSH private key file to use. 14 | * -p, --ssh-port 15 | set the ssh port to connect with 16 | * --ssh-option 17 | set one ssh option, no need for the '-o' flag, can be repeated. 18 | for example: '--ssh-option UserKnownHostsFile=./known_hosts' 19 | * -L, --print-build-logs 20 | print full build logs 21 | * --env-password 22 | set a password used by ssh-copy-id, the password should be set by 23 | the environment variable SSHPASS 24 | * -s, --store-paths 25 | set the store paths to the disko-script and nixos-system directly 26 | if this is given, flake is not needed 27 | * --no-reboot 28 | do not reboot after installation, allowing further customization of the target installation. 29 | * --kexec 30 | use another kexec tarball to bootstrap NixOS 31 | * --kexec-extra-flags 32 | extra flags to add into the call to kexec, e.g. "--no-sync" 33 | * --ssh-store-setting 34 | ssh store settings appended to the store URI, e.g. "compress true". needs to be URI encoded. 35 | * --post-kexec-ssh-port 36 | after kexec is executed, use a custom ssh port to connect. Defaults to 22 37 | * --copy-host-keys 38 | copy over existing /etc/ssh/ssh_host_* host keys to the installation 39 | * --stop-after-disko 40 | exit after disko formatting, you can then proceed to install manually or some other way 41 | * --extra-files 42 | contents of local are recursively copied to the root (/) of the new NixOS installation. Existing files are overwritten 43 | Copied files will be owned by root. See documentation for details. 44 | * --disk-encryption-keys 45 | copy the contents of the file or pipe in local_path to remote_path in the installer environment, 46 | after kexec but before installation. Can be repeated. 47 | * --no-substitute-on-destination 48 | disable passing --substitute-on-destination to nix-copy 49 | * --debug 50 | enable debug output 51 | * --option 52 | nix option to pass to every nix related command 53 | * --from 54 | URL of the source Nix store to copy the nixos and disko closure from 55 | * --build-on-remote 56 | build the closure on the remote machine instead of locally and copy-closuring it 57 | * --vm-test 58 | build the system and test the disk configuration inside a VM without installing it to the target. 59 | * --build-on auto|remote|local 60 | sets the build on settings to auto, remote or local. Default is auto. 61 | auto: tries to figure out, if the build is possible on the local host, if not falls back gracefully to remote build 62 | local: will build on the local host 63 | remote: will build on the remote host 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/flake-module.nix: -------------------------------------------------------------------------------- 1 | { 2 | perSystem = { pkgs, lib, ... }: { 3 | packages.docs = pkgs.runCommand "nixos-anywhere-docs" 4 | { 5 | passthru.serve = pkgs.writeShellScriptBin "serve" '' 6 | set -euo pipefail 7 | cd docs 8 | workdir=$(${pkgs.coreutils}/bin/mktemp -d) 9 | trap 'rm -rf "$workdir"' EXIT 10 | ${pkgs.mdbook}/bin/mdbook serve --dest-dir "$workdir" 11 | ''; 12 | } 13 | '' 14 | cp -r ${lib.cleanSource ./.}/* . 15 | ${pkgs.mdbook}/bin/mdbook build --dest-dir "$out" 16 | ''; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /docs/howtos.md: -------------------------------------------------------------------------------- 1 | # How to Guide 2 | -------------------------------------------------------------------------------- /docs/howtos/INDEX.md: -------------------------------------------------------------------------------- 1 | # How To Guide: nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | [Documentation Index](./INDEX.md) 8 | 9 | ## Contents 10 | 11 | [Installing on a machine with no operating system](./no-os.md) 12 | 13 | [Kexec on systems with limited RAM](./limited-ram.md) 14 | 15 | [Copying files to the new installation](./extra-files.md) 16 | 17 | [Using your own kexec image](./custom-kexec.md) 18 | 19 | [Repair installations without wiping data](./disko-modes.md) 20 | 21 | [Secrets and full disk encryption](./secrets.md) 22 | 23 | [Use without flakes](./use-without-flakes.md) 24 | 25 | [Terraform](./terraform.md) 26 | 27 | [Nix-channels / `NIX_PATH`](./nix-path.md) 28 | 29 | [IPv6-only targets](./ipv6.md) 30 | -------------------------------------------------------------------------------- /docs/howtos/custom-kexec.md: -------------------------------------------------------------------------------- 1 | # Using your own kexec image 2 | 3 | By default, `nixos-anywhere` downloads the kexec image from the 4 | [NixOS images repository](https://github.com/nix-community/nixos-images#kexec-tarballs). 5 | 6 | However, you can provide your own `kexec` image file if you need to use a 7 | different one. This is particularly useful for architectures other than `x86_64` 8 | and `aarch64`, since they don't have a pre-build image. 9 | 10 | To do this, use the `--kexec` command line switch followed by the path to your 11 | image file. The image will be uploaded prior to execution. 12 | 13 | Here's an example command that demonstrates how to use a custom kexec image with 14 | `nixos-anywhere`: 15 | 16 | ``` 17 | nix run github:nix-community/nixos-anywhere -- \ 18 | --kexec "$(nix build --print-out-paths github:nix-community/nixos-images#packages.aarch64-linux.kexec-installer-nixos-unstable-noninteractive)/nixos-kexec-installer-noninteractive-aarch64-linux.tar.gz" \ 19 | --flake 'github:your-user/your-repo#your-system' \ 20 | root@yourip 21 | ``` 22 | 23 | Make sure to replace `github:your-user/your-repo#your-system` with the 24 | appropriate Flake URL representing your NixOS configuration. 25 | 26 | The example above assumes that your local machine can build for aarch64 in one 27 | of the following ways: 28 | 29 | - Natively 30 | 31 | - Through a remote builder 32 | 33 | - By emulating the architecture with qemu using the following NixOS 34 | configuration: 35 | 36 | ```nix 37 | { 38 | boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/howtos/disko-modes.md: -------------------------------------------------------------------------------- 1 | # Repair installations without wiping data 2 | 3 | By default, nixos-anywhere will reformat all configured disks before running the 4 | installation. However it is also possible to mount the filesystems of an 5 | existing installation and run `nixos-install`. This is useful to recover from a 6 | misconfigured NixOS installation by first booting into a NixOS installer or 7 | recovery system. 8 | 9 | To only mount existing filesystems, add `--disko-mode mount` to 10 | `nixos-anywhere`: 11 | 12 | ``` 13 | nix run github:nix-community/nixos-anywhere -- --disko-mode mount --flake # --target-host root@ 14 | ``` 15 | 16 | 1. This will first boot into a nixos-installer 17 | 2. Mounts disks with disko 18 | 3. Runs nixos-install based on the provided flake 19 | 4. Reboots the machine. 20 | -------------------------------------------------------------------------------- /docs/howtos/extra-files.md: -------------------------------------------------------------------------------- 1 | # Copying files to the new installation 2 | 3 | The `--extra-files ` option allows copying files to the target host after 4 | installation. 5 | 6 | The contents of the `` is recursively copied and overwrites the targets 7 | root (/). The contents _must_ be in a structure and permissioned as it should be 8 | on the target. 9 | 10 | In this way, there is no need to repeatedly pass arguments (eg: a fictional 11 | argument: `--copy `) to `nixos-anywhere` to complete the intended 12 | outcome. 13 | 14 | The path and directory structure passed to `--extra-files` should be prepared 15 | beforehand. 16 | 17 | This allows a simple programmatic invocation of `nixos-anywhere` for multiple 18 | hosts. 19 | 20 | ## Simple Example 21 | 22 | You want `/etc/ssh/ssh_host_*` and `/persist` from the local system on the 23 | target. The `` contents will look like this: 24 | 25 | ```console 26 | $ cd /tmp 27 | $ root=$(mktemp -d) 28 | $ sudo cp --verbose --archive --parents /etc/ssh/ssh_host_* ${root} 29 | $ cp --verbose --archive --link /persist ${root} 30 | ``` 31 | 32 | The directory structure would look like this: 33 | 34 | ```console 35 | drwx------ myuser1 users 20 tmp.d6nx5QUwPN 36 | drwxr-xr-x root root 6 ├── etc 37 | drwx------ myuser1 users 160 │ └── ssh 38 | .rw------- root root 399 │ ├── ssh_host_ed25519_key 39 | .rw-r--r-- root root 91 │ ├── ssh_host_ed25519_key.pub 40 | drwxr-xr-x myuser1 users 22 └── persist 41 | drwxr-xr-x myuser1 users 14 ├── all 42 | drwxr-xr-x myuser1 users 22 │ ├── my 43 | .rw-r--r-- myuser1 users 6 │ │ ├── test3 44 | drwxr-xr-x myuser1 users 10 │ │ └── things 45 | .rw-r--r-- myuser1 users 6 │ │ └── test4 46 | .rw-r--r-- myuser1 users 6 │ └── test2 47 | drwxr-xr-x myuser1 users 0 ├── blah 48 | .rw-r--r-- myuser1 users 6 └── test 49 | ``` 50 | 51 | **NOTE**: Permissions will be copied, but ownership on the target will be root. 52 | 53 | Then pass $root like: 54 | 55 | > nixos-anywhere --flake ".#" --extra-files $root --target-host root@newhost 56 | 57 | ## Programmatic Example 58 | 59 | ```sh 60 | for host in host1 host2 host3; do 61 | root="target/${host}" 62 | install -d -m755 ${root}/etc/ssh 63 | ssh-keygen -A -C root@${host} -f ${root} 64 | nixos-anywhere --extra-files "${root}" --flake ".#${host}" --target-host "root@${host}" 65 | done 66 | ``` 67 | 68 | ## Considerations 69 | 70 | ### Ownership 71 | 72 | The new system may have differing UNIX user and group id's for users created 73 | during installation. 74 | 75 | When the files are extracted on the remote the copied data will be owned by 76 | root. 77 | 78 | If you wish to change the ownership after the files are copied onto the system, 79 | you can use the `--chown` option. 80 | 81 | For example, if you did `--chown /home/myuser/.ssh 1000:100`, this would equate 82 | to running `chown -R /home/myuser/.ssh 1000:100` where the uid is 1000 and the 83 | gid is 100. **Only do this when you can _guarantee_ what the uid and gid will 84 | be.** 85 | 86 | ### Symbolic Links 87 | 88 | Do not create symbolic links to reference data to copy. 89 | 90 | GNU `tar` is used to do the copy over ssh. It is an archival tool used to 91 | re/store directory structures as is. Thus `tar` copies symbolic links created 92 | with `ln -s` by default. It does not follow them to copy the underlying file. 93 | 94 | ### Hard links 95 | 96 | **NOTE**: hard links can only be created on the same filesystem. 97 | 98 | If you have larger persistent data to copy to the target. GNU `tar` will copy 99 | data referenced by hard links created with `ln`. A hard link does not create 100 | another copy the data. 101 | 102 | To copy a directory tree to the new target you can use the `cp` command with the 103 | `--link` option which creates hard links. 104 | 105 | #### Example 106 | 107 | ```sh 108 | cd /tmp 109 | root=$(mktemp -d) 110 | cp --verbose --archive --link --parents /persist/home/myuser ${root} 111 | ``` 112 | 113 | `--parents` will create the directory structure of the source at the 114 | destination. 115 | -------------------------------------------------------------------------------- /docs/howtos/ipv6.md: -------------------------------------------------------------------------------- 1 | # NixOS-anywhere on IPv6-only targets 2 | 3 | As GitHub engineers still haven't enabled the IPv6 switch, the kexec image 4 | hosted on GitHub, cannot be used unfortunately on IPv6-only hosts. However it is 5 | possible to use an IPv6 proxy for GitHub content like that: 6 | 7 | ``` 8 | nixos-anywhere \ 9 | --kexec https://gh-v6.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-kexec-installer-noninteractive-x86_64-linux.tar.gz \ 10 | ... 11 | ``` 12 | 13 | This proxy is hosted by [numtide](https://numtide.com/). It also works for IPv4. 14 | 15 | Alternatively it is also possible to reference a local file: 16 | 17 | ``` 18 | nixos-anywhere \ 19 | --kexec ./nixos-kexec-installer-noninteractive-x86_64-linux.tar.gz \ 20 | ... 21 | ``` 22 | 23 | This tarball will be then uploaded via sftp to the target. 24 | -------------------------------------------------------------------------------- /docs/howtos/limited-ram.md: -------------------------------------------------------------------------------- 1 | # Kexec on Systems with Limited RAM 2 | 3 | When working with nixos-anywhere on systems with limited RAM (around 1GB), you 4 | can use the `--no-disko-deps` option to reduce memory usage during installation. 5 | 6 | ## How it works 7 | 8 | The `--no-disko-deps` option uploads only the disko partitioning script without 9 | including its dependencies. This significantly reduces memory usage because: 10 | 11 | 1. The installer normally stores all dependencies in memory 12 | 2. Partitioning tools can be quite large when bundled with their dependencies 13 | 14 | ## Usage example 15 | 16 | ```bash 17 | nix run github:nix-community/nixos-anywhere -- --no-disko-deps --flake # --target-host root@ 18 | ``` 19 | 20 | ## Trade-off 21 | 22 | While this approach saves memory, it means the partitioning tools will be 23 | whatever versions are available on the target system, rather than the specific 24 | versions defined in your NixOS configuration. This could potentially lead to 25 | version inconsistencies between the partitioning tools and the NixOS system 26 | being installed. 27 | 28 | This trade-off is usually acceptable for memory-constrained environments where 29 | installation would otherwise fail due to insufficient RAM. 30 | -------------------------------------------------------------------------------- /docs/howtos/nix-path.md: -------------------------------------------------------------------------------- 1 | # Nix-channels / `NIX_PATH` 2 | 3 | nixos-anywhere does not install channels onto the new system by default to save 4 | time and disk space. This for example results in errors like: 5 | 6 | ``` 7 | (stack trace truncated; use '--show-trace' to show the full trace) 8 | 9 | error: file 'nixpkgs' was not found in the Nix search path (add it using $NIX_PATH or -I) 10 | 11 | at «none»:0: (source not available) 12 | ``` 13 | 14 | when using tools like nix-shell/nix-env that rely on `NIX_PATH` being set. 15 | 16 | # Solution 1: Set the `NIX_PATH` via nixos configuration (recommended) 17 | 18 | Instead of stateful channels, one can also populate the `NIX_PATH` using nixos 19 | configuration instead: 20 | 21 | ```nix 22 | { 23 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 24 | # ... other inputs 25 | 26 | outputs = inputs@{ nixpkgs, ... }: 27 | { 28 | nixosConfigurations.yoursystem = nixpkgs.lib.nixosSystem { 29 | system = "x86_64-linux"; # adapt to your actual system 30 | modules = [ 31 | # This line will populate NIX_PATH 32 | { nix.nixPath = [ "nixpkgs=${inputs.nixpkgs}" ]; } 33 | # ... other modules and your configuration.nix 34 | ]; 35 | }; 36 | }; 37 | } 38 | ``` 39 | 40 | Advantage: This solution will be automatically kept up-to-date every time the 41 | flake is updated. 42 | 43 | In your shell you will see something in your `$NIX_PATH`: 44 | 45 | ```shellSession 46 | $ echo $NIX_PATH 47 | /root/.nix-defexpr/channels:nixpkgs=/nix/store/8b61j28rpy11dg8hanbs2x710d8w3v0d-source 48 | ``` 49 | 50 | # Solution 2: Manually add the channel 51 | 52 | On the installed machine, run: 53 | 54 | ```shellSession 55 | $ nix-channel --add https://nixos.org/channels/nixos-unstable nixos 56 | $ nix-channel --update 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/howtos/no-os.md: -------------------------------------------------------------------------------- 1 | # Installing on a machine with no operating system 2 | 3 | If your machine doesn't currently have an operating system installed, you can 4 | still run `nixos-anywhere` remotely to automate the install. To do this, you 5 | would first need to boot the target machine from the standard NixOS installer. 6 | You can either boot from a USB or use `netboot`. 7 | 8 | The 9 | [NixOS installation guide](https://nixos.org/manual/nixos/stable/index.html#sec-booting-from-usb) 10 | has detailed instructions on how to boot the installer. 11 | 12 | When you run `nixos-anywhere`, it will determine whether a NixOS installer is 13 | present by checking whether the `/etc/os-release` file contains the identifier 14 | `VARIANT_ID=installer`. This identifier is available on releases NixOS 23.05 or 15 | later. 16 | 17 | If an installer is detected, `nixos-anywhere` will not attempt to `kexec` into 18 | its own image. This is particularly useful for targets that don't have enough 19 | RAM for `kexec` or don't support `kexec`. 20 | 21 | NixOS starts an SSH server on the installer by default, but you need to set a 22 | password in order to access it. To set a password for the `nixos` user, run the 23 | following command in a terminal on the NixOS machine: 24 | 25 | ``` 26 | passwd 27 | ``` 28 | 29 | If you don't know the IP address of the installer on your network, you can find 30 | it by running the following command: 31 | 32 | ``` 33 | $ ip addr 34 | 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 35 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 36 | inet 127.0.0.1/8 scope host lo 37 | valid_lft forever preferred_lft forever 38 | inet6 ::1/128 scope host 39 | valid_lft forever preferred_lft forever 40 | 2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000 41 | link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff 42 | altname enp0s3 43 | altname ens3 44 | inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute eth0 45 | valid_lft 86385sec preferred_lft 75585sec 46 | inet6 fec0::5054:ff:fe12:3456/64 scope site dynamic mngtmpaddr noprefixroute 47 | valid_lft 86385sec preferred_lft 14385sec 48 | inet6 fe80::5054:ff:fe12:3456/64 scope link 49 | valid_lft forever preferred_lft forever 50 | ``` 51 | 52 | This will display the IP addresses assigned to your network interface(s), 53 | including the IP address of the installer. In the example output below, the 54 | installer's IP addresses are `10.0.2.15`, `fec0::5054:ff:fe12:3456`, and 55 | `fe80::5054:ff:fe12:3456%eth0`: 56 | 57 | To test if you can connect and your password works, you can use the following 58 | SSH command (replace the IP address with your own): 59 | 60 | ``` 61 | ssh -v nixos@fec0::5054:ff:fe12:3456 62 | ``` 63 | 64 | You can then use the IP address to run `nixos-anywhere` like this: 65 | 66 | ``` 67 | nix run github:nix-community/nixos-anywhere -- --flake '.#myconfig' --target-host nixos@fec0::5054:ff:fe12:3456 68 | ``` 69 | 70 | This example assumes a flake in the current directory containing a configuration 71 | named `myconfig`. 72 | -------------------------------------------------------------------------------- /docs/howtos/secrets.md: -------------------------------------------------------------------------------- 1 | # Secrets and full disk encryption 2 | 3 | The `nixos-anywhere` utility offers the capability to install secrets onto a 4 | target machine. This feature is particularly beneficial when you want to 5 | bootstrap secrets management tools such as 6 | [sops-nix](https://github.com/Mic92/sops-nix) or 7 | [agenix](https://github.com/ryantm/agenix), which rely on machine-specific 8 | secrets to decrypt other uploaded secrets. 9 | 10 | ## Example: Decrypting an OpenSSH Host Key with pass 11 | 12 | In this example, we demonstrate how to use a script to decrypt an OpenSSH host 13 | key from the `pass` password manager and subsequently pass it to 14 | `nixos-anywhere` during the installation process: 15 | 16 | ```bash 17 | #!/usr/bin/env bash 18 | 19 | # Create a temporary directory 20 | temp=$(mktemp -d) 21 | 22 | # Function to cleanup temporary directory on exit 23 | cleanup() { 24 | rm -rf "$temp" 25 | } 26 | trap cleanup EXIT 27 | 28 | # Create the directory where sshd expects to find the host keys 29 | install -d -m755 "$temp/etc/ssh" 30 | 31 | # Decrypt your private key from the password store and copy it to the temporary directory 32 | pass ssh_host_ed25519_key > "$temp/etc/ssh/ssh_host_ed25519_key" 33 | 34 | # Set the correct permissions so sshd will accept the key 35 | chmod 600 "$temp/etc/ssh/ssh_host_ed25519_key" 36 | 37 | # Install NixOS to the host system with our secrets 38 | nixos-anywhere --extra-files "$temp" --flake '.#your-host' --target-host root@yourip 39 | ``` 40 | 41 | ## Example: Uploading Disk Encryption Secrets 42 | 43 | In a similar vein, `nixos-anywhere` can upload disk encryption secrets, which 44 | are necessary during formatting with disko. Here's an example that demonstrates 45 | how to provide your disk encryption password as a file or via the `pass` utility 46 | to `nixos-anywhere`: 47 | 48 | ```bash 49 | # Write your disk encryption password to a file 50 | echo "my-super-safe-password" > /tmp/disk-1.key 51 | 52 | # Call nixos-anywhere with disk encryption keys 53 | nixos-anywhere \ 54 | --disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \ 55 | --disk-encryption-keys /tmp/disk-2.key <(pass my-disk-encryption-password) \ 56 | --flake '.#your-host' \ 57 | root@yourip 58 | ``` 59 | 60 | In the above example, replace `"my-super-safe-password"` with your actual 61 | encryption password, and `my-disk-encryption-password` with the relevant entry 62 | in your pass password store. Also, ensure to replace `'.#your-host'` and 63 | `root@yourip` with your actual flake and IP address, respectively. 64 | 65 | ## Example: Using existing SSH host keys 66 | 67 | If the system contains existing trusted `/etc/ssh/ssh_host_*` SSH host keys and 68 | certificates, `nixos-anywhere` can copy them in case they are necessary during 69 | installation and system activation. 70 | 71 | ``` 72 | nixos-anywhere --copy-host-keys --flake '.#your-host' root@yourip 73 | ``` 74 | 75 | This would copy `/etc/ssh/ssh_host_*` to `/mnt` after kexec but before 76 | installation, ignoring files that already exist in destination. 77 | -------------------------------------------------------------------------------- /docs/howtos/terraform.md: -------------------------------------------------------------------------------- 1 | # Terraform 2 | 3 | The nixos-anywhere terraform modules allow you to use Terraform for installing 4 | and updating NixOS. It simplifies the deployment process by integrating 5 | nixos-anywhere functionality. 6 | 7 | Our terraform module requires the 8 | [null](https://registry.terraform.io/providers/hashicorp/null/latest) and 9 | [external](https://registry.terraform.io/providers/hashicorp/external/latest) 10 | provider. 11 | 12 | You can get these by from nixpkgs like this: 13 | 14 | ```nix 15 | nix-shell -p '(pkgs.terraform.withPlugins (p: [ p.null p.external ]))' 16 | ``` 17 | 18 | You can add this expression the `packages` list in your devshell in flake.nix or 19 | in shell.nix. 20 | 21 | Checkout out the 22 | [module reference](https://github.com/nix-community/nixos-anywhere/tree/main/terraform) 23 | for examples and module parameter on how to use the modules. 24 | -------------------------------------------------------------------------------- /docs/howtos/use-without-flakes.md: -------------------------------------------------------------------------------- 1 | # Use without flakes 2 | 3 | First, 4 | [import the disko NixOS module](https://github.com/nix-community/disko/blob/master/docs/HowTo.md#installing-nixos-module) 5 | in your NixOS configuration and define disko devices as described in the 6 | [examples](https://github.com/nix-community/disko/tree/master/example). 7 | 8 | Let's assume that your NixOS configuration lives in `configuration.nix` and your 9 | target machine is called `machine`: 10 | 11 | ## 1. Download your favourite disk layout: 12 | 13 | See https://github.com/nix-community/disko-templates/ for more examples: 14 | 15 | The example below will work with both UEFI and BIOS-based systems. 16 | 17 | ```bash 18 | curl https://raw.githubusercontent.com/nix-community/disko-templates/main/single-disk-ext4/disko-config.nix > ./disko-config.nix 19 | ``` 20 | 21 | ## 2. Get a hardware-configuration.nix from on the target machine 22 | 23 | - **Option 1**: If NixOS is not installed, boot into an installer without first 24 | installing NixOS. 25 | - **Option 2**: Use the kexec tarball method, as described 26 | [here](https://github.com/nix-community/nixos-images#kexec-tarballs). 27 | 28 | - **Generate Configuration**: Run the following command on the target machine: 29 | 30 | ```bash 31 | nixos-generate-config --no-filesystems --dir /tmp/config 32 | ``` 33 | 34 | This creates the necessary configuration files under `/tmp/config/`. Copy 35 | `/tmp/config/nixos/hardware-configuration.nix` to your local machine into the 36 | same directory as `disko-config.nix`. 37 | 38 | ## 3. Set NixOS version to use 39 | 40 | ```nix 41 | # default.nix 42 | let 43 | # replace nixos-24.11 with your preferred nixos version or revision from here: https://status.nixos.org/ 44 | nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/archive/refs/heads/nixos-24.11.tar.gz"; 45 | in 46 | import (nixpkgs + "/nixos/lib/eval-config.nix") { 47 | modules = [ ./configuration.nix ]; 48 | } 49 | ``` 50 | 51 | ## 4. Write a NixOS configuration 52 | 53 | ```nix 54 | # configuration.nix 55 | { 56 | imports = [ 57 | "${fetchTarball "https://github.com/nix-community/disko/tarball/master"}/module.nix" 58 | ./disko-config.nix 59 | ./hardware-configuration.nix 60 | ]; 61 | # Replace this with the system of the installation target you want to install!!! 62 | disko.devices.disk.main.device = "/dev/sda"; 63 | 64 | # Set this to the NixOS version that you have set in the previous step. 65 | # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . 66 | system.stateVersion = "24.11"; 67 | } 68 | ``` 69 | 70 | ## 5. Build and deploy with nixos-anywhere 71 | 72 | Your current directory now should contain the following files from the previous 73 | step: 74 | 75 | - `configuration.nix`, `default.nix`, `disko-config.nix` and 76 | `hardware-configuration.nix` 77 | 78 | Run `nixos-anywhere` as follows: 79 | 80 | ```bash 81 | nixos-anywhere --store-paths $(nix-build -A config.system.build.formatScript -A config.system.build.toplevel --no-out-link) root@machine 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixos-anywhere/e2382dcc21e1ee026c71dab8cbaad89c9e12fc8b/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 27 | 31 | 32 | 33 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 88 | 93 | 98 | 103 | 109 | 114 | 116 | 117 | 119 | 121 | 122 | 124 | 126 | 128 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart Guide: nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | [Documentation Index](./INDEX.md) 8 | 9 | ## Introduction 10 | 11 | This guide documents a simple installation of NixOS using **nixos-anywhere** on 12 | a target machine running x86_64 Linux with 13 | [kexec](https://man7.org/linux/man-pages/man8/kexec.8.html) support. The example 14 | used in this guide installs NixOS on a Hetzner cloud machine. The configuration 15 | may be different for some other instances. We will be including further examples 16 | in the [How To Guide](./howtos/INDEX.md) as and when they are available. 17 | 18 | You will need: 19 | 20 | - A [flake](https://wiki.nixos.org/wiki/Flakes) that controls the actions to be 21 | performed 22 | - A disk configuration containing details of the file system that will be 23 | created on the new server. 24 | - A target machine that is reachable via SSH, either using keys or a password, 25 | and the privilege to either log in directly as root or a user with 26 | password-less sudo. 27 | 28 | **nixos-anywhere** doesn’t need to be installed. You can run it directly from 29 | [the Github repository.](https://github.com/nix-community/nixos-anywhere) 30 | 31 | Details of the flake, the disk configuration and the CLI command are discussed 32 | below. 33 | 34 | ## Steps required to run nixos-anywhere 35 | 36 | ### 1. Enable Flakes 37 | 38 | Check if your nix has flakes enabled by running `nix flake`. It will tell you if 39 | it's not. To enable flakes, refer to the 40 | [NixOS Wiki](https://wiki.nixos.org/wiki/Flakes#enable-flakes). 41 | 42 | ### 2. Initialize a Flake 43 | 44 | The easiest way to start is to copy our 45 | [example flake.nix](https://github.com/nix-community/nixos-anywhere-examples/blob/main/flake.nix) 46 | into a new directory. This example is tailored for a virtual machine setup 47 | similar to one on [Hetzner Cloud](https://www.hetzner.com/cloud), so you might 48 | need to adapt it for your setup. 49 | 50 | If you already have a flake, you can use it by adding 51 | [disko configuration](https://github.com/nix-community/disko?tab=readme-ov-file#how-to-use-disko) 52 | to it. 53 | 54 | ### 3. Configure your SSH key 55 | 56 | If you cloned 57 | [our nixos-anywhere-example](https://github.com/nix-community/nixos-anywhere-examples/blob/main/configuration.nix) 58 | you will also replace the SSH key like this: In your configuration, locate the 59 | line that reads: 60 | 61 | ```bash 62 | # change this to your ssh key 63 | "CHANGE" 64 | ``` 65 | 66 | Replace the text `CHANGE` with your own SSH key. This is crucial, as you will 67 | not be able to log into the target machine post-installation without it. If you 68 | have a .pem file you can run 69 | 70 | ```bash 71 | ssh-keygen -y -f /path/to/your/key.pem 72 | ``` 73 | 74 | then paste the result in between the quotes like "ssh-rsa AAA..." 75 | 76 | ### 4. Configure Storage 77 | 78 | In the same directory, create a file called `disk-config.nix`. This file will 79 | define the disk layout for the 80 | [disko](https://github.com/nix-community/disko/blob/master/docs/INDEX.md) tool, 81 | which is used by nixos-anywhere to partition, format, and mount the disks. 82 | 83 | For a basic installation, you can copy the contents from the example provided 84 | [here](https://github.com/nix-community/nixos-anywhere-examples/blob/main/disk-config.nix). 85 | This configuration sets up a standard GPT (GUID Partition Table) that is 86 | compatible with both EFI and BIOS systems and mounts the disk as `/dev/sda`. You 87 | may need to adjust `/dev/sda` to match the correct disk on your machine. To 88 | identify the disk, run the `lsblk` command and replace `sda` with the actual 89 | disk name. 90 | 91 | For example, on this machine, we would select `/dev/nvme0n1` as the disk: 92 | 93 | ``` 94 | NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS 95 | nvme0n1 259:0 0 1.8T 0 disk 96 | ``` 97 | 98 | If this setup does not match your requirements, you can choose an example that 99 | better suits your disk layout from the 100 | [disko examples](https://github.com/nix-community/disko/tree/master/example). 101 | For more detailed information, refer to the 102 | [disko documentation](https://github.com/nix-community/disko). 103 | 104 | ### 5. Lock your Flake 105 | 106 | ``` 107 | nix flake lock 108 | ``` 109 | 110 | This will download your flake dependencies and make a `flake.lock` file that 111 | describes how to reproducibly build your system. 112 | 113 | Optionally, you can commit these files to a repo such as Github, or you can 114 | simply reference your local directory when you run **nixos-anywhere**. This 115 | example uses a local directory on the source machine. 116 | 117 | ### 6. Connectivity to the Target Machine 118 | 119 | **nixos-anywhere** will create a temporary SSH key to use for the installation. 120 | If your SSH key is not found, you will be asked for your password. If you are 121 | using a non-root user, you must have access to sudo without a password. To avoid 122 | SSH password prompts, set the `SSHPASS` environment variable to your password 123 | and add `--env-password` to the `nixos-anywhere` command. If providing a 124 | specific SSH key through `-i` (identity_file), this key will then be used for 125 | the installation and no temporary SSH key will be created. 126 | 127 | ### 7. (Optional) Test your NixOS and Disko configuration 128 | 129 | Skip this step and continue with Step 8, if you don't have a hardware 130 | configuration (hardware-configuration.nix or facter.json) generated yet or make 131 | sure you don't import non-existing hardware-configuration.nix or facter.json 132 | during running the vm test. 133 | 134 | The following command will automatically test your nixos configuration and run 135 | disko inside a virtual machine, where 136 | 137 | - `` is the path to the directory or repository 138 | containing `flake.nix` and `disk-config.nix` 139 | 140 | - `` must match the name that immediately follows the text 141 | `nixosConfigurations.` in the flake, as indicated by the comment in the 142 | [example](https://github.com/nix-community/nixos-anywhere-examples/blob/main/flake.nix). 143 | 144 | ``` 145 | nix run github:nix-community/nixos-anywhere -- --flake # --vm-test 146 | ``` 147 | 148 | ### 8. Prepare Hardware Configuration 149 | 150 | If you're not using a virtual machine, it's recommended to allow 151 | `nixos-anywhere` to generate a hardware configuration during installation. This 152 | ensures that essential drivers, such as those required for disk detection, are 153 | properly configured. 154 | 155 | To enable `nixos-anywhere` to integrate its generated configuration into your 156 | NixOS setup, you need to include an import for the hardware configuration 157 | beforehand. 158 | 159 | Here’s an example: 160 | 161 | ```diff 162 | nixosConfigurations.generic = nixpkgs.lib.nixosSystem { 163 | system = "x86_64-linux"; 164 | modules = [ 165 | disko.nixosModules.disko 166 | ./configuration.nix 167 | + ./hardware-configuration.nix 168 | ]; 169 | }; 170 | ``` 171 | 172 | When running `nixos-anywhere`, this file is automatically generated by including 173 | the following flags in your command: 174 | `--generate-hardware-config nixos-generate-config ./hardware-configuration.nix`. 175 | The second flag, `./hardware-configuration.nix`, specifies where 176 | `nixos-generate-config` will store the configuration. Adjust this path to 177 | reflect the location where you want the `hardware-configuration.nix` for this 178 | machine to be saved. 179 | 180 | #### 8.1 nixos-facter 181 | 182 | As an alternative to `nixos-generate-config`, you can use the experimental 183 | [nixos-facter](https://github.com/numtide/nixos-facter) command, which offers 184 | more comprehensive hardware reports and advanced configuration options. 185 | 186 | To use `nixos-facter`, add the following to your flake inputs: 187 | 188 | ```diff 189 | { 190 | + inputs.nixos-facter-modules.url = "github:numtide/nixos-facter-modules"; 191 | } 192 | ``` 193 | 194 | Next, import the module into your configuration and specify `facter.json` as the 195 | path where the hardware report will be saved: 196 | 197 | ```diff 198 | nixosConfigurations.generic-nixos-facter = nixpkgs.lib.nixosSystem { 199 | system = "x86_64-linux"; 200 | modules = [ 201 | disko.nixosModules.disko 202 | ./configuration.nix 203 | + nixos-facter-modules.nixosModules.facter 204 | + { config.facter.reportPath = ./facter.json } 205 | ]; 206 | }; 207 | ``` 208 | 209 | To generate the configuration for `nixos-facter` with `nixos-anywhere`, use the 210 | following flags: `--generate-hardware-config nixos-facter ./facter.json`. The 211 | second flag, `./facter.json`, specifies where `nixos-generate-config` will store 212 | the hardware report. Adjust this path to suit the location where you want the 213 | `facter.json` to be saved. 214 | 215 | ### 9. Run it 216 | 217 | You can now run **nixos-anywhere** from the command line as shown below, where: 218 | 219 | - `` is the path to the directory or repository 220 | containing `flake.nix` and `disk-config.nix` 221 | 222 | - `` must match the name that immediately follows the text 223 | `nixosConfigurations.` in the flake, as indicated by the comment in the 224 | [example](https://github.com/nix-community/nixos-anywhere-examples/blob/main/flake.nix). 225 | 226 | - `` is the IP address of the target machine. 227 | 228 | ``` 229 | nix run github:nix-community/nixos-anywhere -- --flake # --target-host root@ 230 | ``` 231 | 232 | The command would look  like this if you had created your files in a directory 233 | named `/home/mydir/test` and the IP address of your target machine is 234 | `37.27.18.135`: 235 | 236 | ``` 237 | nix run github:nix-community/nixos-anywhere -- --flake /home/mydir/test#hetzner-cloud --target-host root@37.27.18.135 238 | ``` 239 | 240 | If you also need to generate hardware configuration amend flags for 241 | nixos-generate-config: 242 | 243 | ``` 244 | nix run github:nix-community/nixos-anywhere -- --generate-hardware-config nixos-generate-config ./hardware-configuration.nix --flake # --target-host root@ 245 | ``` 246 | 247 | Or these flags if you are using nixos-facter instead: 248 | 249 | ``` 250 | nix run github:nix-community/nixos-anywhere -- --generate-hardware-config nixos-facter ./facter.json --flake # --target-host root@ 251 | ``` 252 | 253 | Adjust the location of `./hardware-configuration.nix` and `./facter.json` 254 | accordingly. 255 | 256 | **nixos-anywhere** will then run, showing various output messages at each stage. 257 | It may take some time to complete, depending on Internet speeds. It should 258 | finish by showing the messages below before returning to the command prompt. 259 | 260 | ``` 261 | Installation finished. No error reported. 262 | Warning: Permanently added '' (ED25519) to the list of known hosts 263 | ``` 264 | 265 | When this happens, the target server will have been overwritten with a new 266 | installation of NixOS. Note that the server's public SSH key will have changed. 267 | 268 | If you have previously accessed this server using SSH, you may see the following 269 | message the next time you try to log in to the target. 270 | 271 | ``` 272 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 273 | @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ 274 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 275 | IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! 276 | Someone could be eavesdropping on you right now (man-in-the-middle attack)! 277 | It is also possible that a host key has just been changed. 278 | The fingerprint for the ED25519 key sent by the remote host is 279 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX. 280 | Please contact your system administrator. 281 | Add correct host key in ~/.ssh/known_hosts to get rid of this message. 282 | Offending ECDSA key in ~/.ssh/known_hosts:6 283 | remove with: 284 | ssh-keygen -f ~/.ssh/known_hosts" -R "" 285 | Host key for has changed and you have requested strict checking. 286 | Host key verification failed. 287 | ``` 288 | 289 | This is because the `known_hosts` file in the `.ssh` directory now contains a 290 | mismatch, since the server has been overwritten. To solve this, use a text 291 | editor to remove the old entry from the `known_hosts` file (or use the command 292 | `ssh-keygen -R `). The next connection attempt will then treat this 293 | as a new server. 294 | 295 | The error message line `Offending ECDSA key in ~/.ssh/known_hosts:6` gives the 296 | line number that needs to be removed from the `known_hosts` file (line 6 in this 297 | example). 298 | 299 | # Finished! 300 | 301 | **nixos-anywhere**'s job is now done, as it is a tool to install NixOS onto the 302 | target machine. 303 | 304 | Any future changes to the configuration should be made to your flake. You would 305 | reference this flake when using the NixOS `nixos-rebuild` command or a separate 306 | 3rd party deployment tool of your choice i.e. 307 | [deploy-rs](https://github.com/serokell/deploy-rs), 308 | [colmena](https://github.com/zhaofengli/colmena), 309 | [nixinate](https://github.com/MatthewCroughan/nixinate), 310 | [clan](https://clan.lol/) (author's choice). 311 | 312 | To update on the machine locally (replace `` with your flake 313 | i.e. `.#` if your flake is in the current directory): 314 | 315 | ``` 316 | nixos-rebuild switch --flake 317 | ``` 318 | 319 | To update remotely you will need to have configured an 320 | [ssh server](https://search.nixos.org/options?show=services.sshd.enable) and 321 | your ssh key for the 322 | [root user](https://search.nixos.org/options?show=users.users.%3Cname%3E.openssh.authorizedKeys.keys): 323 | 324 | ``` 325 | nixos-rebuild switch --flake --target-host "root@" 326 | ``` 327 | 328 | See the Nix documentation for use of the flake 329 | [URL-like syntax](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake#url-like-syntax). 330 | 331 | For more information on different use cases of **nixos-anywhere** please refer 332 | to the [How to Guide](./howtos/INDEX.md), and for more technical information and 333 | explanation of known error messages, refer to the 334 | [Reference Manual](./reference.md). 335 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference Manual: nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | [Documentation Index](./INDEX.md) 8 | 9 | TODO: Populate this guide properly 10 | 11 | ## Contents 12 | 13 | [Command Line Usage](#command-line-usage) 14 | 15 | [Explanation of known error messages](#explanation-of-known-error-messages) 16 | 17 | ## Command Line Usage 18 | 19 | 20 | 21 | ``` 22 | Usage: nixos-anywhere [options] [] 23 | 24 | Options: 25 | 26 | * -f, --flake 27 | set the flake to install the system from. i.e. 28 | nixos-anywhere --flake .#mymachine 29 | Also supports variants: 30 | nixos-anywhere --flake .#nixosConfigurations.mymachine.config.virtualisation.vmVariant 31 | * --target-host 32 | set the SSH target host to deploy onto. 33 | * -i 34 | selects which SSH private key file to use. 35 | * -p, --ssh-port 36 | set the ssh port to connect with 37 | * --ssh-option 38 | set one ssh option, no need for the '-o' flag, can be repeated. 39 | for example: '--ssh-option UserKnownHostsFile=./known_hosts' 40 | * -L, --print-build-logs 41 | print full build logs 42 | * --env-password 43 | set a password used by ssh-copy-id, the password should be set by 44 | the environment variable SSHPASS 45 | * -s, --store-paths 46 | set the store paths to the disko-script and nixos-system directly 47 | if this is given, flake is not needed 48 | * --kexec 49 | use another kexec tarball to bootstrap NixOS 50 | * --kexec-extra-flags 51 | extra flags to add into the call to kexec, e.g. "--no-sync" 52 | * --ssh-store-setting 53 | ssh store settings appended to the store URI, e.g. "compress true". needs to be URI encoded. 54 | * --post-kexec-ssh-port 55 | after kexec is executed, use a custom ssh port to connect. Defaults to 22 56 | * --copy-host-keys 57 | copy over existing /etc/ssh/ssh_host_* host keys to the installation 58 | * --extra-files 59 | contents of local are recursively copied to the root (/) of the new NixOS installation. Existing files are overwritten 60 | Copied files will be owned by root unless specified by --chown option. See documentation for details. 61 | * --chown 62 | change ownership of recursively. Recommended to use uid:gid as opposed to username:groupname for ownership. 63 | Option can be specified more than once. 64 | * --disk-encryption-keys 65 | copy the contents of the file or pipe in local_path to remote_path in the installer environment, 66 | after kexec but before installation. Can be repeated. 67 | * --no-substitute-on-destination 68 | disable passing --substitute-on-destination to nix-copy 69 | * --debug 70 | enable debug output 71 | * --show-trace 72 | show nix build traces 73 | * --option 74 | nix option to pass to every nix related command 75 | * --from 76 | URL of the source Nix store to copy the nixos and disko closure from 77 | * --build-on-remote 78 | build the closure on the remote machine instead of locally and copy-closuring it 79 | * --vm-test 80 | build the system and test the disk configuration inside a VM without installing it to the target. 81 | * --generate-hardware-config nixos-facter|nixos-generate-config 82 | generate a hardware-configuration.nix file using the specified backend and write it to the specified path. 83 | The backend can be either 'nixos-facter' or 'nixos-generate-config'. 84 | * --phases 85 | comma separated list of phases to run. Default is: kexec,disko,install,reboot 86 | kexec: kexec into the nixos installer 87 | disko: first unmount and destroy all filesystems on the disks we want to format, then run the create and mount mode 88 | install: install the system 89 | reboot: unmount the filesystems, export any ZFS pools and reboot the machine 90 | * --disko-mode disko|mount|format 91 | set the disko mode to format, mount or destroy. Default is disko. 92 | disko: first unmount and destroy all filesystems on the disks we want to format, then run the create and mount mode 93 | * --no-disko-deps 94 | This will only upload the disko script and not the partitioning tools dependencies. 95 | Installers usually have dependencies available. 96 | Use this option if your target machine has not enough RAM to store the dependencies in memory. 97 | * --build-on auto|remote|local 98 | sets the build on settings to auto, remote or local. Default is auto. 99 | auto: tries to figure out, if the build is possible on the local host, if not falls back gracefully to remote build 100 | local: will build on the local host 101 | remote: will build on the remote host 102 | ``` 103 | 104 | ## Explanation of known error messages 105 | 106 | TODO: Add additional error messages and meanings. Fill in missing explanations 107 | 108 | This section lists known error messages and their explanations. Some 109 | explanations may refer to the following CLI syntax: 110 | 111 | `nix run github:nix-community/nixos-anywhere -- --flake # root@` 112 | 113 | This list is not comprehensive. It's possible you may encounter errors that 114 | originate from the underlying operating system. These should be documented in 115 | the relevant operating system manual. 116 | 117 | | Id | Message | Explanation | 118 | | -- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 119 | | 1 | Failure unpacking initrd | You don't have enough RAM to hold `kexec` | 120 | | 2 | Flake  does not provide attribute | The configuration name you specified in your flake URI is not defined as a NixOS configuration in your flake eg if your URI was mydir#myconfig, then myconfig should be included in the flake as `nixosConfigurations.myconfig` | 121 | | 3 | Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri. | As for error #2 | 122 | | | For example, to use the output nixosConfigurations.foo from the flake.nix, append "#foo" to the flake-uri | | 123 | | 4 | Retrieving host facts via ssh failed. Check with --debug for the root cause, unless you have done so already | TODO: Explain | 124 | | 5 | ssh-host must be set |  has not been supplied | 125 | | 6 | and must be existing store-paths | This occurs if the -s switch has been used to specify the disko script and store path correctly, and the scripts cannot be found at the given URI | 126 | | 7 | flake must be set | This occurs if both the -flake option (use a flake) and the -s option (specify paths directly) have been omitted. Either one or the other must be specified. | 127 | | 8 | no tar command found, but required to unpack kexec tarball | The destination machine does not have a `tar` command available. This is needed to unpack the `kexec`. | 128 | | 9 | no setsid command found, but required to run the kexec script under a new session | The destination machine does not have the `setsid` command available | 129 | | 10 | This script requires Linux as the operating system, but got | The destination machine is not running Linux | 130 | | 11 | The default kexec image only support x86_64 cpus. Checkout https://github.com/nix-community/nixos-anywhere/#using-your-own-kexec-image for more information. | By default, `nixos-anywhere` uses its own `kexec` image, which will only run on x86_64 CPUs. For other CPU types, you can use your own `kexec` image instead. Refer to the [How To Guide](./howtos#using-your-own-kexec-image) for instructions. | 131 | | 12 | Please specify the name of the NixOS configuration to be installed, as a URI fragment in the flake-uri. | This is a `disko` error. As for Error #2 | 132 | | | For example, to use the output diskoConfigurations.foo from the flake.nix, append \"#foo\" to the flake-uri. | | 133 | | 13 | mode must be either create, mount or zap_create_mount | This is a `disko` error. The `disko` switches have not been used correctly. This could happen if you supplied your own `disko` script using the -s option | 134 | | 14 | disko config must be an existing file or flake must be set | This is a `disko` error. This will happen if the `disko.devices` entry in your flake doesn't match the name of a file in the same location as your flake. | 135 | | | | | 136 | | | | | 137 | | | | | 138 | | | | | 139 | -------------------------------------------------------------------------------- /docs/requirements.md: -------------------------------------------------------------------------------- 1 | # System Requirements: nixos-anywhere 2 | 3 | **_Install NixOS everywhere via ssh_** 4 | 5 | 6 | 7 | [Documentation Index](./INDEX.md) 8 | 9 | ## Requirements 10 | 11 | ### Source Machine 12 | 13 | 1. **Supported Systems:** 14 | - Linux or macOS computers with Nix installed. 15 | - NixOS 16 | - Windows systems using WSL2. 17 | 18 | 2. **Nix Installation:** If Nix is not yet installed on your system, refer to 19 | the [nix installation page](https://nixos.org/download#download-nix). 20 | 21 | ### Destination Machine 22 | 23 | The machine must be reachable over the public internet or local network. 24 | Nixos-anywhere does not support wifi networks. If a VPN is needed, define a 25 | custom installer via the --kexec flag which connects to your VPN. 26 | 27 | 1. **Direct Boot Option:** 28 | - Must be already running a NixOS installer. 29 | 30 | 2. **Alternative Boot Options:** If not booting directly from a NixOS installer 31 | image: 32 | - **Architecture & Support:** Must be operating on: 33 | - x86-64 or aarch64 Linux systems with kexec support. Note: While most 34 | x86-64 Linux systems support kexec, if you're using an architecture other 35 | than those mentioned, you may need to specify a 36 | [different kexec image](./howtos/INDEX.md#using-your-own-kexec-image) 37 | manually. 38 | - **Memory Requirements:** 39 | - At least 1.5 GB of RAM (excluding swap space). 40 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "disko": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1748225455, 11 | "narHash": "sha256-AzlJCKaM4wbEyEpV3I/PUq5mHnib2ryEy32c+qfj6xk=", 12 | "owner": "nix-community", 13 | "repo": "disko", 14 | "rev": "a894f2811e1ee8d10c50560551e50d6ab3c392ba", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "nix-community", 19 | "ref": "master", 20 | "repo": "disko", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-parts": { 25 | "inputs": { 26 | "nixpkgs-lib": [ 27 | "nixpkgs" 28 | ] 29 | }, 30 | "locked": { 31 | "lastModified": 1743550720, 32 | "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", 33 | "owner": "hercules-ci", 34 | "repo": "flake-parts", 35 | "rev": "c621e8422220273271f52058f618c94e405bb0f5", 36 | "type": "github" 37 | }, 38 | "original": { 39 | "owner": "hercules-ci", 40 | "repo": "flake-parts", 41 | "type": "github" 42 | } 43 | }, 44 | "nix-vm-test": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1748765518, 52 | "narHash": "sha256-vftOR+7zwnMWl5UpG32GL1VBeNGTDZZT0hv+2uNuBGw=", 53 | "owner": "Mic92", 54 | "repo": "nix-vm-test", 55 | "rev": "d6642fbaf42fc98883d84bab66cd0ec720d9dd0c", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "Mic92", 60 | "repo": "nix-vm-test", 61 | "type": "github" 62 | } 63 | }, 64 | "nixos-images": { 65 | "inputs": { 66 | "nixos-stable": [ 67 | "nixos-stable" 68 | ], 69 | "nixos-unstable": [ 70 | "nixpkgs" 71 | ] 72 | }, 73 | "locked": { 74 | "lastModified": 1748481078, 75 | "narHash": "sha256-jwKRF2EDzlv0VBF8pImPFT7DAJma7stDun25utHtwBw=", 76 | "owner": "nix-community", 77 | "repo": "nixos-images", 78 | "rev": "191a461dc38313ff41bd3df4b82e49f74a56560d", 79 | "type": "github" 80 | }, 81 | "original": { 82 | "owner": "nix-community", 83 | "repo": "nixos-images", 84 | "type": "github" 85 | } 86 | }, 87 | "nixos-stable": { 88 | "locked": { 89 | "lastModified": 1748437600, 90 | "narHash": "sha256-hYKMs3ilp09anGO7xzfGs3JqEgUqFMnZ8GMAqI6/k04=", 91 | "owner": "NixOS", 92 | "repo": "nixpkgs", 93 | "rev": "7282cb574e0607e65224d33be8241eae7cfe0979", 94 | "type": "github" 95 | }, 96 | "original": { 97 | "owner": "NixOS", 98 | "ref": "nixos-25.05", 99 | "repo": "nixpkgs", 100 | "type": "github" 101 | } 102 | }, 103 | "nixpkgs": { 104 | "locked": { 105 | "lastModified": 1748719291, 106 | "narHash": "sha256-lD4C9HmTBrSZjjAd9o/PlVF4tchH2LKDHPG/qc89wHY=", 107 | "owner": "nixos", 108 | "repo": "nixpkgs", 109 | "rev": "f4d7622a1036200d7061f442be3fcc4cc1d97eda", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "nixos", 114 | "ref": "nixos-unstable-small", 115 | "repo": "nixpkgs", 116 | "type": "github" 117 | } 118 | }, 119 | "root": { 120 | "inputs": { 121 | "disko": "disko", 122 | "flake-parts": "flake-parts", 123 | "nix-vm-test": "nix-vm-test", 124 | "nixos-images": "nixos-images", 125 | "nixos-stable": "nixos-stable", 126 | "nixpkgs": "nixpkgs", 127 | "treefmt-nix": "treefmt-nix" 128 | } 129 | }, 130 | "treefmt-nix": { 131 | "inputs": { 132 | "nixpkgs": [ 133 | "nixpkgs" 134 | ] 135 | }, 136 | "locked": { 137 | "lastModified": 1748243702, 138 | "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", 139 | "owner": "numtide", 140 | "repo": "treefmt-nix", 141 | "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", 142 | "type": "github" 143 | }, 144 | "original": { 145 | "owner": "numtide", 146 | "repo": "treefmt-nix", 147 | "type": "github" 148 | } 149 | } 150 | }, 151 | "root": "root", 152 | "version": 7 153 | } 154 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A universal nixos installer, just needs ssh access to the target system"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable-small"; 6 | flake-parts = { url = "github:hercules-ci/flake-parts"; inputs.nixpkgs-lib.follows = "nixpkgs"; }; 7 | 8 | # used for testing 9 | disko = { url = "github:nix-community/disko/master"; inputs.nixpkgs.follows = "nixpkgs"; }; 10 | nixos-stable.url = "github:NixOS/nixpkgs/nixos-25.05"; 11 | nixos-images.url = "github:nix-community/nixos-images"; 12 | nixos-images.inputs.nixos-unstable.follows = "nixpkgs"; 13 | nixos-images.inputs.nixos-stable.follows = "nixos-stable"; 14 | # https://github.com/numtide/nix-vm-test/pull/105 15 | nix-vm-test = { url = "github:Mic92/nix-vm-test"; inputs.nixpkgs.follows = "nixpkgs"; }; 16 | 17 | # used for development 18 | treefmt-nix = { url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; 19 | }; 20 | 21 | 22 | outputs = inputs: 23 | inputs.flake-parts.lib.mkFlake { inherit inputs; } { 24 | systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; 25 | imports = [ 26 | ./src/flake-module.nix 27 | ./tests/flake-module.nix 28 | ./docs/flake-module.nix 29 | ./terraform/flake-module.nix 30 | # allow to disable treefmt in downstream flakes 31 | ] ++ inputs.nixpkgs.lib.optional (inputs.treefmt-nix ? flakeModule) ./treefmt/flake-module.nix; 32 | 33 | perSystem = { self', lib, ... }: { 34 | checks = 35 | let 36 | packages = lib.mapAttrs' (n: lib.nameValuePair "package-${n}") self'.packages; 37 | devShells = lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells; 38 | in 39 | packages // devShells; 40 | }; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /scripts/create-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix 2 | #! nix shell nixpkgs#bash nixpkgs#gnused nixpkgs#gh --command bash 3 | 4 | set -euo pipefail 5 | 6 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" 7 | cd "$SCRIPT_DIR/.." 8 | readonly repo_url="git@github.com:nix-community/nixos-anywhere" 9 | 10 | version=${1:-} 11 | if [[ -z $version ]]; then 12 | echo "USAGE: $0 version" >&2 13 | exit 1 14 | fi 15 | 16 | if [[ "$(git symbolic-ref --short HEAD)" != "main" ]]; then 17 | echo "must be on main branch" >&2 18 | exit 1 19 | fi 20 | 21 | # ensure we are up-to-date 22 | uncommitted_changes=$(git diff --compact-summary) 23 | if [[ -n $uncommitted_changes ]]; then 24 | echo -e "There are uncommitted changes, exiting:\n${uncommitted_changes}" >&2 25 | exit 1 26 | fi 27 | git pull "$repo_url" main 28 | unpushed_commits=$(git log --format=oneline origin/main..main) 29 | if [[ -n $unpushed_commits ]]; then 30 | echo -e "\nThere are unpushed changes, exiting:\n$unpushed_commits" >&2 31 | exit 1 32 | fi 33 | 34 | if ! gh auth status &>/dev/null; then 35 | gh auth login 36 | fi 37 | 38 | git branch -D "release-${version}" || true 39 | git checkout -b "release-${version}" 40 | 41 | sed -i -e "s!version = \".*\";!version = \"${version}\";!" src/default.nix 42 | git add src/default.nix 43 | 44 | git commit -m "bump version ${version}" 45 | git push origin "release-${version}" 46 | gh pr create \ 47 | --base main \ 48 | --head "release-${version}" \ 49 | --title "Release ${version}" \ 50 | --body "Release ${version} of nixos-anywhere" 51 | 52 | gh pr merge --auto "release-${version}" 53 | git checkout main 54 | 55 | while true; do 56 | if gh pr view "release-${version}" | grep -q 'MERGED'; then 57 | break 58 | fi 59 | echo "Waiting for PR to be merged..." 60 | sleep 5 61 | done 62 | git pull "$repo_url" main 63 | gh release create "${version}" --draft --generate-notes 64 | gh release view "${version}" --web 65 | echo "Please clean up the autogenerated release notes and then publish the release" 66 | -------------------------------------------------------------------------------- /src/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , openssh 3 | , gitMinimal 4 | , nixVersions 5 | , nix 6 | , coreutils 7 | , curl 8 | , gnugrep 9 | , gnutar 10 | , gawk 11 | , findutils 12 | , gnused 13 | , sshpass 14 | , terraform-docs 15 | , lib 16 | , makeWrapper 17 | , mkShellNoCC 18 | }: 19 | let 20 | runtimeDeps = [ 21 | gitMinimal # for git flakes 22 | # pinned because nix-copy-closure hangs if ControlPath provided for SSH: https://github.com/NixOS/nix/issues/8480 23 | (if lib.versionAtLeast nix.version "2.16" then nix else nixVersions.nix_2_16) 24 | coreutils 25 | curl # when uploading tarballs 26 | gnugrep 27 | gawk 28 | findutils 29 | gnused # needed by ssh-copy-id 30 | sshpass # used to provide password for ssh-copy-id 31 | gnutar # used to upload extra-files 32 | ]; 33 | in 34 | stdenv.mkDerivation { 35 | pname = "nixos-anywhere"; 36 | version = "1.11.0"; 37 | src = ./..; 38 | nativeBuildInputs = [ makeWrapper ]; 39 | installPhase = '' 40 | install -D --target-directory=$out/libexec/nixos-anywhere/ -m 0755 src/*.sh 41 | 42 | # We prefer the system's openssh over our own, since it might come with features not present in ours: 43 | # https://github.com/nix-community/nixos-anywhere/issues/62 44 | makeShellWrapper $out/libexec/nixos-anywhere/nixos-anywhere.sh $out/bin/nixos-anywhere \ 45 | --prefix PATH : ${lib.makeBinPath runtimeDeps} --suffix PATH : ${lib.makeBinPath [ openssh ]} 46 | ''; 47 | 48 | # Dependencies for our devshell 49 | passthru.devShell = mkShellNoCC { 50 | packages = runtimeDeps ++ [ openssh terraform-docs ]; 51 | }; 52 | 53 | meta = with lib; { 54 | description = "Install nixos everywhere via ssh"; 55 | homepage = "https://github.com/nix-community/nixos-anywhere"; 56 | license = licenses.mit; 57 | platforms = platforms.all; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/flake-module.nix: -------------------------------------------------------------------------------- 1 | { 2 | perSystem = { config, pkgs, ... }: { 3 | packages = { 4 | nixos-anywhere = pkgs.callPackage ./. { }; 5 | default = config.packages.nixos-anywhere; 6 | }; 7 | devShells.default = config.packages.nixos-anywhere.devShell; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/get-facts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -efu "${enableDebug:-}" 3 | has() { 4 | command -v "$1" >/dev/null && echo "y" || echo "n" 5 | } 6 | isNixos=$(if test -f /etc/os-release && grep -Eq 'ID(_LIKE)?="?nixos"?' /etc/os-release; then echo "y"; else echo "n"; fi) 7 | cat </dev/null 2>/dev/null || ! ip -6 r g :: >/dev/null 2>/dev/null; then echo "n"; else echo "y"; fi) 15 | hasTar=$(has tar) 16 | hasCpio=$(has cpio) 17 | hasSudo=$(has sudo) 18 | hasDoas=$(has doas) 19 | hasWget=$(has wget) 20 | hasCurl=$(has curl) 21 | hasSetsid=$(has setsid) 22 | hasNixOSFacter=$(command -v nixos-facter >/dev/null && echo "y" || echo "n") 23 | FACTS 24 | -------------------------------------------------------------------------------- /terraform/.envrc: -------------------------------------------------------------------------------- 1 | use flake .#terraform 2 | -------------------------------------------------------------------------------- /terraform/.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | output: 2 | mode: inject 3 | template: |- 4 | 5 | {{ .Content }} 6 | 7 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # NixOS-Anywhere Terraform Modules Overview 2 | 3 | The nixos-Anywhere terraform modules allow you to use Terraform for installing 4 | and updating NixOS. It simplifies the deployment process by integrating 5 | nixos-anywhere functionality. 6 | 7 | Here's a brief overview of each module: 8 | 9 | - **[All-in-One](all-in-one.md)**: This is a consolidated module that first 10 | installs NixOS using nixos-anywhere and then keeps it updated with 11 | nixos-rebuild. If you choose this, you won't need additional deployment tools 12 | like colmena. 13 | - **[Install](install.md)**: This module focuses solely on installing NixOS via 14 | nixos-anywhere. 15 | - **[NixOS-Rebuild](nixos-rebuild.md)**: Use this module to remotely update an 16 | existing NixOS machine using nixos-rebuild. 17 | - **[Nix-Build](nix-build.md)**: This is a handy helper module designed to build 18 | a flake attribute or an attribute from a nix file. 19 | 20 | For detailed information and usage examples, click on the respective module 21 | links above. 22 | -------------------------------------------------------------------------------- /terraform/all-in-one.md: -------------------------------------------------------------------------------- 1 | # All-in-one 2 | 3 | Combines the install and nixos-rebuild module in one interface to install NixOS 4 | with nixos-anywhere and then keep it up-to-date with nixos-rebuild. 5 | 6 | ## Example 7 | 8 | ```hcl 9 | locals { 10 | ipv4 = "192.0.2.1" 11 | } 12 | 13 | module "deploy" { 14 | source = "github.com/nix-community/nixos-anywhere//terraform/all-in-one" 15 | # with flakes 16 | nixos_system_attr = ".#nixosConfigurations.mymachine.config.system.build.toplevel" 17 | nixos_partitioner_attr = ".#nixosConfigurations.mymachine.config.system.build.diskoScript" 18 | # without flakes 19 | # file can use (pkgs.nixos []) function from nixpkgs 20 | #file = "${path.module}/../.." 21 | #nixos_system_attr = "config.system.build.toplevel" 22 | #nixos_partitioner_attr = "config.system.build.diskoScript" 23 | 24 | target_host = local.ipv4 25 | # when instance id changes, it will trigger a reinstall 26 | instance_id = local.ipv4 27 | # useful if something goes wrong 28 | # debug_logging = true 29 | # build the closure on the remote machine instead of locally 30 | # build_on_remote = true 31 | # script is below 32 | extra_files_script = "${path.module}/decrypt-ssh-secrets.sh" 33 | disk_encryption_key_scripts = [{ 34 | path = "/tmp/secret.key" 35 | # script is below 36 | script = "${path.module}/decrypt-zfs-key.sh" 37 | }] 38 | # Optional, arguments passed to special_args here will be available from a NixOS module in this example the `terraform` argument: 39 | # { terraform, ... }: { 40 | # networking.interfaces.enp0s3.ipv4.addresses = [{ address = terraform.ip; prefixLength = 24; }]; 41 | # } 42 | # Note that this will means that your NixOS configuration will always depend on terraform! 43 | # Skip to `Pass data persistently to the NixOS` for an alternative approach 44 | #special_args = { 45 | # terraform = { 46 | # ip = "192.0.2.0" 47 | # } 48 | #} 49 | } 50 | ``` 51 | 52 | _Note:_ You need to mark scripts as executable (`chmod +x`) 53 | 54 | ### ./decrypt-ssh-secrets.sh 55 | 56 | ```bash 57 | #!/usr/bin/env bash 58 | 59 | mkdir -p etc/ssh var/lib/secrets 60 | 61 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 62 | 63 | umask 0177 64 | sops --extract '["initrd_ssh_key"]' --decrypt "$SCRIPT_DIR/secrets.yaml" >./var/lib/secrets/initrd_ssh_key 65 | 66 | # restore umask 67 | umask 0022 68 | 69 | for keyname in ssh_host_rsa_key ssh_host_rsa_key.pub ssh_host_ed25519_key ssh_host_ed25519_key.pub; do 70 | if [[ $keyname == *.pub ]]; then 71 | umask 0133 72 | else 73 | umask 0177 74 | fi 75 | sops --extract '["'$keyname'"]' --decrypt "$SCRIPT_DIR/secrets.yaml" >"./etc/ssh/$keyname" 76 | done 77 | ``` 78 | 79 | ### ./decrypt-zfs-key.sh 80 | 81 | ```bash 82 | #!/usr/bin/env bash 83 | 84 | set -euo pipefail 85 | 86 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 87 | cd "$SCRIPT_DIR" 88 | sops --extract '["zfs-key"]' --decrypt "$SCRIPT_DIR/secrets.yaml" 89 | ``` 90 | 91 | ## See also 92 | 93 | - [nixos-wiki setup](https://github.com/NixOS/nixos-wiki-infra/blob/main/terraform/nixos-wiki/main.tf) 94 | for hetzner-cloud 95 | 96 | ## Pass data persistently to the NixOS 97 | 98 | This guide outlines how to pass data from Terraform to NixOS by generating a 99 | file during Terraform execution and including it in your NixOS configuration. 100 | This approach works well if your Terraform and NixOS configurations are stored 101 | in the same Git repository. 102 | 103 | ### Why Use This Method? 104 | 105 | This method provides a straightforward way to transfer values from Terraform to 106 | NixOS without relying on special_args. 107 | 108 | - **Advantages**: 109 | - You can continue to use nix build or nixos-rebuild to evaluate your 110 | configuration without interruption. Simplifies configuration management by 111 | centralizing state in a single repository. 112 | - **Disadvantages**: 113 | - Deploying new machines requires tracking additional state. Every time 114 | Terraform updates the JSON file, you'll need to commit these changes to your 115 | repository. 116 | 117 | ### Implementation 118 | 119 | Add the following snippet to your Terraform configuration to create and manage a 120 | JSON file containing the necessary variables for NixOS. This file will be 121 | automatically added to your Git repository, ensuring the data persists. 122 | 123 | Assuming you have your terraform and nixos configuration in the same git 124 | repository. You can use the following snippet to `git add` a file generated by 125 | `terraform` during execution to pass data from terraform to NixOS. These changes 126 | should be committed afterwards. This is an alternative over using 127 | `special_args`. Advantage: you can still use nix build or nixos-rebuild on your 128 | flake to evaluate your configuration. Disadvantage: Deploying new machines also 129 | means you need to track additional state and make additional commits whenever 130 | terraform updates the json file. 131 | 132 | ```hcl 133 | locals { 134 | nixos_vars_file = "nixos-vars.json" # Path to the JSON file containing NixOS variables 135 | nixos_vars = { 136 | ip = "192.0.2.0" # Replace with actual variables 137 | } 138 | } 139 | resource "local_file" "nixos_vars" { 140 | content = jsonencode(local.nixos_vars) # Converts variables to JSON 141 | filename = local.nixos_vars_file # Specifies the output file path 142 | file_permission = "600" 143 | 144 | # Automatically adds the generated file to Git 145 | provisioner "local-exec" { 146 | interpreter = ["bash", "-c"] 147 | command = "git add -f '${local.nixos_vars_file}'" 148 | } 149 | } 150 | ``` 151 | 152 | After applying the Terraform changes, ensure you commit the updated 153 | `nixos-vars.json` file to your Git repository: 154 | 155 | ```bash 156 | git commit -m "Update NixOS variables from Terraform" 157 | ``` 158 | 159 | You can import this json file into your configuration like this: 160 | 161 | ```nix 162 | let 163 | nixosVars = builtins.fromJSON (builtins.readFile ./nixos-vars.json); 164 | in 165 | { 166 | # Example usage of imported variables 167 | networking.hostName = "example-machine"; 168 | networking.interfaces.eth0.ipv4.addresses = [ 169 | { 170 | address = nixosVars.ip; # Use the IP from nixos-vars.json 171 | prefixLength = 24; 172 | } 173 | ]; 174 | } 175 | ``` 176 | 177 | 178 | 179 | ## Requirements 180 | 181 | No requirements. 182 | 183 | ## Providers 184 | 185 | No providers. 186 | 187 | ## Modules 188 | 189 | | Name | Source | Version | 190 | | -------------------------------------------------------------------------------------- | ---------------- | ------- | 191 | | [install](#module_install) | ../install | n/a | 192 | | [nixos-rebuild](#module_nixos-rebuild) | ../nixos-rebuild | n/a | 193 | | [partitioner-build](#module_partitioner-build) | ../nix-build | n/a | 194 | | [system-build](#module_system-build) | ../nix-build | n/a | 195 | 196 | ## Resources 197 | 198 | No resources. 199 | 200 | ## Inputs 201 | 202 | | Name | Description | Type | Default | Required | 203 | | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | :------: | 204 | | [build\_on\_remote](#input_build_on_remote) | Build the closure on the remote machine instead of building it locally and copying it over | `bool` | `false` | no | 205 | | [debug\_logging](#input_debug_logging) | Enable debug logging | `bool` | `false` | no | 206 | | [deployment\_ssh\_key](#input_deployment_ssh_key) | Content of private key used to deploy to the target\_host after initial installation. To ensure maximum security, it is advisable to connect to your host using ssh-agent instead of relying on this variable | `string` | `null` | no | 207 | | [disk\_encryption\_key\_scripts](#input_disk_encryption_key_scripts) | Each script will be executed locally. Output of each will be created at the given path to disko during installation. The keys will be not copied to the final system |
list(object({
path = string
script = string
}))
| `[]` | no | 208 | | [extra\_environment](#input_extra_environment) | Extra environment variables to be set during installation. This can be useful to set extra variables for the extra\_files\_script or disk\_encryption\_key\_scripts | `map(string)` | `{}` | no | 209 | | [extra\_files\_script](#input_extra_files_script) | A script that should place files in the current directory that will be copied to the targets / directory | `string` | `null` | no | 210 | | [file](#input_file) | Nix file containing the nixos\_system\_attr and nixos\_partitioner\_attr. Use this if you are not using flake | `string` | `null` | no | 211 | | [install\_bootloader](#input_install_bootloader) | Install/re-install the bootloader | `bool` | `false` | no | 212 | | [install\_port](#input_install_port) | SSH port used to connect to the target\_host, before installing NixOS. If null than the value of `target_port` is used | `string` | `null` | no | 213 | | [install\_ssh\_key](#input_install_ssh_key) | Content of private key used to connect to the target\_host during initial installation | `string` | `null` | no | 214 | | [install\_user](#input_install_user) | SSH user used to connect to the target\_host, before installing NixOS. If null than the value of `target_host` is used | `string` | `null` | no | 215 | | [instance\_id](#input_instance_id) | The instance id of the target\_host, used to track when to reinstall the machine | `string` | `null` | no | 216 | | [kexec\_tarball\_url](#input_kexec_tarball_url) | NixOS kexec installer tarball url | `string` | `null` | no | 217 | | [nix\_options](#input_nix_options) | the options of nix | `map(string)` | `{}` | no | 218 | | [nixos\_facter\_path](#input_nixos_facter_path) | Path to which to write a `facter.json` generated by `nixos-facter`. | `string` | `""` | no | 219 | | [nixos\_generate\_config\_path](#input_nixos_generate_config_path) | Path to which to write a `hardware-configuration.nix` generated by `nixos-generate-config`. | `string` | `""` | no | 220 | | [nixos\_partitioner\_attr](#input_nixos_partitioner_attr) | Nixos partitioner and mount script i.e. your-flake#nixosConfigurations.your-evaluated-nixos.config.system.build.diskoNoDeps or just your-evaluated.config.system.build.diskNoDeps. `config.system.build.diskNoDeps` is provided by the disko nixos module | `string` | n/a | yes | 221 | | [nixos\_system\_attr](#input_nixos_system_attr) | The nixos system to deploy i.e. your-flake#nixosConfigurations.your-evaluated-nixos.config.system.build.toplevel or just your-evaluated-nixos.config.system.build.toplevel if you are not using flakes | `string` | n/a | yes | 222 | | [no\_reboot](#input_no_reboot) | DEPRECATED: Use `phases` instead. Do not reboot after installation | `bool` | `false` | no | 223 | | [phases](#input_phases) | Phases to run. See `nixos-anywhere --help` for more information | `set(string)` |
[
"kexec",
"disko",
"install",
"reboot"
]
| no | 224 | | [special\_args](#input_special_args) | A map exposed as NixOS's `specialArgs` thru a file. | `any` | `{}` | no | 225 | | [stop\_after\_disko](#input_stop_after_disko) | DEPRECATED: Use `phases` instead. Exit after disko formatting | `bool` | `false` | no | 226 | | [target\_host](#input_target_host) | DNS host to deploy to | `string` | n/a | yes | 227 | | [target\_port](#input_target_port) | SSH port used to connect to the target\_host after installing NixOS. If install\_port is not set than this port is also used before installing. | `number` | `22` | no | 228 | | [target\_user](#input_target_user) | SSH user used to connect to the target\_host after installing NixOS. If install\_user is not set than this user is also used before installing. | `string` | `"root"` | no | 229 | 230 | ## Outputs 231 | 232 | | Name | Description | 233 | | ----------------------------------------------------- | ----------- | 234 | | [result](#output_result) | n/a | 235 | 236 | 237 | -------------------------------------------------------------------------------- /terraform/all-in-one/main.tf: -------------------------------------------------------------------------------- 1 | module "system-build" { 2 | source = "../nix-build" 3 | attribute = var.nixos_system_attr 4 | debug_logging = var.debug_logging 5 | file = var.file 6 | nix_options = var.nix_options 7 | special_args = var.special_args 8 | } 9 | 10 | module "partitioner-build" { 11 | source = "../nix-build" 12 | attribute = var.nixos_partitioner_attr 13 | debug_logging = var.debug_logging 14 | file = var.file 15 | nix_options = var.nix_options 16 | special_args = var.special_args 17 | } 18 | 19 | locals { 20 | install_user = var.install_user == null ? var.target_user : var.install_user 21 | install_port = var.install_port == null ? var.target_port : var.install_port 22 | } 23 | 24 | module "install" { 25 | source = "../install" 26 | kexec_tarball_url = var.kexec_tarball_url 27 | target_user = local.install_user 28 | target_host = var.target_host 29 | target_port = local.install_port 30 | nixos_partitioner = module.partitioner-build.result.out 31 | nixos_system = module.system-build.result.out 32 | ssh_private_key = var.install_ssh_key 33 | debug_logging = var.debug_logging 34 | extra_files_script = var.extra_files_script 35 | disk_encryption_key_scripts = var.disk_encryption_key_scripts 36 | extra_environment = var.extra_environment 37 | instance_id = var.instance_id 38 | phases = var.phases 39 | nixos_generate_config_path = var.nixos_generate_config_path 40 | nixos_facter_path = var.nixos_facter_path 41 | build_on_remote = var.build_on_remote 42 | # deprecated attributes 43 | stop_after_disko = var.stop_after_disko 44 | no_reboot = var.no_reboot 45 | } 46 | 47 | module "nixos-rebuild" { 48 | depends_on = [ 49 | module.install 50 | ] 51 | 52 | # Do not execute this step if var.stop_after_disko == true 53 | count = var.stop_after_disko ? 0 : 1 54 | 55 | source = "../nixos-rebuild" 56 | nixos_system = module.system-build.result.out 57 | ssh_private_key = var.deployment_ssh_key 58 | target_host = var.target_host 59 | target_user = var.target_user 60 | target_port = var.target_port 61 | install_bootloader = var.install_bootloader 62 | } 63 | 64 | output "result" { 65 | value = module.system-build.result 66 | } 67 | -------------------------------------------------------------------------------- /terraform/all-in-one/variables.tf: -------------------------------------------------------------------------------- 1 | variable "kexec_tarball_url" { 2 | type = string 3 | description = "NixOS kexec installer tarball url" 4 | default = null 5 | } 6 | 7 | # To make this re-usable we maybe should accept a store path here? 8 | variable "nixos_partitioner_attr" { 9 | type = string 10 | description = "Nixos partitioner and mount script i.e. your-flake#nixosConfigurations.your-evaluated-nixos.config.system.build.diskoNoDeps or just your-evaluated.config.system.build.diskNoDeps. `config.system.build.diskNoDeps` is provided by the disko nixos module" 11 | } 12 | 13 | # To make this re-usable we maybe should accept a store path here? 14 | variable "nixos_system_attr" { 15 | type = string 16 | description = "The nixos system to deploy i.e. your-flake#nixosConfigurations.your-evaluated-nixos.config.system.build.toplevel or just your-evaluated-nixos.config.system.build.toplevel if you are not using flakes" 17 | } 18 | 19 | variable "file" { 20 | type = string 21 | description = "Nix file containing the nixos_system_attr and nixos_partitioner_attr. Use this if you are not using flake" 22 | default = null 23 | } 24 | 25 | variable "target_host" { 26 | type = string 27 | description = "DNS host to deploy to" 28 | } 29 | 30 | variable "install_user" { 31 | type = string 32 | description = "SSH user used to connect to the target_host, before installing NixOS. If null than the value of `target_host` is used" 33 | default = null 34 | } 35 | 36 | variable "install_port" { 37 | type = string 38 | description = "SSH port used to connect to the target_host, before installing NixOS. If null than the value of `target_port` is used" 39 | default = null 40 | } 41 | 42 | variable "target_user" { 43 | type = string 44 | description = "SSH user used to connect to the target_host after installing NixOS. If install_user is not set than this user is also used before installing." 45 | default = "root" 46 | } 47 | 48 | variable "target_port" { 49 | type = number 50 | description = "SSH port used to connect to the target_host after installing NixOS. If install_port is not set than this port is also used before installing." 51 | default = 22 52 | } 53 | 54 | variable "instance_id" { 55 | type = string 56 | description = "The instance id of the target_host, used to track when to reinstall the machine" 57 | default = null 58 | } 59 | 60 | variable "install_ssh_key" { 61 | type = string 62 | description = "Content of private key used to connect to the target_host during initial installation" 63 | default = null 64 | } 65 | 66 | variable "deployment_ssh_key" { 67 | type = string 68 | description = "Content of private key used to deploy to the target_host after initial installation. To ensure maximum security, it is advisable to connect to your host using ssh-agent instead of relying on this variable" 69 | default = null 70 | } 71 | 72 | variable "debug_logging" { 73 | type = bool 74 | description = "Enable debug logging" 75 | default = false 76 | } 77 | 78 | variable "stop_after_disko" { 79 | type = bool 80 | description = "DEPRECATED: Use `phases` instead. Exit after disko formatting" 81 | default = false 82 | } 83 | 84 | variable "no_reboot" { 85 | type = bool 86 | description = "DEPRECATED: Use `phases` instead. Do not reboot after installation" 87 | default = false 88 | } 89 | 90 | variable "phases" { 91 | type = set(string) 92 | description = "Phases to run. See `nixos-anywhere --help` for more information" 93 | default = ["kexec", "disko", "install", "reboot"] 94 | } 95 | 96 | variable "extra_files_script" { 97 | type = string 98 | description = "A script that should place files in the current directory that will be copied to the targets / directory" 99 | default = null 100 | } 101 | 102 | variable "disk_encryption_key_scripts" { 103 | type = list(object({ 104 | path = string 105 | script = string 106 | })) 107 | description = "Each script will be executed locally. Output of each will be created at the given path to disko during installation. The keys will be not copied to the final system" 108 | default = [] 109 | } 110 | 111 | variable "extra_environment" { 112 | type = map(string) 113 | description = "Extra environment variables to be set during installation. This can be useful to set extra variables for the extra_files_script or disk_encryption_key_scripts" 114 | default = {} 115 | } 116 | 117 | variable "nix_options" { 118 | type = map(string) 119 | description = "the options of nix" 120 | default = {} 121 | } 122 | 123 | variable "nixos_generate_config_path" { 124 | type = string 125 | description = "Path to which to write a `hardware-configuration.nix` generated by `nixos-generate-config`." 126 | default = "" 127 | } 128 | 129 | variable "nixos_facter_path" { 130 | type = string 131 | description = "Path to which to write a `facter.json` generated by `nixos-facter`." 132 | default = "" 133 | } 134 | 135 | variable "special_args" { 136 | type = any 137 | default = {} 138 | description = "A map exposed as NixOS's `specialArgs` thru a file." 139 | } 140 | 141 | variable "build_on_remote" { 142 | type = bool 143 | description = "Build the closure on the remote machine instead of building it locally and copying it over" 144 | default = false 145 | } 146 | 147 | variable "install_bootloader" { 148 | type = bool 149 | description = "Install/re-install the bootloader" 150 | default = false 151 | } 152 | -------------------------------------------------------------------------------- /terraform/flake-module.nix: -------------------------------------------------------------------------------- 1 | { inputs, ... }: 2 | { 3 | flake.nixosConfigurations.terraform-test = inputs.nixpkgs.lib.nixosSystem { 4 | system = "x86_64-linux"; 5 | modules = [ 6 | ../tests/modules/system-to-install.nix 7 | inputs.disko.nixosModules.disko 8 | (args: { 9 | # Example usage of special args from terraform 10 | networking.hostName = args.terraform.hostname or "nixos-anywhere"; 11 | 12 | # Create testable files in /etc based on terraform special_args 13 | environment.etc = { 14 | "terraform-config.json" = { 15 | text = builtins.toJSON args.terraform or { }; 16 | mode = "0644"; 17 | }; 18 | }; 19 | }) 20 | ]; 21 | }; 22 | 23 | perSystem = { pkgs, ... }: { 24 | devShells.terraform = pkgs.mkShell { 25 | buildInputs = with pkgs; [ 26 | terraform-docs 27 | (opentofu.withPlugins (p: [ 28 | p.digitalocean 29 | p.external 30 | p.hcloud 31 | p.local 32 | p.null 33 | p.tls 34 | ])) 35 | ]; 36 | 37 | shellHook = '' 38 | echo "🚀 Terraform development environment" 39 | echo "Available tools:" 40 | echo " - terraform-docs" 41 | echo " - opentofu" 42 | echo "" 43 | echo "To run tests: cd terraform/tests && tofu test" 44 | echo "To update docs: cd terraform && ./update-docs.sh" 45 | ''; 46 | }; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /terraform/install.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | Install NixOS with nixos-anywhere 4 | 5 | ## Example 6 | 7 | ```hcl 8 | locals { 9 | ipv4 = "192.0.2.1" 10 | } 11 | 12 | module "system-build" { 13 | source = "github.com/nix-community/nixos-anywhere//terraform/nix-build" 14 | # with flakes 15 | attribute = ".#nixosConfigurations.mymachine.config.system.build.toplevel" 16 | # without flakes 17 | # file can use (pkgs.nixos []) function from nixpkgs 18 | #file = "${path.module}/../.." 19 | #attribute = "config.system.build.toplevel" 20 | } 21 | 22 | module "disko" { 23 | source = "github.com/nix-community/nixos-anywhere//terraform/nix-build" 24 | # with flakes 25 | attribute = ".#nixosConfigurations.mymachine.config.system.build.diskoScript" 26 | # without flakes 27 | # file can use (pkgs.nixos []) function from nixpkgs 28 | #file = "${path.module}/../.." 29 | #attribute = "config.system.build.diskoScript" 30 | } 31 | 32 | module "install" { 33 | source = "github.com/nix-community/nixos-anywhere//terraform/install" 34 | nixos_system = module.system-build.result.out 35 | nixos_partitioner = module.disko.result.out 36 | target_host = local.ipv4 37 | } 38 | ``` 39 | 40 | 41 | 42 | ## Requirements 43 | 44 | No requirements. 45 | 46 | ## Providers 47 | 48 | | Name | Version | 49 | | --------------------------------------------------- | ------- | 50 | | [null](#provider_null) | n/a | 51 | 52 | ## Modules 53 | 54 | No modules. 55 | 56 | ## Resources 57 | 58 | | Name | Type | 59 | | ------------------------------------------------------------------------------------------------------------------- | -------- | 60 | | [null_resource.nixos-remote](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 61 | 62 | ## Inputs 63 | 64 | | Name | Description | Type | Default | Required | 65 | | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | :------: | 66 | | [build\_on\_remote](#input_build_on_remote) | Build the closure on the remote machine instead of building it locally and copying it over | `bool` | `false` | no | 67 | | [debug\_logging](#input_debug_logging) | Enable debug logging | `bool` | `false` | no | 68 | | [disk\_encryption\_key\_scripts](#input_disk_encryption_key_scripts) | Each script will be executed locally. Output of each will be created at the given path to disko during installation. The keys will be not copied to the final system |
list(object({
path = string
script = string
}))
| `[]` | no | 69 | | [extra\_environment](#input_extra_environment) | Extra environment variables to be set during installation. This can be useful to set extra variables for the extra\_files\_script or disk\_encryption\_key\_scripts | `map(string)` | `{}` | no | 70 | | [extra\_files\_script](#input_extra_files_script) | A script that should place files in the current directory that will be copied to the targets / directory | `string` | `null` | no | 71 | | [flake](#input_flake) | The flake to install the system from | `string` | `""` | no | 72 | | [instance\_id](#input_instance_id) | The instance id of the target\_host, used to track when to reinstall the machine | `string` | `null` | no | 73 | | [kexec\_tarball\_url](#input_kexec_tarball_url) | NixOS kexec installer tarball url | `string` | `null` | no | 74 | | [nixos\_facter\_path](#input_nixos_facter_path) | Path to which to write a `facter.json` generated by `nixos-facter`. This option cannot be set at the same time as `nixos_generate_config_path`. | `string` | `""` | no | 75 | | [nixos\_generate\_config\_path](#input_nixos_generate_config_path) | Path to which to write a `hardware-configuration.nix` generated by `nixos-generate-config`. This option cannot be set at the same time as `nixos_facter_path`. | `string` | `""` | no | 76 | | [nixos\_partitioner](#input_nixos_partitioner) | nixos partitioner and mount script | `string` | `""` | no | 77 | | [nixos\_system](#input_nixos_system) | The nixos system to deploy | `string` | `""` | no | 78 | | [no\_reboot](#input_no_reboot) | DEPRECATED: Use `phases` instead. Do not reboot after installation | `bool` | `false` | no | 79 | | [phases](#input_phases) | Phases to run. See `nixos-anywhere --help` for more information | `list(string)` |
[
"kexec",
"disko",
"install",
"reboot"
]
| no | 80 | | [ssh\_private\_key](#input_ssh_private_key) | Content of private key used to connect to the target\_host | `string` | `""` | no | 81 | | [stop\_after\_disko](#input_stop_after_disko) | DEPRECATED: Use `phases` instead. Exit after disko formatting | `bool` | `false` | no | 82 | | [target\_host](#input_target_host) | DNS host to deploy to | `string` | n/a | yes | 83 | | [target\_pass](#input_target_pass) | Password used to connect to the target\_host | `string` | `null` | no | 84 | | [target\_port](#input_target_port) | SSH port used to connect to the target\_host | `number` | `22` | no | 85 | | [target\_user](#input_target_user) | SSH user used to connect to the target\_host | `string` | `"root"` | no | 86 | 87 | ## Outputs 88 | 89 | No outputs. 90 | 91 | 92 | -------------------------------------------------------------------------------- /terraform/install/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | disk_encryption_key_scripts = [for k in var.disk_encryption_key_scripts : "\"${k.path}\" \"${k.script}\""] 3 | removed_phases = setunion(var.stop_after_disko ? ["install"] : [], (var.no_reboot ? ["reboot"] : [])) 4 | phases = setsubtract(var.phases, local.removed_phases) 5 | arguments = jsonencode({ 6 | ssh_private_key = var.ssh_private_key 7 | debug_logging = var.debug_logging 8 | kexec_tarball_url = var.kexec_tarball_url 9 | nixos_partitioner = var.nixos_partitioner 10 | nixos_system = var.nixos_system 11 | target_user = var.target_user 12 | target_host = var.target_host 13 | target_port = var.target_port 14 | target_pass = var.target_pass 15 | extra_files_script = var.extra_files_script 16 | build_on_remote = var.build_on_remote 17 | flake = var.flake 18 | phases = join(",", local.phases) 19 | nixos_generate_config_path = var.nixos_generate_config_path 20 | nixos_facter_path = var.nixos_facter_path 21 | }) 22 | } 23 | 24 | resource "null_resource" "nixos-remote" { 25 | triggers = { 26 | instance_id = var.instance_id 27 | } 28 | provisioner "local-exec" { 29 | environment = merge({ 30 | ARGUMENTS = local.arguments 31 | }, var.extra_environment) 32 | command = "${path.module}/run-nixos-anywhere.sh ${join(" ", local.disk_encryption_key_scripts)}" 33 | quiet = var.debug_logging 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /terraform/install/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | null = { source = "hashicorp/null" } 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /terraform/install/run-nixos-anywhere.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | SCRIPT_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" 5 | 6 | declare -A input 7 | 8 | while IFS= read -r -d '' key && IFS= read -r -d '' value; do 9 | input[$key]=$value 10 | done < <(jq -j 'to_entries[] | (.key, "\u0000", .value, "\u0000")' <<<"${ARGUMENTS}") 11 | 12 | args=() 13 | 14 | if [[ ${input[debug_logging]} == "true" ]]; then 15 | set -x 16 | declare -p input 17 | args+=("--debug") 18 | fi 19 | if [[ ${input[kexec_tarball_url]} != "null" ]]; then 20 | args+=("--kexec" "${input[kexec_tarball_url]}") 21 | fi 22 | if [[ ${input[build_on_remote]} == "true" ]]; then 23 | args+=("--build-on-remote") 24 | fi 25 | if [[ -n ${input[flake]} ]]; then 26 | args+=("--flake" "${input[flake]}") 27 | else 28 | args+=("--store-paths" "${input[nixos_partitioner]}" "${input[nixos_system]}") 29 | fi 30 | if [[ -n ${input[nixos_generate_config_path]} ]]; then 31 | if [[ -n ${input[nixos_facter_path]} ]]; then 32 | echo "cannot set both variables 'nixos_generate_config_path' and 'nixos_facter_path'!" >&2 33 | exit 1 34 | fi 35 | args+=("--generate-hardware-config" "nixos-generate-config" "${input[nixos_generate_config_path]}") 36 | elif [[ -n ${input[nixos_facter_path]} ]]; then 37 | args+=("--generate-hardware-config" "nixos-facter" "${input[nixos_facter_path]}") 38 | fi 39 | args+=(--phases "${input[phases]}") 40 | if [[ ${input[ssh_private_key]} != null ]]; then 41 | export SSH_PRIVATE_KEY="${input[ssh_private_key]}" 42 | fi 43 | if [[ ${input[target_pass]} != null ]]; then 44 | export SSHPASS=${input[target_pass]} 45 | args+=("--env-password") 46 | fi 47 | 48 | tmpdir=$(mktemp -d) 49 | cleanup() { 50 | rm -rf "${tmpdir}" 51 | } 52 | trap cleanup EXIT 53 | 54 | if [[ ${input[extra_files_script]} != "null" ]]; then 55 | if [[ ! -f ${input[extra_files_script]} ]]; then 56 | echo "extra_files_script '${input[extra_files_script]}' does not exist" 57 | exit 1 58 | fi 59 | if [[ ! -x ${input[extra_files_script]} ]]; then 60 | echo "extra_files_script '${input[extra_files_script]}' is not executable" 61 | exit 1 62 | fi 63 | extra_files_script=$(realpath "${input[extra_files_script]}") 64 | mkdir "${tmpdir}/extra-files" 65 | pushd "${tmpdir}/extra-files" 66 | $extra_files_script 67 | popd 68 | args+=("--extra-files" "${tmpdir}/extra-files") 69 | fi 70 | 71 | args+=("-p" "${input[target_port]}") 72 | args+=("${input[target_user]}@${input[target_host]}") 73 | 74 | keyIdx=0 75 | while [[ $# -gt 0 ]]; do 76 | if [[ ! -f $2 ]]; then 77 | echo "Script file '$2' does not exist" 78 | exit 1 79 | fi 80 | if [[ ! -x $2 ]]; then 81 | echo "Script file '$2' is not executable" 82 | exit 1 83 | fi 84 | mkdir -p "${tmpdir}/keys" 85 | "$2" >"${tmpdir}/keys/$keyIdx" 86 | args+=("--disk-encryption-keys" "$1" "${tmpdir}/keys/$keyIdx") 87 | shift 88 | shift 89 | keyIdx=$((keyIdx + 1)) 90 | done 91 | 92 | nix run --extra-experimental-features 'nix-command flakes' "path:${SCRIPT_DIR}/../..#nixos-anywhere" -- "${args[@]}" 93 | -------------------------------------------------------------------------------- /terraform/install/variables.tf: -------------------------------------------------------------------------------- 1 | variable "kexec_tarball_url" { 2 | type = string 3 | description = "NixOS kexec installer tarball url" 4 | default = null 5 | } 6 | 7 | # To make this re-usable we maybe should accept a store path here? 8 | variable "nixos_partitioner" { 9 | type = string 10 | description = "nixos partitioner and mount script" 11 | default = "" 12 | } 13 | 14 | # To make this re-usable we maybe should accept a store path here? 15 | variable "nixos_system" { 16 | type = string 17 | description = "The nixos system to deploy" 18 | default = "" 19 | } 20 | 21 | variable "target_host" { 22 | type = string 23 | description = "DNS host to deploy to" 24 | } 25 | 26 | variable "target_user" { 27 | type = string 28 | description = "SSH user used to connect to the target_host" 29 | default = "root" 30 | } 31 | 32 | variable "target_port" { 33 | type = number 34 | description = "SSH port used to connect to the target_host" 35 | default = 22 36 | } 37 | 38 | variable "target_pass" { 39 | type = string 40 | description = "Password used to connect to the target_host" 41 | default = null 42 | } 43 | 44 | variable "ssh_private_key" { 45 | type = string 46 | description = "Content of private key used to connect to the target_host" 47 | default = "" 48 | } 49 | 50 | variable "instance_id" { 51 | type = string 52 | description = "The instance id of the target_host, used to track when to reinstall the machine" 53 | default = null 54 | } 55 | 56 | variable "debug_logging" { 57 | type = bool 58 | description = "Enable debug logging" 59 | default = false 60 | } 61 | 62 | variable "extra_files_script" { 63 | type = string 64 | description = "A script that should place files in the current directory that will be copied to the targets / directory" 65 | default = null 66 | } 67 | 68 | variable "disk_encryption_key_scripts" { 69 | type = list(object({ 70 | path = string 71 | script = string 72 | })) 73 | description = "Each script will be executed locally. Output of each will be created at the given path to disko during installation. The keys will be not copied to the final system" 74 | default = [] 75 | } 76 | 77 | variable "extra_environment" { 78 | type = map(string) 79 | description = "Extra environment variables to be set during installation. This can be useful to set extra variables for the extra_files_script or disk_encryption_key_scripts" 80 | default = {} 81 | } 82 | 83 | variable "stop_after_disko" { 84 | type = bool 85 | description = "DEPRECATED: Use `phases` instead. Exit after disko formatting" 86 | default = false 87 | } 88 | 89 | variable "no_reboot" { 90 | type = bool 91 | description = "DEPRECATED: Use `phases` instead. Do not reboot after installation" 92 | default = false 93 | } 94 | 95 | variable "phases" { 96 | type = list(string) 97 | description = "Phases to run. See `nixos-anywhere --help` for more information" 98 | default = ["kexec", "disko", "install", "reboot"] 99 | } 100 | 101 | variable "build_on_remote" { 102 | type = bool 103 | description = "Build the closure on the remote machine instead of building it locally and copying it over" 104 | default = false 105 | } 106 | 107 | variable "flake" { 108 | type = string 109 | description = "The flake to install the system from" 110 | default = "" 111 | } 112 | 113 | variable "nixos_generate_config_path" { 114 | type = string 115 | description = "Path to which to write a `hardware-configuration.nix` generated by `nixos-generate-config`. This option cannot be set at the same time as `nixos_facter_path`." 116 | default = "" 117 | } 118 | 119 | variable "nixos_facter_path" { 120 | type = string 121 | description = "Path to which to write a `facter.json` generated by `nixos-facter`. This option cannot be set at the same time as `nixos_generate_config_path`." 122 | default = "" 123 | } 124 | -------------------------------------------------------------------------------- /terraform/nix-build.md: -------------------------------------------------------------------------------- 1 | # Nix-build 2 | 3 | Small helper module to run do build a flake attribute or attribute from a nix 4 | file. 5 | 6 | ## Example 7 | 8 | - See [install](install.md) or [nixos-rebuild](nixos-rebuild.md) 9 | 10 | 11 | 12 | ## Requirements 13 | 14 | No requirements. 15 | 16 | ## Providers 17 | 18 | | Name | Version | 19 | | --------------------------------------------------------------- | ------- | 20 | | [external](#provider_external) | n/a | 21 | 22 | ## Modules 23 | 24 | No modules. 25 | 26 | ## Resources 27 | 28 | | Name | Type | 29 | | --------------------------------------------------------------------------------------------------------------------------- | ----------- | 30 | | [external_external.nix-build](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | 31 | 32 | ## Inputs 33 | 34 | | Name | Description | Type | Default | Required | 35 | | ------------------------------------------------------------------------- | --------------------------------------------------- | ------------- | ------- | :------: | 36 | | [attribute](#input_attribute) | the attribute to build, can also be a flake | `string` | n/a | yes | 37 | | [debug\_logging](#input_debug_logging) | Enable debug logging | `bool` | `false` | no | 38 | | [file](#input_file) | the nix file to evaluate, if not run in flake mode | `string` | `null` | no | 39 | | [nix\_options](#input_nix_options) | the options of nix | `map(string)` | `{}` | no | 40 | | [special\_args](#input_special_args) | A map exposed as NixOS's `specialArgs` thru a file. | `any` | `{}` | no | 41 | 42 | ## Outputs 43 | 44 | | Name | Description | 45 | | ----------------------------------------------------- | ----------- | 46 | | [result](#output_result) | n/a | 47 | 48 | 49 | -------------------------------------------------------------------------------- /terraform/nix-build/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | nix_options = jsonencode({ 3 | options = { for k, v in var.nix_options : k => v } 4 | }) 5 | } 6 | data "external" "nix-build" { 7 | program = ["${path.module}/nix-build.sh"] 8 | query = { 9 | attribute = var.attribute 10 | file = var.file 11 | nix_options = local.nix_options 12 | debug_logging = var.debug_logging 13 | special_args = jsonencode(var.special_args) 14 | } 15 | } 16 | output "result" { 17 | value = data.external.nix-build.result 18 | } 19 | -------------------------------------------------------------------------------- /terraform/nix-build/nix-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -efu 3 | 4 | declare file attribute nix_options special_args debug_logging 5 | eval "$(jq -r '@sh "attribute=\(.attribute) file=\(.file) nix_options=\(.nix_options) special_args=\(.special_args) debug_logging=\(.debug_logging)"')" 6 | if [ "${debug_logging}" = "true" ]; then 7 | set -x 8 | fi 9 | if [ "${nix_options}" != '{"options":{}}' ]; then 10 | options=$(echo "${nix_options}" | jq -r '.options | to_entries | map("--option \(.key) \(.value)") | join(" ")') 11 | else 12 | options="" 13 | fi 14 | if [[ ${special_args-} == "{}" ]]; then 15 | # no special arguments, proceed as normal 16 | if [[ -n ${file-} ]] && [[ -e ${file-} ]]; then 17 | # shellcheck disable=SC2086 18 | out=$(nix build --no-link --json $options -f "$file" "$attribute") 19 | else 20 | # shellcheck disable=SC2086 21 | out=$(nix build --no-link --json ${options} "$attribute") 22 | fi 23 | else 24 | if [[ ${file-} != 'null' ]]; then 25 | echo "special_args are currently only supported when using flakes!" >&2 26 | exit 1 27 | fi 28 | # pass the args in a pure fashion by extending the original config 29 | rest="$(echo "${attribute}" | cut -d "#" -f 2)" 30 | # e.g. config_path=nixosConfigurations.aarch64-linux.myconfig 31 | config_path="${rest%.config.*}" 32 | # e.g. config_attribute=config.system.build.toplevel 33 | config_attribute="config.${rest#*.config.}" 34 | 35 | # grab flake nar from error message 36 | flake_rel="$(echo "${attribute}" | cut -d "#" -f 1)" 37 | 38 | # Use nix flake prefetch to get the flake into the store, then use path:// URL with narHash 39 | prefetch_result="$(nix flake prefetch "${flake_rel}" --json)" 40 | store_path="$(echo "${prefetch_result}" | jq -r '.storePath')" 41 | nar_hash="$(echo "${prefetch_result}" | jq -r '.hash')" 42 | flake_url="path:${store_path}?narHash=${nar_hash}" 43 | 44 | # substitute variables into the template 45 | nix_expr="(builtins.getFlake ''${flake_url}'').${config_path}.extendModules { specialArgs = builtins.fromJSON ''${special_args}''; }" 46 | # inject `special_args` into nixos config's `specialArgs` 47 | 48 | # shellcheck disable=SC2086 49 | out=$(nix build --no-link --json ${options} --expr "${nix_expr}" "${config_attribute}") 50 | fi 51 | printf '%s' "$out" | jq -c '.[].outputs' 52 | -------------------------------------------------------------------------------- /terraform/nix-build/variables.tf: -------------------------------------------------------------------------------- 1 | variable "attribute" { 2 | type = string 3 | description = "the attribute to build, can also be a flake" 4 | } 5 | 6 | variable "file" { 7 | type = string 8 | description = "the nix file to evaluate, if not run in flake mode" 9 | default = null 10 | } 11 | 12 | variable "nix_options" { 13 | type = map(string) 14 | description = "the options of nix" 15 | default = {} 16 | } 17 | 18 | variable "special_args" { 19 | type = any 20 | default = {} 21 | description = "A map exposed as NixOS's `specialArgs` thru a file." 22 | } 23 | 24 | variable "debug_logging" { 25 | type = bool 26 | default = false 27 | description = "Enable debug logging" 28 | } 29 | -------------------------------------------------------------------------------- /terraform/nixos-rebuild.md: -------------------------------------------------------------------------------- 1 | # Nixos-rebuild 2 | 3 | Update NixOS machine with nixos-rebuild on a remote machine 4 | 5 | ## Example 6 | 7 | ```hcl 8 | locals { 9 | ipv4 = "192.0.2.1" 10 | } 11 | 12 | module "system-build" { 13 | source = "github.com/nix-community/nixos-anywhere//terraform/nix-build" 14 | # with flakes 15 | attribute = ".#nixosConfigurations.mymachine.config.system.build.toplevel" 16 | # without flakes 17 | # file can use (pkgs.nixos []) function from nixpkgs 18 | #file = "${path.module}/../.." 19 | #attribute = "config.system.build.toplevel" 20 | } 21 | 22 | module "deploy" { 23 | source = "github.com/nix-community/nixos-anywhere//terraform/nixos-rebuild" 24 | nixos_system = module.system-build.result.out 25 | target_host = local.ipv4 26 | } 27 | ``` 28 | 29 | 30 | 31 | ## Requirements 32 | 33 | No requirements. 34 | 35 | ## Providers 36 | 37 | | Name | Version | 38 | | --------------------------------------------------- | ------- | 39 | | [null](#provider_null) | n/a | 40 | 41 | ## Modules 42 | 43 | No modules. 44 | 45 | ## Resources 46 | 47 | | Name | Type | 48 | | -------------------------------------------------------------------------------------------------------------------- | -------- | 49 | | [null_resource.nixos-rebuild](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 50 | 51 | ## Inputs 52 | 53 | | Name | Description | Type | Default | Required | 54 | | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -------- | :------: | 55 | | [ignore\_systemd\_errors](#input_ignore_systemd_errors) | Ignore systemd errors happening during deploy | `bool` | `false` | no | 56 | | [install\_bootloader](#input_install_bootloader) | Install/re-install the bootloader | `bool` | `false` | no | 57 | | [nixos\_system](#input_nixos_system) | The nixos system to deploy | `string` | n/a | yes | 58 | | [ssh\_private\_key](#input_ssh_private_key) | Content of private key used to connect to the target\_host. If set to - no key is passed to openssh and ssh will use its own configuration | `string` | `"-"` | no | 59 | | [target\_host](#input_target_host) | DNS host to deploy to | `string` | n/a | yes | 60 | | [target\_port](#input_target_port) | SSH port used to connect to the target\_host | `number` | `22` | no | 61 | | [target\_user](#input_target_user) | User to deploy as | `string` | `"root"` | no | 62 | 63 | ## Outputs 64 | 65 | No outputs. 66 | 67 | 68 | -------------------------------------------------------------------------------- /terraform/nixos-rebuild/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -uex -o pipefail 4 | 5 | if [ "$#" -ne 6 ]; then 6 | echo "USAGE: $0 NIXOS_SYSTEM TARGET_USER TARGET_HOST TARGET_PORT IGNORE_SYSTEMD_ERRORS INSTALL_BOOTLOADER" >&2 7 | exit 1 8 | fi 9 | 10 | NIXOS_SYSTEM=$1 11 | TARGET_USER=$2 12 | TARGET_HOST=$3 13 | TARGET_PORT=$4 14 | IGNORE_SYSTEMD_ERRORS=$5 15 | INSTALL_BOOTLOADER=$6 16 | 17 | shift 6 18 | 19 | TARGET="${TARGET_USER}@${TARGET_HOST}" 20 | 21 | workDir=$(mktemp -d) 22 | trap 'rm -rf "$workDir"' EXIT 23 | 24 | sshOpts=(-p "${TARGET_PORT}") 25 | sshOpts+=(-o UserKnownHostsFile=/dev/null) 26 | sshOpts+=(-o StrictHostKeyChecking=no) 27 | 28 | set +x 29 | if [[ -n ${SSH_KEY+x} && ${SSH_KEY} != "-" ]]; then 30 | sshPrivateKeyFile="$workDir/ssh_key" 31 | # Create the file with 0700 - umask calculation: 777 - 700 = 077 32 | ( 33 | umask 077 34 | echo "$SSH_KEY" >"$sshPrivateKeyFile" 35 | ) 36 | unset SSH_AUTH_SOCK # don't use system agent if key was supplied 37 | sshOpts+=(-o "IdentityFile=${sshPrivateKeyFile}") 38 | fi 39 | set -x 40 | 41 | try=1 42 | until NIX_SSHOPTS="${sshOpts[*]}" nix copy -s --experimental-features nix-command --to "ssh://$TARGET" "$NIXOS_SYSTEM"; do 43 | if [[ $try -gt 10 ]]; then 44 | echo "retries exhausted" >&2 45 | exit 1 46 | fi 47 | sleep 10 48 | try=$((try + 1)) 49 | done 50 | 51 | if [[ $INSTALL_BOOTLOADER == "true" ]]; then 52 | extra_env='NIXOS_INSTALL_BOOTLOADER=1' 53 | else 54 | extra_env='' 55 | fi 56 | 57 | switchCommand="nix-env -p /nix/var/nix/profiles/system --set $(printf "%q" "$NIXOS_SYSTEM"); $extra_env /nix/var/nix/profiles/system/bin/switch-to-configuration switch" 58 | 59 | if [[ $TARGET_USER != "root" ]]; then 60 | switchCommand="sudo bash -c '$switchCommand'" 61 | fi 62 | deploy_status=0 63 | # shellcheck disable=SC2029 64 | ssh "${sshOpts[@]}" "$TARGET" "$switchCommand" || deploy_status="$?" 65 | if [[ $IGNORE_SYSTEMD_ERRORS == "true" && $deploy_status == "4" ]]; then 66 | exit 0 67 | fi 68 | exit "$deploy_status" 69 | -------------------------------------------------------------------------------- /terraform/nixos-rebuild/main.tf: -------------------------------------------------------------------------------- 1 | resource "null_resource" "nixos-rebuild" { 2 | triggers = { 3 | store_path = var.nixos_system 4 | } 5 | provisioner "local-exec" { 6 | environment = { 7 | SSH_KEY = var.ssh_private_key 8 | } 9 | command = "${path.module}/deploy.sh ${var.nixos_system} ${var.target_user} ${var.target_host} ${var.target_port} ${var.ignore_systemd_errors} ${var.install_bootloader}" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /terraform/nixos-rebuild/variables.tf: -------------------------------------------------------------------------------- 1 | variable "nixos_system" { 2 | type = string 3 | description = "The nixos system to deploy" 4 | } 5 | 6 | variable "target_host" { 7 | type = string 8 | description = "DNS host to deploy to" 9 | } 10 | 11 | variable "target_user" { 12 | type = string 13 | default = "root" 14 | description = "User to deploy as" 15 | } 16 | 17 | variable "target_port" { 18 | type = number 19 | description = "SSH port used to connect to the target_host" 20 | default = 22 21 | } 22 | 23 | variable "ssh_private_key" { 24 | type = string 25 | description = "Content of private key used to connect to the target_host. If set to - no key is passed to openssh and ssh will use its own configuration" 26 | default = "-" 27 | } 28 | 29 | variable "ignore_systemd_errors" { 30 | type = bool 31 | description = "Ignore systemd errors happening during deploy" 32 | default = false 33 | } 34 | 35 | variable "install_bootloader" { 36 | type = bool 37 | description = "Install/re-install the bootloader" 38 | default = false 39 | } 40 | -------------------------------------------------------------------------------- /terraform/tests/README.md: -------------------------------------------------------------------------------- 1 | # NixOS Anywhere Terraform Tests 2 | 3 | This directory contains tests for the `nixos-anywhere` Terraform modules using 4 | OpenTofu's built-in testing framework. 5 | 6 | ## Test Structure 7 | 8 | ``` 9 | terraform/tests/ 10 | ├── main.tf # Core configuration for validation tests 11 | ├── nixos-anywhere.tftest.hcl # Basic validation tests (no external deps) 12 | ├── nix-build-special-args.tftest.hcl # Nix build module tests with special_args 13 | ├── hcloud-deployment.tftest.hcl # Hetzner Cloud integration tests 14 | ├── digitalocean-deployment.tftest.hcl # DigitalOcean integration tests 15 | ├── hcloud/ # Hetzner Cloud deployment configuration 16 | │ ├── main.tf 17 | ├── digitalocean/ # DigitalOcean deployment configuration 18 | ├── main.tf 19 | ``` 20 | 21 | ### For Cloud Provider Tests (Optional) 22 | 23 | - Hetzner Cloud account with API token 24 | - DigitalOcean account with API token 25 | 26 | ## Running Tests 27 | 28 | ### Basic Validation Tests (No External Dependencies) 29 | 30 | ```bash 31 | # Enter development environment 32 | nix develop .#terraform 33 | 34 | # Initialize and run validation tests 35 | cd terraform/tests 36 | tofu init 37 | tofu test 38 | 39 | # Run Specific Test File 40 | tofu test -filter=nixos-anywhere.tftest.hcl 41 | ``` 42 | 43 | **Current Status:** Tests available for nixos-anywhere module and nix-build 44 | module 45 | 46 | ### Cloud Provider Integration Tests 47 | 48 | #### Hetzner Cloud 49 | 50 | ```bash 51 | # Set your Hetzner Cloud token 52 | export TF_VAR_hcloud_token="your-64-character-hcloud-token" 53 | 54 | # Run Hetzner Cloud tests 55 | tofu test -filter hcloud-deployment.tftest.hcl 56 | ``` 57 | 58 | #### DigitalOcean 59 | 60 | ```bash 61 | # Set your DigitalOcean token 62 | export TF_VAR_digitalocean_token="your-digitalocean-api-token" 63 | 64 | # Run DigitalOcean tests 65 | tofu test -filter digitalocean-deployment.tftest.hcl 66 | ``` 67 | 68 | **Note:** These tests will fail if no valid token is provided. 69 | 70 | ## Test Categories 71 | 72 | ### 1. Validation Tests (`nixos-anywhere.tftest.hcl`) 73 | 74 | - **Purpose:** Validate configuration structure and variables 75 | - **Dependencies:** None (nixos-anywhere module only) 76 | - **Duration:** 30-60 seconds 77 | - **Cost:** Free 78 | 79 | ### 2. Nix Build Special Args Tests (`nix-build-special-args.tftest.hcl`) 80 | 81 | - **Purpose:** Test nix-build module with special_args functionality 82 | - **Dependencies:** None (nix-build module only) 83 | - **Duration:** 30-60 seconds 84 | - **Cost:** Free 85 | 86 | ### 3. Hetzner Cloud Tests (`hcloud-deployment.tftest.hcl`) 87 | 88 | - **Purpose:** Test complete deployment workflow 89 | - **Dependencies:** Valid Hetzner Cloud token 90 | - **Duration:** 5 minutes (apply tests) 91 | - **Cost:** ~€ 0.006 per hour 92 | 93 | ### 4. DigitalOcean Tests (`digitalocean-deployment.tftest.hcl`) 94 | 95 | - **Purpose:** Test complete deployment workflow 96 | - **Dependencies:** Valid DigitalOcean API token 97 | - **Duration:** 5 minutes (apply tests) 98 | - **Cost:** ~$0,02679 per hour 99 | 100 | ## Usage Examples 101 | 102 | ### Environment Variables 103 | 104 | ```bash 105 | export TF_VAR_hcloud_token="your-hcloud-token" 106 | export TF_VAR_digitalocean_token="your-digitalocean-token" 107 | export TF_VAR_test_name_prefix="my-test" 108 | ``` 109 | 110 | ### Variable File (Optional) 111 | 112 | Create `terraform.tfvars`: 113 | 114 | ```hcl 115 | hcloud_token = "your-hcloud-token-here" 116 | digitalocean_token = "your-digitalocean-token-here" 117 | test_name_prefix = "tftest-nixos-anywhere" 118 | ``` 119 | 120 | ## Cleanup 121 | 122 | OpenTofu test automatically cleans up resources. Manual cleanup if needed: 123 | 124 | ### Hetzner Cloud 125 | 126 | ```bash 127 | # List test resources 128 | hcloud server list | grep tftest-nixos-anywhere 129 | hcloud ssh-key list | grep tftest-nixos-anywhere 130 | 131 | # Force cleanup 132 | hcloud server delete 133 | hcloud ssh-key delete 134 | ``` 135 | 136 | ### DigitalOcean 137 | 138 | ```bash 139 | # List test resources 140 | doctl compute droplet list | grep tftest-nixos-anywhere 141 | doctl compute ssh-key list | grep tftest-nixos-anywhere 142 | 143 | # Force cleanup 144 | doctl compute droplet delete 145 | doctl compute ssh-key delete 146 | ``` 147 | 148 | ## Development 149 | 150 | ### Test Best Practices 151 | 152 | - Use `command = plan` for validation tests 153 | - Use `command = apply` sparingly for integration tests 154 | - Include proper assertions with clear error messages 155 | - Make tests conditional based on available credentials 156 | -------------------------------------------------------------------------------- /terraform/tests/digitalocean-deployment.tftest.hcl: -------------------------------------------------------------------------------- 1 | # Terraform Test Configuration for nixos-anywhere DigitalOcean deployment 2 | # Run with: tofu test -var="digitalocean_token=your-token-here" 3 | # These tests require a valid DigitalOcean API token 4 | 5 | variables { 6 | test_name_prefix = "tftest-nixos-anywhere" 7 | } 8 | 9 | run "test_digitalocean_deployment_apply" { 10 | command = apply 11 | 12 | module { 13 | source = "./digitalocean" 14 | } 15 | 16 | variables { 17 | nixos_system_attr = "github:nix-community/nixos-anywhere-examples#nixosConfigurations.digitalocean.config.system.build.toplevel" 18 | nixos_partitioner_attr = "github:nix-community/nixos-anywhere-examples#nixosConfigurations.digitalocean.config.system.build.diskoNoDeps" 19 | debug_logging = true 20 | } 21 | 22 | assert { 23 | condition = output.nixos_anywhere_result != null 24 | error_message = "nixos-anywhere deployment should produce a result" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /terraform/tests/digitalocean/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | digitalocean = { 4 | source = "digitalocean/digitalocean" 5 | version = "~> 2.34" 6 | } 7 | tls = { 8 | source = "hashicorp/tls" 9 | version = "~> 4.0" 10 | } 11 | local = { 12 | source = "hashicorp/local" 13 | version = "~> 2.4" 14 | } 15 | } 16 | } 17 | 18 | provider "digitalocean" { 19 | token = var.digitalocean_token 20 | } 21 | 22 | variable "digitalocean_token" { 23 | description = "DigitalOcean API token" 24 | type = string 25 | sensitive = true 26 | } 27 | 28 | variable "test_name_prefix" { 29 | description = "Prefix for test resource names" 30 | type = string 31 | default = "tftest-nixos-anywhere" 32 | } 33 | 34 | variable "nixos_system_attr" { 35 | description = "NixOS system attribute to deploy" 36 | type = string 37 | } 38 | 39 | variable "nixos_partitioner_attr" { 40 | description = "NixOS partitioner attribute" 41 | type = string 42 | } 43 | 44 | variable "debug_logging" { 45 | description = "Enable debug logging" 46 | type = bool 47 | default = false 48 | } 49 | 50 | # Generate SSH key pair 51 | resource "tls_private_key" "test_key" { 52 | algorithm = "ED25519" 53 | } 54 | 55 | # Save private key to file 56 | resource "local_file" "private_key" { 57 | content = tls_private_key.test_key.private_key_openssh 58 | filename = "${path.root}/test_key" 59 | file_permission = "0600" 60 | } 61 | 62 | # Save public key to file 63 | resource "local_file" "public_key" { 64 | content = tls_private_key.test_key.public_key_openssh 65 | filename = "${path.root}/test_key.pub" 66 | } 67 | 68 | # Create DigitalOcean SSH key 69 | resource "digitalocean_ssh_key" "test_key" { 70 | name = "${var.test_name_prefix}-deployment-key" 71 | public_key = tls_private_key.test_key.public_key_openssh 72 | } 73 | 74 | # Create test droplet 75 | # Note: Using s-2vcpu-2gb (minimum 2GB RAM required for nixos-anywhere kexec) 76 | # DigitalOcean uses /dev/vda for disk devices (handled by digitalocean config) 77 | resource "digitalocean_droplet" "test_server" { 78 | name = "${var.test_name_prefix}-server" 79 | image = "ubuntu-22-04-x64" 80 | size = "s-2vcpu-2gb" 81 | region = "nyc3" 82 | ssh_keys = [digitalocean_ssh_key.test_key.id] 83 | 84 | tags = [ 85 | "nixos-anywhere-test", 86 | replace(replace(replace(timestamp(), ":", "-"), "T", "-"), "Z", "") 87 | ] 88 | } 89 | 90 | # nixos-anywhere all-in-one module 91 | # Uses digitalocean configuration from nixos-anywhere-examples which: 92 | # - Sets disk device to /dev/vda (DigitalOcean standard) 93 | # - Configures cloud-init for network setup 94 | # - Disables DHCP in favor of cloud-init provisioning 95 | module "nixos_anywhere" { 96 | source = "../../all-in-one" 97 | 98 | nixos_system_attr = var.nixos_system_attr 99 | nixos_partitioner_attr = var.nixos_partitioner_attr 100 | target_host = digitalocean_droplet.test_server.ipv4_address 101 | target_port = 22 102 | target_user = "root" 103 | debug_logging = var.debug_logging 104 | deployment_ssh_key = tls_private_key.test_key.private_key_openssh 105 | install_ssh_key = tls_private_key.test_key.private_key_openssh 106 | 107 | special_args = { 108 | extraPublicKeys = [tls_private_key.test_key.public_key_openssh] 109 | } 110 | } 111 | 112 | output "nixos_anywhere_result" { 113 | description = "nixos-anywhere module result" 114 | value = module.nixos_anywhere.result 115 | } 116 | 117 | output "droplet_ip" { 118 | description = "DigitalOcean droplet public IP address" 119 | value = digitalocean_droplet.test_server.ipv4_address 120 | } 121 | 122 | output "droplet_id" { 123 | description = "DigitalOcean droplet ID for cleanup" 124 | value = digitalocean_droplet.test_server.id 125 | } 126 | 127 | output "ssh_key_id" { 128 | description = "DigitalOcean SSH key ID for cleanup" 129 | value = digitalocean_ssh_key.test_key.id 130 | } 131 | 132 | output "ssh_connection_command" { 133 | description = "SSH command to connect to the deployed server" 134 | value = "ssh -i ${local_file.private_key.filename} root@${digitalocean_droplet.test_server.ipv4_address}" 135 | } -------------------------------------------------------------------------------- /terraform/tests/hcloud-deployment.tftest.hcl: -------------------------------------------------------------------------------- 1 | # Terraform Test Configuration for nixos-anywhere Hetzner Cloud deployment 2 | # Run with: tofu test -var="hcloud_token=your-token-here" 3 | # These tests require a valid Hetzner Cloud API token 4 | 5 | variables { 6 | test_name_prefix = "tftest-nixos-anywhere" 7 | } 8 | 9 | run "test_hcloud_deployment_apply" { 10 | command = apply 11 | 12 | module { 13 | source = "./hcloud" 14 | } 15 | 16 | variables { 17 | nixos_system_attr = "github:nix-community/nixos-anywhere-examples#nixosConfigurations.hetzner-cloud.config.system.build.toplevel" 18 | nixos_partitioner_attr = "github:nix-community/nixos-anywhere-examples#nixosConfigurations.hetzner-cloud.config.system.build.diskoNoDeps" 19 | debug_logging = true 20 | } 21 | 22 | assert { 23 | condition = output.nixos_anywhere_result != null 24 | error_message = "nixos-anywhere deployment should produce a result" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /terraform/tests/hcloud/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | hcloud = { 4 | source = "hetznercloud/hcloud" 5 | version = "~> 1.45" 6 | } 7 | tls = { 8 | source = "hashicorp/tls" 9 | version = "~> 4.0" 10 | } 11 | local = { 12 | source = "hashicorp/local" 13 | version = "~> 2.4" 14 | } 15 | } 16 | } 17 | 18 | provider "hcloud" { 19 | token = var.hcloud_token 20 | } 21 | 22 | variable "hcloud_token" { 23 | description = "Hetzner Cloud API token (64 characters)" 24 | type = string 25 | sensitive = true 26 | } 27 | 28 | variable "test_name_prefix" { 29 | description = "Prefix for test resource names" 30 | type = string 31 | default = "tftest-nixos-anywhere" 32 | } 33 | 34 | variable "nixos_system_attr" { 35 | description = "NixOS system attribute to deploy" 36 | type = string 37 | } 38 | 39 | variable "nixos_partitioner_attr" { 40 | description = "NixOS partitioner attribute" 41 | type = string 42 | } 43 | 44 | 45 | variable "debug_logging" { 46 | description = "Enable debug logging" 47 | type = bool 48 | default = false 49 | } 50 | 51 | # Generate SSH key pair 52 | resource "tls_private_key" "test_key" { 53 | algorithm = "ED25519" 54 | } 55 | 56 | # Save private key to file 57 | resource "local_file" "private_key" { 58 | content = tls_private_key.test_key.private_key_openssh 59 | filename = "${path.root}/test_key" 60 | file_permission = "0600" 61 | } 62 | 63 | # Save public key to file 64 | resource "local_file" "public_key" { 65 | content = tls_private_key.test_key.public_key_openssh 66 | filename = "${path.root}/test_key.pub" 67 | } 68 | 69 | # Create Hetzner Cloud SSH key 70 | resource "hcloud_ssh_key" "test_key" { 71 | name = "${var.test_name_prefix}-deployment-key" 72 | public_key = tls_private_key.test_key.public_key_openssh 73 | } 74 | 75 | # Create test server 76 | resource "hcloud_server" "test_server" { 77 | name = "${var.test_name_prefix}-server" 78 | image = "ubuntu-22.04" 79 | server_type = "cx22" 80 | location = "hel1" 81 | ssh_keys = [hcloud_ssh_key.test_key.id] 82 | 83 | labels = { 84 | purpose = "nixos-anywhere-test" 85 | test_run = replace(replace(replace(timestamp(), ":", "-"), "T", "-"), "Z", "") 86 | } 87 | } 88 | 89 | # nixos-anywhere all-in-one module 90 | module "nixos_anywhere" { 91 | source = "../../all-in-one" 92 | 93 | nixos_system_attr = var.nixos_system_attr 94 | nixos_partitioner_attr = var.nixos_partitioner_attr 95 | target_host = hcloud_server.test_server.ipv4_address 96 | target_port = 22 97 | target_user = "root" 98 | debug_logging = var.debug_logging 99 | deployment_ssh_key = tls_private_key.test_key.private_key_openssh 100 | install_ssh_key = tls_private_key.test_key.private_key_openssh 101 | 102 | special_args = { 103 | extraPublicKeys = [tls_private_key.test_key.public_key_openssh] 104 | } 105 | } 106 | 107 | output "nixos_anywhere_result" { 108 | description = "nixos-anywhere module result" 109 | value = module.nixos_anywhere.result 110 | } 111 | -------------------------------------------------------------------------------- /terraform/tests/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | external = { 4 | source = "hashicorp/external" 5 | version = "~> 2.3" 6 | } 7 | null = { 8 | source = "hashicorp/null" 9 | version = "~> 3.2" 10 | } 11 | } 12 | } 13 | 14 | variable "nixos_system_attr" { 15 | description = "NixOS system attribute to deploy" 16 | type = string 17 | default = "" 18 | } 19 | 20 | variable "nixos_partitioner_attr" { 21 | description = "NixOS partitioner attribute" 22 | type = string 23 | default = "" 24 | } 25 | 26 | variable "target_host" { 27 | description = "Target host for deployment" 28 | type = string 29 | default = "test.example.com" 30 | } 31 | 32 | variable "target_port" { 33 | description = "Target SSH port" 34 | type = number 35 | default = 22 36 | } 37 | 38 | variable "target_user" { 39 | description = "Target SSH user" 40 | type = string 41 | default = "root" 42 | } 43 | 44 | variable "debug_logging" { 45 | description = "Enable debug logging" 46 | type = bool 47 | default = false 48 | } 49 | 50 | variable "phases" { 51 | description = "Deployment phases to run" 52 | type = set(string) 53 | default = ["kexec", "disko", "install"] 54 | } 55 | 56 | variable "build_on_remote" { 57 | description = "Build on remote machine" 58 | type = bool 59 | default = false 60 | } 61 | 62 | variable "install_ssh_key" { 63 | description = "SSH private key for installation" 64 | type = string 65 | default = "" 66 | sensitive = true 67 | } 68 | 69 | # nixos-anywhere all-in-one module 70 | module "nixos_anywhere" { 71 | source = "../all-in-one" 72 | 73 | nixos_system_attr = var.nixos_system_attr 74 | nixos_partitioner_attr = var.nixos_partitioner_attr 75 | target_host = var.target_host 76 | target_port = var.target_port 77 | target_user = var.target_user 78 | debug_logging = var.debug_logging 79 | phases = var.phases 80 | build_on_remote = var.build_on_remote 81 | install_ssh_key = var.install_ssh_key 82 | } 83 | 84 | output "nixos_anywhere_result" { 85 | description = "nixos-anywhere module result" 86 | value = module.nixos_anywhere.result 87 | } 88 | -------------------------------------------------------------------------------- /terraform/tests/nix-build-special-args.tftest.hcl: -------------------------------------------------------------------------------- 1 | # Terraform Test Configuration for nix-build module with special_args 2 | # Tests the fix for nested flakes in git repositories 3 | # Run with: tofu test 4 | 5 | variables { 6 | test_name_prefix = "tftest-nix-build-special-args" 7 | } 8 | 9 | # Test: nix-build module with special_args using terraform-test configuration 10 | run "validate_nix_build_special_args" { 11 | command = plan 12 | 13 | module { 14 | source = "../nix-build" 15 | } 16 | 17 | variables { 18 | attribute = ".#nixosConfigurations.terraform-test.config.system.build.toplevel" 19 | special_args = { 20 | terraform = { 21 | hostname = "special-args-test-host" 22 | ip_address = "192.168.1.100" 23 | environment = "testing" 24 | deployment_id = "test-001" 25 | } 26 | } 27 | } 28 | 29 | assert { 30 | condition = var.special_args.terraform.hostname == "special-args-test-host" 31 | error_message = "special_args should preserve hostname value" 32 | } 33 | 34 | assert { 35 | condition = var.special_args.terraform.environment == "testing" 36 | error_message = "special_args should preserve environment value" 37 | } 38 | 39 | assert { 40 | condition = var.attribute == ".#nixosConfigurations.terraform-test.config.system.build.toplevel" 41 | error_message = "should use terraform-test configuration" 42 | } 43 | } -------------------------------------------------------------------------------- /terraform/tests/nixos-anywhere.tftest.hcl: -------------------------------------------------------------------------------- 1 | # Terraform Test Configuration for nixos-anywhere all-in-one module 2 | # Run with: tofu test 3 | 4 | variables { 5 | test_name_prefix = "tftest-nixos-anywhere" 6 | } 7 | 8 | # Test: Basic variable validation 9 | run "validate_basic_config" { 10 | command = plan 11 | 12 | variables { 13 | nixos_system_attr = ".#nixosConfigurations.terraform-test.config.system.build.toplevel" 14 | nixos_partitioner_attr = ".#nixosConfigurations.terraform-test.config.system.build.diskoNoDeps" 15 | target_host = "1.2.3.4" 16 | debug_logging = true 17 | } 18 | 19 | assert { 20 | condition = var.nixos_system_attr != "" 21 | error_message = "NixOS system attribute must be specified" 22 | } 23 | 24 | assert { 25 | condition = var.nixos_partitioner_attr != "" 26 | error_message = "NixOS partitioner attribute must be specified" 27 | } 28 | 29 | assert { 30 | condition = var.target_host != "" 31 | error_message = "Target host must be specified" 32 | } 33 | } 34 | 35 | # Test: Variable validation with custom values 36 | run "validate_variables" { 37 | command = plan 38 | 39 | variables { 40 | nixos_system_attr = ".#nixosConfigurations.terraform-test.config.system.build.toplevel" 41 | nixos_partitioner_attr = ".#nixosConfigurations.terraform-test.config.system.build.diskoNoDeps" 42 | target_host = "test.example.com" 43 | target_port = 2222 44 | target_user = "deploy" 45 | debug_logging = true 46 | phases = ["kexec", "disko", "install"] 47 | build_on_remote = true 48 | } 49 | 50 | assert { 51 | condition = var.target_port == 2222 52 | error_message = "Target port should be configurable" 53 | } 54 | 55 | assert { 56 | condition = var.target_user == "deploy" 57 | error_message = "Target user should be configurable" 58 | } 59 | 60 | assert { 61 | condition = contains(var.phases, "kexec") 62 | error_message = "Phases should include kexec" 63 | } 64 | 65 | assert { 66 | condition = var.build_on_remote == true 67 | error_message = "Build on remote should be configurable" 68 | } 69 | 70 | assert { 71 | condition = var.debug_logging == true 72 | error_message = "Debug logging should be configurable" 73 | } 74 | } 75 | 76 | # Test: Default values 77 | run "validate_defaults" { 78 | command = plan 79 | 80 | variables { 81 | nixos_system_attr = ".#nixosConfigurations.terraform-test.config.system.build.toplevel" 82 | nixos_partitioner_attr = ".#nixosConfigurations.terraform-test.config.system.build.diskoNoDeps" 83 | target_host = "192.168.1.100" 84 | } 85 | 86 | assert { 87 | condition = var.target_port == 22 88 | error_message = "Default target port should be 22" 89 | } 90 | 91 | assert { 92 | condition = var.target_user == "root" 93 | error_message = "Default target user should be root" 94 | } 95 | 96 | assert { 97 | condition = var.debug_logging == false 98 | error_message = "Default debug logging should be false" 99 | } 100 | 101 | assert { 102 | condition = var.build_on_remote == false 103 | error_message = "Default build_on_remote should be false" 104 | } 105 | 106 | assert { 107 | condition = contains(var.phases, "kexec") 108 | error_message = "Default phases should include kexec" 109 | } 110 | 111 | assert { 112 | condition = contains(var.phases, "disko") 113 | error_message = "Default phases should include disko" 114 | } 115 | 116 | assert { 117 | condition = contains(var.phases, "install") 118 | error_message = "Default phases should include install" 119 | } 120 | } -------------------------------------------------------------------------------- /terraform/tests/terraform.tfvars.example: -------------------------------------------------------------------------------- 1 | # Terraform variables for nixos-anywhere test suite 2 | # Copy this file to terraform.tfvars and fill in your values 3 | 4 | # Required: Hetzner Cloud API token 5 | # Get this from https://console.hetzner.cloud/ 6 | hcloud_token = "your-hcloud-token-here" 7 | 8 | # Required: DigitalOcean API token 9 | # Get this from https://cloud.digitalocean.com/account/api/tokens 10 | digitalocean_token = "your-digitalocean-token-here" 11 | 12 | # Optional: Prefix for test resource names 13 | # This helps identify and clean up test resources 14 | test_name_prefix = "tftest-nixos-anywhere" -------------------------------------------------------------------------------- /terraform/update-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 5 | cd "$SCRIPT_DIR" 6 | files=() 7 | git ls-files "${SCRIPT_DIR}"/*/ | xargs -n1 dirname | sort -u | while read -r i; do 8 | # ignore test directory 9 | if [[ $i == "tests"* ]]; then 10 | continue 11 | fi 12 | echo "Processing module: $i" 13 | module_name=$(basename "$i") 14 | markdown_file="${SCRIPT_DIR}/${module_name}.md" 15 | terraform-docs --config "${SCRIPT_DIR}/.terraform-docs.yml" markdown table --output-file "${markdown_file}" --output-mode inject "${module_name}" 16 | files+=("${markdown_file}") 17 | done 18 | cd .. 19 | nix fmt -- --no-cache 20 | -------------------------------------------------------------------------------- /tests/flake-module.nix: -------------------------------------------------------------------------------- 1 | { withSystem, inputs, ... }: 2 | 3 | { 4 | flake.checks.x86_64-linux = withSystem "x86_64-linux" ({ pkgs, system, inputs', config, ... }: 5 | let 6 | system-to-install = pkgs.nixos [ 7 | ./modules/system-to-install.nix 8 | inputs.disko.nixosModules.disko 9 | ]; 10 | testInputsUnstable = { 11 | inherit pkgs; 12 | inherit (inputs.disko.nixosModules) disko; 13 | nixos-anywhere = config.packages.nixos-anywhere; 14 | kexec-installer = "${inputs'.nixos-images.packages.kexec-installer-nixos-unstable-noninteractive}/nixos-kexec-installer-noninteractive-${system}.tar.gz"; 15 | inherit system-to-install; 16 | }; 17 | testInputsStable = testInputsUnstable // { 18 | kexec-installer = "${inputs'.nixos-images.packages.kexec-installer-nixos-stable-noninteractive}/nixos-kexec-installer-noninteractive-${system}.tar.gz"; 19 | }; 20 | linuxTestInputs = testInputsUnstable // { 21 | nix-vm-test = inputs.nix-vm-test; 22 | }; 23 | in 24 | { 25 | from-nixos = import ./from-nixos.nix testInputsUnstable; 26 | from-nixos-stable = import ./from-nixos.nix testInputsStable; 27 | from-nixos-with-sudo = import ./from-nixos-with-sudo.nix testInputsUnstable; 28 | from-nixos-with-sudo-stable = import ./from-nixos-with-sudo.nix testInputsStable; 29 | from-nixos-with-generated-config = import ./from-nixos-generate-config.nix testInputsUnstable; 30 | from-nixos-build-on-remote = import ./from-nixos-build-on-remote.nix testInputsUnstable; 31 | from-nixos-separated-phases = import ./from-nixos-separated-phases.nix testInputsUnstable; 32 | ubuntu-kexec-test = import ./linux-kexec-test.nix (linuxTestInputs // { 33 | distribution = "ubuntu"; 34 | version = "24_04"; 35 | }); 36 | fedora-kexec-test = import ./linux-kexec-test.nix (linuxTestInputs // { 37 | distribution = "fedora"; 38 | version = "40"; 39 | }); 40 | debian-kexec-test = import ./linux-kexec-test.nix (linuxTestInputs // { 41 | distribution = "debian"; 42 | version = "12"; 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /tests/from-nixos-build-on-remote.nix: -------------------------------------------------------------------------------- 1 | (import ./lib/test-base.nix) ( 2 | { pkgs, ... }: 3 | { 4 | name = "from-nixos-build-on-remote"; 5 | nodes = { 6 | installer = ./modules/installer.nix; 7 | installed = { 8 | services.openssh.enable = true; 9 | virtualisation.memorySize = 1500; 10 | 11 | users.users.root.openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; 12 | }; 13 | }; 14 | testScript = '' 15 | def create_test_machine( 16 | oldmachine=None, **kwargs 17 | ): # taken from 18 | start_command = [ 19 | "${pkgs.qemu_test}/bin/qemu-kvm", 20 | "-cpu", 21 | "max", 22 | "-m", 23 | "1024", 24 | "-virtfs", 25 | "local,path=/nix/store,security_model=none,mount_tag=nix-store", 26 | "-drive", 27 | f"file={oldmachine.state_dir}/installed.qcow2,id=drive1,if=none,index=1,werror=report", 28 | "-device", 29 | "virtio-blk-pci,drive=drive1", 30 | ] 31 | machine = create_machine(start_command=" ".join(start_command), **kwargs) 32 | driver.machines.append(machine) 33 | return machine 34 | start_all() 35 | 36 | installer.succeed(""" 37 | nixos-anywhere \ 38 | -i /root/.ssh/install_key \ 39 | --debug \ 40 | --build-on-remote \ 41 | --kexec /etc/nixos-anywhere/kexec-installer \ 42 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 43 | root@installed >&2 44 | """) 45 | try: 46 | installed.shutdown() 47 | except BrokenPipeError: 48 | # qemu has already exited 49 | pass 50 | new_machine = create_test_machine(oldmachine=installed, name="after_install") 51 | new_machine.start() 52 | hostname = new_machine.succeed("hostname").strip() 53 | assert "nixos-anywhere" == hostname, f"'nixos-anywhere' != '{hostname}'" 54 | ''; 55 | } 56 | ) 57 | -------------------------------------------------------------------------------- /tests/from-nixos-generate-config.nix: -------------------------------------------------------------------------------- 1 | (import ./lib/test-base.nix) { 2 | name = "from-nixos-generate-config"; 3 | nodes = { 4 | installer = { pkgs, ... }: { 5 | imports = [ 6 | ./modules/installer.nix 7 | ]; 8 | environment.systemPackages = [ pkgs.jq ]; 9 | }; 10 | installed = { 11 | services.openssh.enable = true; 12 | virtualisation.memorySize = 1500; 13 | 14 | users.users.root.openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; 15 | }; 16 | }; 17 | testScript = '' 18 | start_all() 19 | installer.fail("test -f /tmp/hw/config.nix") 20 | installer.succeed("echo super-secret > /tmp/disk-1.key") 21 | output = installer.succeed(""" 22 | nixos-anywhere \ 23 | -i /root/.ssh/install_key \ 24 | --debug \ 25 | --kexec /etc/nixos-anywhere/kexec-installer \ 26 | --disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \ 27 | --disk-encryption-keys /tmp/disk-2.key <(echo another-secret) \ 28 | --phases kexec,disko \ 29 | --generate-hardware-config nixos-generate-config /tmp/hw/config.nix \ 30 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 31 | root@installed >&2 32 | echo "disk-1.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 33 | root@installed cat /tmp/disk-1.key)'" 34 | echo "disk-2.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 35 | root@installed cat /tmp/disk-2.key)'" 36 | """) 37 | 38 | installer.succeed("cat /tmp/hw/config.nix >&2") 39 | installer.succeed("nix-instantiate --parse /tmp/hw/config.nix") 40 | 41 | assert "disk-1.key: 'super-secret'" in output, f"output does not contain expected values: {output}" 42 | assert "disk-2.key: 'another-secret'" in output, f"output does not contain expected values: {output}" 43 | 44 | installer.fail("test -f /test/hw/config.json") 45 | 46 | output = installer.succeed(""" 47 | nixos-anywhere \ 48 | -i /root/.ssh/install_key \ 49 | --debug \ 50 | --kexec /etc/nixos-anywhere/kexec-installer \ 51 | --disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \ 52 | --disk-encryption-keys /tmp/disk-2.key <(echo another-secret) \ 53 | --phases kexec,disko \ 54 | --generate-hardware-config nixos-facter /tmp/hw/config.json \ 55 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 56 | installed >&2 57 | echo "disk-1.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 58 | root@installed cat /tmp/disk-1.key)'" 59 | echo "disk-2.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 60 | root@installed cat /tmp/disk-2.key)'" 61 | """) 62 | 63 | installer.succeed("cat /tmp/hw/config.json >&2") 64 | installer.succeed("jq < /tmp/hw/config.json") 65 | 66 | assert "disk-1.key: 'super-secret'" in output, f"output does not contain expected values: {output}" 67 | assert "disk-2.key: 'another-secret'" in output, f"output does not contain expected values: {output}" 68 | ''; 69 | } 70 | -------------------------------------------------------------------------------- /tests/from-nixos-separated-phases.nix: -------------------------------------------------------------------------------- 1 | (import ./lib/test-base.nix) { 2 | name = "from-nixos-separated-phases"; 3 | nodes = { 4 | installer = ./modules/installer.nix; 5 | installed = { 6 | services.openssh.enable = true; 7 | virtualisation.memorySize = 1500; 8 | 9 | users.users.nixos = { 10 | isNormalUser = true; 11 | openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; 12 | extraGroups = [ "wheel" ]; 13 | }; 14 | security.sudo.enable = true; 15 | security.sudo.wheelNeedsPassword = false; 16 | }; 17 | }; 18 | testScript = '' 19 | start_all() 20 | 21 | with subtest("Kexec Phase"): 22 | installer.succeed(""" 23 | nixos-anywhere \ 24 | -i /root/.ssh/install_key \ 25 | --debug \ 26 | --kexec /etc/nixos-anywhere/kexec-installer \ 27 | --phases kexec \ 28 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 29 | nixos@installed >&2 30 | """) 31 | 32 | with subtest("Disko Phase"): 33 | output = installer.succeed(""" 34 | nixos-anywhere \ 35 | -i /root/.ssh/install_key \ 36 | --debug \ 37 | --phases disko \ 38 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 39 | installed >&2 40 | """) 41 | 42 | with subtest("Install Phase"): 43 | installer.succeed(""" 44 | nixos-anywhere \ 45 | -i /root/.ssh/install_key \ 46 | --debug \ 47 | --phases install \ 48 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 49 | root@installed >&2 50 | """) 51 | ''; 52 | } 53 | -------------------------------------------------------------------------------- /tests/from-nixos-with-sudo.nix: -------------------------------------------------------------------------------- 1 | (import ./lib/test-base.nix) { 2 | name = "from-nixos-with-sudo"; 3 | nodes = { 4 | installer = ./modules/installer.nix; 5 | installed = { 6 | services.openssh.enable = true; 7 | virtualisation.memorySize = 1500; 8 | 9 | users.users.nixos = { 10 | isNormalUser = true; 11 | openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; 12 | extraGroups = [ "wheel" ]; 13 | }; 14 | security.sudo.enable = true; 15 | security.sudo.wheelNeedsPassword = false; 16 | }; 17 | }; 18 | testScript = '' 19 | start_all() 20 | installer.succeed("echo super-secret > /tmp/disk-1.key") 21 | output = installer.succeed(""" 22 | nixos-anywhere \ 23 | -i /root/.ssh/install_key \ 24 | --debug \ 25 | --kexec /etc/nixos-anywhere/kexec-installer \ 26 | --phases kexec,disko \ 27 | --disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \ 28 | --disk-encryption-keys /tmp/disk-2.key <(echo another-secret) \ 29 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 30 | nixos@installed >&2 31 | echo "disk-1.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 32 | root@installed cat /tmp/disk-1.key)'" 33 | echo "disk-2.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 34 | root@installed cat /tmp/disk-2.key)'" 35 | """) 36 | 37 | assert "disk-1.key: 'super-secret'" in output, f"output does not contain expected values: {output}" 38 | assert "disk-2.key: 'another-secret'" in output, f"output does not contain expected values: {output}" 39 | ''; 40 | } 41 | -------------------------------------------------------------------------------- /tests/from-nixos.nix: -------------------------------------------------------------------------------- 1 | (import ./lib/test-base.nix) ( 2 | { pkgs, ... }: 3 | { 4 | name = "from-nixos"; 5 | nodes = { 6 | installer = ./modules/installer.nix; 7 | installed = { 8 | services.openssh.enable = true; 9 | virtualisation.memorySize = 1500; 10 | 11 | users.users.root.openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; 12 | }; 13 | }; 14 | testScript = '' 15 | def create_test_machine( 16 | oldmachine=None, **kwargs 17 | ): # taken from 18 | start_command = [ 19 | "${pkgs.qemu_test}/bin/qemu-kvm", 20 | "-cpu", 21 | "max", 22 | "-m", 23 | "1024", 24 | "-virtfs", 25 | "local,path=/nix/store,security_model=none,mount_tag=nix-store", 26 | "-drive", 27 | f"file={oldmachine.state_dir}/installed.qcow2,id=drive1,if=none,index=1,werror=report", 28 | "-device", 29 | "virtio-blk-pci,drive=drive1", 30 | ] 31 | machine = create_machine(start_command=" ".join(start_command), **kwargs) 32 | driver.machines.append(machine) 33 | return machine 34 | start_all() 35 | installer.succeed("mkdir -p /tmp/extra-files/var/lib/secrets") 36 | installer.succeed("echo value > /tmp/extra-files/var/lib/secrets/key") 37 | installer.succeed("mkdir -p /tmp/extra-files/home/user/.ssh") 38 | installer.succeed("echo secretkey > /tmp/extra-files/home/user/.ssh/id_ed25519") 39 | installer.succeed("echo publickey > /tmp/extra-files/home/user/.ssh/id_ed25519.pub") 40 | installer.succeed("chmod 600 /tmp/extra-files/home/user/.ssh/id_ed25519") 41 | ssh_key_path = "/etc/ssh/ssh_host_ed25519_key.pub" 42 | ssh_key_output = installer.wait_until_succeeds(f""" 43 | ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ 44 | root@installed cat {ssh_key_path} 45 | """) 46 | installer.succeed(""" 47 | nixos-anywhere \ 48 | -i /root/.ssh/install_key \ 49 | --debug \ 50 | --kexec /etc/nixos-anywhere/kexec-installer \ 51 | --extra-files /tmp/extra-files \ 52 | --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ 53 | --chown /home/user 1000:100 \ 54 | --copy-host-keys \ 55 | root@installed >&2 56 | """) 57 | try: 58 | installed.shutdown() 59 | except BrokenPipeError: 60 | # qemu has already exited 61 | pass 62 | new_machine = create_test_machine(oldmachine=installed, name="after_install") 63 | new_machine.start() 64 | hostname = new_machine.succeed("hostname").strip() 65 | assert "nixos-anywhere" == hostname, f"'nixos-anywhere' != '{hostname}'" 66 | content = new_machine.succeed("cat /var/lib/secrets/key").strip() 67 | assert "value" == content, f"secret does not have expected value: {content}" 68 | ssh_key_content = new_machine.succeed(f"cat {ssh_key_path}").strip() 69 | assert ssh_key_content in ssh_key_output, "SSH host identity changed" 70 | priv_key_perms = new_machine.succeed("stat -c %a /home/user/.ssh/id_ed25519").strip() 71 | assert priv_key_perms == "600", f"unexpected permissions for private key: {priv_key_perms}" 72 | user_dir_ownership = new_machine.succeed("stat -c %u:%g /home/user").strip() 73 | assert user_dir_ownership == "1000:100", f"unexpected user home dir permissions: {user_dir_ownership}" 74 | ''; 75 | } 76 | ) 77 | -------------------------------------------------------------------------------- /tests/lib/test-base.nix: -------------------------------------------------------------------------------- 1 | test: 2 | { pkgs 3 | , nixos-anywhere 4 | , disko 5 | , kexec-installer 6 | , system-to-install 7 | , ... 8 | }: 9 | let 10 | inherit (pkgs) lib; 11 | nixos-lib = import (pkgs.path + "/nixos/lib") { }; 12 | in 13 | (nixos-lib.runTest { 14 | hostPkgs = pkgs; 15 | # speed-up evaluation 16 | defaults.documentation.enable = lib.mkDefault false; 17 | # to accept external dependencies such as disko 18 | node.specialArgs.inputs = { inherit nixos-anywhere disko kexec-installer system-to-install; }; 19 | imports = [ test ]; 20 | }).config.result 21 | -------------------------------------------------------------------------------- /tests/linux-kexec-test.nix: -------------------------------------------------------------------------------- 1 | { pkgs, nixos-anywhere, kexec-installer, nix-vm-test, system-to-install, distribution, version, ... }: 2 | 3 | (nix-vm-test.lib.${pkgs.system}.${distribution}.${version} { 4 | sharedDirs = { }; 5 | 6 | # Configure VM with 2GB memory 7 | machineConfigModule = { ... }: { 8 | nodes.vm.virtualisation.memorySize = 1500; 9 | }; 10 | 11 | # The test script 12 | testScript = '' 13 | # Python imports 14 | import subprocess 15 | import tempfile 16 | import shutil 17 | import os 18 | 19 | # Wait for the system to be fully booted 20 | vm.wait_for_unit("multi-user.target") 21 | 22 | # Detect SSH service name (ssh on Ubuntu/Debian, sshd on Fedora/RHEL) 23 | ssh_service = "sshd" if "${distribution}" in ["fedora", "centos", "rhel"] else "ssh" 24 | 25 | # Unmask SSH service (which is masked by default in the test VM) 26 | vm.succeed(f"systemctl unmask {ssh_service}.service || true") 27 | vm.succeed(f"systemctl unmask {ssh_service}.socket || true") 28 | 29 | # Generate SSH host keys (required for SSH to start) 30 | vm.succeed("ssh-keygen -A") 31 | 32 | # Setup SSH with the existing keys 33 | vm.succeed("mkdir -p /root/.ssh") 34 | vm.succeed( 35 | "echo '${builtins.replaceStrings ["\n"] [""] (builtins.readFile ./modules/ssh-keys/ssh.pub)}' > /root/.ssh/authorized_keys" 36 | ) 37 | vm.succeed("chmod 644 /root/.ssh/authorized_keys") 38 | 39 | # Setup SSH for connection from host 40 | vm.succeed( 41 | "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config" 42 | ) 43 | 44 | # Start SSH service 45 | vm.succeed(f"systemctl start {ssh_service}") 46 | 47 | # Forward SSH port using vm.forward_port method 48 | ssh_port = 2222 49 | vm.forward_port(host_port=ssh_port, guest_port=22) 50 | 51 | # Use temporary file for SSH key with automatic cleanup 52 | with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix='_ssh_key') as temp_key: 53 | temp_key_path = temp_key.name 54 | 55 | # Copy SSH private key to temp file with correct permissions 56 | shutil.copy2("${./modules/ssh-keys/ssh}", temp_key_path) 57 | os.chmod(temp_key_path, 0o600) 58 | 59 | nixos_anywhere_cmd = [ 60 | "${nixos-anywhere}/bin/nixos-anywhere", 61 | "-i", temp_key_path, 62 | "--ssh-port", str(ssh_port), 63 | "--post-kexec-ssh-port", "2222", 64 | "--phases", "kexec", 65 | "--kexec", "${kexec-installer}", 66 | "--store-paths", "${system-to-install.config.system.build.diskoScriptNoDeps}", 67 | "${system-to-install.config.system.build.toplevel}", 68 | "--debug", 69 | "root@localhost" 70 | ] 71 | 72 | result = subprocess.run(nixos_anywhere_cmd, check=False) 73 | 74 | if result.returncode != 0: 75 | print(f"nixos-anywhere failed with exit code {result.returncode}") 76 | vm.succeed("dmesg | tail -n 50") 77 | vm.succeed("journalctl -n 50") 78 | raise Exception(f"nixos-anywhere command failed with exit code {result.returncode}") 79 | 80 | # Test SSH connection to verify we're in NixOS kexec environment 81 | check_cmd = [ 82 | "${pkgs.openssh}/bin/ssh", "-v", 83 | "-i", temp_key_path, 84 | "-p", "2222", 85 | "-o", "StrictHostKeyChecking=no", 86 | "-o", "UserKnownHostsFile=/dev/null", 87 | "root@localhost", 88 | "cat /etc/os-release" 89 | ] 90 | 91 | test_result = subprocess.run(check_cmd, check=True, stdout=subprocess.PIPE, text=True) 92 | assert "nixos" in test_result.stdout.lower(), f"Expected NixOS environment but got: {test_result.stdout}" 93 | 94 | # After kexec we no longer have the machine driver, 95 | # so we need to let the VM crash because the test driver backdoor gets confused by the terminal output. 96 | vm.crash() 97 | ''; 98 | }).sandboxed 99 | -------------------------------------------------------------------------------- /tests/modules/installer.nix: -------------------------------------------------------------------------------- 1 | { pkgs, inputs, ... }: 2 | { 3 | system.activationScripts.rsa-key = '' 4 | ${pkgs.coreutils}/bin/install -D -m600 ${./ssh-keys/ssh} /root/.ssh/install_key 5 | ''; 6 | 7 | environment.systemPackages = [ inputs.nixos-anywhere ]; 8 | 9 | environment.etc = { 10 | "nixos-anywhere/disko".source = inputs.system-to-install.config.system.build.diskoScriptNoDeps; 11 | "nixos-anywhere/system-to-install".source = inputs.system-to-install.config.system.build.toplevel; 12 | "nixos-anywhere/kexec-installer".source = inputs.kexec-installer; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /tests/modules/ssh-keys/ssh: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACDM4kPg4V7DMYceuA8VIUdBvw30gM9qagERA8v1VGBBBQAAAJDpULAq6VCw 4 | KgAAAAtzc2gtZWQyNTUxOQAAACDM4kPg4V7DMYceuA8VIUdBvw30gM9qagERA8v1VGBBBQ 5 | AAAECpjfl5WMMIDvEyZJTeXzRNFzpDpj4fqdIXHZauKAlE5MziQ+DhXsMxhx64DxUhR0G/ 6 | DfSAz2pqAREDy/VUYEEFAAAACWxhc3NAbW9ycwECAwQ= 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /tests/modules/ssh-keys/ssh.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMziQ+DhXsMxhx64DxUhR0G/DfSAz2pqAREDy/VUYEEF 2 | -------------------------------------------------------------------------------- /tests/modules/system-to-install.nix: -------------------------------------------------------------------------------- 1 | { modulesPath, self, lib, ... }: { 2 | imports = [ 3 | (modulesPath + "/testing/test-instrumentation.nix") 4 | (modulesPath + "/profiles/qemu-guest.nix") 5 | (modulesPath + "/profiles/minimal.nix") 6 | ]; 7 | networking.hostName = lib.mkDefault "nixos-anywhere"; 8 | documentation.enable = false; 9 | hardware.enableAllFirmware = false; 10 | networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs 11 | boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms 12 | disko.devices = { 13 | disk = { 14 | vda = { 15 | device = "/dev/vda"; 16 | type = "disk"; 17 | content = { 18 | type = "gpt"; 19 | partitions = { 20 | boot = { 21 | size = "1M"; 22 | type = "EF02"; 23 | }; 24 | ESP = { 25 | size = "100M"; 26 | type = "EF00"; 27 | content = { 28 | type = "filesystem"; 29 | format = "vfat"; 30 | mountpoint = "/boot"; 31 | }; 32 | }; 33 | root = { 34 | size = "100%"; 35 | content = { 36 | type = "filesystem"; 37 | format = "ext4"; 38 | mountpoint = "/"; 39 | }; 40 | }; 41 | }; 42 | }; 43 | }; 44 | }; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /treefmt/flake-module.nix: -------------------------------------------------------------------------------- 1 | { inputs, ... }: { 2 | imports = [ 3 | inputs.treefmt-nix.flakeModule 4 | ]; 5 | perSystem = { config, pkgs, ... }: { 6 | treefmt = { 7 | projectRootFile = "flake.nix"; 8 | programs.mdsh.enable = true; 9 | programs.nixpkgs-fmt.enable = true; 10 | programs.shellcheck.enable = true; 11 | programs.shfmt.enable = true; 12 | programs.terraform.enable = true; 13 | programs.deno.enable = !pkgs.deno.meta.broken; 14 | settings.formatter.shellcheck.options = [ "-s" "bash" ]; 15 | }; 16 | formatter = config.treefmt.build.wrapper; 17 | }; 18 | } 19 | --------------------------------------------------------------------------------