Merge 8620c0b5cc into haskell-updates
This commit is contained in:
@@ -67,7 +67,7 @@ jobs:
|
||||
# Sandbox is disabled on MacOS by default.
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1
|
||||
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1
|
||||
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
- uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1
|
||||
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
|
||||
- uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1
|
||||
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
continue-on-error: true
|
||||
with:
|
||||
# The nixpkgs-gha cache should not be trusted or used outside of Nixpkgs and its forks' CI.
|
||||
|
||||
@@ -150,6 +150,8 @@
|
||||
|
||||
- `python3Packages.gradio` has been updated to version 6. See upstream's migration guide at https://www.gradio.app/main/guides/gradio-6-migration-guide.
|
||||
|
||||
- `python3Packages.pikepdf` no longer builds with mupdf support by default, which may be nice in Jupyter and iPython. Build with `withMupdf = true` if this is required.
|
||||
|
||||
- `vicinae` has been updated to v0.20. This includes, among several other breaking changes, a complete overhaul of the configuration system. For update instructions, see the [upstream configuration documentation](https://docs.vicinae.com/config#migration-from-v0-16-x-to-v0-17-x).
|
||||
|
||||
- The `man-pages` package's outputs have been split. The manual pages are installed into the `man` output, which is installed by default. Binaries (including `diffman-git`, `mansect`, `pdfman`, and `sortman`) are installed into the `out` output, which is not installed by default.
|
||||
@@ -229,6 +231,8 @@
|
||||
If your SQLite database is corrupted, the migration might fail and require [manual intervention](https://github.com/louislam/uptime-kuma/issues/5281).
|
||||
See the [migration guide](https://github.com/louislam/uptime-kuma/wiki/Migration-From-v1-To-v2) for more information.
|
||||
|
||||
- The `libcxxhardeningextensive` hardening flag has been **disabled** by default. Enabling it by default in 25.11 was unintentional and may have had a negative effect on performance in some cases. `libcxxhardeningfast` remains enabled by default.
|
||||
|
||||
- Switch inhibitors were introduced, which add a pre-switch check that compares a list of strings between the previous and the new generation, and refuses to switch into the new generation when there is a difference between the two lists. This allows to avoid switching into a system when for instance the systemd version changed by adding `config.systemd.package.version` to the switch inhibitors for your system. You can still forcefully switch into any generation by setting `NIXOS_NO_CHECK=1`.
|
||||
|
||||
- GNU Taler has been updated to version 1.3.
|
||||
@@ -239,7 +243,7 @@
|
||||
|
||||
- `fetchPnpmDeps` and `pnpmConfigHook` were added as top-level attributes, replacing the now deprecated `pnpm.fetchDeps` and `pnpm.configHook` attributes.
|
||||
|
||||
- `buildNpmPackage` now supports `npmDepsCacheVersion`. Set to `2` to enable packument caching, which fixes builds for projects using npm workspaces.
|
||||
- `buildNpmPackage` now supports `npmDepsFetcherVersion` (and `fetchNpmDeps` now supports `fetcherVersion`). Set to `2` to enable packument caching, which fixes builds for projects using npm workspaces.
|
||||
|
||||
- Added `dell-bios-fan-control` package and service.
|
||||
|
||||
|
||||
@@ -1631,13 +1631,11 @@ Adds the `-fzero-call-used-regs=used-gpr` compiler option. This causes the gener
|
||||
|
||||
This flag adds the `-fstack-clash-protection` compiler option, which causes growth of a program's stack to access each successive page in order. This should force the guard page to be accessed and cause an attempt to "jump over" this guard page to crash.
|
||||
|
||||
### Hardening flags disabled by default {#sec-hardening-flags-disabled-by-default}
|
||||
#### `libcxxhardeningfast` {#libcxxhardeningfast}
|
||||
|
||||
The following flags are disabled by default and should be enabled with `hardeningEnable` for packages that take untrusted input like network services.
|
||||
Adds the `-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST` compiler flag. This flag only has an effect on libc++ targets, and when defined, enables a set of assertions that prevent undefined behavior caused by violating preconditions of the standard library. libc++ provides several hardening modes, and this "fast" mode contains a set of security-critical checks that can be done with relatively little overhead in constant time.
|
||||
|
||||
#### `nostrictaliasing` {#nostrictaliasing}
|
||||
|
||||
This flag adds the `-fno-strict-aliasing` compiler option, which prevents the compiler from assuming code has been written strictly following the standard in regards to pointer aliasing and therefore performing optimizations that may be unsafe for code that has not followed these rules.
|
||||
Disabling `libcxxhardeningfast` implies disablement of checks from `libcxxhardeningextensive`.
|
||||
|
||||
#### `strictflexarrays1` {#strictflexarrays1}
|
||||
|
||||
@@ -1647,6 +1645,14 @@ Enabling this flag on packages that still use length declarations of flexible ar
|
||||
|
||||
Disabling `strictflexarrays1` implies disablement of `strictflexarrays3`.
|
||||
|
||||
### Hardening flags disabled by default {#sec-hardening-flags-disabled-by-default}
|
||||
|
||||
The following flags are disabled by default and should be enabled with `hardeningEnable` for packages that take untrusted input like network services.
|
||||
|
||||
#### `nostrictaliasing` {#nostrictaliasing}
|
||||
|
||||
This flag adds the `-fno-strict-aliasing` compiler option, which prevents the compiler from assuming code has been written strictly following the standard in regards to pointer aliasing and therefore performing optimizations that may be unsafe for code that has not followed these rules.
|
||||
|
||||
#### `strictflexarrays3` {#strictflexarrays3}
|
||||
|
||||
This flag adds the `-fstrict-flex-arrays=3` compiler option, which reduces the cases the compiler treats as "flexible arrays" to only those declared with length as (the correct) `[]`. This increases the coverage of fortify checks, because such arrays declared as the trailing element of a structure can normally not have their intended length determined by the compiler.
|
||||
@@ -1683,12 +1689,6 @@ Adds the `-D_GLIBCXX_ASSERTIONS` compiler flag. This flag only has an effect on
|
||||
|
||||
These checks may have an impact on performance in some cases.
|
||||
|
||||
#### `libcxxhardeningfast` {#libcxxhardeningfast}
|
||||
|
||||
Adds the `-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST` compiler flag. This flag only has an effect on libc++ targets, and when defined, enables a set of assertions that prevent undefined behavior caused by violating preconditions of the standard library. libc++ provides several hardening modes, and this "fast" mode contains a set of security-critical checks that can be done with relatively little overhead in constant time.
|
||||
|
||||
Disabling `libcxxhardeningfast` implies disablement of checks from `libcxxhardeningextensive`.
|
||||
|
||||
#### `libcxxhardeningextensive` {#libcxxhardeningextensive}
|
||||
|
||||
Adds the `-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE` compiler flag. This flag only has an effect on libc++ targets, and when defined, enables a set of assertions that prevent undefined behavior caused by violating preconditions of the standard library. libc++ provides several hardening modes, and this "extensive" mode adds checks for undefined behavior that incur relatively little overhead but aren’t security-critical. The additional rigour impacts performance more than fast mode: benchmarking is recommended to determine if it is acceptable for a particular application.
|
||||
|
||||
+2
-1
@@ -434,8 +434,9 @@ rec {
|
||||
getLicenseFromSpdxId =
|
||||
licstr:
|
||||
getLicenseFromSpdxIdOr licstr (
|
||||
lib.warn "getLicenseFromSpdxId: No license matches the given SPDX ID: ${licstr}" {
|
||||
lib.warn "getLicenseFromSpdxId: No license with the given SPDX ID found: ${licstr}" {
|
||||
shortName = licstr;
|
||||
spdxId = licstr;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -13697,6 +13697,13 @@
|
||||
github = "karaolidis";
|
||||
githubId = 46189100;
|
||||
};
|
||||
karlbeecken = {
|
||||
name = "Karl Beecken";
|
||||
email = "karl@beecken.berlin";
|
||||
github = "karlbeecken";
|
||||
githubId = 32744028;
|
||||
keys = [ { fingerprint = "7577 13A4 9609 0C2F 51C4 018C B5C8 89A2 F195 28F6"; } ];
|
||||
};
|
||||
KarlJoad = {
|
||||
email = "karl@hallsby.com";
|
||||
github = "KarlJoad";
|
||||
@@ -14176,6 +14183,12 @@
|
||||
github = "klDen";
|
||||
githubId = 5478260;
|
||||
};
|
||||
klea = {
|
||||
email = "klea+nixos@iwnp.org";
|
||||
github = "notklea";
|
||||
githubId = 231780064;
|
||||
name = "Klea";
|
||||
};
|
||||
klntsky = {
|
||||
email = "klntsky@gmail.com";
|
||||
name = "Vladimir Kalnitsky";
|
||||
@@ -15050,6 +15063,12 @@
|
||||
githubId = 31388299;
|
||||
name = "Leonardo Eugênio";
|
||||
};
|
||||
lenianiva = {
|
||||
name = "Leni Aniva";
|
||||
email = "aniva@stanford.edu";
|
||||
github = "lenianiva";
|
||||
githubId = 107011294;
|
||||
};
|
||||
lenivaya = {
|
||||
name = "Danylo Osipchuk";
|
||||
email = "danylo.osipchuk@proton.me";
|
||||
|
||||
@@ -10,6 +10,17 @@ $ ./result/bin/nixos-test-driver
|
||||
>>>
|
||||
```
|
||||
|
||||
::: {.note}
|
||||
Tests using `systemd-nspawn` container machines require root privileges to run interactively,
|
||||
since the driver calls `systemd-nspawn` directly to start the containers:
|
||||
|
||||
```
|
||||
$ sudo ./result/bin/nixos-test-driver
|
||||
[...]
|
||||
>>>
|
||||
```
|
||||
:::
|
||||
|
||||
::: {.note}
|
||||
By executing the test driver in this way,
|
||||
the VMs executed may gain network & Internet access via their backdoor control interface,
|
||||
@@ -30,7 +41,7 @@ back into the test driver command line upon its completion. This allows
|
||||
you to inspect the state of the VMs after the test (e.g. to debug the
|
||||
test script).
|
||||
|
||||
## Shell access in interactive mode {#sec-nixos-test-shell-access}
|
||||
## Shell access to VMs in interactive mode {#sec-nixos-test-shell-access}
|
||||
|
||||
The function `<yourmachine>.shell_interact()` grants access to a shell running
|
||||
inside a virtual machine. To use it, replace `<yourmachine>` with the name of a
|
||||
@@ -63,7 +74,7 @@ using:
|
||||
Once the connection is established, you can enter commands in the socat terminal
|
||||
where socat is running.
|
||||
|
||||
## SSH Access for test machines {#sec-nixos-test-ssh-access}
|
||||
## SSH Access for test VMs {#sec-nixos-test-ssh-access}
|
||||
|
||||
An SSH-based backdoor to log into machines can be enabled with
|
||||
|
||||
@@ -149,10 +160,10 @@ must be configured to allow these connections.
|
||||
## Reuse VM state {#sec-nixos-test-reuse-vm-state}
|
||||
|
||||
You can re-use the VM states coming from a previous run by setting the
|
||||
`--keep-vm-state` flag.
|
||||
`--keep-machine-state` flag.
|
||||
|
||||
```ShellSession
|
||||
$ ./result/bin/nixos-test-driver --keep-vm-state
|
||||
$ ./result/bin/nixos-test-driver --keep-machine-state
|
||||
```
|
||||
|
||||
The machine state is stored in the `$TMPDIR/vm-state-machinename`
|
||||
|
||||
@@ -21,10 +21,44 @@ $ nix-store --read-log result
|
||||
|
||||
## System Requirements {#sec-running-nixos-tests-requirements}
|
||||
|
||||
NixOS tests require virtualization support.
|
||||
NixOS tests using QEMU virtual machine [`nodes`](#test-opt-nodes) require virtualization support.
|
||||
This means that the machine must have `kvm` in its [system features](https://nixos.org/manual/nix/stable/command-ref/conf-file.html?highlight=system-features#conf-system-features) list, or `apple-virt` in case of macOS.
|
||||
These features are autodetected locally, but `apple-virt` is only autodetected since Nix 2.19.0.
|
||||
|
||||
Features of **remote builders** must additionally be configured manually on the client, e.g. on NixOS with [`nix.buildMachines.*.supportedFeatures`](https://search.nixos.org/options?show=nix.buildMachines.*.supportedFeatures&sort=alpha_asc&query=nix.buildMachines) or through general [Nix configuration](https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds).
|
||||
|
||||
If you run the tests on a **macOS** machine, you also need a "remote" builder for Linux; possibly a VM. [nix-darwin](https://daiderd.com/nix-darwin/) users may enable [`nix.linux-builder.enable`](https://daiderd.com/nix-darwin/manual/index.html#opt-nix.linux-builder.enable) to launch such a VM.
|
||||
|
||||
NixOS tests using `systemd-nspawn` [`containers`](#test-opt-containers) require the Nix daemon to be
|
||||
configured with the following settings:
|
||||
|
||||
```nix
|
||||
{
|
||||
nix.settings = {
|
||||
auto-allocate-uids = true;
|
||||
extra-system-features = [ "uid-range" ];
|
||||
experimental-features = [
|
||||
"auto-allocate-uids"
|
||||
"cgroups"
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
See the documentation of the settings
|
||||
[`auto-allocate-uids`](https://nix.dev/manual/nix/stable/command-ref/conf-file#conf-auto-allocate-uids),
|
||||
[`uid-range`](https://nix.dev/manual/nix/stable/command-ref/conf-file.html?highlight=uid-range#conf-system-features), and
|
||||
[`cgroups`](https://nix.dev/manual/nix/stable/development/experimental-features#xp-feature-cgroups)
|
||||
for more information.
|
||||
|
||||
If the test uses both `systemd-nspawn` [`containers`](#test-opt-containers) and QEMU virtual machine [`nodes`](#test-opt-nodes)
|
||||
and requires them share a common VLAN,
|
||||
`/dev/net` must be present in the sandbox.
|
||||
This allows them to be bridged over a TAP interface.
|
||||
To make this path available, set the following option:
|
||||
|
||||
```nix
|
||||
{
|
||||
nix.settings.sandbox-paths = [ "/dev/net" ];
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,15 +4,14 @@ A NixOS test is a module that has the following structure:
|
||||
|
||||
```nix
|
||||
{
|
||||
|
||||
# One or more machines:
|
||||
# QEMU virtual machines:
|
||||
nodes = {
|
||||
machine =
|
||||
vm1 =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
# ...
|
||||
};
|
||||
machine2 =
|
||||
vm2 =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
# ...
|
||||
@@ -20,6 +19,20 @@ A NixOS test is a module that has the following structure:
|
||||
# …
|
||||
};
|
||||
|
||||
# systemd-nspawn containers:
|
||||
containers = {
|
||||
container1 =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
# ...
|
||||
};
|
||||
container2 =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
# ...
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
Python code…
|
||||
'';
|
||||
@@ -27,12 +40,13 @@ A NixOS test is a module that has the following structure:
|
||||
```
|
||||
|
||||
We refer to the whole test above as a test module, whereas the values
|
||||
in [`nodes.<name>`](#test-opt-nodes) are NixOS modules themselves.
|
||||
in [`nodes.<name>`](#test-opt-nodes) and [`containers.<name>`](#test-opt-containers)
|
||||
are NixOS modules themselves.
|
||||
|
||||
The option [`testScript`](#test-opt-testScript) is a piece of Python code that executes the
|
||||
test (described below). During the test, it will start one or more
|
||||
virtual machines, the configuration of which is described by
|
||||
the option [`nodes`](#test-opt-nodes).
|
||||
test (described [below](#ssec-test-script)). During the test, it will start one or more
|
||||
virtual machines and/or `systemd-nspawn` containers, the configuration of which is described by
|
||||
the options [`nodes`](#test-opt-nodes) and [`containers`](#test-opt-containers), respectively.
|
||||
|
||||
An example of a single-node test is
|
||||
[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix).
|
||||
@@ -42,6 +56,12 @@ when switching between consoles, and so on. An interesting multi-node test is
|
||||
[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix).
|
||||
It uses two client nodes to test correct locking across server crashes.
|
||||
|
||||
A test can contain both virtual machines and containers.
|
||||
If configured to share a common VLAN,
|
||||
they can reach each other over the network.
|
||||
See [](https://github.com/applicative-systems/nixpkgs/blob/78100f9077ab50604ab9c9514e442bbc7ac7ca5b/nixos/tests/nixos-test-driver/containers.nix)
|
||||
for an example of this and [](#sec-running-nixos-tests-requirements) for the system requirements for this scenario.
|
||||
|
||||
## Calling a test {#sec-calling-nixos-tests}
|
||||
|
||||
Tests are invoked differently depending on whether the test is part of NixOS or lives in a different project.
|
||||
@@ -90,13 +110,54 @@ pkgs.testers.runNixOSTest {
|
||||
|
||||
`runNixOSTest` returns a derivation that runs the test.
|
||||
|
||||
## Configuring the nodes {#sec-nixos-test-nodes}
|
||||
## Test machines {#ssec-nixos-test-machines}
|
||||
|
||||
There are a few special NixOS options for test VMs:
|
||||
A NixOS test usually consists of one or more test machines. Each machine is either a
|
||||
QEMU virtual machine or a `systemd-nspawn` container.
|
||||
|
||||
`virtualisation.memorySize`
|
||||
QEMU virtual machines are defined in the
|
||||
[`nodes`](#test-opt-nodes) attribute set, whereas `systemd-nspawn` containers are defined in the
|
||||
[`containers`](#test-opt-containers) attribute set.
|
||||
|
||||
: The memory of the VM in MiB (1024×1024 bytes).
|
||||
To set NixOS options for all machines in the test, use the attribute
|
||||
[`defaults`](#test-opt-defaults). These options are applied to both virtual machines
|
||||
and containers. You can set separate defaults for virtual machines and containers
|
||||
using the attributes [`nodeDefaults`](#test-opt-nodeDefaults) and
|
||||
[`containerDefaults`](#test-opt-containerDefaults), respectively.
|
||||
|
||||
### Virtual machines vs. containers {#sec-nixos-test-vms-vs-containers}
|
||||
|
||||
QEMU virtual machines and `systemd-nspawn` containers offer different
|
||||
trade-offs which make them suitable for different use cases.
|
||||
|
||||
Some advantages of containers over virtual machines are:
|
||||
|
||||
- Containers share the kernel of the host system; they are
|
||||
significantly faster to start up than virtual machines.
|
||||
- Containers are more lightweight in terms of resource usage, which
|
||||
allows running more of them in parallel on a single host.
|
||||
- Containers can easily be run in virtualised environments, e.g., CI systems.
|
||||
- Containers allow direct bind-mounting of host device nodes, which enables
|
||||
testing of GPU code (CUDA), for example.
|
||||
|
||||
Some advantages of virtual machines over containers are:
|
||||
|
||||
- Virtual machines run a separate kernel, which allows testing kernel features
|
||||
(kernel modules, etc.).
|
||||
- Virtual machines support testing graphical applications on X11.
|
||||
- Virtual machines allow testing NixOS modules that use systemd's namespacing options (such as `ProtectSystem=` or `MountAPIVFS=`).
|
||||
- Virtual machines allow testing [`spcialisation`](options.html#opt-specialisation).
|
||||
(Switching to a specialisation requires the creation of SUID/SGID wrappers, which is disallowed in `systemd-nspawn` within the Nix sandbox.)
|
||||
- Virtual machines allow the execution of `setuid` binaries.
|
||||
|
||||
Refer to the sections on [QEMU virtual machines](#ssec-nixos-test-qemu-vms)
|
||||
and [systemd-nspawn containers](#ssec-nixos-test-nspawn-containers) below
|
||||
for more details on configuring each type of machine.
|
||||
|
||||
### Configuring test machines {#sec-nixos-test-machines-config}
|
||||
|
||||
The following special NixOS option can be used to configure
|
||||
machines in a NixOS test, whether they are virtual machines or containers:
|
||||
|
||||
`virtualisation.vlans`
|
||||
|
||||
@@ -104,6 +165,35 @@ There are a few special NixOS options for test VMs:
|
||||
[`nat.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix)
|
||||
for an example.
|
||||
|
||||
#### Configuring `systemd-nspawn` containers {#ssec-nixos-test-nspawn-containers}
|
||||
|
||||
Some options are specific to `systemd-nspawn` containers:
|
||||
|
||||
`virtualisation.systemd-nspawn.options`
|
||||
|
||||
: A list of additional command-line options to pass to
|
||||
`systemd-nspawn` when starting the container. For example, to
|
||||
bind mount a directory from the host into the container, you could
|
||||
use: `[ "--bind=/host/dir:/container/dir" ]`.
|
||||
|
||||
For more options, see the module
|
||||
[`nspawn-container`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/nspawn-container/default.nix).
|
||||
|
||||
Note that the paths used in `--bind` or `--bind-ro` options have to be accessible from within the Nix sandbox.
|
||||
Use the Nix option
|
||||
[`sandbox-paths`](https://nix.dev/manual/nix/stable/command-ref/conf-file#conf-sandbox-paths)
|
||||
and/or the module [`programs.nix-required-mounts`](#opt-programs.nix-required-mounts.enable) on the host
|
||||
to add additional paths to the sandbox.
|
||||
|
||||
#### Configuring QEMU virtual machines {#ssec-nixos-test-qemu-vms}
|
||||
|
||||
Some options are specific to QEMU virtual machines:
|
||||
|
||||
`virtualisation.memorySize`
|
||||
|
||||
: The memory of the VM in MiB (1024×1024 bytes).
|
||||
|
||||
|
||||
`virtualisation.writableStore`
|
||||
|
||||
: By default, the Nix store in the VM is not writable. If you enable
|
||||
@@ -114,13 +204,15 @@ There are a few special NixOS options for test VMs:
|
||||
For more options, see the module
|
||||
[`qemu-vm.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix).
|
||||
|
||||
## Writing the test script {#ssec-test-script}
|
||||
|
||||
The test script is a sequence of Python statements that perform various
|
||||
actions, such as starting VMs, executing commands in the VMs, and so on.
|
||||
Each virtual machine is represented as an object stored in the variable
|
||||
`name` if this is also the identifier of the machine in the declarative
|
||||
config. If you specified a node `nodes.machine`, the following example starts the
|
||||
machine, waits until it has finished booting, then executes a command
|
||||
and checks that the output is more-or-less correct:
|
||||
actions, such as starting machines, executing commands in them, and so on. For
|
||||
example, if you specified a virtual machine in `nodes.machine`, there will be
|
||||
a Python variable `machine` available in the test script that represents that
|
||||
virtual machine. The following example would start the machine, wait until it
|
||||
has finished booting, and then execute a command and check that the output is
|
||||
more-or-less correct:
|
||||
|
||||
```py
|
||||
machine.start()
|
||||
@@ -139,17 +231,20 @@ start_all()
|
||||
|
||||
Under the variable `t`, all assertions from [`unittest.TestCase`](https://docs.python.org/3/library/unittest.html) are available.
|
||||
|
||||
If the hostname of a node contains characters that can't be used in a
|
||||
If the hostname of a machine contains characters that can't be used in a
|
||||
Python variable name, those characters will be replaced with
|
||||
underscores in the variable name, so `nodes.machine-a` will be exposed
|
||||
to Python as `machine_a`.
|
||||
|
||||
## Machine objects {#ssec-machine-objects}
|
||||
### Methods available on machine objects {#ssec-machine-objects}
|
||||
|
||||
The following methods are available on machine objects:
|
||||
The following methods are available on machine objects (like `machine` in
|
||||
the examples above):
|
||||
|
||||
@PYTHON_MACHINE_METHODS@
|
||||
|
||||
### Testing user units {#ssec-testing-user-units}
|
||||
|
||||
To test user units declared by `systemd.user.services` the optional
|
||||
`user` argument can be used:
|
||||
|
||||
@@ -162,6 +257,84 @@ machine.wait_for_unit("xautolock.service", "x-session-user")
|
||||
This applies to `systemctl`, `get_unit_info`, `wait_for_unit`,
|
||||
`start_job` and `stop_job`.
|
||||
|
||||
### Failing tests early {#ssec-failing-tests-early}
|
||||
|
||||
To fail tests early when certain invariants are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following:
|
||||
|
||||
```py
|
||||
@polling_condition
|
||||
def foo_running():
|
||||
machine.succeed("pgrep -x foo")
|
||||
|
||||
|
||||
machine.succeed("foo --start")
|
||||
machine.wait_until_succeeds("pgrep -x foo")
|
||||
|
||||
with foo_running:
|
||||
... # Put `foo` through its paces
|
||||
```
|
||||
|
||||
`polling_condition` takes the following (optional) arguments:
|
||||
|
||||
`seconds_interval`
|
||||
|
||||
: specifies how often the condition should be polled:
|
||||
|
||||
```py
|
||||
@polling_condition(seconds_interval=10)
|
||||
def foo_running():
|
||||
machine.succeed("pgrep -x foo")
|
||||
```
|
||||
|
||||
`description`
|
||||
|
||||
: is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent:
|
||||
|
||||
```py
|
||||
@polling_condition
|
||||
def foo_running():
|
||||
"check that foo is running"
|
||||
machine.succeed("pgrep -x foo")
|
||||
```
|
||||
|
||||
```py
|
||||
@polling_condition(description="check that foo is running")
|
||||
def foo_running():
|
||||
machine.succeed("pgrep -x foo")
|
||||
```
|
||||
|
||||
### Adding Python packages to the test script {#ssec-python-packages-in-test-script}
|
||||
|
||||
When additional Python libraries are required in the test script, they can be
|
||||
added using the parameter `extraPythonPackages`. For example, you could add
|
||||
`numpy` like this:
|
||||
|
||||
```nix
|
||||
{
|
||||
extraPythonPackages = p: [ p.numpy ];
|
||||
|
||||
nodes = { };
|
||||
|
||||
# Type checking on extra packages doesn't work yet
|
||||
skipTypeCheck = true;
|
||||
|
||||
testScript = ''
|
||||
import numpy as np
|
||||
assert str(np.zeros(4)) == "[0. 0. 0. 0.]"
|
||||
'';
|
||||
}
|
||||
```
|
||||
|
||||
In that case, `numpy` is chosen from the generic `python3Packages`.
|
||||
|
||||
### Linting and type checking test scripts {#ssec-test-script-checks}
|
||||
|
||||
Test scripts are automatically linted with
|
||||
[Pyflakes](https://pypi.org/project/pyflakes/) and type-checked with
|
||||
[Mypy](https://mypy.readthedocs.io/en/stable/).
|
||||
If there are any linting or type checking errors, the test will
|
||||
fail to evaluate.
|
||||
|
||||
For faster dev cycles it's also possible to disable the code-linters
|
||||
(this shouldn't be committed though):
|
||||
|
||||
@@ -209,76 +382,6 @@ way:
|
||||
}
|
||||
```
|
||||
|
||||
## Failing tests early {#ssec-failing-tests-early}
|
||||
|
||||
To fail tests early when certain invariants are no longer met (instead of waiting for the build to time out), the decorator `polling_condition` is provided. For example, if we are testing a program `foo` that should not quit after being started, we might write the following:
|
||||
|
||||
```py
|
||||
@polling_condition
|
||||
def foo_running():
|
||||
machine.succeed("pgrep -x foo")
|
||||
|
||||
|
||||
machine.succeed("foo --start")
|
||||
machine.wait_until_succeeds("pgrep -x foo")
|
||||
|
||||
with foo_running:
|
||||
... # Put `foo` through its paces
|
||||
```
|
||||
|
||||
`polling_condition` takes the following (optional) arguments:
|
||||
|
||||
`seconds_interval`
|
||||
|
||||
: specifies how often the condition should be polled:
|
||||
|
||||
```py
|
||||
@polling_condition(seconds_interval=10)
|
||||
def foo_running():
|
||||
machine.succeed("pgrep -x foo")
|
||||
```
|
||||
|
||||
`description`
|
||||
|
||||
: is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent:
|
||||
|
||||
```py
|
||||
@polling_condition
|
||||
def foo_running():
|
||||
"check that foo is running"
|
||||
machine.succeed("pgrep -x foo")
|
||||
```
|
||||
|
||||
```py
|
||||
@polling_condition(description="check that foo is running")
|
||||
def foo_running():
|
||||
machine.succeed("pgrep -x foo")
|
||||
```
|
||||
|
||||
## Adding Python packages to the test script {#ssec-python-packages-in-test-script}
|
||||
|
||||
When additional Python libraries are required in the test script, they can be
|
||||
added using the parameter `extraPythonPackages`. For example, you could add
|
||||
`numpy` like this:
|
||||
|
||||
```nix
|
||||
{
|
||||
extraPythonPackages = p: [ p.numpy ];
|
||||
|
||||
nodes = { };
|
||||
|
||||
# Type checking on extra packages doesn't work yet
|
||||
skipTypeCheck = true;
|
||||
|
||||
testScript = ''
|
||||
import numpy as np
|
||||
assert str(np.zeros(4)) == "[0. 0. 0. 0.]"
|
||||
'';
|
||||
}
|
||||
```
|
||||
|
||||
In that case, `numpy` is chosen from the generic `python3Packages`.
|
||||
|
||||
## Overriding a test {#sec-override-nixos-test}
|
||||
|
||||
The NixOS test framework returns tests with multiple overriding methods.
|
||||
@@ -297,7 +400,7 @@ The NixOS test framework returns tests with multiple overriding methods.
|
||||
: Evaluates the test with additional NixOS modules and/or arguments.
|
||||
|
||||
`module`
|
||||
: A NixOS module to add to all the nodes in the test. Sets test option [`extraBaseModules`](#test-opt-extraBaseModules).
|
||||
: A NixOS module to add to all the machines in the test. Sets test option [`extraBaseModules`](#test-opt-extraBaseModules).
|
||||
|
||||
`specialArgs`
|
||||
: An attribute set of arguments to pass to all NixOS modules. These override the existing arguments, as well as any `_module.args.<name>` that the modules may define. Sets test option [`node.specialArgs`](#test-opt-node.specialArgs).
|
||||
@@ -345,7 +448,52 @@ list-id: test-options-list
|
||||
source: @NIXOS_TEST_OPTIONS_JSON@
|
||||
```
|
||||
|
||||
## Accessing VMs in the sandbox with SSH {#sec-test-sandbox-breakpoint}
|
||||
## Debugging test machines {#sec-test-sandbox-breakpoint}
|
||||
|
||||
You can set the [`enableDebugHook`](#test-opt-enableDebugHook) option to pause
|
||||
a test on the first failure and have it print instructions on how to enter the
|
||||
sandbox shell of the test. Suppose you have the following test module:
|
||||
|
||||
```nix
|
||||
{
|
||||
name = "foo";
|
||||
|
||||
nodes.machine = { };
|
||||
|
||||
enableDebugHook = true;
|
||||
sshBackdoor.enable = true;
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("false") # this will fail
|
||||
'';
|
||||
}
|
||||
```
|
||||
|
||||
The test will fail with an output like this:
|
||||
|
||||
```
|
||||
vm-test-run-foo> !!! Breakpoint reached, run 'sudo /nix/store/eeeee-attach/bin/attach <PID>'
|
||||
```
|
||||
|
||||
You can then enter the sandbox shell:
|
||||
|
||||
```
|
||||
$ sudo /nix/store/eeeee-attach/bin/attach <PID>
|
||||
bash#
|
||||
```
|
||||
|
||||
There, you can attach to a [`pdb`](https://docs.python.org/3/library/pdb.html) session
|
||||
to step through the Python test script:
|
||||
|
||||
```
|
||||
bash# telnet 127.0.0.1 4444
|
||||
pdb$
|
||||
```
|
||||
|
||||
Note that it is also possible to set breakpoints in the test script using `debug.breakpoint()`.
|
||||
|
||||
### SSH access to test VMs {#sec-test-vm-ssh-access}
|
||||
|
||||
::: {.note}
|
||||
For debugging with SSH access into the machines, it's recommended to try using
|
||||
@@ -356,24 +504,15 @@ This feature is mostly intended to debug flaky test failures that aren't
|
||||
reproducible elsewhere.
|
||||
:::
|
||||
|
||||
As explained in [](#sec-nixos-test-ssh-access), it's possible to configure an
|
||||
SSH backdoor based on AF_VSOCK. This can be used to SSH into a VM of a running
|
||||
build in a sandbox.
|
||||
|
||||
This can be done when something in the test fails, e.g.
|
||||
If you set the [`sshBackdoor.enable`](#test-opt-sshBackdoor.enable) option,
|
||||
QEMU virtual machines will open an SSH backdoor based on AF_VSOCK
|
||||
(see [](#sec-nixos-test-ssh-access)).
|
||||
Once you are in the sandbox shell, you can access the VMs (for example, `machine`)
|
||||
with SSH over vsock:
|
||||
|
||||
```nix
|
||||
{
|
||||
nodes.machine = { };
|
||||
|
||||
sshBackdoor.enable = true;
|
||||
enableDebugHook = true;
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("false") # this will fail
|
||||
'';
|
||||
}
|
||||
```
|
||||
bash# ssh -F ./ssh_config vsock/3
|
||||
```
|
||||
|
||||
For the AF_VSOCK feature to work, `/dev/vhost-vsock` is needed in the sandbox
|
||||
@@ -383,24 +522,24 @@ which can be done with e.g.
|
||||
nix-build -A nixosTests.foo --option sandbox-paths /dev/vhost-vsock
|
||||
```
|
||||
|
||||
This will halt the test execution on a test-failure and print instructions
|
||||
on how to enter the sandbox shell of the VM test. Inside, one can log into
|
||||
e.g. `machine` with
|
||||
|
||||
```
|
||||
ssh -F ./ssh_config vsock/3
|
||||
```
|
||||
|
||||
As described in [](#sec-nixos-test-ssh-access), the numbers for vsock start at
|
||||
`3` instead of `1`. So the first VM in the network (sorted alphabetically) can
|
||||
be accessed with `vsock/3`.
|
||||
|
||||
Alternatively, it's possible to explicitly set a breakpoint with
|
||||
`debug.breakpoint()`. This also has the benefit, that one can step through
|
||||
`testScript` with `pdb` like this:
|
||||
### SSH access to test containers {#sec-test-container-ssh-access}
|
||||
|
||||
If you set the [`sshBackdoor.enable`](#test-opt-sshBackdoor.enable) option,
|
||||
each `systemd-nspawn` container will open an SSH backdoor.
|
||||
Once the container starts,
|
||||
it will print instructions on how to log into the container via SSH.
|
||||
If the test fails,
|
||||
attach to the sandbox as described above,
|
||||
and then use the provided SSH command to log into the container.
|
||||
For example:
|
||||
|
||||
```
|
||||
$ sudo /nix/store/eeeee-attach <id>
|
||||
bash# telnet 127.0.0.1 4444
|
||||
pdb$ …
|
||||
$ sudo /nix/store/eeeee-attach <PID>
|
||||
bash# ssh -o User=root -o ProxyCommand="socat - UNIX-CLIENT:/run/systemd/nspawn/unix-export/machine/ssh" bash
|
||||
[root@machine:~]# hostname
|
||||
machine
|
||||
```
|
||||
|
||||
@@ -88,6 +88,9 @@
|
||||
"module-virtualisation-xen-introduction": [
|
||||
"index.html#module-virtualisation-xen-introduction"
|
||||
],
|
||||
"sec-nixos-test-vms-vs-containers": [
|
||||
"index.html#sec-nixos-test-vms-vs-containers"
|
||||
],
|
||||
"sec-override-nixos-test": [
|
||||
"index.html#sec-override-nixos-test"
|
||||
],
|
||||
@@ -100,6 +103,51 @@
|
||||
"sec-wireless-imperative": [
|
||||
"index.html#sec-wireless-imperative"
|
||||
],
|
||||
"sec-test-container-ssh-access": [
|
||||
"index.html#sec-test-container-ssh-access"
|
||||
],
|
||||
"sec-test-vm-ssh-access": [
|
||||
"index.html#sec-test-vm-ssh-access"
|
||||
],
|
||||
"ssec-all-machine-objects": [
|
||||
"index.html#ssec-all-machine-objects"
|
||||
],
|
||||
"ssec-nixos-test-machines": [
|
||||
"index.html#ssec-nixos-test-machines"
|
||||
],
|
||||
"ssec-nixos-test-nspawn-containers": [
|
||||
"index.html#ssec-nixos-test-nspawn-containers"
|
||||
],
|
||||
"ssec-nixos-test-qemu-vms": [
|
||||
"index.html#ssec-nixos-test-qemu-vms"
|
||||
],
|
||||
"ssec-nspawn-machine-objects": [
|
||||
"index.html#ssec-nspawn-machine-objects"
|
||||
],
|
||||
"ssec-qemu-machine-objects": [
|
||||
"index.html#ssec-qemu-machine-objects"
|
||||
],
|
||||
"ssec-test-script": [
|
||||
"index.html#ssec-test-script"
|
||||
],
|
||||
"ssec-test-script-checks": [
|
||||
"index.html#ssec-test-script-checks"
|
||||
],
|
||||
"ssec-testing-user-units": [
|
||||
"index.html#ssec-testing-user-units"
|
||||
],
|
||||
"test-opt-containerDefaults": [
|
||||
"index.html#test-opt-containerDefaults"
|
||||
],
|
||||
"test-opt-containers": [
|
||||
"index.html#test-opt-containers"
|
||||
],
|
||||
"test-opt-extraBaseModules": [
|
||||
"index.html#test-opt-extraBaseModules"
|
||||
],
|
||||
"test-opt-nodeDefaults": [
|
||||
"index.html#test-opt-nodeDefaults"
|
||||
],
|
||||
"test-opt-rawTestDerivationArg": [
|
||||
"index.html#test-opt-rawTestDerivationArg"
|
||||
],
|
||||
@@ -2037,7 +2085,8 @@
|
||||
"sec-call-nixos-test-outside-nixos": [
|
||||
"index.html#sec-call-nixos-test-outside-nixos"
|
||||
],
|
||||
"sec-nixos-test-nodes": [
|
||||
"sec-nixos-test-machines-config": [
|
||||
"index.html#sec-nixos-test-machines-config",
|
||||
"index.html#sec-nixos-test-nodes"
|
||||
],
|
||||
"ssec-machine-objects": [
|
||||
@@ -2070,8 +2119,8 @@
|
||||
"test-opt-enableOCR": [
|
||||
"index.html#test-opt-enableOCR"
|
||||
],
|
||||
"test-opt-extraBaseModules": [
|
||||
"index.html#test-opt-extraBaseModules"
|
||||
"test-opt-extraBaseNodeModules": [
|
||||
"index.html#test-opt-extraBaseNodeModules"
|
||||
],
|
||||
"test-opt-extraDriverArgs": [
|
||||
"index.html#test-opt-extraDriverArgs"
|
||||
|
||||
@@ -247,6 +247,8 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.
|
||||
|
||||
- `services.caddy` now supports setting `httpPort` and `httpsPort` and opening them in the firewall via `openFirewall`.
|
||||
|
||||
- `boot.initrd.secrets` is now deprecated in favour of `boot.initrd.secretPaths` and `boot.initrd.extraSecretsHook`.
|
||||
|
||||
- The latest available version of Nextcloud is v33 (available as `pkgs.nextcloud33`). The installation logic is as follows:
|
||||
- If [`services.nextcloud.package`](#opt-services.nextcloud.package) is specified explicitly, this package will be installed (**recommended**)
|
||||
- If [`system.stateVersion`](#opt-system.stateVersion) is >=26.05, `pkgs.nextcloud33` will be installed by default.
|
||||
@@ -306,3 +308,5 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.
|
||||
```
|
||||
|
||||
**Do not set this globally!** This makes your setup inherently less secure.
|
||||
|
||||
- `services.radicle` now supports importing the private key and passphrase as systemd creds.
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
let
|
||||
inherit (systemdUtils.lib)
|
||||
assertValueOneOf
|
||||
automountConfig
|
||||
checkUnitConfig
|
||||
makeJobScript
|
||||
mountConfig
|
||||
serviceConfig
|
||||
unitConfig
|
||||
unitNameType
|
||||
;
|
||||
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
qemu_test,
|
||||
setuptools,
|
||||
socat,
|
||||
systemd,
|
||||
tesseract4,
|
||||
util-linux,
|
||||
vde2,
|
||||
|
||||
enableNspawn ? false,
|
||||
enableOCR ? false,
|
||||
extraPythonPackages ? (_: [ ]),
|
||||
}:
|
||||
@@ -51,8 +54,12 @@ buildPythonApplication {
|
||||
netpbm
|
||||
qemu_pkg
|
||||
socat
|
||||
util-linux
|
||||
vde2
|
||||
]
|
||||
++ lib.optionals enableNspawn [
|
||||
systemd
|
||||
]
|
||||
++ lib.optionals enableOCR [
|
||||
imagemagick_light
|
||||
tesseract4
|
||||
|
||||
@@ -42,6 +42,43 @@ some_function(param1, param2)
|
||||
"""
|
||||
|
||||
|
||||
def function_docstrings(functions: list[ast.FunctionDef]) -> str | None:
|
||||
"""Extracts docstrings from a list of function definitions."""
|
||||
documented_functions = [f for f in functions if ast.get_docstring(f) is not None]
|
||||
|
||||
if not documented_functions:
|
||||
return None
|
||||
|
||||
docstrings = []
|
||||
for function in documented_functions:
|
||||
docstr = ast.get_docstring(function)
|
||||
assert docstr is not None
|
||||
|
||||
args = ", ".join(a.arg for a in function.args.args[1:])
|
||||
args = f"({args})"
|
||||
|
||||
docstr = "\n".join(f" {line}" for line in docstr.strip().splitlines())
|
||||
|
||||
docstrings.append(f"{function.name}{args}\n\n:{docstr[1:]}\n")
|
||||
return "\n".join(docstrings)
|
||||
|
||||
|
||||
def machine_methods(
|
||||
class_name: str, class_definitions: list[ast.ClassDef]
|
||||
) -> list[ast.FunctionDef]:
|
||||
"""Given a class name and a list of class definitions, returns the list of function definitions
|
||||
for the class matching the given name.
|
||||
"""
|
||||
machine_class = next(filter(lambda x: x.name == class_name, class_definitions))
|
||||
assert machine_class is not None
|
||||
|
||||
function_definitions = [
|
||||
node for node in machine_class.body if isinstance(node, ast.FunctionDef)
|
||||
]
|
||||
function_definitions.sort(key=lambda x: x.name)
|
||||
return function_definitions
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: {sys.argv[0]} <path-to-test-driver>")
|
||||
@@ -49,25 +86,35 @@ def main() -> None:
|
||||
|
||||
module = ast.parse(Path(sys.argv[1]).read_text())
|
||||
|
||||
class_definitions = (node for node in module.body if isinstance(node, ast.ClassDef))
|
||||
class_definitions = [node for node in module.body if isinstance(node, ast.ClassDef)]
|
||||
|
||||
machine_class = next(filter(lambda x: x.name == "Machine", class_definitions))
|
||||
assert machine_class is not None
|
||||
base_machine_methods = machine_methods("BaseMachine", class_definitions)
|
||||
base_method_names = {method.name for method in base_machine_methods}
|
||||
|
||||
function_definitions = [
|
||||
node for node in machine_class.body if isinstance(node, ast.FunctionDef)
|
||||
qemu_machine_methods = [
|
||||
method
|
||||
for method in machine_methods("QemuMachine", class_definitions)
|
||||
if method.name not in base_method_names
|
||||
]
|
||||
function_definitions.sort(key=lambda x: x.name)
|
||||
|
||||
for function in function_definitions:
|
||||
docstr = ast.get_docstring(function)
|
||||
if docstr is not None:
|
||||
args = ", ".join(a.arg for a in function.args.args[1:])
|
||||
args = f"({args})"
|
||||
nspawn_machine_methods = [
|
||||
method
|
||||
for method in machine_methods("NspawnMachine", class_definitions)
|
||||
if method.name not in base_method_names
|
||||
]
|
||||
|
||||
docstr = "\n".join(f" {line}" for line in docstr.strip().splitlines())
|
||||
|
||||
print(f"{function.name}{args}\n\n:{docstr[1:]}\n")
|
||||
print("#### Generic machine objects {#ssec-all-machine-objects} \n")
|
||||
print(function_docstrings(base_machine_methods))
|
||||
print("#### QEMU VM objects {#ssec-qemu-machine-objects}\n")
|
||||
print(
|
||||
function_docstrings(qemu_machine_methods)
|
||||
or "No methods specific to QEMU virtual machines."
|
||||
)
|
||||
print("#### `systemd-nspawn` container objects {#ssec-nspawn-machine-objects}\n")
|
||||
print(
|
||||
function_docstrings(nspawn_machine_methods)
|
||||
or "No methods specific to `systemd-nspawn` containers."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Executable → Regular
+77
-18
@@ -1,6 +1,8 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import ptpython.ipython
|
||||
@@ -16,7 +18,7 @@ from test_driver.logger import (
|
||||
|
||||
|
||||
class EnvDefault(argparse.Action):
|
||||
"""An argpars Action that takes values from the specified
|
||||
"""An argparse Action that takes values from the specified
|
||||
environment variable as the flags default value.
|
||||
"""
|
||||
|
||||
@@ -55,9 +57,15 @@ def writeable_dir(arg: str) -> Path:
|
||||
def main() -> None:
|
||||
arg_parser = argparse.ArgumentParser(prog="nixos-test-driver")
|
||||
arg_parser.add_argument(
|
||||
"-K",
|
||||
"--keep-vm-state",
|
||||
help="re-use a VM state coming from a previous run",
|
||||
help=argparse.SUPPRESS,
|
||||
dest="keep_machine_state",
|
||||
action="store_true",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"-K",
|
||||
"--keep-machine-state",
|
||||
help="re-use a machine state coming from a previous run",
|
||||
action="store_true",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
@@ -71,13 +79,37 @@ def main() -> None:
|
||||
help="Enable interactive debugging breakpoints for sandboxed runs",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--start-scripts",
|
||||
metavar="START-SCRIPT",
|
||||
"--vm-names",
|
||||
metavar="VM-NAME",
|
||||
action=EnvDefault,
|
||||
envvar="startScripts",
|
||||
envvar="vmNames",
|
||||
nargs="*",
|
||||
help="names of participating virtual machines",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--vm-start-scripts",
|
||||
metavar="VM-START-SCRIPT",
|
||||
action=EnvDefault,
|
||||
envvar="vmStartScripts",
|
||||
nargs="*",
|
||||
help="start scripts for participating virtual machines",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--container-names",
|
||||
metavar="CONTAINER-NAME",
|
||||
action=EnvDefault,
|
||||
envvar="containerNames",
|
||||
nargs="*",
|
||||
help="names of participating containers",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--container-start-scripts",
|
||||
metavar="CONTAINER-START-SCRIPT",
|
||||
action=EnvDefault,
|
||||
envvar="containerStartScripts",
|
||||
nargs="*",
|
||||
help="start scripts for participating containers",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--vlans",
|
||||
metavar="VLAN",
|
||||
@@ -97,8 +129,8 @@ def main() -> None:
|
||||
arg_parser.add_argument(
|
||||
"-o",
|
||||
"--output_directory",
|
||||
help="""The path to the directory where outputs copied from the VM will be placed.
|
||||
By e.g. Machine.copy_from_vm or Machine.screenshot""",
|
||||
help="""The path to the directory where outputs copied from the machine will be placed.
|
||||
By e.g. NspawnMachine.copy_from_machine or QemuMachine.screenshot""",
|
||||
default=Path.cwd(),
|
||||
type=writeable_dir,
|
||||
)
|
||||
@@ -122,6 +154,12 @@ def main() -> None:
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
if "--keep-vm-state" in sys.argv:
|
||||
warnings.warn(
|
||||
"The flag '--keep-vm-state' is deprecated. Use '--keep-machine-state' instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
output_directory = args.output_directory.resolve()
|
||||
logger = CompositeLogger([TerminalLogger()])
|
||||
|
||||
@@ -131,21 +169,33 @@ def main() -> None:
|
||||
if args.junit_xml:
|
||||
logger.add_logger(JunitXMLLogger(output_directory / args.junit_xml))
|
||||
|
||||
if not args.keep_vm_state:
|
||||
logger.info("Machine state will be reset. To keep it, pass --keep-vm-state")
|
||||
if not args.keep_machine_state:
|
||||
logger.info(
|
||||
"Machine state will be reset. To keep it, pass --keep-machine-state"
|
||||
)
|
||||
|
||||
debugger: DebugAbstract = DebugNop()
|
||||
if args.debug_hook_attach is not None:
|
||||
debugger = Debug(logger, args.debug_hook_attach)
|
||||
|
||||
assert len(args.vm_names) == len(args.vm_start_scripts), (
|
||||
f"the number of vm names and vm start scripts must be the same: {args.vm_names} vs. {args.vm_start_scripts}"
|
||||
)
|
||||
assert len(args.container_names) == len(args.container_start_scripts), (
|
||||
f"the number of container names and container start scripts must be the same: {args.container_names} vs. {args.container_start_scripts}"
|
||||
)
|
||||
|
||||
with Driver(
|
||||
args.start_scripts,
|
||||
args.vlans,
|
||||
args.testscript.read_text(),
|
||||
output_directory,
|
||||
logger,
|
||||
args.keep_vm_state,
|
||||
args.global_timeout,
|
||||
vm_names=args.vm_names,
|
||||
vm_start_scripts=args.vm_start_scripts,
|
||||
container_names=args.container_names,
|
||||
container_start_scripts=args.container_start_scripts,
|
||||
vlans=args.vlans,
|
||||
tests=args.testscript.read_text(),
|
||||
out_dir=output_directory,
|
||||
logger=logger,
|
||||
keep_machine_state=args.keep_machine_state,
|
||||
global_timeout=args.global_timeout,
|
||||
debug=debugger,
|
||||
) as driver:
|
||||
if offset := args.dump_vsocks:
|
||||
@@ -170,7 +220,16 @@ def generate_driver_symbols() -> None:
|
||||
in user's test scripts. That list is then used by pyflakes to lint those
|
||||
scripts.
|
||||
"""
|
||||
d = Driver([], [], "", Path(), CompositeLogger([]))
|
||||
d = Driver(
|
||||
vm_names=[],
|
||||
vm_start_scripts=[],
|
||||
container_names=[],
|
||||
container_start_scripts=[],
|
||||
vlans=[],
|
||||
tests="",
|
||||
out_dir=Path(),
|
||||
logger=CompositeLogger([]),
|
||||
)
|
||||
test_symbols = d.test_symbols()
|
||||
with open("driver-symbols", "w") as fp:
|
||||
fp.write(",".join(test_symbols.keys()))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -16,7 +17,12 @@ from colorama import Style
|
||||
from test_driver.debug import DebugAbstract, DebugNop
|
||||
from test_driver.errors import MachineError, RequestedAssertionFailed
|
||||
from test_driver.logger import AbstractLogger
|
||||
from test_driver.machine import Machine, NixStartScript, retry
|
||||
from test_driver.machine import (
|
||||
BaseMachine,
|
||||
NspawnMachine,
|
||||
QemuMachine,
|
||||
retry,
|
||||
)
|
||||
from test_driver.polling_condition import PollingCondition
|
||||
from test_driver.vlan import VLan
|
||||
|
||||
@@ -63,7 +69,8 @@ class Driver:
|
||||
|
||||
tests: str
|
||||
vlans: list[VLan]
|
||||
machines: list[Machine]
|
||||
machines_qemu: list[QemuMachine]
|
||||
machines_nspawn: list[NspawnMachine]
|
||||
polling_conditions: list[PollingCondition]
|
||||
global_timeout: int
|
||||
race_timer: threading.Timer
|
||||
@@ -72,12 +79,15 @@ class Driver:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_scripts: list[str],
|
||||
vm_names: list[str],
|
||||
vm_start_scripts: list[str],
|
||||
container_names: list[str],
|
||||
container_start_scripts: list[str],
|
||||
vlans: list[int],
|
||||
tests: str,
|
||||
out_dir: Path,
|
||||
logger: AbstractLogger,
|
||||
keep_vm_state: bool = False,
|
||||
keep_machine_state: bool = False,
|
||||
global_timeout: int = 24 * 60 * 60 * 7,
|
||||
debug: DebugAbstract = DebugNop(),
|
||||
):
|
||||
@@ -94,25 +104,95 @@ class Driver:
|
||||
vlans = list(set(vlans))
|
||||
self.vlans = [VLan(nr, tmp_dir, self.logger) for nr in vlans]
|
||||
|
||||
def cmd(scripts: list[str]) -> Iterator[NixStartScript]:
|
||||
for s in scripts:
|
||||
yield NixStartScript(s)
|
||||
|
||||
self.polling_conditions = []
|
||||
|
||||
self.machines = [
|
||||
Machine(
|
||||
start_command=cmd,
|
||||
keep_vm_state=keep_vm_state,
|
||||
name=cmd.machine_name,
|
||||
self.machines_qemu = [
|
||||
QemuMachine(
|
||||
name=name,
|
||||
start_command=vm_start_script,
|
||||
keep_machine_state=keep_machine_state,
|
||||
tmp_dir=tmp_dir,
|
||||
callbacks=[self.check_polling_conditions],
|
||||
out_dir=self.out_dir,
|
||||
logger=self.logger,
|
||||
)
|
||||
for cmd in cmd(start_scripts)
|
||||
for name, vm_start_script in zip(vm_names, vm_start_scripts)
|
||||
]
|
||||
|
||||
if len(container_start_scripts) > 0:
|
||||
self._init_nspawn_environment()
|
||||
|
||||
self.machines_nspawn = [
|
||||
NspawnMachine(
|
||||
name=name,
|
||||
start_command=container_start_script,
|
||||
tmp_dir=tmp_dir,
|
||||
logger=self.logger,
|
||||
keep_machine_state=keep_machine_state,
|
||||
callbacks=[self.check_polling_conditions],
|
||||
out_dir=self.out_dir,
|
||||
)
|
||||
for name, container_start_script in zip(
|
||||
container_names,
|
||||
container_start_scripts,
|
||||
)
|
||||
]
|
||||
|
||||
def _init_nspawn_environment(self) -> None:
|
||||
assert os.geteuid() == 0, (
|
||||
f"systemd-nspawn requires root to work. You are {os.geteuid()}"
|
||||
)
|
||||
|
||||
# set up prerequisites for systemd-nspawn containers.
|
||||
# these are not guaranteed to be set up in the Nix sandbox.
|
||||
# if running interactively as root, these will already be set up.
|
||||
|
||||
# check if /run is writable by root
|
||||
if not os.access("/run", os.W_OK):
|
||||
Path("/run").mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["mount", "-t", "tmpfs", "none", "/run"], check=True)
|
||||
Path("/run/netns").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# check if /var/run is a symlink to /run
|
||||
if not (os.path.exists("/var/run") and os.path.samefile("/var/run", "/run")):
|
||||
Path("/var").mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["ln", "-s", "/run", "/var/run"], check=True)
|
||||
|
||||
# check if /sys/fs/cgroup is mounted as cgroup2
|
||||
with open("/proc/mounts", encoding="utf-8") as mounts:
|
||||
for line in mounts:
|
||||
parts = line.split()
|
||||
if len(parts) >= 3 and parts[1] == "/sys/fs/cgroup":
|
||||
if parts[2] == "cgroup2":
|
||||
break
|
||||
else:
|
||||
Path("/sys/fs/cgroup").mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(
|
||||
["mount", "-t", "cgroup2", "none", "/sys/fs/cgroup"], check=True
|
||||
)
|
||||
|
||||
# systemd-nspawn requires that /etc/os-release exists
|
||||
# It supports SYSTEMD_NSPAWN_CHECK_OS_RELEASE=0, but that
|
||||
# would try to "fix" it by bind mounting, which is worse.
|
||||
if not os.path.isfile("/etc/os-release"):
|
||||
subprocess.run(["touch", "/etc/os-release"], check=True)
|
||||
|
||||
# ensure /etc/machine-id exists and is non-empty
|
||||
if (
|
||||
not os.path.isfile("/etc/machine-id")
|
||||
or os.path.getsize("/etc/machine-id") == 0
|
||||
):
|
||||
subprocess.run(
|
||||
["systemd-machine-id-setup"], check=True
|
||||
) # set up /etc/machine-id
|
||||
|
||||
@property
|
||||
def machines(self) -> list[QemuMachine | NspawnMachine]:
|
||||
machines = self.machines_qemu + self.machines_nspawn
|
||||
# Sort the machines by name for consistency with `nodesAndContainers` in <nixos/lib/testing/network.nix>.
|
||||
machines.sort(key=lambda machine: machine.name)
|
||||
return machines
|
||||
|
||||
def __enter__(self) -> "Driver":
|
||||
return self
|
||||
|
||||
@@ -148,7 +228,8 @@ class Driver:
|
||||
general_symbols = dict(
|
||||
start_all=self.start_all,
|
||||
test_script=self.test_script,
|
||||
machines=self.machines,
|
||||
machines_qemu=self.machines_qemu,
|
||||
machines_nspawn=self.machines_nspawn,
|
||||
vlans=self.vlans,
|
||||
driver=self,
|
||||
log=self.logger,
|
||||
@@ -161,7 +242,7 @@ class Driver:
|
||||
serial_stdout_off=self.serial_stdout_off,
|
||||
serial_stdout_on=self.serial_stdout_on,
|
||||
polling_condition=self.polling_condition,
|
||||
Machine=Machine, # for typing
|
||||
BaseMachine=BaseMachine, # for typing
|
||||
t=AssertionTester(),
|
||||
debug=self.debug,
|
||||
)
|
||||
@@ -186,14 +267,14 @@ class Driver:
|
||||
def dump_machine_ssh(self, offset: int) -> None:
|
||||
print("SSH backdoor enabled, the machines can be accessed like this:")
|
||||
print(
|
||||
f"{Style.BRIGHT}Note:{Style.RESET_ALL} this requires {Style.BRIGHT}systemd-ssh-proxy(1){Style.RESET_ALL} to be enabled (default on NixOS 25.05 and newer)."
|
||||
f"{Style.BRIGHT}Note:{Style.RESET_ALL} vsocks require {Style.BRIGHT}systemd-ssh-proxy(1){Style.RESET_ALL} to be enabled (default on NixOS 25.05 and newer)."
|
||||
)
|
||||
names = [machine.name for machine in self.machines]
|
||||
longest_name = len(max(names, key=len))
|
||||
for num, name in enumerate(names, start=offset + 1):
|
||||
longest_name = len(max((machine.name for machine in self.machines), key=len))
|
||||
for index, machine in enumerate(self.machines, start=offset + 1):
|
||||
name = machine.name
|
||||
spaces = " " * (longest_name - len(name) + 2)
|
||||
print(
|
||||
f" {name}:{spaces}{Style.BRIGHT}ssh -o User=root vsock/{num}{Style.RESET_ALL}"
|
||||
f" {name}:{spaces}{Style.BRIGHT}{machine.ssh_backdoor_command(index)}{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
def test_script(self) -> None:
|
||||
@@ -252,8 +333,16 @@ class Driver:
|
||||
def start_all(self) -> None:
|
||||
"""Start all machines"""
|
||||
with self.logger.nested("start all VMs"):
|
||||
threads = []
|
||||
for machine in self.machines:
|
||||
machine.start()
|
||||
# Create a thread for each machine's start method
|
||||
t = threading.Thread(target=machine.start, name=f"start-{machine.name}")
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
# Wait for all startup threads to complete before proceeding
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
def join_all(self) -> None:
|
||||
"""Wait for all machines to shut down"""
|
||||
@@ -279,19 +368,19 @@ class Driver:
|
||||
start_command: str,
|
||||
*,
|
||||
name: str | None = None,
|
||||
keep_vm_state: bool = False,
|
||||
) -> Machine:
|
||||
keep_machine_state: bool = False,
|
||||
) -> BaseMachine:
|
||||
"""
|
||||
Create a `QemuMachine`. This currently only supports qemu "nodes", not containers.
|
||||
"""
|
||||
tmp_dir = get_tmp_dir()
|
||||
|
||||
cmd = NixStartScript(start_command)
|
||||
name = name or cmd.machine_name
|
||||
|
||||
return Machine(
|
||||
return QemuMachine(
|
||||
tmp_dir=tmp_dir,
|
||||
out_dir=self.out_dir,
|
||||
start_command=cmd,
|
||||
start_command=start_command,
|
||||
name=name,
|
||||
keep_vm_state=keep_vm_state,
|
||||
keep_machine_state=keep_machine_state,
|
||||
logger=self.logger,
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,8 @@ class VLan:
|
||||
pid: int
|
||||
fd: io.TextIOBase
|
||||
|
||||
plug_process: subprocess.Popen
|
||||
|
||||
logger: AbstractLogger
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -58,6 +60,7 @@ class VLan:
|
||||
def __init__(self, nr: int, tmp_dir: Path, logger: AbstractLogger):
|
||||
self.nr = nr
|
||||
self.socket_dir = tmp_dir / f"vde{self.nr}.ctl"
|
||||
self.tap_name = f"vde-tap{self.nr}"
|
||||
self.logger = logger
|
||||
|
||||
# TODO: don't side-effect environment here
|
||||
@@ -114,6 +117,13 @@ class VLan:
|
||||
if "1000 Success" in line:
|
||||
break
|
||||
|
||||
# This is needed to allow systemd-nspawn containers to communicate
|
||||
# with VMs connected to the VLAN.
|
||||
self.logger.info(f"creating tap interface {self.tap_name}")
|
||||
self.plug_process = subprocess.Popen(
|
||||
["vde_plug2tap", "-s", self.socket_dir, self.tap_name],
|
||||
)
|
||||
|
||||
assert (self.socket_dir / "ctl").exists(), "cannot start vde_switch"
|
||||
|
||||
self.logger.info(f"running vlan (pid {self.pid}; ctl {self.socket_dir})")
|
||||
@@ -122,4 +132,7 @@ class VLan:
|
||||
self.logger.info(f"kill vlan (pid {self.pid})")
|
||||
assert self.process.stdin is not None
|
||||
self.process.stdin.close()
|
||||
if self.plug_process:
|
||||
self.plug_process.terminate()
|
||||
self.plug_process.wait()
|
||||
self.process.terminate()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from test_driver.debug import DebugAbstract
|
||||
from test_driver.driver import Driver
|
||||
from test_driver.vlan import VLan
|
||||
from test_driver.machine import Machine
|
||||
from test_driver.machine import BaseMachine, NspawnMachine, QemuMachine
|
||||
from test_driver.logger import AbstractLogger
|
||||
from typing import Callable, Iterator, ContextManager, Optional, List, Dict, Any, Union
|
||||
from typing_extensions import Protocol
|
||||
@@ -34,8 +34,9 @@ class CreateMachineProtocol(Protocol):
|
||||
start_command: str | dict,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
keep_vm_state: bool = False,
|
||||
) -> Machine:
|
||||
keep_machine_state: bool = False,
|
||||
**kwargs: Any, # to allow usage of deprecated keep_vm_state
|
||||
) -> BaseMachine:
|
||||
raise Exception("This is just type information for the Nix test driver")
|
||||
|
||||
|
||||
@@ -43,7 +44,7 @@ start_all: Callable[[], None]
|
||||
subtest: Callable[[str], ContextManager[None]]
|
||||
retry: RetryProtocol
|
||||
test_script: Callable[[], None]
|
||||
machines: List[Machine]
|
||||
machines: List[BaseMachine]
|
||||
vlans: List[VLan]
|
||||
driver: Driver
|
||||
log: AbstractLogger
|
||||
|
||||
@@ -56,6 +56,7 @@ pkgs.lib.throwIf (args ? specialArgs)
|
||||
{
|
||||
machine ? null,
|
||||
nodes ? { },
|
||||
containers ? { },
|
||||
testScript,
|
||||
enableOCR ? false,
|
||||
globalTimeout ? (60 * 60),
|
||||
|
||||
@@ -14,18 +14,17 @@ let
|
||||
qemu_pkg = config.qemu.package;
|
||||
imagemagick_light = hostPkgs.imagemagick_light.override { inherit (hostPkgs) libtiff; };
|
||||
tesseract4 = hostPkgs.tesseract4.override { enableLanguages = [ "eng" ]; };
|
||||
|
||||
enableNspawn = config.containers != { };
|
||||
# We want `pkgs.systemd`, *not* `python3Packages.system`.
|
||||
systemd = hostPkgs.systemd;
|
||||
};
|
||||
|
||||
vlans = map (
|
||||
m: (m.virtualisation.vlans ++ (lib.mapAttrsToList (_: v: v.vlan) m.virtualisation.interfaces))
|
||||
) (lib.attrValues config.nodes);
|
||||
) ((lib.attrValues config.nodes) ++ (lib.attrValues config.containers));
|
||||
vms = map (m: m.system.build.vm) (lib.attrValues config.nodes);
|
||||
|
||||
nodeHostNames =
|
||||
let
|
||||
nodesList = map (c: c.system.name) (lib.attrValues config.nodes);
|
||||
in
|
||||
nodesList ++ lib.optional (lib.length nodesList == 1 && !lib.elem "machine" nodesList) "machine";
|
||||
containers = map (m: m.system.build.nspawn) (lib.attrValues config.containers);
|
||||
|
||||
pythonizeName =
|
||||
name:
|
||||
@@ -38,8 +37,22 @@ let
|
||||
|
||||
uniqueVlans = lib.unique (builtins.concatLists vlans);
|
||||
vlanNames = map (i: "vlan${toString i}: VLan;") uniqueVlans;
|
||||
pythonizedNames = map pythonizeName nodeHostNames;
|
||||
machineNames = map (name: "${name}: Machine;") pythonizedNames;
|
||||
|
||||
vmMachineNames = map (c: c.system.name) (lib.attrValues config.nodes);
|
||||
containerMachineNames = map (c: c.system.name) (lib.attrValues config.containers);
|
||||
|
||||
theOnlyMachine =
|
||||
let
|
||||
exactlyOneMachine = lib.length (lib.attrValues config.nodes) == 1;
|
||||
allMachineNames = map (c: c.system.name) (lib.attrValues config.allMachines);
|
||||
in
|
||||
lib.optional (exactlyOneMachine && !lib.elem "machine" allMachineNames) "machine";
|
||||
|
||||
pythonizedVmNames = map pythonizeName (vmMachineNames ++ theOnlyMachine);
|
||||
vmMachineTypeHints = map (name: "${name}: QemuMachine;") pythonizedVmNames;
|
||||
|
||||
pythonizedContainerNames = map pythonizeName containerMachineNames;
|
||||
containerMachineTypeHints = map (name: "${name}: NspawnMachine;") pythonizedContainerNames;
|
||||
|
||||
withChecks = lib.warnIf config.skipLint "Linting is disabled";
|
||||
|
||||
@@ -62,12 +75,16 @@ let
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
|
||||
vmStartScripts=($(for i in ${toString vms}; do echo $i/bin/run-*-vm; done))
|
||||
vmNames=(${lib.escapeShellArgs vmMachineNames})
|
||||
vmStartScripts=(${lib.escapeShellArgs (map lib.getExe vms)})
|
||||
containerNames=(${lib.escapeShellArgs containerMachineNames})
|
||||
containerStartScripts=(${lib.escapeShellArgs (map lib.getExe containers)})
|
||||
|
||||
${lib.optionalString (!config.skipTypeCheck) ''
|
||||
# prepend type hints so the test script can be type checked with mypy
|
||||
cat "${../test-script-prepend.py}" >> testScriptWithTypes
|
||||
echo "${toString machineNames}" >> testScriptWithTypes
|
||||
echo "${toString vmMachineTypeHints}" >> testScriptWithTypes
|
||||
echo "${toString containerMachineTypeHints}" >> testScriptWithTypes
|
||||
echo "${toString vlanNames}" >> testScriptWithTypes
|
||||
echo -n "$testScript" >> testScriptWithTypes
|
||||
|
||||
@@ -90,7 +107,9 @@ let
|
||||
echo "See https://nixos.org/manual/nixos/stable/#test-opt-skipLint"
|
||||
|
||||
PYFLAKES_BUILTINS="$(
|
||||
echo -n ${lib.escapeShellArg (lib.concatStringsSep "," pythonizedNames)},
|
||||
echo -n ${
|
||||
lib.escapeShellArg (lib.concatStringsSep "," (pythonizedVmNames ++ pythonizedContainerNames))
|
||||
},
|
||||
cat ${lib.escapeShellArg "driver-symbols"}
|
||||
)" ${hostPkgs.python3Packages.pyflakes}/bin/pyflakes $out/test-script
|
||||
''}
|
||||
@@ -98,7 +117,10 @@ let
|
||||
# set defaults through environment
|
||||
# see: ./test-driver/test-driver.py argparse implementation
|
||||
wrapProgram $out/bin/nixos-test-driver \
|
||||
--set startScripts "''${vmStartScripts[*]}" \
|
||||
--set vmStartScripts "''${vmStartScripts[*]}" \
|
||||
--set vmNames "''${vmNames[*]}" \
|
||||
--set containerStartScripts "''${containerStartScripts[*]}" \
|
||||
--set containerNames "''${containerNames[*]}" \
|
||||
--set testScript "$out/test-script" \
|
||||
--set globalTimeout "${toString config.globalTimeout}" \
|
||||
--set vlans '${toString vlans}' \
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{ lib, nodes, ... }:
|
||||
testModuleArgs@{
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
attrNames
|
||||
concatMap
|
||||
concatMapAttrsStringSep
|
||||
concatMapStrings
|
||||
flip
|
||||
forEach
|
||||
head
|
||||
listToAttrs
|
||||
@@ -20,22 +22,15 @@ let
|
||||
zipLists
|
||||
;
|
||||
|
||||
nodeNumbers = listToAttrs (zipListsWith nameValuePair (attrNames nodes) (range 1 254));
|
||||
nodeNumbers = listToAttrs (
|
||||
zipListsWith nameValuePair (attrNames testModuleArgs.config.allMachines) (range 1 254)
|
||||
);
|
||||
|
||||
networkModule =
|
||||
{
|
||||
config,
|
||||
nodes,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{ config, ... }:
|
||||
let
|
||||
qemu-common = import ../qemu-common.nix { inherit (pkgs) lib stdenv; };
|
||||
|
||||
interfaces = lib.attrValues config.virtualisation.allInterfaces;
|
||||
|
||||
interfacesNumbered = zipLists interfaces (range 1 255);
|
||||
|
||||
# Automatically assign IP addresses to requested interfaces.
|
||||
assignIPs = lib.filter (i: i.assignIP) interfaces;
|
||||
ipInterfaces = forEach assignIPs (
|
||||
@@ -56,17 +51,6 @@ let
|
||||
}
|
||||
);
|
||||
|
||||
qemuOptions = lib.flatten (
|
||||
forEach interfacesNumbered (
|
||||
{ fst, snd }: qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber
|
||||
)
|
||||
);
|
||||
udevRules = forEach interfaces (
|
||||
interface:
|
||||
# MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive.
|
||||
''SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="${toLower (qemu-common.qemuNicMac interface.vlan config.virtualisation.test.nodeNumber)}",NAME="${interface.name}"''
|
||||
);
|
||||
|
||||
networkConfig = {
|
||||
networking.hostName = mkDefault config.virtualisation.test.nodeName;
|
||||
|
||||
@@ -80,33 +64,51 @@ let
|
||||
optionalString (ipInterfaces != [ ])
|
||||
(head (head ipInterfaces).value.ipv6.addresses).address;
|
||||
|
||||
# Put the IP addresses of all VMs in this machine's
|
||||
# /etc/hosts file. If a machine has multiple
|
||||
# interfaces, use the IP address corresponding to
|
||||
# the first interface (i.e. the first network in its
|
||||
# virtualisation.vlans option).
|
||||
networking.extraHosts = flip concatMapStrings (attrNames nodes) (
|
||||
m':
|
||||
# Generate /etc/hosts including every remote's primary IP addresses
|
||||
# (whichever VLAN they may belong to) as well as all IP addresses from
|
||||
# VLANs that both the local machine and the remote machine share.
|
||||
networking.extraHosts =
|
||||
let
|
||||
config = nodes.${m'};
|
||||
hostnames =
|
||||
optionalString (
|
||||
config.networking.domain != null
|
||||
) "${config.networking.hostName}.${config.networking.domain} "
|
||||
+ "${config.networking.hostName}\n";
|
||||
localVlans = config.virtualisation.vlans;
|
||||
in
|
||||
optionalString (
|
||||
config.networking.primaryIPAddress != ""
|
||||
) "${config.networking.primaryIPAddress} ${hostnames}"
|
||||
+ optionalString (
|
||||
config.networking.primaryIPv6Address != ""
|
||||
) "${config.networking.primaryIPv6Address} ${hostnames}"
|
||||
);
|
||||
concatMapAttrsStringSep "" (
|
||||
mName: remoteConfig:
|
||||
let
|
||||
remoteInterfaces = remoteConfig.networking.interfaces;
|
||||
sharedIps = lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
ifaceName: ifaceCfg:
|
||||
let
|
||||
remoteIfaceMeta = remoteConfig.virtualisation.allInterfaces."${ifaceName}" or { };
|
||||
vlanId = remoteIfaceMeta.vlan or null;
|
||||
in
|
||||
if vlanId != null && builtins.elem vlanId localVlans then
|
||||
builtins.map (addr: addr.address) ifaceCfg.ipv4.addresses
|
||||
++ builtins.map (addr: addr.address) ifaceCfg.ipv6.addresses
|
||||
else
|
||||
[ ]
|
||||
) remoteInterfaces
|
||||
);
|
||||
|
||||
virtualisation.qemu.options = qemuOptions;
|
||||
boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules;
|
||||
# We also want to test router protocols that enable connections
|
||||
# between nodes even if they don't share a VLAN, so we include
|
||||
# the primary IPs of all machines in the hosts file.
|
||||
primaryIPs = [
|
||||
remoteConfig.networking.primaryIPAddress
|
||||
remoteConfig.networking.primaryIPv6Address
|
||||
];
|
||||
|
||||
allReachableIps = lib.lists.uniqueStrings (sharedIps ++ primaryIPs);
|
||||
|
||||
hostnames =
|
||||
optionalString (
|
||||
remoteConfig.networking.domain != null
|
||||
) "${remoteConfig.networking.hostName}.${remoteConfig.networking.domain} "
|
||||
+ "${remoteConfig.networking.hostName}\n";
|
||||
in
|
||||
builtins.concatStringsSep "" (map (ip: "${ip} ${hostnames}") allReachableIps)
|
||||
) testModuleArgs.config.allMachines;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
key = "network-interfaces";
|
||||
@@ -117,6 +119,31 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
qemuNetworkModule =
|
||||
{ config, pkgs, ... }:
|
||||
let
|
||||
qemu-common = import ../qemu-common.nix { inherit (pkgs) lib stdenv; };
|
||||
|
||||
interfaces = lib.attrValues config.virtualisation.allInterfaces;
|
||||
|
||||
interfacesNumbered = zipLists interfaces (range 1 255);
|
||||
|
||||
qemuOptions = lib.flatten (
|
||||
forEach interfacesNumbered (
|
||||
{ fst, snd }: qemu-common.qemuNICFlags snd fst.vlan config.virtualisation.test.nodeNumber
|
||||
)
|
||||
);
|
||||
udevRules = map (
|
||||
interface:
|
||||
# MAC Addresses for QEMU network devices are lowercase, and udev string comparison is case-sensitive.
|
||||
''SUBSYSTEM=="net",ACTION=="add",ATTR{address}=="${toLower (qemu-common.qemuNicMac interface.vlan config.virtualisation.test.nodeNumber)}",NAME="${interface.name}"''
|
||||
) interfaces;
|
||||
in
|
||||
{
|
||||
virtualisation.qemu.options = qemuOptions;
|
||||
boot.initrd.services.udev.rules = concatMapStrings (x: x + "\n") udevRules;
|
||||
};
|
||||
|
||||
nodeNumberModule = (
|
||||
regular@{ config, name, ... }:
|
||||
{
|
||||
@@ -127,7 +154,7 @@ let
|
||||
# We need to force this in specialisations, otherwise it'd be
|
||||
# readOnly = true;
|
||||
description = ''
|
||||
The `name` in `nodes.<name>`; stable across `specialisations`.
|
||||
The `name` in `nodes.<name>` and `containers.<name>`; stable across `specialisations`.
|
||||
'';
|
||||
};
|
||||
virtualisation.test.nodeNumber = mkOption {
|
||||
@@ -136,7 +163,7 @@ let
|
||||
readOnly = true;
|
||||
default = nodeNumbers.${config.virtualisation.test.nodeName};
|
||||
description = ''
|
||||
A unique number assigned for each node in `nodes`.
|
||||
A unique number assigned for each machine in `nodes` and `containers`.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -172,5 +199,10 @@ in
|
||||
nodeNumberModule
|
||||
];
|
||||
};
|
||||
extraBaseNodeModules = {
|
||||
imports = [
|
||||
qemuNetworkModule
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ let
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../../modules/virtualisation/qemu-vm.nix
|
||||
../../modules/testing/test-instrumentation.nix # !!! should only get added for automated test runs
|
||||
{
|
||||
key = "no-manual";
|
||||
@@ -32,7 +31,9 @@ in
|
||||
# This is mostly a Hydra optimization, so we don't rebuild all the tests every time switch-to-configuration-ng changes.
|
||||
key = "no-switch-to-configuration";
|
||||
system.switch.enable = mkDefault (
|
||||
config.isSpecialisation || config.specialisation != { } || config.virtualisation.installBootLoader
|
||||
config.isSpecialisation
|
||||
|| config.specialisation != { }
|
||||
|| (!config.boot.isContainer && config.virtualisation.installBootLoader)
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
+157
-40
@@ -2,7 +2,6 @@ testModuleArgs@{
|
||||
config,
|
||||
lib,
|
||||
hostPkgs,
|
||||
nodes,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
@@ -12,12 +11,9 @@ let
|
||||
literalExpression
|
||||
literalMD
|
||||
mapAttrs
|
||||
mkDefault
|
||||
mkIf
|
||||
mkMerge
|
||||
mkOption
|
||||
mkForce
|
||||
optional
|
||||
optionalAttrs
|
||||
types
|
||||
;
|
||||
@@ -49,15 +45,11 @@ let
|
||||
./nixos-test-base.nix
|
||||
{
|
||||
key = "nodes";
|
||||
_module.args.nodes = config.nodesCompat;
|
||||
_module.args = {
|
||||
inherit (config) containers;
|
||||
nodes = config.nodesCompat;
|
||||
};
|
||||
}
|
||||
(
|
||||
{ config, ... }:
|
||||
{
|
||||
virtualisation.qemu.package = testModuleArgs.config.qemu.package;
|
||||
virtualisation.host.pkgs = hostPkgs;
|
||||
}
|
||||
)
|
||||
(
|
||||
{ options, ... }:
|
||||
{
|
||||
@@ -73,6 +65,62 @@ let
|
||||
testModuleArgs.config.extraBaseModules
|
||||
];
|
||||
};
|
||||
baseQemuOS = baseOS.extendModules {
|
||||
modules = [
|
||||
../../modules/virtualisation/qemu-vm.nix
|
||||
config.nodeDefaults
|
||||
{
|
||||
key = "base-qemu";
|
||||
virtualisation.qemu.package = testModuleArgs.config.qemu.package;
|
||||
virtualisation.host.pkgs = hostPkgs;
|
||||
}
|
||||
testModuleArgs.config.extraBaseNodeModules
|
||||
];
|
||||
};
|
||||
baseNspawnOS = baseOS.extendModules {
|
||||
modules = [
|
||||
../../modules/virtualisation/nspawn-container
|
||||
config.containerDefaults
|
||||
(
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
key = "base-nspawn";
|
||||
|
||||
# PAM requires setuid and doesn't work in the build sandbox.
|
||||
# https://github.com/NixOS/nix/blob/959c244a1265f4048390f3ad21679219d7b27a99/src/libstore/unix/build/linux-derivation-builder.cc#L63
|
||||
services.openssh.settings.UsePAM = false;
|
||||
|
||||
# Networking for tests is statically configured by default.
|
||||
# dhcpcd times out after blocking for a long time, which slows down tests.
|
||||
# See https://github.com/NixOS/nixpkgs/pull/478109#discussion_r2867570799
|
||||
networking.useDHCP = lib.mkDefault false;
|
||||
|
||||
# Disable Info manual directory generation to prevent build failures.
|
||||
#
|
||||
# Context: 'install-info' (from texinfo) is triggered during system-path
|
||||
# generation to index manuals, but it requires 'gzip' in the $PATH to
|
||||
# decompress them.
|
||||
# When 'networking.useDHCP' is set to false, transitive dependencies
|
||||
# (like dhcpcd or other network tools) that normally pull 'gzip' into
|
||||
# the system environment are removed. This leaves 'install-info'
|
||||
# stranded without 'gzip', causing the 'system-path' derivation to fail.
|
||||
# Since nspawn containers are typically minimal, disabling 'info'
|
||||
# is a cleaner fix than explicitly adding 'gzip' to systemPackages.
|
||||
documentation.info.enable = lib.mkDefault false;
|
||||
|
||||
# Gross, insecure hack to make login work. See above.
|
||||
security.pam.services.login = {
|
||||
text = ''
|
||||
auth sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so
|
||||
account sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so
|
||||
password sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so
|
||||
session sufficient ${pkgs.linux-pam}/lib/security/pam_permit.so
|
||||
'';
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
|
||||
# TODO (lib): Dedup with run.nix, add to lib/options.nix
|
||||
mkOneUp = opt: f: lib.mkOverride (opt.highestPrio - 1) (f opt.value);
|
||||
@@ -109,25 +157,74 @@ in
|
||||
|
||||
node.type = mkOption {
|
||||
type = types.raw;
|
||||
default = baseOS.type;
|
||||
default = baseQemuOS.type;
|
||||
internal = true;
|
||||
};
|
||||
|
||||
nodes = mkOption {
|
||||
type = types.lazyAttrsOf config.node.type;
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
description = ''
|
||||
An attribute set of NixOS configuration modules.
|
||||
An attribute set of NixOS configuration modules representing QEMU vms that can be started during a test.
|
||||
|
||||
The configurations are augmented by the [`defaults`](#test-opt-defaults) option.
|
||||
|
||||
They are assigned network addresses according to the `nixos/lib/testing/network.nix` module.
|
||||
|
||||
A few special options are available, that aren't in a plain NixOS configuration. See [Configuring the nodes](#sec-nixos-test-nodes)
|
||||
A few special options are available, that aren't in a plain NixOS configuration. See [Configuring virtual machines](#ssec-nixos-test-qemu-vms)
|
||||
'';
|
||||
};
|
||||
|
||||
container.type = mkOption {
|
||||
type = types.raw;
|
||||
default = baseNspawnOS.type;
|
||||
internal = true;
|
||||
};
|
||||
|
||||
containers = mkOption {
|
||||
type = types.lazyAttrsOf config.container.type;
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
description = ''
|
||||
An attribute set of NixOS configuration modules representing systemd-nspawn containers that can be started during a test.
|
||||
|
||||
The configurations are augmented by the [`defaults`](#test-opt-defaults) option.
|
||||
|
||||
They are assigned network addresses according to the `nixos/lib/testing/network.nix` module.
|
||||
|
||||
A few special options are available, that aren't in a plain NixOS configuration. See [Configuring containers](#ssec-nixos-test-nspawn-containers)
|
||||
'';
|
||||
};
|
||||
|
||||
allMachines = mkOption {
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
description = ''
|
||||
Basically a merge of [{option}`nodes`](#test-opt-nodes) and [{option}`containers`](#test-opt-containers).
|
||||
|
||||
This ensures that there are no name collisions between nodes and containers.
|
||||
'';
|
||||
default =
|
||||
let
|
||||
overlappingNames = lib.intersectLists (lib.attrNames config.nodes) (
|
||||
lib.attrNames config.containers
|
||||
);
|
||||
in
|
||||
lib.throwIfNot (overlappingNames == [ ])
|
||||
"The following names are used in both `nodes` and `containers`: ${lib.concatStringsSep ", " overlappingNames}"
|
||||
(config.nodes // config.containers);
|
||||
};
|
||||
|
||||
defaults = mkOption {
|
||||
description = ''
|
||||
NixOS configuration that is applied to all [{option}`nodes`](#test-opt-nodes) and [{option}`containers`](#test-opt-containers).
|
||||
'';
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
};
|
||||
|
||||
nodeDefaults = mkOption {
|
||||
description = ''
|
||||
NixOS configuration that is applied to all [{option}`nodes`](#test-opt-nodes).
|
||||
'';
|
||||
@@ -135,7 +232,23 @@ in
|
||||
default = { };
|
||||
};
|
||||
|
||||
containerDefaults = mkOption {
|
||||
description = ''
|
||||
NixOS configuration that is applied to all [{option}`containers`](#test-opt-containers).
|
||||
'';
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
};
|
||||
|
||||
extraBaseModules = mkOption {
|
||||
description = ''
|
||||
NixOS configuration that, like [{option}`defaults`](#test-opt-defaults), is applied to all [{option}`nodes`](#test-opt-nodes) and [{option}`containers`](#test-opt-containers) and can not be undone with [`specialisation.<name>.inheritParentConfig`](https://search.nixos.org/options?show=specialisation.%3Cname%3E.inheritParentConfig&from=0&size=50&sort=relevance&type=packages&query=specialisation).
|
||||
'';
|
||||
type = types.deferredModule;
|
||||
default = { };
|
||||
};
|
||||
|
||||
extraBaseNodeModules = mkOption {
|
||||
description = ''
|
||||
NixOS configuration that, like [{option}`defaults`](#test-opt-defaults), is applied to all [{option}`nodes`](#test-opt-nodes) and can not be undone with [`specialisation.<name>.inheritParentConfig`](https://search.nixos.org/options?show=specialisation.%3Cname%3E.inheritParentConfig&from=0&size=50&sort=relevance&type=packages&query=specialisation).
|
||||
'';
|
||||
@@ -145,7 +258,7 @@ in
|
||||
|
||||
node.pkgs = mkOption {
|
||||
description = ''
|
||||
The Nixpkgs to use for the nodes.
|
||||
The Nixpkgs to use for the nodes and containers.
|
||||
|
||||
Setting this will make the `nixpkgs.*` options read-only, to avoid mistakenly testing with a Nixpkgs configuration that diverges from regular use.
|
||||
'';
|
||||
@@ -160,7 +273,7 @@ in
|
||||
description = ''
|
||||
Whether to make the `nixpkgs.*` options read-only. This is only relevant when [`node.pkgs`](#test-opt-node.pkgs) is set.
|
||||
|
||||
Set this to `false` when any of the [`nodes`](#test-opt-nodes) needs to configure any of the `nixpkgs.*` options. This will slow down evaluation of your test a bit.
|
||||
Set this to `false` when any of the [`nodes`](#test-opt-nodes) or [{option}`containers`](#test-opt-containers) need to configure any of the `nixpkgs.*` options. This will slow down evaluation of your test a bit.
|
||||
'';
|
||||
type = types.bool;
|
||||
default = config.node.pkgs != null;
|
||||
@@ -188,6 +301,7 @@ in
|
||||
};
|
||||
|
||||
config = {
|
||||
_module.args.containers = config.containers;
|
||||
_module.args.nodes = config.nodesCompat;
|
||||
nodesCompat = mapAttrs (
|
||||
name: config:
|
||||
@@ -201,6 +315,7 @@ in
|
||||
) config.nodes;
|
||||
|
||||
passthru.nodes = config.nodesCompat;
|
||||
passthru.containers = config.containers;
|
||||
|
||||
extraDriverArgs = mkIf config.sshBackdoor.enable [
|
||||
"--dump-vsocks=${toString config.sshBackdoor.vsockOffset}"
|
||||
@@ -211,33 +326,35 @@ in
|
||||
nixpkgs.pkgs = config.node.pkgs;
|
||||
imports = [ ../../modules/misc/nixpkgs/read-only.nix ];
|
||||
})
|
||||
(mkIf config.sshBackdoor.enable (
|
||||
let
|
||||
inherit (config.sshBackdoor) vsockOffset;
|
||||
in
|
||||
{ config, ... }:
|
||||
{
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PermitRootLogin = "yes";
|
||||
PermitEmptyPasswords = "yes";
|
||||
};
|
||||
(mkIf config.sshBackdoor.enable {
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PermitRootLogin = "yes";
|
||||
PermitEmptyPasswords = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
security.pam.services.sshd = {
|
||||
allowNullPassword = true;
|
||||
};
|
||||
|
||||
virtualisation.qemu.options = [
|
||||
"-device vhost-vsock-pci,guest-cid=${
|
||||
toString (config.virtualisation.test.nodeNumber + vsockOffset)
|
||||
}"
|
||||
];
|
||||
}
|
||||
))
|
||||
security.pam.services.sshd = {
|
||||
allowNullPassword = true;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
nodeDefaults = mkIf config.sshBackdoor.enable (
|
||||
let
|
||||
inherit (config.sshBackdoor) vsockOffset;
|
||||
in
|
||||
{ config, ... }:
|
||||
{
|
||||
virtualisation.qemu.options = [
|
||||
"-device vhost-vsock-pci,guest-cid=${
|
||||
toString (config.virtualisation.test.nodeNumber + vsockOffset)
|
||||
}"
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
# Docs: nixos/doc/manual/development/writing-nixos-tests.section.md
|
||||
/**
|
||||
See https://nixos.org/manual/nixos/unstable#sec-override-nixos-test
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
hostPkgs,
|
||||
lib,
|
||||
containers,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
@@ -96,12 +97,15 @@ in
|
||||
requiredSystemFeatures = [
|
||||
"nixos-test"
|
||||
]
|
||||
# Containers use systemd-nspawn, which requires pid 0 inside of the sandbox.
|
||||
++ lib.optional (builtins.length (lib.attrNames containers) > 0) "uid-range"
|
||||
++ lib.optional isLinux "kvm"
|
||||
++ lib.optional isDarwin "apple-virt";
|
||||
|
||||
nativeBuildInputs = lib.optionals config.enableDebugHook [
|
||||
hostPkgs.openssh
|
||||
hostPkgs.inetutils
|
||||
hostPkgs.socat # to allow SSH backdoor connections for systemd-nspawn containers
|
||||
];
|
||||
|
||||
buildCommand = ''
|
||||
|
||||
@@ -56,11 +56,12 @@ in
|
||||
# reuse memoized config
|
||||
v
|
||||
) config.nodesCompat;
|
||||
containers = config.containers;
|
||||
}
|
||||
else
|
||||
config.testScript;
|
||||
|
||||
defaults =
|
||||
nodeDefaults =
|
||||
{ config, name, ... }:
|
||||
{
|
||||
# Make sure all derivations referenced by the test
|
||||
|
||||
@@ -214,6 +214,7 @@ in
|
||||
postBuild = ''
|
||||
# Remove wrapped binaries, they shouldn't be accessible via PATH.
|
||||
find $out/bin -maxdepth 1 -name ".*-wrapped" -type l -delete
|
||||
find $out/bin -maxdepth 1 -name ".*-wrapped_*" -type l -delete
|
||||
|
||||
if [ -x $out/bin/glib-compile-schemas -a -w $out/share/glib-2.0/schemas ]; then
|
||||
$out/bin/glib-compile-schemas $out/share/glib-2.0/schemas
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{ config, lib, ... }:
|
||||
# unixODBC drivers (this solution is not perfect.. Because the user has to
|
||||
# unixodbc drivers (this solution is not perfect.. Because the user has to
|
||||
# ask the admin to add a driver.. but it's simple and works
|
||||
|
||||
let
|
||||
@@ -17,11 +17,11 @@ in
|
||||
environment.unixODBCDrivers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.package;
|
||||
default = [ ];
|
||||
example = lib.literalExpression "with pkgs.unixODBCDrivers; [ sqlite psql ]";
|
||||
example = lib.literalExpression "with pkgs.unixodbcDrivers; [ sqlite psql ]";
|
||||
description = ''
|
||||
Specifies Unix ODBC drivers to be registered in
|
||||
{file}`/etc/odbcinst.ini`. You may also want to
|
||||
add `pkgs.unixODBC` to the system path to get
|
||||
add `pkgs.unixodbc` to the system path to get
|
||||
a command line client to connect to ODBC databases.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -175,7 +175,7 @@ in
|
||||
(umask 0077; printf "${cfg.accessUser}:@password_placeholder@" >"$auth_file")
|
||||
"${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "$access_token_file" "$auth_file"
|
||||
|
||||
if ! "${pkgs.jenkins}/bin/jenkins-cli" -s "$jenkins_url" -auth "@$auth_file" reload-configuration; then
|
||||
if ! "${pkgs.jenkins}/bin/jenkins-cli" -http -s "$jenkins_url" -auth "@$auth_file" reload-configuration; then
|
||||
echo "error: failed to reload configuration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -566,6 +566,16 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
finalPackage = mkOption {
|
||||
default = package;
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
type = types.package;
|
||||
description = ''
|
||||
The final Home Assistant package which is being used in the service.
|
||||
'';
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
utils,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -15,6 +16,11 @@ let
|
||||
RAD_HOME = HOME;
|
||||
};
|
||||
|
||||
credentials = {
|
||||
privateKey = "xyz.radicle.node.secret";
|
||||
privateKeyPassphrase = "xyz.radicle.node.passphrase";
|
||||
};
|
||||
|
||||
# Convenient wrapper to run `rad` in the namespaces of `radicle-node.service`
|
||||
rad-system = pkgs.writeShellScriptBin "rad-system" ''
|
||||
set -o allexport
|
||||
@@ -131,16 +137,34 @@ in
|
||||
services.radicle = {
|
||||
enable = lib.mkEnableOption "Radicle Seed Node";
|
||||
package = lib.mkPackageOption pkgs "radicle-node" { };
|
||||
privateKeyFile = lib.mkOption {
|
||||
# Note that a key encrypted by systemd-creds is not a path but a str.
|
||||
type = with lib.types; either path str;
|
||||
privateKey = lib.mkOption {
|
||||
type = with lib.types; nullOr (either path str);
|
||||
default = null;
|
||||
description = ''
|
||||
Absolute file path to an SSH private key,
|
||||
An SSH private key (as an absolute file path or Systemd credential name),
|
||||
usually generated by `rad auth`.
|
||||
|
||||
If it contains a colon (`:`) the string before the colon
|
||||
is taken as the credential name
|
||||
and the string after as a path encrypted with `systemd-creds`.
|
||||
If set to the default value of `null`, radicle will import the private key from a credential
|
||||
named `${credentials.privateKey}`.
|
||||
|
||||
If configured as a credential name it will be imported via `ImportCredential=` in the service configuration.
|
||||
Refer to the systemd-creds documentation for more details <https://systemd.io/CREDENTIALS/>
|
||||
'';
|
||||
};
|
||||
privateKeyPassphrase = lib.mkOption {
|
||||
type = with lib.types; nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
A passphrase for an SSH private key (as a Systemd credential name),
|
||||
usually provided on generation of the key with `rad auth`.
|
||||
|
||||
If set to the default value of `null`, radicle will optionally import the passphrase from a
|
||||
credential named `${credentials.privateKeyPassphrase}`.
|
||||
|
||||
If the passphrase is not set, radicle will prompt for it.
|
||||
|
||||
If configured as a credential name it will be imported via `ImportCredential=` in the service configuration.
|
||||
Refer to the systemd-creds documentation for more details <https://systemd.io/CREDENTIALS/>
|
||||
'';
|
||||
};
|
||||
publicKey = lib.mkOption {
|
||||
@@ -304,23 +328,28 @@ in
|
||||
# Give only access to the private key to radicle-node.
|
||||
{
|
||||
serviceConfig =
|
||||
let
|
||||
keyCred = builtins.split ":" "${cfg.privateKeyFile}";
|
||||
in
|
||||
if lib.length keyCred > 1 then
|
||||
if cfg.privateKey == null then
|
||||
{
|
||||
LoadCredentialEncrypted = [ cfg.privateKeyFile ];
|
||||
# Note that neither %d nor ${CREDENTIALS_DIRECTORY} works in BindReadOnlyPaths=
|
||||
BindReadOnlyPaths = [
|
||||
"/run/credentials/radicle-node.service/${lib.head keyCred}:${env.RAD_HOME}/keys/radicle"
|
||||
];
|
||||
ImportCredential = [ credentials.privateKey ];
|
||||
}
|
||||
else if lib.types.path.check cfg.privateKey then
|
||||
{
|
||||
LoadCredential = [ "${credentials.privateKey}:${cfg.privateKey}" ];
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadCredential = [ "radicle:${cfg.privateKeyFile}" ];
|
||||
BindReadOnlyPaths = [
|
||||
"/run/credentials/radicle-node.service/radicle:${env.RAD_HOME}/keys/radicle"
|
||||
];
|
||||
ImportCredential = [ "${cfg.privateKey}:${credentials.privateKey}" ];
|
||||
};
|
||||
}
|
||||
{
|
||||
serviceConfig =
|
||||
if cfg.privateKeyPassphrase == null then
|
||||
{
|
||||
ImportCredential = [ credentials.privateKeyPassphrase ];
|
||||
}
|
||||
else
|
||||
{
|
||||
ImportCredential = [ "${cfg.privateKeyPassphrase}:${credentials.privateKeyPassphrase}" ];
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
@@ -86,7 +86,7 @@ in
|
||||
description = ''
|
||||
Append an additional file's contents to `/etc/iscsid.conf`. Use a non-store path
|
||||
and store passwords in this file. Note: the file specified here must be available
|
||||
in the initrd, see: `boot.initrd.secrets`.
|
||||
in the initrd, see: `boot.initrd.secretPaths`.
|
||||
'';
|
||||
default = null;
|
||||
type = nullOr str;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
let
|
||||
cfg = config.services.murmur;
|
||||
acmeHostDir = config.security.acme.certs."${cfg.tls.useACMEHost}".directory;
|
||||
|
||||
forking = cfg.logToFile;
|
||||
configFile = pkgs.writeText "murmurd.ini" ''
|
||||
database=${cfg.stateDir}/murmur.sqlite
|
||||
@@ -41,9 +43,9 @@ let
|
||||
${lib.optionalString (cfg.registerHostname != "") "registerHostname=${cfg.registerHostname}"}
|
||||
|
||||
certrequired=${lib.boolToString cfg.clientCertRequired}
|
||||
${lib.optionalString (cfg.sslCert != null) "sslCert=${cfg.sslCert}"}
|
||||
${lib.optionalString (cfg.sslKey != null) "sslKey=${cfg.sslKey}"}
|
||||
${lib.optionalString (cfg.sslCa != null) "sslCA=${cfg.sslCa}"}
|
||||
${lib.optionalString (cfg.tls.certPath != null) "sslCert=${cfg.tls.certPath}"}
|
||||
${lib.optionalString (cfg.tls.keyPath != null) "sslKey=${cfg.tls.keyPath}"}
|
||||
${lib.optionalString (cfg.tls.caPath != null) "sslCA=${cfg.tls.caPath}"}
|
||||
|
||||
${lib.optionalString (cfg.dbus != null) "dbus=${cfg.dbus}"}
|
||||
|
||||
@@ -58,6 +60,12 @@ in
|
||||
"murmur"
|
||||
"logFile"
|
||||
] "This option has been superseded by services.murmur.logToFile")
|
||||
(lib.mkRenamedOptionModule [ "services" "murmur" "sslCa" ] [ "services" "murmur" "tls" "caPath" ])
|
||||
(lib.mkRenamedOptionModule [ "services" "murmur" "sslKey" ] [ "services" "murmur" "tls" "keyPath" ])
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "services" "murmur" "sslCert" ]
|
||||
[ "services" "murmur" "tls" "certPath" ]
|
||||
)
|
||||
];
|
||||
|
||||
options = {
|
||||
@@ -237,22 +245,41 @@ in
|
||||
|
||||
clientCertRequired = lib.mkEnableOption "requiring clients to authenticate via certificates";
|
||||
|
||||
sslCert = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to your SSL certificate.";
|
||||
};
|
||||
tls = {
|
||||
certPath = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = if (cfg.tls.useACMEHost != null) then "${acmeHostDir}/cert.pem" else null;
|
||||
defaultText = lib.literalMD "If {option}`services.murmur.tls.useACMEHost` is set, defaults to what's provided by the ACME module.";
|
||||
description = "Path to your TLS certificate.";
|
||||
};
|
||||
|
||||
sslKey = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to your SSL key.";
|
||||
};
|
||||
keyPath = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = if (cfg.tls.useACMEHost != null) then "${acmeHostDir}/key.pem" else null;
|
||||
defaultText = lib.literalMD "If {option}`services.murmur.tls.useACMEHost` is set, defaults to what's provided by the ACME module.";
|
||||
description = "Path to your TLS key.";
|
||||
};
|
||||
|
||||
sslCa = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = "Path to your SSL CA certificate.";
|
||||
caPath = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = if (cfg.tls.useACMEHost != null) then "${acmeHostDir}/chain.pem" else null;
|
||||
defaultText = lib.literalMD "If {option}`services.murmur.tls.useACMEHost` is set, defaults to what's provided by the ACME module.";
|
||||
description = "Path to your TLS CA certificate.";
|
||||
};
|
||||
|
||||
useACMEHost = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "mumble.example.com";
|
||||
description = ''
|
||||
Host of an existing Let's Encrypt certificate to use for TLS.
|
||||
Make sure that the certificate directory is readable by the
|
||||
`murmur` user or group. *Note that this option does not
|
||||
create any certificates and it doesn't add subdomains to
|
||||
existing ones – you will need to create them manually using
|
||||
{option}`security.acme.certs`.*
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = lib.mkOption {
|
||||
@@ -316,10 +343,18 @@ in
|
||||
allowedUDPPorts = [ cfg.port ];
|
||||
};
|
||||
|
||||
security.acme.certs = lib.mkIf (cfg.tls.useACMEHost != null) {
|
||||
"${cfg.tls.useACMEHost}".reloadServices = [ "murmur.service" ];
|
||||
};
|
||||
|
||||
systemd.services.murmur = {
|
||||
description = "Murmur Chat Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
after = [
|
||||
"network.target"
|
||||
]
|
||||
++ lib.optional (cfg.tls.useACMEHost != null) "acme-${cfg.tls.useACMEHost}.service";
|
||||
wants = lib.mkIf (cfg.tls.useACMEHost != null) [ "acme-${cfg.tls.useACMEHost}.service" ];
|
||||
preStart = ''
|
||||
${pkgs.envsubst}/bin/envsubst \
|
||||
-o /run/murmur/murmurd.ini \
|
||||
@@ -422,14 +457,14 @@ in
|
||||
${lib.optionalString cfg.logToFile ''
|
||||
/var/log/murmur/murmurd.log rw,
|
||||
''}
|
||||
${lib.optionalString (cfg.sslCert != null) ''
|
||||
${cfg.sslCert} r,
|
||||
${lib.optionalString (cfg.tls.certPath != null) ''
|
||||
${cfg.tls.certPath} r,
|
||||
''}
|
||||
${lib.optionalString (cfg.sslKey != null) ''
|
||||
${cfg.sslKey} r,
|
||||
${lib.optionalString (cfg.tls.keyPath != null) ''
|
||||
${cfg.tls.keyPath} r,
|
||||
''}
|
||||
${lib.optionalString (cfg.sslCa != null) ''
|
||||
${cfg.sslCa} r,
|
||||
${lib.optionalString (cfg.tls.caPath != null) ''
|
||||
${cfg.tls.caPath} r,
|
||||
''}
|
||||
${lib.optionalString (cfg.dbus != null) ''
|
||||
dbus bus=${cfg.dbus},
|
||||
|
||||
@@ -701,6 +701,7 @@ in
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
diogotcorreia
|
||||
tmarkus
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,17 @@ in
|
||||
options.services.olivetin = {
|
||||
enable = lib.mkEnableOption "OliveTin";
|
||||
|
||||
package = lib.mkPackageOption pkgs "olivetin" { };
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
description = "The olivetin package to use.";
|
||||
default =
|
||||
if lib.versionAtLeast config.system.stateVersion "26.05" then pkgs.olivetin-3k else pkgs.olivetin;
|
||||
defaultText = lib.literalExpression ''
|
||||
if lib.versionAtLeast config.system.stateVersion "26.05"
|
||||
then pkgs.olivetin-3k
|
||||
else pkgs.olivetin
|
||||
'';
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
|
||||
@@ -14,7 +14,9 @@ let
|
||||
children = lib.mapAttrs (
|
||||
childName: childConfig: childConfig.configuration.system.build.toplevel
|
||||
) config.specialisation;
|
||||
hasAtLeastOneInitrdSecret = lib.length (lib.attrNames config.boot.initrd.secrets) > 0;
|
||||
hasInitrdSecrets =
|
||||
(lib.length (lib.attrNames config.boot.initrd.secretPaths) > 0)
|
||||
|| (config.boot.initrd.extraSecretsHook != "");
|
||||
schemas = {
|
||||
v1 = rec {
|
||||
filename = "boot.json";
|
||||
@@ -33,7 +35,7 @@ let
|
||||
// lib.optionalAttrs config.boot.initrd.enable {
|
||||
initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
|
||||
}
|
||||
// lib.optionalAttrs hasAtLeastOneInitrdSecret {
|
||||
// lib.optionalAttrs hasInitrdSecrets {
|
||||
initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ in
|
||||
sed -i $out/bin/clevis-decrypt-tpm2 -e 's,tpm2_,tpm2 ,'
|
||||
'';
|
||||
|
||||
secrets = lib.mapAttrs' (
|
||||
name: value: lib.nameValuePair "/etc/clevis/${name}.jwe" value.secretFile
|
||||
secretPaths = lib.mapAttrs' (
|
||||
name: value: lib.nameValuePair "/etc/clevis/${name}.jwe" { source = value.secretFile; }
|
||||
) cfg.devices;
|
||||
|
||||
systemd = {
|
||||
|
||||
@@ -29,7 +29,7 @@ in
|
||||
};
|
||||
|
||||
boot.initrd.network.openvpn.configuration = mkOption {
|
||||
type = types.path; # Same type as boot.initrd.secrets
|
||||
type = types.path; # Same type as boot.initrd.secretPaths.*.source
|
||||
description = ''
|
||||
The configuration file for OpenVPN.
|
||||
|
||||
@@ -74,8 +74,8 @@ in
|
||||
"${pkgs.glibc}/lib/libnss_dns.so.2"
|
||||
];
|
||||
|
||||
boot.initrd.secrets = {
|
||||
"/etc/initrd.ovpn" = cfg.configuration;
|
||||
boot.initrd.secretPaths = {
|
||||
"/etc/initrd.ovpn".source = cfg.configuration;
|
||||
};
|
||||
|
||||
# openvpn --version would exit with 1 instead of 0
|
||||
|
||||
@@ -302,8 +302,8 @@ in
|
||||
fi
|
||||
'';
|
||||
|
||||
boot.initrd.secrets = listToAttrs (
|
||||
map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys
|
||||
boot.initrd.secretPaths = listToAttrs (
|
||||
map (path: nameValuePair (initrdKeyPath path) { source = path; }) cfg.hostKeys
|
||||
);
|
||||
|
||||
# Systemd initrd stuff
|
||||
|
||||
@@ -414,7 +414,7 @@ in
|
||||
|
||||
ln -s ${initrdPath} $out/initrd
|
||||
|
||||
${optionalString (config.boot.initrd.secrets != { }) ''
|
||||
${optionalString (config.boot.initrd.secretPaths != { }) ''
|
||||
ln -s ${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets $out
|
||||
''}
|
||||
|
||||
|
||||
@@ -935,21 +935,21 @@ in
|
||||
'')
|
||||
(mkRemovedOptionModule [ "boot" "loader" "grub" "extraInitrd" ] ''
|
||||
This option has been replaced with the bootloader agnostic
|
||||
boot.initrd.secrets option. To migrate to the initrd secrets system,
|
||||
boot.initrd.secretPaths option. To migrate to the initrd secrets system,
|
||||
extract the extraInitrd archive into your main filesystem:
|
||||
|
||||
# zcat /boot/extra_initramfs.gz | cpio -idvmD /etc/secrets/initrd
|
||||
/path/to/secret1
|
||||
/path/to/secret2
|
||||
|
||||
then replace boot.loader.grub.extraInitrd with boot.initrd.secrets:
|
||||
then replace boot.loader.grub.extraInitrd with boot.initrd.secretPaths:
|
||||
|
||||
boot.initrd.secrets = {
|
||||
"/path/to/secret1" = "/etc/secrets/initrd/path/to/secret1";
|
||||
"/path/to/secret2" = "/etc/secrets/initrd/path/to/secret2";
|
||||
boot.initrd.secretPaths = {
|
||||
"/path/to/secret1".source = "/etc/secrets/initrd/path/to/secret1";
|
||||
"/path/to/secret2".source = "/etc/secrets/initrd/path/to/secret2";
|
||||
};
|
||||
|
||||
See the boot.initrd.secrets option documentation for more information.
|
||||
See the boot.initrd.secretPaths option documentation for more information.
|
||||
'')
|
||||
];
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ sub addEntry {
|
||||
die "failed to create initrd secrets $!\n";
|
||||
} else {
|
||||
say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation";
|
||||
say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`";
|
||||
say STDERR "note: this is normal after having modified or removed an entry in `boot.initrd.secretPaths`";
|
||||
}
|
||||
}
|
||||
# Check whether any secrets were actually added
|
||||
|
||||
@@ -202,7 +202,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
|
||||
print("warning: failed to create initrd secrets "
|
||||
f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
|
||||
print("note: this is normal after having removed "
|
||||
"or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
|
||||
"or modified an entry in `boot.initrd.secretPaths`", file=sys.stderr)
|
||||
entry_file = BOOT_MOUNT_POINT / "loader/entries" / generation_conf_filename(profile, generation, specialisation)
|
||||
tmp_path = entry_file.with_suffix(".tmp")
|
||||
kernel_params = "init=%s " % bootspec.init
|
||||
|
||||
@@ -148,23 +148,22 @@ let
|
||||
# Copy secrets if needed.
|
||||
#
|
||||
# TODO: move out to a separate script; see #85000.
|
||||
${optionalString (!config.boot.loader.supportsInitrdSecrets) (
|
||||
concatStringsSep "\n" (
|
||||
mapAttrsToList (
|
||||
dest: source:
|
||||
let
|
||||
source' = if source == null then dest else source;
|
||||
in
|
||||
''
|
||||
mkdir -p $(dirname "$out/secrets/${dest}")
|
||||
# Some programs (e.g. ssh) doesn't like secrets to be
|
||||
# symlinks, so we use `cp -L` here to match the
|
||||
# behaviour when secrets are natively supported.
|
||||
cp -Lr ${source'} "$out/secrets/${dest}"
|
||||
''
|
||||
) config.boot.initrd.secrets
|
||||
)
|
||||
)}
|
||||
${optionalString (!config.boot.loader.supportsInitrdSecrets) ''
|
||||
${concatStringsSep "\n" (
|
||||
mapAttrsToList (_: scfg: ''
|
||||
mkdir -p $(dirname "$out/secrets${scfg.path}")
|
||||
# Some programs (e.g. ssh) doesn't like secrets to be
|
||||
# symlinks, so we use `cp -L` here to match the
|
||||
# behaviour when secrets are natively supported.
|
||||
# The assertion further up in this file (stage-1.nix)
|
||||
# checks that all secretPaths are Nix store paths set via
|
||||
# boot.initrd.secretPaths.*.source if the bootloader doesn't
|
||||
# support initrd secrets.
|
||||
cp -Lr ${scfg.source} "$out/secrets${scfg.path}"
|
||||
'') config.boot.initrd.secretPaths
|
||||
)}
|
||||
${config.boot.initrd.extraSecretsHook}
|
||||
''}
|
||||
|
||||
${config.boot.initrd.extraUtilsCommands}
|
||||
|
||||
@@ -436,7 +435,9 @@ let
|
||||
exit 0
|
||||
fi
|
||||
|
||||
${lib.optionalString (config.boot.initrd.secrets == { }) "exit 0"}
|
||||
${lib.optionalString (
|
||||
config.boot.initrd.secretPaths == { } && config.boot.initrd.extraSecretsHook == ""
|
||||
) "exit 0"}
|
||||
|
||||
export PATH=${pkgs.coreutils}/bin:${pkgs.cpio}/bin:${pkgs.gzip}/bin:${pkgs.findutils}/bin
|
||||
|
||||
@@ -451,17 +452,25 @@ let
|
||||
|
||||
${lib.concatStringsSep "\n" (
|
||||
mapAttrsToList (
|
||||
dest: source:
|
||||
_: scfg:
|
||||
let
|
||||
source' = if source == null then dest else toString source;
|
||||
prefix = lib.optionalString scfg.intermediateSecretsDir "/.initrd-secrets";
|
||||
in
|
||||
''
|
||||
mkdir -p $(dirname "$tmp/.initrd-secrets/${dest}")
|
||||
cp -a ${source'} "$tmp/.initrd-secrets/${dest}"
|
||||
mkdir -p $(dirname "$tmp${prefix}${scfg.path}")
|
||||
(
|
||||
export out="$tmp${prefix}${scfg.path}"
|
||||
${scfg.generateSecretCommand}
|
||||
)
|
||||
''
|
||||
) config.boot.initrd.secrets
|
||||
) config.boot.initrd.secretPaths
|
||||
)}
|
||||
|
||||
(
|
||||
cd "$tmp"
|
||||
${config.boot.initrd.extraSecretsHook}
|
||||
)
|
||||
|
||||
# mindepth 1 so that we don't change the mode of /
|
||||
(cd "$tmp" && find . -mindepth 1 | xargs touch -amt 197001010000 && find . -mindepth 1 -print0 | sort -z | cpio --quiet -o -H newc -R +0:+0 --reproducible --null) | \
|
||||
${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
|
||||
@@ -650,21 +659,113 @@ in
|
||||
boot.initrd.secrets = mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (types.nullOr types.path);
|
||||
visible = false;
|
||||
description = ''
|
||||
Secrets to append to the initrd. The attribute name is the
|
||||
path the secret should have inside the initrd, the value
|
||||
is the path it should be copied from (or null for the same
|
||||
path inside and out).
|
||||
Secrets to append to the initrd. This option has been deprecated in
|
||||
favour of `boot.initrd.secretPaths`.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{ "/etc/dropbear/dropbear_rsa_host_key" =
|
||||
./secret-dropbear-key;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
boot.initrd.secretPaths = mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ config, name, ... }:
|
||||
{
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.path;
|
||||
default = name;
|
||||
description = ''
|
||||
The path the secret should be placed at in the initrd. Defaults
|
||||
to the attribute name.
|
||||
'';
|
||||
};
|
||||
|
||||
intermediateSecretsDir = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
By default, the secrets will be copied over to the
|
||||
`/.initrd-secrets` dir at initrd generation time, and then copied
|
||||
over to their final location at boot time. This is because initrd secrets
|
||||
that are supposed to be placed in `/run` would be overridden by
|
||||
the tmpfs mount over `/run` otherwise.
|
||||
|
||||
Set this option to `false` to skip this intermediate step and
|
||||
place the secret at its final location straightaway.
|
||||
'';
|
||||
};
|
||||
|
||||
source = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
The absolute path on the filesystem to copy the secret from.
|
||||
'';
|
||||
example = "/var/lib/secrets/initrd/ssh_host_ed25519_key";
|
||||
};
|
||||
|
||||
generateSecretCommand = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
The command to run to generate the secret. It should write
|
||||
the secret to `$out`.
|
||||
|
||||
This is useful if you have a more advanced secrets provisioning
|
||||
mechanism.
|
||||
'';
|
||||
example = ''
|
||||
pkgs.writeShellScript "generate-secret" '''
|
||||
''${lib.getExe pkgs.age} -d -i /etc/ssh/ssh_host_ed25519_key -o "$out" ''${./secret.age}
|
||||
'''
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
generateSecretCommand = lib.mkIf (config.source != null) (
|
||||
pkgs.writeShellScript "copy-secret" ''
|
||||
cp -Lr ${config.source} "$out"
|
||||
''
|
||||
);
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
description = ''
|
||||
Secret paths to append to the initrd. The attribute name is the
|
||||
path the secret should have inside the initrd.
|
||||
|
||||
Note that `nixos-rebuild switch` will generate the initrd
|
||||
also for past generations, so if secrets are moved or deleted
|
||||
you will also have to garbage collect the generations that
|
||||
use those secrets.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{ "/etc/dropbear/dropbear_rsa_host_key" =
|
||||
./secret-dropbear-key;
|
||||
}
|
||||
example = {
|
||||
"/etc/ssh/ssh_host_ed25519_key".source = "/var/lib/secrets/initrd/ssh_host_ed25519_key";
|
||||
};
|
||||
};
|
||||
|
||||
boot.initrd.extraSecretsHook = mkOption {
|
||||
default = "";
|
||||
type = types.lines;
|
||||
description = ''
|
||||
Extra commands to be executed after the initrd secrets generation phase.
|
||||
|
||||
This script should place files into the current workdir. These files
|
||||
will then be copied over to the initrd to the corresponding absolute
|
||||
paths, e.g. `etc/ssh/ssh_host_ed25519_key` will be copied over to
|
||||
`/etc/ssh/ssh_host_ed25519_key`.
|
||||
'';
|
||||
example = ''
|
||||
# Generate a new SSH host key for every generation.
|
||||
ssh-keygen -f etc/ssh/ssh_host_ed25519_key
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -746,15 +847,18 @@ in
|
||||
assertion =
|
||||
!config.boot.loader.supportsInitrdSecrets
|
||||
-> all (
|
||||
source: builtins.isPath source || (builtins.isString source && hasPrefix builtins.storeDir source)
|
||||
) (attrValues config.boot.initrd.secrets);
|
||||
scfg:
|
||||
builtins.isPath scfg.source
|
||||
|| (builtins.isString scfg.source && hasPrefix builtins.storeDir scfg.source)
|
||||
) (attrValues config.boot.initrd.secretPaths);
|
||||
message = ''
|
||||
boot.initrd.secrets values must be unquoted paths when
|
||||
using a bootloader that doesn't natively support initrd
|
||||
secrets, e.g.:
|
||||
When using a bootloader that doesn't natively support initrd secrets,
|
||||
all `boot.initrd.secretPaths` values must be defined via
|
||||
`boot.initrd.secretsPaths.*.source`, and the `source` values must be
|
||||
unquoted paths, e.g.
|
||||
|
||||
boot.initrd.secrets = {
|
||||
"/etc/secret" = /path/to/secret;
|
||||
boot.initrd.secretPaths = {
|
||||
"/etc/secret".source = /path/to/secret;
|
||||
};
|
||||
|
||||
Note that this will result in all secrets being stored
|
||||
@@ -763,6 +867,18 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
warnings = lib.optional (config.boot.initrd.secrets != { }) ''
|
||||
The option `boot.initrd.secrets` has been deprecated in favour of `boot.initrd.secretPaths`.
|
||||
'';
|
||||
|
||||
# Backwards compatibility to the legacy `boot.initrd.secrets` option.
|
||||
boot.initrd.secretPaths = lib.mapAttrs' (dest: source: {
|
||||
# The legacy boot.initrd.secrets option didn't type-check the attr
|
||||
# names, so we need to optionally prepend a slash.
|
||||
name = "${lib.optionalString (!lib.hasPrefix "/" dest) "/"}${dest}";
|
||||
value.source = if dest != null then source else dest;
|
||||
}) config.boot.initrd.secrets;
|
||||
|
||||
system.build = mkMerge [
|
||||
{
|
||||
inherit
|
||||
|
||||
@@ -10,14 +10,21 @@
|
||||
# Copy secrets into the initrd if they cannot be appended
|
||||
boot.initrd.systemd.contents = lib.mkIf (!config.boot.loader.supportsInitrdSecrets) (
|
||||
lib.mapAttrs' (
|
||||
dest: source:
|
||||
lib.nameValuePair "/.initrd-secrets/${dest}" { source = if source == null then dest else source; }
|
||||
) config.boot.initrd.secrets
|
||||
_: scfg:
|
||||
let
|
||||
prefix = lib.optionalString scfg.intermediateSecretsDir "/.initrd-secrets";
|
||||
in
|
||||
lib.nameValuePair "${prefix}${scfg.path}" { inherit (scfg) source; }
|
||||
) config.boot.initrd.secretPaths
|
||||
);
|
||||
|
||||
# Copy secrets to their respective locations
|
||||
boot.initrd.systemd.services.initrd-nixos-copy-secrets =
|
||||
lib.mkIf (config.boot.initrd.secrets != { })
|
||||
lib.mkIf
|
||||
(
|
||||
(builtins.any (x: x.intermediateSecretsDir) (builtins.attrValues config.boot.initrd.secretPaths))
|
||||
|| config.boot.initrd.extraSecretsHook != ""
|
||||
)
|
||||
{
|
||||
description = "Copy secrets into place";
|
||||
# Run as early as possible
|
||||
@@ -34,10 +41,12 @@
|
||||
# drop this service, we'd mount the /run tmpfs over the secret, making it
|
||||
# invisible in stage 2.
|
||||
script = ''
|
||||
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
|
||||
mkdir -p "$(dirname "/$secret")"
|
||||
cp "/.initrd-secrets/$secret" "/$secret"
|
||||
done
|
||||
if [ -d /.initrd-secrets ]; then
|
||||
for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
|
||||
mkdir -p "$(dirname "/$secret")"
|
||||
cp "/.initrd-secrets/$secret" "/$secret"
|
||||
done
|
||||
fi
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
|
||||
@@ -51,7 +51,7 @@ in
|
||||
config = mkMerge [
|
||||
{
|
||||
# minimal configuration file to make lvmconfig/lvm2-activation-generator happy
|
||||
environment.etc."lvm/lvm.conf".text = "config {}";
|
||||
environment.etc."lvm/lvm.conf".text = lib.mkBefore "config {}";
|
||||
}
|
||||
(mkIf cfg.enable {
|
||||
systemd.tmpfiles.packages = [ cfg.package.out ];
|
||||
@@ -59,6 +59,8 @@ in
|
||||
systemd.packages = [ cfg.package ];
|
||||
|
||||
services.udev.packages = [ cfg.package.out ];
|
||||
environment.etc."lvm/lvm.conf".text =
|
||||
"global/lvresize_fs_helper_executable = ${pkgs.lvm2.scripts}/libexec/lvresize_fs_helper";
|
||||
})
|
||||
(mkIf config.boot.initrd.services.lvm.enable {
|
||||
# We need lvm2 for the device-mapper rules
|
||||
@@ -71,9 +73,7 @@ in
|
||||
systemd.sockets."dm-event".wantedBy = [ "sockets.target" ];
|
||||
systemd.services."lvm2-monitor".wantedBy = [ "sysinit.target" ];
|
||||
|
||||
environment.etc."lvm/lvm.conf".text = ''
|
||||
dmeventd/executable = "${cfg.package}/bin/dmeventd"
|
||||
'';
|
||||
environment.etc."lvm/lvm.conf".text = "dmeventd/executable = ${cfg.package}/bin/dmeventd";
|
||||
services.lvm.package = mkDefault pkgs.lvm2_dmeventd;
|
||||
})
|
||||
(mkIf cfg.boot.thin.enable {
|
||||
|
||||
@@ -86,7 +86,8 @@ in
|
||||
|
||||
options.testing = {
|
||||
backdoor = lib.mkEnableOption "backdoor service in stage 2" // {
|
||||
default = true;
|
||||
# See assertion below for why the backdoor doesn't work with containers.
|
||||
default = !config.boot.isContainer;
|
||||
};
|
||||
|
||||
initrdBackdoor = lib.mkEnableOption ''
|
||||
@@ -105,7 +106,20 @@ in
|
||||
{
|
||||
assertion = cfg.initrdBackdoor -> config.boot.initrd.systemd.enable;
|
||||
message = ''
|
||||
testing.initrdBackdoor requires boot.initrd.systemd.enable to be enabled.
|
||||
`testing.initrdBackdoor` requires `boot.initrd.systemd.enable` to be enabled.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = config.boot.isContainer -> !cfg.backdoor;
|
||||
message = ''
|
||||
`testing.backdoor` uses virtio console, which does not work with
|
||||
containers (we use `nsenter` instead).
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = config.boot.isContainer -> !cfg.initrdBackdoor;
|
||||
message = ''
|
||||
`testing.initrdBackdoor` does not work with containers as there is no initrd.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
@@ -71,7 +71,7 @@ in
|
||||
virtualisation.vlans = lib.mkOption {
|
||||
type = types.listOf types.ints.unsigned;
|
||||
default = if cfg.interfaces == { } then [ 1 ] else [ ];
|
||||
defaultText = lib.literalExpression "if cfg.interfaces == {} then [ 1 ] else [ ]";
|
||||
defaultText = lib.literalExpression "if config.virtualisation.interfaces == {} then [ 1 ] else [ ]";
|
||||
example = [
|
||||
1
|
||||
2
|
||||
|
||||
@@ -70,6 +70,45 @@ in
|
||||
config = {
|
||||
boot.isNspawnContainer = true;
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.specialisation == { };
|
||||
message = ''
|
||||
Setting 'specialisation' is disallowed for systemd-nspawn container configurations.
|
||||
Activating a specialisation requires creating SUID wrappers (e.g., for 'sudo'),
|
||||
which is prohibited within the Nix build sandbox where the test is run.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# Check every interface defined in allInterfaces.
|
||||
# Containers try to create a bridge "${config.system.name}-${interfaceName}"
|
||||
assertion = lib.all (
|
||||
iface:
|
||||
let
|
||||
hostName = "${config.system.name}-${iface.name}";
|
||||
in
|
||||
lib.stringLength hostName <= 15
|
||||
) (lib.attrValues cfg.allInterfaces);
|
||||
|
||||
message =
|
||||
let
|
||||
offendingInterfaces = lib.filter (
|
||||
iface: lib.stringLength "${config.system.name}-${iface.name}" > 15
|
||||
) (lib.attrValues cfg.allInterfaces);
|
||||
offenderList = map (
|
||||
i:
|
||||
"${config.system.name}-${i.name} (${toString (lib.stringLength "${config.system.name}-${i.name}")} chars)"
|
||||
) offendingInterfaces;
|
||||
in
|
||||
''
|
||||
The following generated host interface names exceed the Linux 15-character limit:
|
||||
${lib.concatStringsSep "\n " offenderList}
|
||||
|
||||
Please shorten 'config.system.name' or the interface names in 'virtualisation.interfaces'.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
# TODO(arianvp): Remove after https://github.com/NixOS/nixpkgs/pull/480686 is merged
|
||||
console.enable = true;
|
||||
|
||||
@@ -94,6 +133,9 @@ in
|
||||
# > kind of unit allocation or registration with systemd-machined.
|
||||
"--keep-unit"
|
||||
"--register=no"
|
||||
|
||||
# Send a READY=1 notification to a socket when the container is fully booted.
|
||||
"--notify-ready=yes"
|
||||
];
|
||||
|
||||
system.build.nspawn =
|
||||
|
||||
@@ -68,7 +68,9 @@ def ensure_vlan_bridge(vlan: int) -> typing.Generator[str, None, None]:
|
||||
ipv6_addr = f"2001:db8:{vlan}::fe/64"
|
||||
|
||||
bridge_name = f"br{vlan}"
|
||||
tap_name = f"vde-tap{vlan}"
|
||||
bridge_path = Path("/sys/class/net") / bridge_name
|
||||
tap_path = Path("/sys/class/net") / tap_name
|
||||
try:
|
||||
# To avoid racing against other nspawn containers that also
|
||||
# need this vlan, grab an exclusive lock.
|
||||
@@ -80,6 +82,19 @@ def ensure_vlan_bridge(vlan: int) -> typing.Generator[str, None, None]:
|
||||
run_ip("addr", "add", ipv4_addr, "dev", bridge_name)
|
||||
run_ip("addr", "add", ipv6_addr, "dev", bridge_name)
|
||||
|
||||
if tap_path.exists():
|
||||
logger.info(f"attaching {tap_name} to {bridge_name}")
|
||||
run_ip("link", "set", tap_name, "master", bridge_name)
|
||||
run_ip("link", "set", tap_name, "up")
|
||||
else:
|
||||
logger.warning(
|
||||
f"TAP {tap_name} not found; container will be isolated from VDE"
|
||||
)
|
||||
if not Path("/dev/net").exists():
|
||||
logger.warning(
|
||||
"A common reason for this is that /dev/net is not available in the Nix sandbox. Try adding /dev/net to extra-sandbox-paths."
|
||||
)
|
||||
|
||||
yield bridge_name
|
||||
finally:
|
||||
# To avoid racing against other nspawn containers that also
|
||||
@@ -126,6 +141,7 @@ def mk_veth(
|
||||
def run(
|
||||
container_name: str,
|
||||
root_dir_str: str,
|
||||
shared_dir_str: typing.Optional[str],
|
||||
interfaces: dict,
|
||||
nspawn_options: list[str],
|
||||
init: str,
|
||||
@@ -166,12 +182,19 @@ def run(
|
||||
flush=True,
|
||||
)
|
||||
|
||||
shared_dir = Path(shared_dir_str) if shared_dir_str else None
|
||||
|
||||
cp = subprocess.Popen(
|
||||
[
|
||||
"@systemd-nspawn@",
|
||||
*nspawn_options,
|
||||
f"--directory={root_dir}",
|
||||
f"--network-namespace-path={netns.path}",
|
||||
*(
|
||||
[f"--bind={shared_dir}:/tmp/shared"]
|
||||
if shared_dir is not None
|
||||
else []
|
||||
),
|
||||
init,
|
||||
*cmdline,
|
||||
],
|
||||
@@ -218,6 +241,11 @@ def main():
|
||||
required=True,
|
||||
help="Path to container root directory (overridable with RUN_NSPAWN_ROOT_DIR)",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--shared-dir",
|
||||
required=False,
|
||||
help="Path to a shared directory to bind-mount into the container at /tmp/shared (overridable with RUN_NSPAWN_SHARED_DIR)",
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
"--interfaces-json",
|
||||
dest="interfaces",
|
||||
@@ -239,6 +267,7 @@ def main():
|
||||
run(
|
||||
container_name=args.container_name,
|
||||
root_dir_str=os.getenv("RUN_NSPAWN_ROOT_DIR", default=args.root_dir),
|
||||
shared_dir_str=os.getenv("RUN_NSPAWN_SHARED_DIR", default=args.shared_dir),
|
||||
interfaces=args.interfaces,
|
||||
nspawn_options=nspawn_options,
|
||||
init=args.init,
|
||||
|
||||
@@ -168,6 +168,7 @@ in
|
||||
node-name = runTest ./nixos-test-driver/node-name.nix;
|
||||
busybox = runTest ./nixos-test-driver/busybox.nix;
|
||||
console-log = runTest ./nixos-test-driver/console-log.nix;
|
||||
containers = runTest ./nixos-test-driver/containers.nix;
|
||||
driver-timeout =
|
||||
pkgs.runCommand "ensure-timeout-induced-failure"
|
||||
{
|
||||
@@ -1138,9 +1139,6 @@ in
|
||||
nixos-rebuild-target-host = runTest {
|
||||
imports = [ ./nixos-rebuild-target-host.nix ];
|
||||
};
|
||||
nixos-rebuild-target-host-interrupted = runTest {
|
||||
imports = [ ./nixos-rebuild-target-host-interrupted.nix ];
|
||||
};
|
||||
nixpkgs = pkgs.callPackage ../modules/misc/nixpkgs/test.nix { inherit evalMinimalConfig; };
|
||||
nixpkgs-config-allow-unfree =
|
||||
pkgs.callPackage ../modules/misc/nixpkgs/test-nixpkgs-config-allow-unfree.nix
|
||||
@@ -1649,6 +1647,7 @@ in
|
||||
teleports = runTest ./teleports.nix;
|
||||
temporal = runTest ./temporal.nix;
|
||||
terminal-emulators = handleTest ./terminal-emulators.nix { };
|
||||
test-containers-bittorrent = runTest ./test-containers-bittorrent.nix;
|
||||
thanos = runTest ./thanos.nix;
|
||||
thelounge = handleTest ./thelounge.nix { };
|
||||
tiddlywiki = runTest ./tiddlywiki.nix;
|
||||
|
||||
@@ -128,7 +128,7 @@ in
|
||||
environment.systemPackages = [ pkgs.jq ];
|
||||
# It's probably the case, but we want to make it explicit here.
|
||||
boot.initrd.enable = true;
|
||||
boot.initrd.secrets."/some/example" = pkgs.writeText "example-secret" "test";
|
||||
boot.initrd.secretPaths."/some/example".source = pkgs.writeText "example-secret" "test";
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
|
||||
@contextmanager
|
||||
def record_audio(machine: Machine):
|
||||
def record_audio(machine: BaseMachine):
|
||||
"""
|
||||
Perform actions while recording the
|
||||
machine audio output.
|
||||
@@ -56,7 +56,7 @@
|
||||
machine.systemctl("stop audio-recorder")
|
||||
|
||||
|
||||
def wait_for_sound(machine: Machine):
|
||||
def wait_for_sound(machine: BaseMachine):
|
||||
"""
|
||||
Wait until any sound has been emitted.
|
||||
"""
|
||||
|
||||
@@ -47,7 +47,7 @@ in
|
||||
};
|
||||
};
|
||||
virtualisation.rootDevice = "/dev/mapper/cryptroot";
|
||||
boot.initrd.secrets."/etc/cryptroot.key" = keyfile;
|
||||
boot.initrd.secretPaths."/etc/cryptroot.key".source = keyfile;
|
||||
};
|
||||
|
||||
specialisation.boot-luks-missing-keyfile.configuration = {
|
||||
|
||||
@@ -21,16 +21,16 @@ testing.makeTest {
|
||||
|
||||
boot.loader.grub.device = "/dev/vda";
|
||||
|
||||
boot.initrd.secrets = {
|
||||
"/test" = secret1InStore;
|
||||
"/run/keys/test" = secret1InStore;
|
||||
boot.initrd.secretPaths = {
|
||||
"/test".source = secret1InStore;
|
||||
"/run/keys/test".source = secret1InStore;
|
||||
};
|
||||
boot.initrd.postMountCommands = "cp /test /mnt-root/secret-from-initramfs";
|
||||
|
||||
specialisation.secrets2System.configuration = {
|
||||
boot.initrd.secrets = lib.mkForce {
|
||||
"/test" = secret2InStore;
|
||||
"/run/keys/test" = secret2InStore;
|
||||
boot.initrd.secretPaths = lib.mkForce {
|
||||
"/test".source = secret2InStore;
|
||||
"/run/keys/test".source = secret2InStore;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,14 +21,26 @@ let
|
||||
{ ... }:
|
||||
{
|
||||
virtualisation.useBootLoader = true;
|
||||
boot.initrd.secrets = {
|
||||
"/test" = secretInStore;
|
||||
boot.initrd.secretPaths = {
|
||||
"/test" = {
|
||||
source = secretInStore;
|
||||
intermediateSecretsDir = false;
|
||||
};
|
||||
|
||||
# This should *not* need to be copied in postMountCommands
|
||||
"/run/keys/test" = secretInStore;
|
||||
"/run/keys/test1".source = secretInStore;
|
||||
|
||||
"/run/keys/test2".generateSecretCommand = pkgs.writeShellScript "copy-secret" ''
|
||||
cp ${secretInStore} "$out"
|
||||
'';
|
||||
};
|
||||
boot.initrd.extraSecretsHook = ''
|
||||
mkdir -p etc/secrets
|
||||
cp ${secretInStore} etc/secrets/test2
|
||||
'';
|
||||
boot.initrd.postMountCommands = ''
|
||||
cp /test /mnt-root/secret-from-initramfs
|
||||
cp /test /mnt-root/secret-from-initramfs-1
|
||||
cp /etc/secrets/test2 /mnt-root/secret-from-initramfs-2
|
||||
'';
|
||||
boot.initrd.compressor = compressor;
|
||||
# zstd compression is only supported from 5.9 onwards. Remove when 5.10 becomes default.
|
||||
@@ -39,8 +51,10 @@ let
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
machine.succeed(
|
||||
"cmp ${secretInStore} /secret-from-initramfs",
|
||||
"cmp ${secretInStore} /run/keys/test",
|
||||
"cmp ${secretInStore} /secret-from-initramfs-1",
|
||||
"cmp ${secretInStore} /secret-from-initramfs-2",
|
||||
"cmp ${secretInStore} /run/keys/test1",
|
||||
"cmp ${secretInStore} /run/keys/test2",
|
||||
)
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ let
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
''}
|
||||
|
||||
boot.initrd.secrets."/etc/secret" = "/etc/nixos/secret";
|
||||
boot.initrd.secretPaths."/etc/secret".source = "/etc/nixos/secret";
|
||||
|
||||
${optionalString clevisTest ''
|
||||
boot.kernelParams = [ "console=tty0" "ip=192.168.1.1:::255.255.255.0::eth1:none" ];
|
||||
@@ -1385,7 +1385,7 @@ in
|
||||
};
|
||||
|
||||
# Full disk encryption (root, kernel and initrd encrypted) using GRUB, GPT/UEFI,
|
||||
# LVM-on-LUKS and a keyfile in initrd.secrets to enter the passphrase once
|
||||
# LVM-on-LUKS and a keyfile in initrd.secretPaths to enter the passphrase once
|
||||
fullDiskEncryption = makeInstallerTest "fullDiskEncryption" {
|
||||
createPartitions = ''
|
||||
installer.succeed(
|
||||
@@ -1419,7 +1419,7 @@ in
|
||||
boot.loader.grub.enableCryptodisk = true;
|
||||
boot.loader.efi.efiSysMountPoint = "/boot/efi";
|
||||
|
||||
boot.initrd.secrets."/luks.key" = "/etc/nixos/luks.key";
|
||||
boot.initrd.secretPaths."/luks.key" = "/etc/nixos/luks.key";
|
||||
boot.initrd.luks.devices.crypt =
|
||||
{ device = "/dev/vda2";
|
||||
keyFile = "/luks.key";
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
assert "http://0.0.0.0:8080" in machine.succeed("echo $JENKINS_URL")
|
||||
|
||||
machine.succeed(
|
||||
"jenkins-cli -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword)"
|
||||
"jenkins-cli -http -auth admin:$(cat /var/lib/jenkins/secrets/initialAdminPassword)"
|
||||
)
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
master.wait_until_succeeds("test -f ${jenkinsHome}/jobs/folder-1/jobs/job-2/config.xml")
|
||||
|
||||
# Verify that jenkins also sees the jobs.
|
||||
out = master.succeed("${pkgs.jenkins}/bin/jenkins-cli -s ${jenkinsUrl} -auth admin:$(cat ${jenkinsHome}/secrets/initialAdminPassword) list-jobs")
|
||||
out = master.succeed("${pkgs.jenkins}/bin/jenkins-cli -http -s ${jenkinsUrl} -auth admin:$(cat ${jenkinsHome}/secrets/initialAdminPassword) list-jobs")
|
||||
jobs = [x.strip() for x in out.splitlines()]
|
||||
# Seeing jobs inside folders requires the Folders plugin
|
||||
# (https://plugins.jenkins.io/cloudbees-folder/), which we don't have
|
||||
@@ -135,7 +135,7 @@
|
||||
master.wait_until_fails("test -f ${jenkinsHome}/jobs/folder-1/jobs/job-2/config.xml")
|
||||
|
||||
# Verify that jenkins also sees the jobs as removed.
|
||||
out = master.succeed("${pkgs.jenkins}/bin/jenkins-cli -s ${jenkinsUrl} -auth admin:$(cat ${jenkinsHome}/secrets/initialAdminPassword) list-jobs")
|
||||
out = master.succeed("${pkgs.jenkins}/bin/jenkins-cli -http -s ${jenkinsUrl} -auth admin:$(cat ${jenkinsHome}/secrets/initialAdminPassword) list-jobs")
|
||||
jobs = [x.strip() for x in out.splitlines()]
|
||||
assert jobs == [], f"jobs != []: {jobs}"
|
||||
'';
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
{ hostPkgs, ... }:
|
||||
|
||||
# This test recreates a remote deployment scenario where the connection
|
||||
# between deployer and target is closed during the deployment - in this
|
||||
# case because the connection goes over a 'reverse ssh' tunnel service
|
||||
# that has changes that are being deployed.
|
||||
|
||||
# This is not seamless (the deployer doesn't get to see the logs after
|
||||
# the disconnect), but is a lot better than the old behaviour, where
|
||||
# the switch was aborted and the connection never restored.
|
||||
|
||||
{
|
||||
name = "nixos-rebuild-target-host-interrupted";
|
||||
|
||||
# TODO: remove overlay from nixos/modules/profiles/installation-device.nix
|
||||
# make it a _small package instead, then remove pkgsReadOnly = false;.
|
||||
node.pkgsReadOnly = false;
|
||||
|
||||
nodes = {
|
||||
deployer =
|
||||
{
|
||||
nodes,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../modules/profiles/installation-device.nix
|
||||
];
|
||||
|
||||
nix.settings = {
|
||||
substituters = lib.mkForce [ ];
|
||||
hashed-mirrors = null;
|
||||
connect-timeout = 1;
|
||||
};
|
||||
|
||||
system.includeBuildDependencies = true;
|
||||
|
||||
virtualisation = {
|
||||
cores = 2;
|
||||
memorySize = 3072;
|
||||
};
|
||||
|
||||
services.openssh.enable = true;
|
||||
users.users.root.openssh.authorizedKeys.keys = [ nodes.target.system.build.publicKey ];
|
||||
system.extraDependencies = [
|
||||
# so that it doesn't need to be built inside the test
|
||||
pkgs.nixVersions.latest
|
||||
];
|
||||
|
||||
system.build.privateKey = snakeOilPrivateKey;
|
||||
system.build.publicKey = snakeOilPublicKey;
|
||||
system.switch.enable = true;
|
||||
|
||||
services.getty.autologinUser = lib.mkForce "root";
|
||||
};
|
||||
|
||||
target =
|
||||
{
|
||||
nodes,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
|
||||
targetConfig = {
|
||||
documentation.enable = false;
|
||||
services.openssh.enable = true;
|
||||
system.build.privateKey = snakeOilPrivateKey;
|
||||
system.build.publicKey = snakeOilPublicKey;
|
||||
|
||||
users.users.root.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ];
|
||||
users.users.alice.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ];
|
||||
users.users.bob.openssh.authorizedKeys.keys = [ nodes.deployer.system.build.publicKey ];
|
||||
|
||||
users.users.alice.extraGroups = [ "wheel" ];
|
||||
users.users.bob.extraGroups = [ "wheel" ];
|
||||
|
||||
# Disable sudo for root to ensure sudo isn't called without `--sudo`
|
||||
security.sudo.extraRules = lib.mkForce [
|
||||
{
|
||||
groups = [ "wheel" ];
|
||||
commands = [ { command = "ALL"; } ];
|
||||
}
|
||||
{
|
||||
users = [ "alice" ];
|
||||
commands = [
|
||||
{
|
||||
command = "ALL";
|
||||
options = [ "NOPASSWD" ];
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
nix.settings.trusted-users = [ "@wheel" ];
|
||||
|
||||
services.autossh-ng.sessions.will-be-interrupted-by-rebuild = {
|
||||
user = "root";
|
||||
destination = "deployer";
|
||||
extraArguments = "-R2222:localhost:22";
|
||||
hostKeyChecking = false;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [ ./common/user-account.nix ];
|
||||
|
||||
config = lib.mkMerge [
|
||||
targetConfig
|
||||
{
|
||||
system.build = {
|
||||
inherit targetConfig;
|
||||
};
|
||||
system.switch.enable = true;
|
||||
|
||||
networking.hostName = "target";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
sshConfig = builtins.toFile "ssh.conf" ''
|
||||
UserKnownHostsFile=/dev/null
|
||||
StrictHostKeyChecking=no
|
||||
'';
|
||||
|
||||
targetConfigJSON = hostPkgs.writeText "target-configuration.json" (
|
||||
builtins.toJSON nodes.target.system.build.targetConfig
|
||||
);
|
||||
|
||||
targetNetworkJSON = hostPkgs.writeText "target-network.json" (
|
||||
builtins.toJSON nodes.target.system.build.networkConfig
|
||||
);
|
||||
|
||||
configFile =
|
||||
hostname:
|
||||
hostPkgs.writeText "configuration.nix" # nix
|
||||
''
|
||||
{ lib, pkgs, modulesPath, ... }: {
|
||||
imports = [
|
||||
(modulesPath + "/virtualisation/qemu-vm.nix")
|
||||
(modulesPath + "/testing/test-instrumentation.nix")
|
||||
(modulesPath + "/../tests/common/user-account.nix")
|
||||
(lib.modules.importJSON ./target-configuration.json)
|
||||
(lib.modules.importJSON ./target-network.json)
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
|
||||
boot.loader.grub = {
|
||||
enable = true;
|
||||
device = "/dev/vda";
|
||||
forceInstall = true;
|
||||
};
|
||||
|
||||
# needed to make NIX_SSHOPTS work for nix-copy-closure
|
||||
# 2.31.3 (current default) break, 2.32.6 and 2.33.3 (current latest) work
|
||||
# let's use the default here again once the fix has made it there.
|
||||
nix.package = pkgs.nixVersions.latest;
|
||||
|
||||
# We're changing the '-E' parameter to the new hostname here,
|
||||
# not because we care about the logs, but because we want to
|
||||
# force the scenario where the connection is broken during the
|
||||
# deployment (because the autossh-ng service is stopped and
|
||||
# started):
|
||||
services.autossh-ng.sessions.will-be-interrupted-by-rebuild.extraArguments = "-R2222:localhost:22 -E ${hostname}";
|
||||
|
||||
# this will be asserted to validate the switch happened:
|
||||
networking.hostName = "${hostname}";
|
||||
}
|
||||
'';
|
||||
in
|
||||
# python
|
||||
''
|
||||
start_all()
|
||||
target.wait_for_open_port(22)
|
||||
|
||||
deployer.wait_until_succeeds("ping -c1 target")
|
||||
deployer.succeed("install -Dm 600 ${nodes.deployer.system.build.privateKey} ~root/.ssh/id_ecdsa")
|
||||
deployer.succeed("install ${sshConfig} ~root/.ssh/config")
|
||||
|
||||
target.succeed("nixos-generate-config")
|
||||
target.succeed("install -Dm 600 ${nodes.target.system.build.privateKey} ~root/.ssh/id_ecdsa")
|
||||
deployer.succeed("scp alice@target:/etc/nixos/hardware-configuration.nix /root/hardware-configuration.nix")
|
||||
target.wait_for_unit("autossh-ng-will-be-interrupted-by-rebuild.service")
|
||||
|
||||
deployer.copy_from_host("${configFile "config-1-deployed"}", "/root/configuration-1.nix")
|
||||
deployer.copy_from_host("${configFile "config-2-deployed"}", "/root/configuration-2.nix")
|
||||
deployer.copy_from_host("${targetNetworkJSON}", "/root/target-network.json")
|
||||
deployer.copy_from_host("${targetConfigJSON}", "/root/target-configuration.json")
|
||||
|
||||
with subtest("Deploy to alice@target via reverse ssh"):
|
||||
deployer.wait_for_unit("multi-user.target")
|
||||
# Uses TTY/send_chars instead of deployer.succeed to set NIX_SSHOPTS
|
||||
deployer.send_chars("NIX_SSHOPTS=\"-p 2222\" nixos-rebuild switch -I nixos-config=/root/configuration-1.nix --target-host alice@localhost --sudo\n")
|
||||
|
||||
# the connection breaks, but the 'switch' should now continue in the background:
|
||||
deployer.wait_until_tty_matches("1", "error: while running command with remote sudo")
|
||||
|
||||
def deployed(last_try: bool) -> bool:
|
||||
target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname", timeout=20).rstrip()
|
||||
if last_try:
|
||||
print(f"Still seeing hostname {target_hostname}")
|
||||
return target_hostname == "config-1-deployed"
|
||||
retry(deployed)
|
||||
|
||||
with subtest("Deploy to bob@target via reverse ssh with password-based sudo"):
|
||||
deployer.wait_for_unit("multi-user.target")
|
||||
# Uses TTY/send_chars instead of deployer.succeed to set NIX_SSHOPTS and for ask-sudo-password
|
||||
deployer.send_chars("NIX_SSHOPTS=\"-p 2222\" nixos-rebuild switch -I nixos-config=/root/configuration-2.nix --target-host bob@localhost --ask-sudo-password\n")
|
||||
deployer.wait_until_tty_matches("1", "password for bob")
|
||||
deployer.send_chars("${nodes.target.users.users.bob.password}\n")
|
||||
|
||||
# the connection breaks, but the 'switch' should now continue in the background:
|
||||
deployer.wait_until_tty_matches("1", "error: while running command with remote sudo")
|
||||
|
||||
def deployed(last_try: bool) -> bool:
|
||||
target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname", timeout=20).rstrip()
|
||||
if last_try:
|
||||
print(f"Still seeing hostname {target_hostname}")
|
||||
return target_hostname == "config-2-deployed"
|
||||
retry(deployed)
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
name = "containers";
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ jfly ];
|
||||
|
||||
nodes = {
|
||||
n1 = {
|
||||
virtualisation.vlans = [ 1 ];
|
||||
};
|
||||
n2 = {
|
||||
virtualisation.vlans = [
|
||||
2
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
containers = {
|
||||
c1 = {
|
||||
virtualisation.vlans = [ 1 ];
|
||||
};
|
||||
c2 = {
|
||||
virtualisation.vlans = [ 2 ];
|
||||
};
|
||||
c12 = {
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = /* python */ ''
|
||||
c1.start()
|
||||
c2.start()
|
||||
c12.start()
|
||||
|
||||
c1.succeed("echo hello > /hello.txt")
|
||||
c1.copy_from_machine("/hello.txt")
|
||||
|
||||
c1.systemctl("start network-online.target")
|
||||
c2.systemctl("start network-online.target")
|
||||
c12.systemctl("start network-online.target")
|
||||
c1.wait_for_unit("network-online.target")
|
||||
c2.wait_for_unit("network-online.target")
|
||||
c12.wait_for_unit("network-online.target")
|
||||
|
||||
# Confirm containers in vlan 1 can talk to each other.
|
||||
c1.succeed("ping -c 1 c12")
|
||||
c12.succeed("ping -c 1 c1")
|
||||
|
||||
# Confirm containers in vlan 2 can talk to each other.
|
||||
c2.succeed("ping -c 1 c12")
|
||||
c12.succeed("ping -c 1 c2")
|
||||
|
||||
# Confirm containers in separate vlans cannot talk to each other.
|
||||
c1.fail("ping -c 1 -W 1 c2")
|
||||
|
||||
n1.start()
|
||||
n2.start()
|
||||
n1.systemctl("start network-online.target")
|
||||
n2.systemctl("start network-online.target")
|
||||
n1.wait_for_unit("network-online.target")
|
||||
n2.wait_for_unit("network-online.target")
|
||||
|
||||
# Confirm containers and nodes in the same vlan can talk to each other.
|
||||
c1.succeed("ping -c 1 n1")
|
||||
n1.succeed("ping -c 1 c1")
|
||||
c2.succeed("ping -c 1 n2")
|
||||
n2.succeed("ping -c 1 c2")
|
||||
|
||||
# Confirm containers and nodes in different vlans cannot talk to each other.
|
||||
c1.fail("ping -c 1 -W 1 n2")
|
||||
n1.fail("ping -c 1 -W 1 c2")
|
||||
c2.fail("ping -c 1 -W 1 n1")
|
||||
n2.fail("ping -c 1 -W 1 c1")
|
||||
'';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{ lib, ... }:
|
||||
{ config, lib, ... }:
|
||||
|
||||
{
|
||||
name = "olivetin";
|
||||
@@ -41,16 +41,26 @@
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
import shlex
|
||||
|
||||
machine.wait_for_unit("olivetin.service")
|
||||
machine.wait_for_open_port(8000)
|
||||
|
||||
response = json.loads(machine.succeed("curl http://localhost:8000/api/StartActionByGetAndWait/hello_world"))
|
||||
def start_action(action):
|
||||
if "${config.nodes.machine.services.olivetin.package.releaseSeries}" == "2k":
|
||||
cmd = f"curl http://localhost:8000/api/StartActionByGetAndWait/{action}"
|
||||
else:
|
||||
req = {"actionId": action}
|
||||
cmd = f"curl -H 'Content-Type: application/json' http://localhost:8000/api/olivetin.api.v1.OliveTinApiService/StartActionAndWait -d {shlex.quote(json.dumps(req))}"
|
||||
|
||||
return json.loads(machine.succeed(cmd))
|
||||
|
||||
response = start_action("hello_world")
|
||||
assert response["logEntry"]["exitCode"] == 0
|
||||
assert response["logEntry"]["output"] == "Hello World!"
|
||||
assert machine.succeed("cat /tmp/result") == "Hello World!"
|
||||
|
||||
response = json.loads(machine.succeed("curl http://localhost:8000/api/StartActionByGetAndWait/secret"))
|
||||
response = start_action("secret")
|
||||
assert response["logEntry"]["exitCode"] == 0
|
||||
assert machine.succeed("cat /tmp/result2") == "secret"
|
||||
'';
|
||||
|
||||
@@ -19,9 +19,12 @@ in
|
||||
meta.maintainers = with lib.maintainers; [ defelo ];
|
||||
|
||||
nodes.seed = {
|
||||
virtualisation.credentials = {
|
||||
"xyz.radicle.node.secret".source = "${seed-ssh-keys.snakeOilEd25519PrivateKey}";
|
||||
};
|
||||
|
||||
services.radicle = {
|
||||
enable = true;
|
||||
privateKeyFile = seed-ssh-keys.snakeOilEd25519PrivateKey;
|
||||
publicKey = seed-ssh-keys.snakeOilEd25519PublicKey;
|
||||
node.openFirewall = true;
|
||||
settings = {
|
||||
|
||||
@@ -76,9 +76,12 @@ in
|
||||
{
|
||||
imports = [ commonHostConfig ];
|
||||
|
||||
virtualisation.credentials = {
|
||||
"xyz.radicle.node.secret".source = "${seed-ssh-keys.snakeOilEd25519PrivateKey}";
|
||||
};
|
||||
|
||||
services.radicle = {
|
||||
enable = true;
|
||||
privateKeyFile = seed-ssh-keys.snakeOilEd25519PrivateKey;
|
||||
publicKey = seed-ssh-keys.snakeOilEd25519PublicKey;
|
||||
node = {
|
||||
openFirewall = true;
|
||||
|
||||
@@ -38,7 +38,7 @@ in
|
||||
};
|
||||
};
|
||||
virtualisation.rootDevice = "/dev/mapper/cryptroot";
|
||||
boot.initrd.secrets."/etc/cryptroot.key" = keyfile;
|
||||
boot.initrd.secretPaths."/etc/cryptroot.key".source = keyfile;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# This test runs a Bittorrent tracker on one machine, and verifies
|
||||
# that two client machines can download the torrent using
|
||||
# `aria2c'. The first client (behind a NAT router) downloads
|
||||
# from the initial seeder running on the tracker. Then we kill the
|
||||
# initial seeder. The second client downloads from the first client,
|
||||
# which only works if the first client successfully uses the UPnP-IGD
|
||||
# protocol to poke a hole in the NAT.
|
||||
|
||||
# We use aria2 as the initial seeder because transmission
|
||||
# fails in the sandbox because of systemd hardening settings,
|
||||
# namely MountAPIVFS=yes, so we get the following error:
|
||||
|
||||
# $ journalctl --unit transmission.service
|
||||
# (n-daemon)[417]: transmission.service: Failed to create destination mount point node '/run/transmission/run/host/.os-release-stage/', ignoring: Read-only file system
|
||||
# (n-daemon)[417]: transmission.service: Failed to mount /run/systemd/propagate/.os-release-stage to /run/transmission/run/host/.os-release-stage/: No such file or directory
|
||||
# (n-daemon)[417]: transmission.service: Failed to set up mount namespacing: /run/host/.os-release-stage/: No such file or directory
|
||||
# (n-daemon)[417]: transmission.service: Failed at step NAMESPACE spawning /nix/store/zfksw9bllp95pl45d1nxmpd2lks42bkj-transmission-4.0.6/bin/transmission-daemon: No such file or directory
|
||||
# systemd[1]: transmission.service: Main process exited, code=exited, status=226/NAMESPACE
|
||||
|
||||
{ lib, hostPkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
# Some random file to serve.
|
||||
file = hostPkgs.hello.src;
|
||||
|
||||
internalRouterAddress = "192.168.3.1";
|
||||
internalClient1Address = "192.168.3.2";
|
||||
|
||||
# cannot use documentation networks (198.51.100.0/24 or 192.0.2.0/24) here
|
||||
# because miniupnpd recognizes them as such and refuses to work with them
|
||||
# https://github.com/miniupnp/miniupnp/blob/2a74cb2f27cacf06d2b50c187e8f90aa1f5c2528/miniupnpd/miniupnpd.c#L998
|
||||
externalRouterAddress = "80.100.100.1";
|
||||
externalClient2Address = "80.100.100.2";
|
||||
externalTrackerAddress = "80.100.100.3";
|
||||
|
||||
download-dir = "/tmp/aria2-downloads";
|
||||
peerConfig =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
pkgs.aria2
|
||||
pkgs.transmission_4 # only needed for transmission-create
|
||||
];
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
name = "bittorrent";
|
||||
meta = {
|
||||
maintainers = [
|
||||
lib.maintainers.kmein
|
||||
];
|
||||
};
|
||||
|
||||
containers = {
|
||||
tracker =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ peerConfig ];
|
||||
|
||||
virtualisation.vlans = [ 1 ];
|
||||
networking.firewall.enable = false;
|
||||
networking.interfaces.eth1.ipv4.addresses = [
|
||||
{
|
||||
address = externalTrackerAddress;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
|
||||
# We need Apache on the tracker to serve the torrents.
|
||||
services.httpd = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
"torrentserver.org" = {
|
||||
adminAddr = "foo@example.org";
|
||||
documentRoot = "/tmp";
|
||||
};
|
||||
};
|
||||
};
|
||||
services.opentracker.enable = true;
|
||||
};
|
||||
|
||||
router =
|
||||
{ pkgs, containers, ... }:
|
||||
{
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
networking.nat.enable = true;
|
||||
networking.nat.internalInterfaces = [ "eth2" ];
|
||||
networking.nat.externalInterface = "eth1";
|
||||
networking.firewall.enable = true;
|
||||
networking.firewall.trustedInterfaces = [ "eth2" ];
|
||||
networking.interfaces.eth0.ipv4.addresses = [ ];
|
||||
networking.interfaces.eth1.ipv4.addresses = [
|
||||
{
|
||||
address = externalRouterAddress;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
networking.interfaces.eth2.ipv4.addresses = [
|
||||
{
|
||||
address = internalRouterAddress;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
networking.nftables.enable = true;
|
||||
services.miniupnpd = {
|
||||
enable = true;
|
||||
externalInterface = "eth1";
|
||||
internalIPs = [ "eth2" ];
|
||||
appendConfig = ''
|
||||
ext_ip=${externalRouterAddress}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
client1 =
|
||||
{ pkgs, containers, ... }:
|
||||
{
|
||||
imports = [ peerConfig ];
|
||||
environment.systemPackages = [ pkgs.miniupnpc ];
|
||||
|
||||
virtualisation.vlans = [ 2 ];
|
||||
networking.interfaces.eth0.ipv4.addresses = [ ];
|
||||
networking.interfaces.eth1.ipv4.addresses = [
|
||||
{
|
||||
address = internalClient1Address;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
networking.defaultGateway = internalRouterAddress;
|
||||
networking.firewall.enable = false;
|
||||
};
|
||||
|
||||
client2 =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ peerConfig ];
|
||||
|
||||
virtualisation.vlans = [ 1 ];
|
||||
networking.interfaces.eth0.ipv4.addresses = [ ];
|
||||
networking.interfaces.eth1.ipv4.addresses = [
|
||||
{
|
||||
address = externalClient2Address;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
networking.firewall.enable = false;
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ containers, ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
# Wait for network and miniupnpd.
|
||||
router.systemctl("start network-online.target")
|
||||
router.wait_for_unit("network-online.target")
|
||||
router.wait_for_unit("miniupnpd")
|
||||
|
||||
# Create the torrent.
|
||||
tracker.succeed("mkdir -p ${download-dir}")
|
||||
tracker.succeed(
|
||||
"cp ${file} ${download-dir}/test.tar.bz2"
|
||||
)
|
||||
tracker.succeed(
|
||||
"transmission-create ${download-dir}/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent"
|
||||
)
|
||||
tracker.succeed("chmod 644 /tmp/test.torrent")
|
||||
|
||||
# Start the tracker
|
||||
tracker.systemctl("start network-online.target")
|
||||
tracker.wait_for_unit("network-online.target")
|
||||
tracker.wait_for_unit("opentracker.service")
|
||||
tracker.wait_for_open_port(6969)
|
||||
|
||||
# --- Start the initial seeder using aria2 ---
|
||||
# https://stackoverflow.com/a/44528978
|
||||
tracker.execute(
|
||||
"aria2c --enable-dht=false --seed-time=999 --dir=${download-dir} "
|
||||
"-V --seed-ratio=0.0 "
|
||||
"/tmp/test.torrent >/dev/null &"
|
||||
)
|
||||
|
||||
# --- Wait until the tracker shows we are seeding ---
|
||||
tracker.wait_until_succeeds("curl -s http://localhost:6969/stats | grep -q 'serving 1 torrents'")
|
||||
|
||||
# Now we should be able to download from the client behind the NAT.
|
||||
tracker.wait_for_unit("httpd")
|
||||
|
||||
def connect_from(machine):
|
||||
machine.systemctl("start network-online.target")
|
||||
machine.wait_for_unit("network-online.target")
|
||||
machine.execute(
|
||||
"aria2c --enable-dht=false --seed-time=999 --dir=${download-dir} "
|
||||
"http://${externalTrackerAddress}/test.torrent >/dev/null &"
|
||||
)
|
||||
machine.wait_until_succeeds(
|
||||
"cmp ${download-dir}/test.tar.bz2 ${file}"
|
||||
) # Wait for download to finish and verify
|
||||
|
||||
connect_from(client1)
|
||||
|
||||
# --- Bring down the initial seeder ---
|
||||
tracker.succeed("pkill aria2c")
|
||||
|
||||
# Now download from the second client. This can only succeed if
|
||||
# the first client created a NAT hole in the router.
|
||||
connect_from(client2)
|
||||
'';
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
autoreconfHook,
|
||||
automake,
|
||||
fftw,
|
||||
ladspaH,
|
||||
ladspa-header,
|
||||
libxml2,
|
||||
pkg-config,
|
||||
perlPackages,
|
||||
@@ -41,7 +41,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
];
|
||||
buildInputs = [
|
||||
fftw
|
||||
ladspaH
|
||||
ladspa-header
|
||||
libxml2
|
||||
];
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
ntk,
|
||||
libjack2,
|
||||
libsndfile,
|
||||
ladspaH,
|
||||
ladspa-header,
|
||||
liblo,
|
||||
libsigcxx,
|
||||
lrdf,
|
||||
@@ -37,7 +37,7 @@ stdenv.mkDerivation {
|
||||
ntk
|
||||
libjack2
|
||||
libsndfile
|
||||
ladspaH
|
||||
ladspa-header
|
||||
liblo
|
||||
libsigcxx
|
||||
lrdf
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
alsa-lib,
|
||||
dssiSupport ? false,
|
||||
dssi,
|
||||
ladspaH,
|
||||
ladspa-header,
|
||||
jackSupport ? true,
|
||||
libjack2,
|
||||
ossSupport ? true,
|
||||
@@ -108,7 +108,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
++ lib.optionals alsaSupport [ alsa-lib ]
|
||||
++ lib.optionals dssiSupport [
|
||||
dssi
|
||||
ladspaH
|
||||
ladspa-header
|
||||
]
|
||||
++ lib.optionals jackSupport [ libjack2 ]
|
||||
++ lib.optionals portaudioSupport [ portaudio ]
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
diff --git a/Cargo.lock b/Cargo.lock
|
||||
index 35bb10e..71c79eb 100644
|
||||
--- a/Cargo.lock
|
||||
+++ b/Cargo.lock
|
||||
@@ -144,9 +144,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "frizbee"
|
||||
-version = "0.7.0"
|
||||
+version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
-checksum = "4d024031f1a5bc5f19917baa0b618f1067610e35ba23e9f105653fcb27e74f5c"
|
||||
+checksum = "c3365720de81dac18e889afa72f5907aa061c975548da68e2400c056ebc94aec"
|
||||
dependencies = [
|
||||
"multiversion",
|
||||
"rayon",
|
||||
diff --git a/Cargo.toml b/Cargo.toml
|
||||
index 392d1bb..c776c7d 100644
|
||||
--- a/Cargo.toml
|
||||
+++ b/Cargo.toml
|
||||
@@ -9,7 +9,7 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
regex = "1.11.2"
|
||||
-frizbee = "0.7.0"
|
||||
+frizbee = "0.6.0"
|
||||
mlua = { version = "0.11.3", features = ["module", "luajit"] }
|
||||
thiserror = "2.0.16"
|
||||
blake3 = "1.8.2"
|
||||
@@ -8,24 +8,18 @@
|
||||
gitMinimal,
|
||||
}:
|
||||
let
|
||||
version = "1.9.1";
|
||||
version = "1.10.1";
|
||||
src = fetchFromGitHub {
|
||||
owner = "Saghen";
|
||||
repo = "blink.cmp";
|
||||
tag = "v${version}";
|
||||
hash = "sha256-GgodXdWpQoF2z1g1/WvnSpfuhskw0aMcOoyZM5l66q8=";
|
||||
hash = "sha256-y8f+bmPkb3M6DzcUkJMxd2woDLoBYslne7aB8A0ejCk=";
|
||||
};
|
||||
blink-fuzzy-lib = rustPlatform.buildRustPackage {
|
||||
inherit version src;
|
||||
pname = "blink-fuzzy-lib";
|
||||
|
||||
cargoHash = "sha256-Qdt8O7IGj2HySb1jxsv3m33ZxJg96Ckw26oTEEyQjfs=";
|
||||
|
||||
# NOTE: The only change in frizbee 0.7.0 was nixpkgs incompatible rust semantic changes
|
||||
# Patch just reverts https://github.com/saghen/blink.cmp/commit/cc824ec85b789a54d05241389993c6ab8c040810
|
||||
cargoPatches = [
|
||||
./0001-pin-frizbee-0.6.0.patch
|
||||
];
|
||||
cargoHash = "sha256-3o2n4xwNF9Fc3VlPKf3lnvmN7FVus5jQB8gcXXwz50c=";
|
||||
|
||||
nativeBuildInputs = [ gitMinimal ];
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ vscode-utils.buildVscodeMarketplaceExtension {
|
||||
mktplcRef = {
|
||||
name = "claude-code";
|
||||
publisher = "anthropic";
|
||||
version = "2.1.78";
|
||||
hash = "sha256-A8i7yU4W8Fjp1h7EFZKAEdyoqoKBVzJDvEZ+Y06dBXg=";
|
||||
version = "2.1.79";
|
||||
hash = "sha256-vQuSSpBcvd7XRTeprk8sMZmdRU6JiwPSmIQyBs94I5M=";
|
||||
};
|
||||
|
||||
postInstall = ''
|
||||
|
||||
@@ -7,8 +7,8 @@ vscode-utils.buildVscodeMarketplaceExtension {
|
||||
mktplcRef = {
|
||||
name = "jjk";
|
||||
publisher = "jjk";
|
||||
version = "0.9.1";
|
||||
hash = "sha256-7CK2fmYVAd12kLTnq3vwDmgL22Tmi9Ljt9+tpXqRWuo=";
|
||||
version = "0.9.3";
|
||||
hash = "sha256-wkHMZTLi3dDV6JQdfJ4hI5uwGAlCmKwg+v8Z9RMU1wU=";
|
||||
};
|
||||
meta = {
|
||||
changelog = "https://github.com/keanemind/jjk/releases";
|
||||
|
||||
@@ -1,80 +1,25 @@
|
||||
{
|
||||
lib,
|
||||
stdenvNoCC,
|
||||
fetchFromGitHub,
|
||||
pnpm,
|
||||
fetchPnpmDeps,
|
||||
pnpmConfigHook,
|
||||
nodejs,
|
||||
vscode-utils,
|
||||
nix-update-script,
|
||||
vscode-extension-update-script,
|
||||
}:
|
||||
|
||||
let
|
||||
vsix = stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
name = "kilo-code-${finalAttrs.version}.vsix";
|
||||
pname = "kilo-code-vsix";
|
||||
version = "4.124.0";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "Kilo-Org";
|
||||
repo = "kilocode";
|
||||
tag = "v${finalAttrs.version}";
|
||||
hash = "sha256-Dy0dd07pWsSbrO6BX7GEYf7CunXD0itaeIFRv9mQJks=";
|
||||
};
|
||||
|
||||
pnpmDeps = fetchPnpmDeps {
|
||||
inherit (finalAttrs) pname version src;
|
||||
fetcherVersion = 2;
|
||||
hash = "sha256-hxgzmJD+Sl7E+ape1M1/Xl8XLtAhtht3AE45zHFctsQ=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
nodejs
|
||||
pnpmConfigHook
|
||||
pnpm
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
node --run build
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
cp ./bin/kilo-code-$version.vsix $out
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
});
|
||||
in
|
||||
vscode-utils.buildVscodeExtension (finalAttrs: {
|
||||
pname = "kilo-code";
|
||||
inherit (finalAttrs.src) version;
|
||||
|
||||
vscodeExtPublisher = "kilocode";
|
||||
vscodeExtName = "Kilo-Code";
|
||||
vscodeExtUniqueId = "${finalAttrs.vscodeExtPublisher}.${finalAttrs.vscodeExtName}";
|
||||
|
||||
src = vsix;
|
||||
|
||||
passthru = {
|
||||
vsix = finalAttrs.src;
|
||||
updateScript = nix-update-script {
|
||||
attrPath = "vscode-extensions.kilocode.kilo-kode.vsix";
|
||||
};
|
||||
vscode-utils.buildVscodeMarketplaceExtension {
|
||||
mktplcRef = {
|
||||
publisher = "kilocode";
|
||||
name = "Kilo-Code";
|
||||
version = "7.0.51";
|
||||
hash = "sha256-1NzwFTFM1gkTcrAVbP6wNctePMAGdy2T9UDn24xhixM=";
|
||||
};
|
||||
|
||||
passthru.updateScript = vscode-extension-update-script { };
|
||||
|
||||
meta = {
|
||||
description = "Open Source AI coding assistant for planning, building, and fixing code";
|
||||
homepage = "https://kilocode.ai";
|
||||
homepage = "https://kilo.ai";
|
||||
downloadPage = "https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code";
|
||||
license = lib.licenses.asl20;
|
||||
license = lib.licenses.mit;
|
||||
sourceProvenance = with lib.sourceTypes; [ fromSource ];
|
||||
maintainers = with lib.maintainers; [ xiaoxiangmoe ];
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ vscode-utils.buildVscodeMarketplaceExtension {
|
||||
mktplcRef = {
|
||||
publisher = "ms-azuretools";
|
||||
name = "vscode-bicep";
|
||||
version = "0.38.33";
|
||||
hash = "sha256-gmSUPHdbxXu5jUASsbu+yVO2ZdVBo5+uQNeLdTsvQVU=";
|
||||
version = "0.41.2";
|
||||
hash = "sha256-8k2de208t/ZAVJzxkjd0qcqgVx523hEWWe5d1uvthFU=";
|
||||
};
|
||||
|
||||
buildInputs = [
|
||||
|
||||
@@ -14,8 +14,8 @@ vscode-utils.buildVscodeMarketplaceExtension {
|
||||
mktplcRef = {
|
||||
name = "r";
|
||||
publisher = "reditorsupport";
|
||||
version = "2.8.6";
|
||||
hash = "sha256-T/Qh0WfTfXMzPonbg9NMII5qFptfNoApFFiZCT5rR3Y=";
|
||||
version = "2.8.7";
|
||||
hash = "sha256-pA3/81UYrieDfGYn1fVI6KY9B7A5KAhGIzftZtzXQVc=";
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
jq
|
||||
|
||||
@@ -10,8 +10,8 @@ vscode-utils.buildVscodeMarketplaceExtension {
|
||||
mktplcRef = {
|
||||
name = "wgsl-analyzer";
|
||||
publisher = "wgsl-analyzer";
|
||||
version = "0.11.141";
|
||||
hash = "sha256-egd9B5mS5pqzDWVry3dEQKfnxT4zI0RdLMJ/x5n6Nek=";
|
||||
version = "0.11.262";
|
||||
hash = "sha256-a2TVwTmxP9wBt0tMkQcVCyzM0RoihGag56ITd+xjtl8=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
}:
|
||||
mkLibretroCore {
|
||||
core = "mednafen-psx" + lib.optionalString withHw "-hw";
|
||||
version = "0-unstable-2026-03-06";
|
||||
version = "0-unstable-2026-03-20";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "libretro";
|
||||
repo = "beetle-psx-libretro";
|
||||
rev = "3ea167a60bc37bd0c257592bb9a7f559a50465c4";
|
||||
hash = "sha256-gSNsTV1w7i6bTLngU/Zbo7bwLmb+Bu26JwUre6Rj4qc=";
|
||||
rev = "8b5dea564b687dd07d91cea48615fab213d1e2c9";
|
||||
hash = "sha256-vIEjwhE86TsXbAPk6IbMlWWFWE2Nn6R/+gBe9mMmio4=";
|
||||
};
|
||||
|
||||
extraBuildInputs = lib.optionals withHw [
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
}:
|
||||
mkLibretroCore {
|
||||
core = "fbneo";
|
||||
version = "0-unstable-2026-03-08";
|
||||
version = "0-unstable-2026-03-17";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "libretro";
|
||||
repo = "fbneo";
|
||||
rev = "14ff80a2e0611d039321a3ac0dd76bf6b4e3210f";
|
||||
hash = "sha256-L6KYyEb95L9rDnaMVh49afaWxsshTy3eujsTQWbPfl0=";
|
||||
rev = "baafb100b487f2ac06f9e78ac322e3ecf36b8924";
|
||||
hash = "sha256-46hVbQN8QO1FNm56wJ7Q323blUWV9sn529tMwdAOhW8=";
|
||||
};
|
||||
|
||||
makefile = "Makefile";
|
||||
|
||||
@@ -174,7 +174,7 @@ stdenv.mkDerivation (
|
||||
++ lib.optional cupsSupport pkgs.cups
|
||||
++ lib.optional dbusSupport pkgs.dbus
|
||||
++ lib.optional cairoSupport pkgs.cairo
|
||||
++ lib.optional odbcSupport pkgs.unixODBC
|
||||
++ lib.optional odbcSupport pkgs.unixodbc
|
||||
++ lib.optional netapiSupport pkgs.samba4
|
||||
++ lib.optional cursesSupport pkgs.ncurses
|
||||
++ lib.optional vaSupport pkgs.libva
|
||||
|
||||
@@ -758,6 +758,10 @@ let
|
||||
${ungoogler}/utils/domain_substitution.py apply -r ${ungoogler}/domain_regex.list -f ${ungoogler}/domain_substitution.list -c ./ungoogled-domsubcache.tar.gz .
|
||||
'';
|
||||
|
||||
# Sadly, Chromium is not even -fstrict-flex-array=1 clean
|
||||
# See https://github.com/NixOS/nixpkgs/issues/499982#issuecomment-4062355720
|
||||
hardeningDisable = [ "strictflexarrays1" ];
|
||||
|
||||
llvmCcAndBintools = symlinkJoin {
|
||||
name = "llvmCcAndBintools";
|
||||
paths = [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
buildGo124Module,
|
||||
buildGo125Module,
|
||||
fetchFromGitHub,
|
||||
nixosTests,
|
||||
@@ -96,7 +95,7 @@ rec {
|
||||
};
|
||||
|
||||
nomad_1_10 = generic {
|
||||
buildGoModule = buildGo124Module;
|
||||
buildGoModule = buildGo125Module;
|
||||
version = "1.10.5";
|
||||
hash = "sha256-NFH++oYWb6vQN6cOPByscI/ZBWDNy4YbcLiBMO3/jVU=";
|
||||
vendorHash = "sha256-QcTw9kKwoHIvXZoxfDohFG+sBs8OLvYPeygygDClsn8=";
|
||||
@@ -108,7 +107,7 @@ rec {
|
||||
};
|
||||
|
||||
nomad_1_9 = generic {
|
||||
buildGoModule = buildGo124Module;
|
||||
buildGoModule = buildGo125Module;
|
||||
version = "1.9.7";
|
||||
hash = "sha256-U02H6DPr1friQ9EwqD/wQnE2Fm20OE5xNccPDJfnsqI=";
|
||||
vendorHash = "sha256-9GnwqkexJAxrhW9yJFaDTdSaZ+p+/dcMuhlusp4cmyw=";
|
||||
|
||||
@@ -851,11 +851,11 @@
|
||||
"vendorHash": "sha256-HzBj7TSjiv/Ov10KOt5V4oQaKK7CKjBjxpSrhmOl1lQ="
|
||||
},
|
||||
"loafoe_htpasswd": {
|
||||
"hash": "sha256-1o2kgeTFxegzOgGXWP4OYZ3uC3WbAkCXPqScMvVpHr0=",
|
||||
"hash": "sha256-1HCvAGWsYlcYCA8iOmBb/AawxHPLuoxxQWLzNy0x79M=",
|
||||
"homepage": "https://registry.terraform.io/providers/loafoe/htpasswd",
|
||||
"owner": "loafoe",
|
||||
"repo": "terraform-provider-htpasswd",
|
||||
"rev": "v2.0.0",
|
||||
"rev": "v2.1.0",
|
||||
"spdx": "MIT",
|
||||
"vendorHash": "sha256-1gzJdcSvFPE8Spkwxe24U7DUyrz4qpDoFVHm6vCWF6A="
|
||||
},
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
diff --git a/src/api/video/nv12_buffer.cc b/src/api/video/nv12_buffer.cc
|
||||
index ca9dcd867..89d28f23c 100644
|
||||
--- a/src/api/video/nv12_buffer.cc
|
||||
+++ b/src/api/video/nv12_buffer.cc
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "third_party/libyuv/include/libyuv/convert.h"
|
||||
#include "third_party/libyuv/include/libyuv/scale.h"
|
||||
|
||||
+#include <cstring>
|
||||
+
|
||||
namespace webrtc {
|
||||
|
||||
namespace {
|
||||
diff --git a/src/audio/utility/channel_mixer.cc b/src/audio/utility/channel_mixer.cc
|
||||
index 0f1e66387..33b771b0c 100644
|
||||
--- a/src/audio/utility/channel_mixer.cc
|
||||
+++ b/src/audio/utility/channel_mixer.cc
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/numerics/safe_conversions.h"
|
||||
|
||||
+#include <cstring>
|
||||
+
|
||||
namespace webrtc {
|
||||
|
||||
ChannelMixer::ChannelMixer(ChannelLayout input_layout,
|
||||
diff --git a/src/modules/audio_processing/aec3/alignment_mixer.cc b/src/modules/audio_processing/aec3/alignment_mixer.cc
|
||||
index 7f076dea8..ffd7242b5 100644
|
||||
--- a/src/modules/audio_processing/aec3/alignment_mixer.cc
|
||||
+++ b/src/modules/audio_processing/aec3/alignment_mixer.cc
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "modules/audio_processing/aec3/alignment_mixer.h"
|
||||
|
||||
#include <algorithm>
|
||||
+#include <cstring>
|
||||
|
||||
#include "rtc_base/checks.h"
|
||||
|
||||
diff --git a/src/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc b/src/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
|
||||
index 7ef1a030e..5b9ab7137 100644
|
||||
--- a/src/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
|
||||
+++ b/src/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <spa/param/video/format-utils.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
+#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/memory/memory.h"
|
||||
diff --git a/src/modules/video_coding/utility/ivf_file_reader.cc b/src/modules/video_coding/utility/ivf_file_reader.cc
|
||||
index 4c08ca613..f82f2bfcb 100644
|
||||
--- a/src/modules/video_coding/utility/ivf_file_reader.cc
|
||||
+++ b/src/modules/video_coding/utility/ivf_file_reader.cc
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "modules/video_coding/utility/ivf_file_reader.h"
|
||||
|
||||
+#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
diff --git a/src/net/dcsctp/packet/bounded_byte_writer.h b/src/net/dcsctp/packet/bounded_byte_writer.h
|
||||
index d754549e4..bf5e3ed42 100644
|
||||
--- a/src/net/dcsctp/packet/bounded_byte_writer.h
|
||||
+++ b/src/net/dcsctp/packet/bounded_byte_writer.h
|
||||
@@ -12,6 +12,7 @@
|
||||
#define NET_DCSCTP_PACKET_BOUNDED_BYTE_WRITER_H_
|
||||
|
||||
#include <algorithm>
|
||||
+#include <cstring>
|
||||
|
||||
#include "api/array_view.h"
|
||||
|
||||
@@ -47,6 +47,9 @@ stdenv.mkDerivation {
|
||||
# fix build with abseil 202508
|
||||
# upstream PR: https://github.com/desktop-app/tg_owt/pull/164
|
||||
./abseil-202508.patch
|
||||
# fix build with latest glibc
|
||||
# upstream PR: https://github.com/desktop-app/tg_owt/pull/172
|
||||
./cstring-includes.patch
|
||||
];
|
||||
|
||||
postPatch = lib.optionalString stdenv.hostPlatform.isLinux ''
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
ffmpeg_6,
|
||||
protobuf,
|
||||
openal-soft,
|
||||
minizip,
|
||||
minizip-ng,
|
||||
range-v3,
|
||||
tl-expected,
|
||||
hunspell,
|
||||
@@ -74,7 +74,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
xxHash
|
||||
ffmpeg_6
|
||||
openal-soft
|
||||
minizip
|
||||
minizip-ng
|
||||
range-v3
|
||||
tl-expected
|
||||
rnnoise
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
clucene-core_2,
|
||||
libcdr,
|
||||
lcms2,
|
||||
unixODBC,
|
||||
unixodbc,
|
||||
sane-backends,
|
||||
mythes,
|
||||
libexttextcat,
|
||||
@@ -481,7 +481,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
libpq
|
||||
python3
|
||||
sane-backends
|
||||
unixODBC
|
||||
unixodbc
|
||||
util-linux
|
||||
which
|
||||
xmlsec
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
libsecret,
|
||||
openjdk,
|
||||
sqlite,
|
||||
unixODBC,
|
||||
unixodbc,
|
||||
gtk2,
|
||||
libxtst,
|
||||
glibcLocales,
|
||||
@@ -57,7 +57,7 @@ stdenv.mkDerivation rec {
|
||||
gsettings-desktop-schemas
|
||||
gtk2
|
||||
sqlite
|
||||
unixODBC
|
||||
unixodbc
|
||||
libxtst
|
||||
glibcLocales
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
ncurses,
|
||||
opencv4,
|
||||
openssl,
|
||||
unixODBC,
|
||||
unixodbc,
|
||||
xkeyboard_config,
|
||||
libxtst,
|
||||
libxrender,
|
||||
@@ -69,7 +69,7 @@ stdenv.mkDerivation rec {
|
||||
opencv4
|
||||
openssl
|
||||
(lib.getLib stdenv.cc.cc)
|
||||
unixODBC
|
||||
unixodbc
|
||||
xkeyboard_config
|
||||
libxml2
|
||||
libuuid
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
buildKodiAddon rec {
|
||||
pname = "trakt";
|
||||
namespace = "script.trakt";
|
||||
version = "3.6.1";
|
||||
version = "3.8.2";
|
||||
|
||||
src = fetchzip {
|
||||
url = "https://mirrors.kodi.tv/addons/${lib.toLower rel}/${namespace}/${namespace}-${version}.zip";
|
||||
sha256 = "sha256-ZlBucYYRA1cL5c0H1jhXeKE1itReZe2gAJYFFxuUebo=";
|
||||
sha256 = "sha256-75neHPVWpHhzMIOfNFvvX/Xqy3n1DO3SGg16zv/r9dU=";
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
"format"
|
||||
"fortify"
|
||||
"fortify3"
|
||||
"libcxxhardeningextensive"
|
||||
"libcxxhardeningfast"
|
||||
"pic"
|
||||
"relro"
|
||||
"stackclashprotection"
|
||||
"stackprotector"
|
||||
"strictflexarrays1"
|
||||
"strictoverflow"
|
||||
"zerocallusedregs"
|
||||
],
|
||||
|
||||
Executable → Regular
@@ -20,7 +20,7 @@ set -o noglob
|
||||
success=
|
||||
for repository in $repositories; do
|
||||
echo "Trying to clone $repository $tagtext into $out …"
|
||||
if darcs clone --lazy $tagflags "$repository" "$out"; then
|
||||
if darcs clone --lazy --no-cache $tagflags "$repository" "$out"; then
|
||||
# remove metadata, because it can change
|
||||
rm -rf "$out/_darcs"
|
||||
success=1
|
||||
|
||||
@@ -27,7 +27,7 @@ usage() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--quiet)
|
||||
quiet=1; shift 1 ;;
|
||||
quiet=1; shift 1 ;;
|
||||
--name)
|
||||
name="$2"; shift 2 ;;
|
||||
--repo)
|
||||
@@ -127,11 +127,7 @@ if [ -z "$final_path" ]; then
|
||||
|
||||
cd "$tmp_clone"
|
||||
# Do not print Darcs progress to stdout (else stdout isn’t parsable JSON)
|
||||
if [ -t 1 ]; then
|
||||
darcs clone $clone_args "$repository" "$name" >/dev/tty
|
||||
else
|
||||
darcs clone $clone_args "$repository" "$name" >/dev/null
|
||||
fi
|
||||
darcs clone "$clone_args" "$repository" "$name" 1>&2
|
||||
# Will put the current Darcs context into the store.
|
||||
new_context="$tmp_clone/${name}-context.txt"
|
||||
darcs log --repodir="$tmp_clone/$name" --context > "$new_context"
|
||||
|
||||
@@ -11,6 +11,36 @@ use std::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
fn warn_unresolved_packages(
|
||||
packages_without_resolved: &[(String, Package)],
|
||||
total_count: usize,
|
||||
) {
|
||||
let missing_count = packages_without_resolved.len();
|
||||
if missing_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let percentage = (missing_count as f64 / total_count as f64) * 100.0;
|
||||
eprintln!(
|
||||
"warning: {} out of {} packages ({:.1}%) are missing 'resolved' URLs and will not be cached.",
|
||||
missing_count, total_count, percentage
|
||||
);
|
||||
eprintln!("warning: Packages without 'resolved' URLs:");
|
||||
for (name, _) in packages_without_resolved.iter().take(10) {
|
||||
eprintln!("warning: - {}", name.trim_start_matches("node_modules/"));
|
||||
}
|
||||
if missing_count > 10 {
|
||||
eprintln!("warning: ... and {} more", missing_count - 10);
|
||||
}
|
||||
if percentage > 50.0 {
|
||||
eprintln!(
|
||||
"warning: More than 50% of packages are missing 'resolved' URLs. This may indicate an issue with the lockfile.\n\
|
||||
warning: This is a known issue with some npm versions. See: https://github.com/npm/cli/issues/6301\n\
|
||||
warning: Consider regenerating upstream's lockfile with: npm install --package-lock-only (sending an upstream PR is best)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn packages(content: &str) -> anyhow::Result<Vec<Package>> {
|
||||
let lockfile: Lockfile = serde_json::from_str(content)?;
|
||||
|
||||
@@ -20,18 +50,27 @@ pub(super) fn packages(content: &str) -> anyhow::Result<Vec<Package>> {
|
||||
|
||||
to_new_packages(lockfile.dependencies.unwrap_or_default(), &initial_url)?
|
||||
}
|
||||
2 | 3 => lockfile
|
||||
.packages
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|(n, p)| !n.is_empty() && matches!(p.resolved, Some(UrlOrString::Url(_))))
|
||||
.map(|(n, p)| Package {
|
||||
// Use the package's own name if present (for aliases like string-width-cjs -> string-width),
|
||||
// otherwise extract from the lockfile key
|
||||
name: Some(p.name.unwrap_or(n)),
|
||||
..p
|
||||
})
|
||||
.collect(),
|
||||
2 | 3 => {
|
||||
let (packages_with_resolved, packages_without_resolved): (Vec<_>, Vec<_>) = lockfile
|
||||
.packages
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|(n, _)| !n.is_empty())
|
||||
.partition(|(_, p)| matches!(p.resolved, Some(UrlOrString::Url(_))));
|
||||
|
||||
let total_count = packages_with_resolved.len() + packages_without_resolved.len();
|
||||
warn_unresolved_packages(&packages_without_resolved, total_count);
|
||||
|
||||
packages_with_resolved
|
||||
.into_iter()
|
||||
.map(|(n, p)| Package {
|
||||
// Use the package's own name if present (for aliases like string-width-cjs -> string-width),
|
||||
// otherwise extract from the lockfile key
|
||||
name: Some(p.name.unwrap_or(n)),
|
||||
..p
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => bail!(
|
||||
"We don't support lockfile version {}, please file an issue.",
|
||||
lockfile.version
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any, TypedDict, cast
|
||||
from urllib.parse import unquote
|
||||
|
||||
import requests
|
||||
import tomli_w
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
eprint = functools.partial(print, file=sys.stderr)
|
||||
@@ -172,8 +173,12 @@ def get_manifest_metadata(manifest_path: Path) -> dict[str, Any]:
|
||||
return json.loads(output)
|
||||
|
||||
|
||||
def try_get_crate_manifest_path_from_mainfest_path(manifest_path: Path, crate_name: str) -> Path | None:
|
||||
metadata = get_manifest_metadata(manifest_path)
|
||||
def try_get_crate_manifest_path_from_manifest_path(manifest_path: Path, crate_name: str) -> Path | None:
|
||||
try:
|
||||
metadata = get_manifest_metadata(manifest_path)
|
||||
except subprocess.CalledProcessError:
|
||||
eprint(f"Warning: cargo metadata failed for {manifest_path}, skipping")
|
||||
return None
|
||||
|
||||
for pkg in metadata["packages"]:
|
||||
if pkg["name"] == crate_name:
|
||||
@@ -183,11 +188,15 @@ def try_get_crate_manifest_path_from_mainfest_path(manifest_path: Path, crate_na
|
||||
|
||||
|
||||
def find_crate_manifest_in_tree(tree: Path, crate_name: str) -> Path:
|
||||
# in some cases Cargo.toml is not located at the top level, so we also look at subdirectories
|
||||
manifest_paths = tree.glob("**/Cargo.toml")
|
||||
# Scan all Cargo.toml files; sort by depth/path to make ordering deterministic
|
||||
# and prefer less-nested manifests first.
|
||||
manifest_paths = sorted(
|
||||
tree.glob("**/Cargo.toml"),
|
||||
key=lambda path: (len(path.parts), str(path)),
|
||||
)
|
||||
|
||||
for manifest_path in manifest_paths:
|
||||
res = try_get_crate_manifest_path_from_mainfest_path(manifest_path, crate_name)
|
||||
res = try_get_crate_manifest_path_from_manifest_path(manifest_path, crate_name)
|
||||
if res is not None:
|
||||
return res
|
||||
|
||||
@@ -256,6 +265,21 @@ def extract_crate_tarball_contents(tarball_path: Path, crate_out_dir: Path) -> N
|
||||
subprocess.check_output(cmd)
|
||||
|
||||
|
||||
def make_git_source_selector(source_info: GitSourceInfo) -> dict[str, str]:
|
||||
selector = {}
|
||||
selector["git"] = source_info["url"]
|
||||
if source_info["type"] is not None:
|
||||
selector[source_info["type"]] = source_info["value"]
|
||||
return selector
|
||||
|
||||
|
||||
def make_registry_source_selector(source: str) -> dict[str, str]:
|
||||
registry = source[9:] if source.startswith("registry+") else source
|
||||
selector = {}
|
||||
selector["registry"] = registry
|
||||
return selector
|
||||
|
||||
|
||||
def create_vendor(vendor_staging_dir: Path, out_dir: Path) -> None:
|
||||
lockfile_path = vendor_staging_dir / "Cargo.lock"
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
@@ -264,14 +288,67 @@ def create_vendor(vendor_staging_dir: Path, out_dir: Path) -> None:
|
||||
cargo_lock_toml = load_toml(lockfile_path)
|
||||
lockfile_version = get_lockfile_version(cargo_lock_toml)
|
||||
|
||||
config_lines = [
|
||||
'[source.vendored-sources]',
|
||||
'directory = "@vendor@"',
|
||||
'[source.crates-io]',
|
||||
'replace-with = "vendored-sources"',
|
||||
]
|
||||
source_to_ind: dict[str, str] = {}
|
||||
source_config = {}
|
||||
next_registry_ind = 0
|
||||
next_git_ind = 0
|
||||
|
||||
def add_source_replacement(
|
||||
orig_key: str,
|
||||
orig_selector: dict[str, str],
|
||||
vendored_key: str,
|
||||
vendored_dir: str
|
||||
) -> None:
|
||||
source_config[vendored_key] = {}
|
||||
source_config[vendored_key]["directory"] = vendored_dir
|
||||
source_config[orig_key] = orig_selector
|
||||
source_config[orig_key]["replace-with"] = vendored_key
|
||||
|
||||
# we reserve registry index 0 for crates-io
|
||||
source_to_ind["registry+https://github.com/rust-lang/crates.io-index"] = "registry-0"
|
||||
source_to_ind["sparse+https://index.crates.io/"] = "registry-0"
|
||||
add_source_replacement(
|
||||
orig_key="crates-io",
|
||||
orig_selector={}, # there is an internal selector defined for the `crates-io` source
|
||||
vendored_key="vendored-source-registry-0",
|
||||
vendored_dir="@vendor@/source-registry-0"
|
||||
)
|
||||
next_registry_ind += 1
|
||||
|
||||
for pkg in cargo_lock_toml["package"]:
|
||||
# ignore local dependencies
|
||||
if "source" not in pkg.keys():
|
||||
continue
|
||||
source: str = pkg["source"]
|
||||
if source in source_to_ind:
|
||||
continue
|
||||
|
||||
if source.startswith("git+"):
|
||||
ind = f"git-{next_git_ind}"
|
||||
next_git_ind += 1
|
||||
source_info = parse_git_source(source, lockfile_version)
|
||||
selector = make_git_source_selector(source_info)
|
||||
elif source.startswith("registry+") or source.startswith("sparse+"):
|
||||
ind = f"registry-{next_registry_ind}"
|
||||
next_registry_ind += 1
|
||||
selector = make_registry_source_selector(source)
|
||||
else:
|
||||
raise Exception(f"Can't process source: {source}.")
|
||||
|
||||
source_to_ind[source] = ind
|
||||
add_source_replacement(
|
||||
orig_key=f"original-source-{ind}",
|
||||
orig_selector=selector,
|
||||
vendored_key=f"vendored-source-{ind}",
|
||||
vendored_dir=f"@vendor@/source-{ind}"
|
||||
)
|
||||
|
||||
config_path = out_dir / ".cargo" / "config.toml"
|
||||
config_path.parent.mkdir()
|
||||
|
||||
with open(config_path, "wb") as config_file:
|
||||
tomli_w.dump({"source": source_config}, config_file)
|
||||
|
||||
seen_source_keys = set()
|
||||
for pkg in cargo_lock_toml["package"]:
|
||||
|
||||
# ignore local dependenices
|
||||
@@ -279,13 +356,15 @@ def create_vendor(vendor_staging_dir: Path, out_dir: Path) -> None:
|
||||
continue
|
||||
|
||||
source: str = pkg["source"]
|
||||
|
||||
dir_name = f"{pkg["name"]}-{pkg["version"]}"
|
||||
crate_out_dir = out_dir / dir_name
|
||||
source_ind = source_to_ind[source]
|
||||
crate_dir_name = f"{pkg["name"]}-{pkg["version"]}"
|
||||
source_dir_name = f"source-{source_ind}"
|
||||
crate_out_dir = out_dir / source_dir_name / crate_dir_name
|
||||
crate_out_dir.parent.mkdir(exist_ok=True)
|
||||
|
||||
if source.startswith("git+"):
|
||||
|
||||
source_info = parse_git_source(pkg["source"], lockfile_version)
|
||||
source_info = parse_git_source(source, lockfile_version)
|
||||
|
||||
git_sha_rev = source_info["git_sha_rev"]
|
||||
git_tree = vendor_staging_dir / "git" / git_sha_rev
|
||||
@@ -296,23 +375,13 @@ def create_vendor(vendor_staging_dir: Path, out_dir: Path) -> None:
|
||||
with open(crate_out_dir / ".cargo-checksum.json", "w") as f:
|
||||
json.dump({"files": {}}, f)
|
||||
|
||||
source_key = source[0:source.find("#")]
|
||||
|
||||
if source_key in seen_source_keys:
|
||||
continue
|
||||
|
||||
seen_source_keys.add(source_key)
|
||||
|
||||
config_lines.append(f'[source."{source_key}"]')
|
||||
config_lines.append(f'git = "{source_info["url"]}"')
|
||||
if source_info["type"] is not None:
|
||||
config_lines.append(f'{source_info["type"]} = "{source_info["value"]}"')
|
||||
config_lines.append('replace-with = "vendored-sources"')
|
||||
|
||||
elif source.startswith("registry+"):
|
||||
|
||||
elif source.startswith("registry+") or source.startswith("sparse+"):
|
||||
filename = f"{pkg["name"]}-{pkg["version"]}.tar.gz"
|
||||
tarball_path = vendor_staging_dir / "tarballs" / filename
|
||||
|
||||
# TODO: change this when non-crates-io registries are supported
|
||||
dir_name = "tarballs"
|
||||
|
||||
tarball_path = vendor_staging_dir / dir_name / filename
|
||||
|
||||
extract_crate_tarball_contents(tarball_path, crate_out_dir)
|
||||
|
||||
@@ -323,10 +392,6 @@ def create_vendor(vendor_staging_dir: Path, out_dir: Path) -> None:
|
||||
else:
|
||||
raise Exception(f"Can't process source: {source}.")
|
||||
|
||||
(out_dir / ".cargo").mkdir()
|
||||
with open(out_dir / ".cargo" / "config.toml", "w") as config_file:
|
||||
config_file.writelines(line + "\n" for line in config_lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
subcommand = sys.argv[1]
|
||||
|
||||
@@ -27,6 +27,7 @@ let
|
||||
with python3Packages;
|
||||
[
|
||||
requests
|
||||
tomli-w
|
||||
]
|
||||
++ requests.optional-dependencies.socks; # to support socks proxy envs like ALL_PROXY in requests
|
||||
flakeIgnore = [
|
||||
|
||||
Executable → Regular
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user