diff --git a/.editorconfig b/.editorconfig index 4753adb..061699c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,5 @@ +root = true + [*] end_of_line = lf insert_final_newline = true diff --git a/README.md b/README.md deleted file mode 100644 index c58f57e..0000000 --- a/README.md +++ /dev/null @@ -1,119 +0,0 @@ -# Dynix — WIP modular dynamicism for NixOS systems - -Dynix is a prototype for modifying an append-only NixOS configuration, and dynamically - -## Running the tests - -There are currently 3 implemented dynamicism modules: `gotosocial`, `harmonia`, and `distccd`. -Each test uses the [NixOS test infrastructure](https://nixos.org/manual/nixos/unstable/#sec-nixos-tests) to: - -1. Setup a virtual machine running NixOS -2. Configure the VM's NixOS to run a given service, with certain settings -3. Verify that the running service is using those settings -4. Use Dynix to change a setting for that service -5. Verify that the running service is now using the new setting - -The tests themselves can be run with Nix. -To run the test for, e.g., Gotosocial, you can run: - -```bash -$ nix --experimental-features "nix-command flakes pipe-operator pipe-operators" build .#default.tests.gotosocial -``` - -The experimental [piping operator](https://github.com/NixOS/rfcs/pull/148) is currently used in Dynix, so you must enable the experimental feature `pipe-operator` for Lix or `pipe-operators` for CppNix (you can add both to cover both Nix implementations); flakes are used for locked inputs. - - -To run a test with fewer experimental features, and without locked inputs, you can use the old CLI: - -```bash -$ nix-build --experimental-features "pipe-operator pipe-operators" -A tests.gotosocial -``` - -All the tests at once can be run with: - -```bash -$ nix build --experimental-features "nix-command flakes pipe-operator pipe-operators" .#default.allTests -``` - -## Gotosocial - -This example, implemented in [./modules/dynamicism/gotosocial.nix](./modules/dynamicism/gotosocial.nix), is tested in [./tests/gotosocial](./tests/gotosocial). -The test sets up a VM using NixOS's `services.gotosocial` module with the following *static* [configuration](./tests/gotosocial/configuration.nix): - -```nix -{ - services.gotosocial = { - enable = true; - setupPostgresqlDB = true; - settings = { - application-name = "gotosocial-for-machine"; - host = "gotosocial-machine.local"; - }; - }; -} -``` - -The automated [test script](./tests/gotosocial/test-script.py): - -1. Asserts that that the above *static* configuration is in effect (by extracting the configuration from the running Gotosocial process) -2. Runs `dynix append "services.gotosocial.settings.application-name" "yay!"`, which modifies the append-only configuration file `/etc/nixos/dynamic.nix` in the VM filesystem -3. Runs the dynamic activation script built by `(import ).config.dynamicism.applyDynamicConfiguration { }`, which *applies* the dynamic configuration -4. Asserts that the dynamically configured `application-name` is in effect -5. Runs `nixos-rebuild switch` to re-apply the *static* configuration -6. Asserts that the dynamic configuration is *no longer* in effect, and we are back to the static configuration - -## Harmonia - -This example, implemented in [./modules/dynamicism/harmonia.nix](./modules/dynamicism/harmonia.nix), is tested in [./tests/harmonia](./tests/harmonia). -The test sets up a VM using NixOS's `services.harmonia` module with the following *static* [configuration](./tests/harmonia/configuration.nix) - -```nix -{ - services.harmonia = { - enable = true; - settings = { - workers = 4; - max_connection_rate = 256; - }; - }; -} -``` - -The VM [test script](./tests/harmonia/test-script.py): - -1. Asserts that the above *static* configuration is in effect (by extracting the configuration from the running Harmonia process) -2. Runs `dynix append "services.harmonia.settings.workers" 20`, to modify the append-only configuration file `/etc/nixos/dynamic.nix` in the VM filesystem -3. Runs the dynamic activation script built by `(import ).config.dynamicism.applyDynamicConfiguration { }`, to *apply* the dynamic configuration -4. Asserts that the dynamically configured `workers` is in effect -5. Runs `dynix append "services.harmonia.settings.max_connection_rate" 100` -6. Runs the dynamic activation script -7. Asserts that *both* `max_connection_rate` and `workers` dynamic values are in effect -8. Runs `nixos-rebuild switch` to re-apply the *static* configuration -9. Asserts that the dynamic configuration is *no longer* in effect, and we are back to the static configuration - -## Distccd - -This example, implemented in [./modules/dynamicism/distccd.nix](./modules/dynamicism/distccd.nix), is tested in [./tests/distccd](./tests/distccd). -The test sets up a VM using NixOS's `services.distccd` module with the following *static* [configuration](./tests/distccd/configuration.nix): - -```nix -{ - services.distccd = { - jobTimeout = 900; - maxJobs = 12; - logLevel = "warning"; - }; -} -``` - -The VM [test script](./tests/distccd/test-script.py): - -1. Asserts that the above *static* configuration is in effect (by extracting the configuration from the running Distccd process) -2. Runs `dynix append "services.distccd.maxJobs" 4`, to modify the append-only configuration file `/etc/nixos/dynamic.nix` in the VM filesystem -3. Runs the dynamic activation script built by `(import ).config.dynamicism.applyDynamicConfiguration { }`, to *apply* the dynamic configuration -4. Asserts that the dynamically configured `maxJobs` is in effect -5. Runs `dynix append "services.distccd.logLevel" "error"` -6. Runs the dynamic activation script -7. Asserts that *both* `maxJobs` and `logLevel` dynamic values are in effect -8. Runs `nixos-rebuild switch` to re-apply the *static* configuration -9. Asserts that the dynamic configuration is *no longer* in effect, and we are back to the static configuration. diff --git a/default.nix b/default.nix index 64072a2..4372c35 100644 --- a/default.nix +++ b/default.nix @@ -1,18 +1,18 @@ { - pkgs ? import { }, - qpkgs ? let - src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; - in import src { inherit pkgs; }, + pkgs ? import { }, + qpkgs ? let + src = fetchTree (builtins.parseFlakeRef "github:Qyriad/nur-packages"); + in import src { inherit pkgs; }, }: let - inherit (qpkgs) lib; - dynix = qpkgs.callPackage ./package.nix { } - |> qpkgs.stdlib.mkStdenvPretty; - byStdenv = lib.mapAttrs (stdenvName: stdenv: let - withStdenv = dynix.override { clangStdenv = stdenv; }; - dynix' = withStdenv.overrideAttrs (prev: { - pname = "${prev.pname}-${stdenvName}"; - }); - in dynix') qpkgs.validStdenvs; + inherit (qpkgs) lib; + dynix = qpkgs.callPackage ./package.nix { } + |> qpkgs.stdlib.mkStdenvPretty; + byStdenv = lib.mapAttrs (stdenvName: stdenv: let + withStdenv = dynix.override { inherit stdenv; }; + dynix' = withStdenv.overrideAttrs (prev: { + pname = "${prev.pname}-${stdenvName}"; + }); + in dynix') qpkgs.validStdenvs; in dynix.overrideAttrs (prev: lib.recursiveUpdate prev { - passthru = { inherit byStdenv; }; + passthru = { inherit byStdenv; }; }) diff --git a/flake.nix b/flake.nix index 276cfb0..f4270f0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,55 +1,51 @@ { - inputs = { - nixpkgs = { - url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake = false; - }; - flake-utils.url = "github:numtide/flake-utils"; - fenix = { - url = "github:nix-community/fenix"; - flake = false; - }; - qyriad-nur = { - url = "github:Qyriad/nur-packages"; - flake = false; - }; - }; + inputs = { + nixpkgs = { + url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake = false; + }; + flake-utils.url = "github:numtide/flake-utils"; + fenix = { + url = "github:nix-community/fenix"; + flake = false; + }; + qyriad-nur = { + url = "github:Qyriad/nur-packages"; + flake = false; + }; + }; - outputs = { - self, - nixpkgs, - flake-utils, - fenix, - qyriad-nur, - }: flake-utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { inherit system; }; - qpkgs = import qyriad-nur { inherit pkgs; }; - inherit (qpkgs) lib; - fenixLib = import fenix { inherit pkgs; }; + outputs = { + self, + nixpkgs, + flake-utils, + fenix, + qyriad-nur, + }: flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { inherit system; }; + qpkgs = import qyriad-nur { inherit pkgs; }; + inherit (qpkgs) lib; + fenixLib = import fenix { inherit pkgs; }; - dynix = import ./default.nix { inherit pkgs qpkgs; }; - extraVersions = lib.mapAttrs' (stdenvName: value: { - name = "${stdenvName}-dynix"; - inherit value; - }) dynix.byStdenv; + dynix = import ./default.nix { inherit pkgs qpkgs; }; + extraVersions = lib.mapAttrs' (stdenvName: value: { + name = "${stdenvName}-dynix"; + inherit value; + }) dynix.byStdenv; - devShell = import ./shell.nix { inherit pkgs qpkgs dynix fenixLib; }; - extraDevShells = lib.mapAttrs' (stdenvName: value: { - name = "${stdenvName}-dynix"; - inherit value; - }) dynix.byStdenv; - in { - packages = extraVersions // { - default = dynix; - inherit dynix; - }; + devShell = import ./shell.nix { inherit pkgs qpkgs dynix fenixLib; }; + extraDevShells = lib.mapAttrs' (stdenvName: value: { + name = "${stdenvName}-dynix"; + inherit value; + }) dynix.byStdenv; + in { + packages = extraVersions // { + default = dynix; + inherit dynix; + }; - devShells = extraDevShells // { - default = devShell; - }; - - checks = self.packages.${system}.default.tests // { - default = self.packages.${system}.default.allTests; - }; - }); + devShells = extraDevShells // { + default = devShell; + }; + }); } diff --git a/modules/dynamicism/dynamicism.nix b/modules/dynamicism/dynamicism.nix index 75f9fcb..6f79993 100644 --- a/modules/dynamicism/dynamicism.nix +++ b/modules/dynamicism/dynamicism.nix @@ -11,6 +11,7 @@ let inherit (import ./lib.nix { inherit lib; }) typeCheck + convenientAttrPath executablePathInStore concatFoldl recUpdateFoldl diff --git a/package.nix b/package.nix index 309f2d4..448ab31 100644 --- a/package.nix +++ b/package.nix @@ -1,160 +1,160 @@ { - lib, - clangStdenv, - callPackage, - linkFarm, - rustHooks, - rustPackages, - versionCheckHook, + lib, + clangStdenv, + callPackage, + linkFarm, + rustHooks, + rustPackages, + versionCheckHook, }: lib.callWith' rustPackages ({ - rustPlatform, - cargo, + rustPlatform, + cargo, }: let - stdenv = clangStdenv; - cargoToml = lib.importTOML ./Cargo.toml; - cargoPackage = cargoToml.package; + stdenv = clangStdenv; + cargoToml = lib.importTOML ./Cargo.toml; + cargoPackage = cargoToml.package; in stdenv.mkDerivation (finalAttrs: let - self = finalAttrs.finalPackage; + self = finalAttrs.finalPackage; in { - pname = cargoPackage.name; - version = cargoPackage.version; + pname = cargoPackage.name; + version = cargoPackage.version; - strictDeps = true; - __structuredAttrs = true; + strictDeps = true; + __structuredAttrs = true; - outputs = [ "out" "modules" ]; + outputs = [ "out" "modules" ]; - doCheck = true; - doInstallCheck = true; + doCheck = true; + doInstallCheck = true; - phases = [ "unpackPhase" "patchPhase" "installPhase" ]; + phases = [ "unpackPhase" "patchPhase" "installPhase" ]; - src = linkFarm "dynix-source" { - inherit (self) dynixCommand dynixModules; - }; + src = linkFarm "dynix-source" { + inherit (self) dynixCommand dynixModules; + }; - installPhase = '' - runHook preInstall + installPhase = lib.dedent '' + runHook preInstall - mkdir -p "$out" - cp -r --reflink=auto "$dynixCommand/"* "$out/" - mkdir -p "$modules" - cp -r --reflink=auto "$dynixModules/"* "$modules/" + mkdir -p "$out" + cp -r --reflink=auto "$dynixCommand/"* "$out/" + mkdir -p "$modules" + cp -r --reflink=auto "$dynixModules/"* "$modules/" - runHook postInstall - ''; + runHook postInstall + ''; - # - # SUB-DERIVATONS - # + # + # SUB-DERIVATONS + # - dynixCommand = stdenv.mkDerivation (finalAttrs: let - commandSelf = finalAttrs.finalPackage; - in { - pname = "${self.pname}-command"; - inherit (self) version; - inherit (self) strictDeps __structuredAttrs; - inherit (self) doCheck doInstallCheck; + dynixCommand = stdenv.mkDerivation (finalAttrs: let + commandSelf = finalAttrs.finalPackage; + in { + pname = "${self.pname}-command"; + inherit (self) version; + inherit (self) strictDeps __structuredAttrs; + inherit (self) doCheck doInstallCheck; - src = lib.fileset.toSource { - root = ./.; - fileset = lib.fileset.unions [ - ./Cargo.toml - ./Cargo.lock - ./src - ]; - }; + src = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./Cargo.toml + ./Cargo.lock + ./src + ]; + }; - cargoDeps = rustPlatform.importCargoLock { - lockFile = ./Cargo.lock; - }; + cargoDeps = rustPlatform.importCargoLock { + lockFile = ./Cargo.lock; + }; - nativeBuildInputs = rustHooks.asList ++ [ - cargo - ]; + nativeBuildInputs = rustHooks.asList ++ [ + cargo + ]; - nativeInstallCheckInputs = [ - versionCheckHook - ]; + nativeInstallCheckInputs = [ + versionCheckHook + ]; - meta = { - mainProgram = "dynix"; - }; - }); + meta = { + mainProgram = "dynix"; + }; + }); - dynixModules = stdenv.mkDerivation (finalAttrs: let - modulesSelf = finalAttrs.finalPackage; - in { - pname = "${self.pname}-modules"; - inherit (self) version; - inherit (self) strictDeps __structuredAttrs; - inherit (self) doCheck doInstallCheck; + dynixModules = stdenv.mkDerivation (finalAttrs: let + modulesSelf = finalAttrs.finalPackage; + in { + pname = "${self.pname}-modules"; + inherit (self) version; + inherit (self) strictDeps __structuredAttrs; + inherit (self) doCheck doInstallCheck; - src = lib.fileset.toSource { - root = ./modules/dynamicism; - fileset = lib.fileset.unions [ - ./modules/dynamicism - ]; - }; + src = lib.fileset.toSource { + root = ./modules/dynamicism; + fileset = lib.fileset.unions [ + ./modules/dynamicism + ]; + }; - phases = [ "unpackPhase" "patchPhase" "installPhase" ]; + phases = [ "unpackPhase" "patchPhase" "installPhase" ]; - modulesOut = "${placeholder "out"}/share/nixos/modules/dynix"; + modulesOut = "${placeholder "out"}/share/nixos/modules/dynix"; - installPhase = lib.dedent '' - runHook preInstall + installPhase = lib.dedent '' + runHook preInstall - mkdir -p "$modulesOut" - cp -r "$src/"* "$modulesOut/" + mkdir -p "$modulesOut" + cp -r "$src/"* "$modulesOut/" - runHook postInstall - ''; - }); + runHook postInstall + ''; + }); - # - # ---------------------------------------------------------------------------- - # + # + # ---------------------------------------------------------------------------- + # - passthru.mkDevShell = { - path, - mkShell, - python3Packages, - fenixToolchain, - }: let - mkShell' = mkShell.override { inherit stdenv; }; - pyEnv = python3Packages.python.withPackages (p: [ p.beartype ]); - in mkShell' { - name = "devshell-for-${self.name}"; - inputsFrom = [ self ]; - packages = [ - pyEnv - stdenv.cc - fenixToolchain - ]; - env.PYTHONPATH = [ - "${pyEnv}/${pyEnv.sitePackages}" - # Cursed. - "${path}/nixos/lib/test-driver/src" - ] |> lib.concatStringsSep ":"; - }; + passthru.mkDevShell = { + path, + mkShell, + python3Packages, + fenixToolchain, + }: let + mkShell' = mkShell.override { inherit stdenv; }; + pyEnv = python3Packages.python.withPackages (p: [ p.beartype ]); + in mkShell' { + name = "devshell-for-${self.name}"; + inputsFrom = [ self ]; + packages = [ + pyEnv + stdenv.cc + fenixToolchain + ]; + env.PYTHONPATH = [ + "${pyEnv}/${pyEnv.sitePackages}" + # Cursed. + "${path}/nixos/lib/test-driver/src" + ] |> lib.concatStringsSep ":"; + }; - passthru.modulesPath = self.modules + "/share/nixos/modules"; - passthru.dynix = self.modulesPath + "/dynix"; + passthru.modulesPath = self.modules + "/share/nixos/modules"; + passthru.dynix = self.modulesPath + "/dynix"; - passthru.tests = lib.fix (callPackage ./tests { - dynix = self; - }).packages; + passthru.tests = lib.fix (callPackage ./tests { + dynix = self; + }).packages; - passthru.allTests = linkFarm "dynix-all-tests" self.tests; + passthru.allTests = linkFarm "dynix-all-tests" self.tests; - meta = { - longDescription = lib.dedent '' - Default output contains the Rust binary. - The `modules` output contains the modules prefixed under `/share/nixos/modules/dynix`. - The `dynix` passthru attr is a shortcut for the modules output *with* the modules prefix, - and thus `dynix.dynix` can be passed directly to `imports = [`. - ''; - mainProgram = "dynix"; - outputsToInstall = [ "out" "modules" ]; - }; + meta = { + longDescription = lib.dedent '' + Default output contains the Rust binary. + The `modules` output contains the modules prefixed under `/share/nixos/modules/dynix`. + The `dynix` passthru attr is a shortcut for the modules output *with* the modules prefix, + and thus `dynix.dynix` can be passed directly to `imports = [`. + ''; + mainProgram = "dynix"; + outputsToInstall = [ "out" "modules" ]; + }; })) diff --git a/shell.nix b/shell.nix index 4b93b7f..e2af1b4 100644 --- a/shell.nix +++ b/shell.nix @@ -1,26 +1,26 @@ { - pkgs ? import { - config = { - checkMeta = true; - allowAliases = false; - }; - }, - qpkgs ? let - src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; - in import src { inherit pkgs; }, - dynix ? import ./default.nix { inherit pkgs qpkgs; }, - fenixLib ? let - src = fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz"; - in import src { inherit pkgs; }, - fenixToolchain ? fenixLib.latest.toolchain, + pkgs ? import { + config = { + checkMeta = true; + allowAliases = false; + }; + }, + qpkgs ? let + src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; + in import src { inherit pkgs; }, + dynix ? import ./default.nix { inherit pkgs qpkgs; }, + fenixLib ? let + src = fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz"; + in import src { inherit pkgs; }, + fenixToolchain ? fenixLib.latest.toolchain, }: let - inherit (pkgs) lib; + inherit (pkgs) lib; - mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { inherit fenixToolchain; }; - devShell = mkDevShell dynix; + mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { inherit fenixToolchain; }; + devShell = mkDevShell dynix; - byStdenv = lib.mapAttrs (lib.const mkDevShell) dynix.byStdenv; + byStdenv = lib.mapAttrs (lib.const mkDevShell) dynix.byStdenv; in devShell.overrideAttrs (prev: { - passthru = { inherit byStdenv; }; + passthru = { inherit byStdenv; }; }) diff --git a/tests/default.nix b/tests/default.nix index 02842fa..535c54b 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -1,7 +1,7 @@ { pkgs ? import { }, qpkgs ? let - src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; + src = fetchTree (builtins.parseFlakeRef "github:Qyriad/nur-packages"); in import src { inherit pkgs; }, callPackage ? qpkgs.callPackage, lib ? qpkgs.lib, diff --git a/tests/distccd/configuration.nix b/tests/distccd/configuration.nix index b0d90b5..a8fb343 100644 --- a/tests/distccd/configuration.nix +++ b/tests/distccd/configuration.nix @@ -1,11 +1,9 @@ { ... }: - { services.distccd = { enable = true; jobTimeout = 900; maxJobs = 12; - logLevel = "warning"; nice = -10; }; diff --git a/tests/distccd/test-script.py b/tests/distccd/test-script.py index 3678155..d5867bf 100644 --- a/tests/distccd/test-script.py +++ b/tests/distccd/test-script.py @@ -1,5 +1,7 @@ import argparse import functools +#from pathlib import Path +#from pprint import pformat import shlex import textwrap from typing import Any, cast, TYPE_CHECKING @@ -62,6 +64,7 @@ def get_cli_args() -> argparse.Namespace: machine.log(f"{cmdline_args=}") print(f"{cmdline_args=}") + #return shlex.join(cmdline_args[1:]) args, rest = parser.parse_known_args(cmdline_args) return args @@ -74,7 +77,12 @@ def dynix_append(option: str, value: Any): @beartype def do_apply(): expr = textwrap.dedent(""" - (import { }).config.dynamicism.applyDynamicConfiguration { } + let + nixos = import { }; + in nixos.config.dynamicism.applyDynamicConfiguration { + baseConfiguration = /etc/nixos/configuration.nix; + newConfiguration = /etc/nixos/dynamic.nix; + } """).strip() machine.succeed(rf""" @@ -82,13 +90,12 @@ def do_apply(): """.strip()) machine.wait_for_unit("default.target") -machine.wait_for_unit("install-dynix.service") - -dynix_out = machine.succeed("dynix --version") -assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" +assert "lix" in machine.succeed("nix --version").lower() +machine.log("INIT") # Config should have our initial values. args = get_cli_args() +#assert '--jobs 12' in args, f'--jobs 12 not in {args=}' assert args.jobs == 12, f'{args.jobs=} != 12' assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' assert args.log_level == 'warning', f'{args.log_level=} != warning' @@ -98,16 +105,17 @@ with machine.nested("must succeed: initial nixos-rebuild switch"): # Config should not have changed. args = get_cli_args() -assert args.jobs == 12, f'{args.jobs=} != 12' -assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' -assert args.log_level == 'warning', f'{args.log_level=} != warning' - +#machine.log(f"config.toml after first rebuild: {indent(pformat(args))}") +#assert int(args['workers']) == 4, f"{args['workers']=} != 4" +#assert int(args['max_connection_rate']) == 256, f"{args['max_connection_rate']=} != 256" +# new_jobs = 4 dynix_append("services.distccd.maxJobs", new_jobs) do_apply() -# Only jobs should have changed. The others should still be default. args = get_cli_args() + +# Only jobs should have changed. The others should still be default. assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}' assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' assert args.log_level == 'warning', f'{args.log_level=} != warning' @@ -117,13 +125,6 @@ dynix_append("services.distccd.logLevel", f'"{new_log_level}"') do_apply() args = get_cli_args() -assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}' -assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' +#assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}' +#assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' assert args.log_level == new_log_level, f'{args.log_level=} != {new_log_level=}' - -# And this should set everything back. -machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") -args = get_cli_args() -assert args.jobs == 12, f'{args.jobs=} != 12' -assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' -assert args.log_level == 'warning', f'{args.log_level=} != warning' diff --git a/tests/gotosocial/configuration-package.nix b/tests/gotosocial/configuration-package.nix new file mode 100644 index 0000000..96a89d8 --- /dev/null +++ b/tests/gotosocial/configuration-package.nix @@ -0,0 +1,7 @@ +{ + runCommand, +}: runCommand "tests-gotosocial-configuration-dot-nix" { +} '' + set -euo pipefail + install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" +'' diff --git a/tests/gotosocial/default.nix b/tests/gotosocial/default.nix new file mode 100644 index 0000000..7396103 --- /dev/null +++ b/tests/gotosocial/default.nix @@ -0,0 +1,7 @@ +/** + * Convenience shortcut for running this test from the command-line. + * Normally this test is initialized from /tests/default.nix. + */ +{ + pkgs ? import { }, +}: pkgs.testers.runNixOSTest ./test.nix diff --git a/tests/gotosocial/hardware-configuration.nix b/tests/gotosocial/hardware-configuration.nix new file mode 100644 index 0000000..f036a72 --- /dev/null +++ b/tests/gotosocial/hardware-configuration.nix @@ -0,0 +1,7 @@ +/** Dummy hardware configuration. + * Will be replaced with the real one in the test VM. + */ +{ ... }: +{ + +} diff --git a/tests/gotosocial/test-script.py b/tests/gotosocial/test-script.py index 51a303f..9799dfb 100644 --- a/tests/gotosocial/test-script.py +++ b/tests/gotosocial/test-script.py @@ -61,7 +61,12 @@ def dynix_append(option: str, value: str): @beartype def do_apply(): expr = textwrap.dedent(""" - (import { }).config.dynamicism.applyDynamicConfiguration { } + let + nixos = import { }; + in nixos.config.dynamicism.applyDynamicConfiguration { + baseConfiguration = /etc/nixos/configuration.nix; + newConfiguration = /etc/nixos/dynamic.nix; + } """).strip() machine.succeed(rf""" @@ -70,6 +75,7 @@ def do_apply(): machine.wait_for_unit("default.target") +assert "lix" in machine.succeed("nix --version").lower() machine.wait_for_unit("install-dynix.service") dynix_out = machine.succeed("dynix --version") @@ -88,12 +94,6 @@ except StopIteration: raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" -try: - host = next(line for line in lines if line.startswith("host:")) -except StopIteration: - raise AssertionError(f"no 'host:' found in config file: {textwrap.indent(config_text, " ")}") -assert "gotosocial-machine" in host, f"'gotosocial-machine' should be in {host=}" - new_app_name = "yay!" dynix_append("services.gotosocial.settings.application-name", f'"{new_app_name}"') do_apply() diff --git a/tests/harmonia/configuration.nix b/tests/harmonia/configuration.nix index d5cc833..37494cb 100644 --- a/tests/harmonia/configuration.nix +++ b/tests/harmonia/configuration.nix @@ -1,6 +1,20 @@ -{ ... }: +{ lib, modulesPath, ... }: +let + moduleList = import "${modulesPath}/module-list.nix"; + dynixFromSearchPath = let + res = builtins.tryEval ; + in lib.optional res.success res.value; +in { + imports = [ + #"${modulesPath}/testing/test-instrumentation.nix" + #./hardware-configuration.nix + ] ++ lib.concatLists [ + #dynixFromSearchPath + moduleList + ]; + dynamicism.for.harmonia.enable = true; services.harmonia = { enable = true; diff --git a/tests/harmonia/hardware-configuration.nix b/tests/harmonia/hardware-configuration.nix new file mode 100644 index 0000000..facb35d --- /dev/null +++ b/tests/harmonia/hardware-configuration.nix @@ -0,0 +1,4 @@ +{ ... }: +{ + +} diff --git a/tests/harmonia/test-script.py b/tests/harmonia/test-script.py index bdac573..215ea38 100644 --- a/tests/harmonia/test-script.py +++ b/tests/harmonia/test-script.py @@ -1,5 +1,6 @@ import functools from pathlib import Path +from pprint import pformat import shlex import textwrap import tomllib @@ -63,7 +64,12 @@ def dynix_append(option: str, value: Any): @beartype def do_apply(): expr = textwrap.dedent(""" - (import { }).config.dynamicism.applyDynamicConfiguration { } + let + nixos = import { }; + in nixos.config.dynamicism.applyDynamicConfiguration { + baseConfiguration = /etc/nixos/configuration.nix; + newConfiguration = /etc/nixos/dynamic.nix; + } """).strip() machine.succeed(rf""" @@ -71,13 +77,12 @@ def do_apply(): """.strip()) machine.wait_for_unit("default.target") -machine.wait_for_unit("install-dynix.service") - -dynix_out = machine.succeed("dynix --version") -assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" +assert "lix" in machine.succeed("nix --version").lower() +machine.log("INIT") # Config should have our initial values. config_toml = get_config_file() +machine.log(f"config.toml BEFORE first rebuild (initial): {indent(pformat(config_toml))}") assert int(config_toml['workers']) == 4, f"{config_toml['workers']=} != 4" assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" @@ -86,6 +91,7 @@ with machine.nested("must succeed: initial nixos-rebuild switch"): # Config should not have changed. config_toml = get_config_file() +machine.log(f"config.toml after first rebuild: {indent(pformat(config_toml))}") assert int(config_toml['workers']) == 4, f"{config_toml['workers']=} != 4" assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" @@ -95,6 +101,7 @@ do_apply() # Workers, but not max connection rate, should have changed. config_toml = get_config_file() +machine.log(f"config.toml after DYNAMIC ACTIVATION: {indent(pformat(config_toml))}") assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} != {new_workers}" assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" @@ -102,14 +109,16 @@ new_max_connection_rate = 100 dynix_append("services.harmonia.settings.max_connection_rate", new_max_connection_rate) do_apply() -# Max connection rate should have changed, and workers should be the same as before. +# Max connection rate should have changed, but workers should have reverted. config_toml = get_config_file() +machine.log(f"config_toml after SECOND dynamic activation: {indent(pformat(config_toml))}") assert int(config_toml['max_connection_rate']) == new_max_connection_rate, f"{config_toml['max_connection_rate']=} != {new_max_connection_rate}" -assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} != {new_workers}" # And this should set everything back. machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") +machine.systemctl("restart harmonia.service") machine.wait_for_unit("harmonia.service") config_toml = get_config_file() +machine.log(f"config_toml after NORMAL activation: {indent(pformat(config_toml))}") assert int(config_toml['max_connection_rate']) == 256, f'{config_toml["max_connection_rate"]=} != 256' assert int(config_toml['workers']) == 4, f'{config_toml["workers"]=} != 4' diff --git a/tests/tzupdate/configuration.nix b/tests/tzupdate/configuration.nix new file mode 100644 index 0000000..176975c --- /dev/null +++ b/tests/tzupdate/configuration.nix @@ -0,0 +1,67 @@ +{ pkgs, lib, config, modulesPath, ... }: +let + name = config.networking.hostName; + moduleList = import "${modulesPath}/module-list.nix"; + + dynixFromSearchPath = let + res = builtins.tryEval ; + in lib.optional res.success res.value; +in +{ + imports = [ + "${modulesPath}/testing/test-instrumentation.nix" + ./hardware-configuration.nix + ] ++ lib.concatLists [ + moduleList + dynixFromSearchPath + ]; + + dynamicism.for.tzupdate.enable = true; + services.tzupdate = { + enable = true; + }; + + system.switch.enable = true; + documentation.enable = false; + + networking.hostName = "tzupdate-machine"; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + nix = { + package = pkgs.lixPackageSets.latest.lix; + nixPath = [ + "nixpkgs=${pkgs.path}" + "/nix/var/nix/profiles/per-user/root/profile/share/nixos/modules" + ]; + + settings = { + experimental-features = [ "nix-command" "pipe-operator" ]; + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 1; + # For my debugging purposes. + show-trace = true; + }; + }; + + environment.pathsToLink = [ "/share" ]; + environment.extraOutputsToInstall = [ "modules" ]; + environment.variables = { + "NIXOS_CONFIG" = "/etc/nixos/configuration.nix"; + }; + + environment.shellAliases = { + ls = "eza --long --header --group --group-directories-first --classify --binary"; + }; + + environment.systemPackages = with pkgs; [ + eza + fd + ripgrep + ]; +} diff --git a/tests/tzupdate/hardware-configuration.nix b/tests/tzupdate/hardware-configuration.nix new file mode 100644 index 0000000..facb35d --- /dev/null +++ b/tests/tzupdate/hardware-configuration.nix @@ -0,0 +1,4 @@ +{ ... }: +{ + +} diff --git a/tests/tzupdate/test-script.py b/tests/tzupdate/test-script.py new file mode 100644 index 0000000..53762a0 --- /dev/null +++ b/tests/tzupdate/test-script.py @@ -0,0 +1,52 @@ +#from pathlib import Path +#import shlex +#import textwrap +from typing import cast, TYPE_CHECKING + +from beartype import beartype + +from test_driver.machine import Machine +from test_driver.errors import RequestedAssertionFailed + +if TYPE_CHECKING: + global machine + machine = cast(Machine, ...) + assert machine.shell is not None + +ls = "eza -lah --color=always --group-directories-first" + +@beartype +def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: + output = "" + for command in commands: + with machine.nested(f"must succeed: {command}"): + (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) + if status != 0: + machine.log(f"output: {out}") + raise RequestedAssertionFailed( + f"command `{command}` failed (exit code {status})", + ) + output += out + + return output + +machine.wait_for_unit("default.target") +assert "lix" in machine.succeed("nix --version").lower() +machine.log("INIT") + + +machine.succeed("nixos-generate-config") +machine.succeed("mkdir -vp /etc/nixos") +# Dereference is required since that configuration.nix is probably a symlink to the store. +machine.succeed("cp -rv --dereference /run/current-system/sw/share/nixos/configuration.nix /etc/nixos/") + +machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") + +@beartype +def get_interval() -> str: + prop = machine.get_unit_property("tzupdate.timer", "OnCalendar") + print(f"{prop=}") + + return prop + +get_interval() diff --git a/tests/tzupdate/test.nix b/tests/tzupdate/test.nix new file mode 100644 index 0000000..2d42c10 --- /dev/null +++ b/tests/tzupdate/test.nix @@ -0,0 +1,24 @@ +{ ... }: + +{ + name = "nixos-test-dynamicism-tzupdate"; + + extraPythonPackages = p: [ + p.beartype + ]; + + nodes.machine = { name, pkgs, ... }: { + imports = [ ./configuration.nix ]; + + environment.systemPackages = let + configFileTree = pkgs.runCommand "${name}-configuration-dot-nix" { } '' + set -euo pipefail + install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" + ''; + in [ + configFileTree + ]; + }; + + testScript = builtins.readFile ./test-script.py; +}