diff options
Diffstat (limited to 'tests/vm.nix')
| -rw-r--r-- | tests/vm.nix | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/tests/vm.nix b/tests/vm.nix new file mode 100644 index 0000000..cb6385d --- /dev/null +++ b/tests/vm.nix @@ -0,0 +1,142 @@ +# Runs scenarios in a NixOS VM test with two machines: deployer, which runs +# populate, and target, which it deploys to via ssh. Local scenarios deploy +# to deployer itself. +{ pkgs, name, scenarios, gitHead }: + +let + inherit (pkgs) lib; + + sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs; + + # The scenarios as JSON, with scripts as files. + plan = pkgs.writeText "krops-test-${name}.json" (builtins.toJSON (map + (scenario: scenario // { + prepare = pkgs.writeText "prepare" scenario.prepare; + phases = map + (phase: phase // { + check = pkgs.writeText "check" phase.check; + mutate = + if phase.mutate == null then null + else pkgs.writeText "mutate" phase.mutate; + }) + scenario.phases; + }) + scenarios)); + + common = { pkgs, ... }: { + # The plan refers to all scripts the scenarios run. Being part of the + # system, they survive the scenarios collecting garbage. + system.extraDependencies = [ plan "${./fixtures.sh}" ]; + nix.settings.substituters = lib.mkForce []; + environment.systemPackages = [ pkgs.git pkgs.rsync ]; + # Only warnings and errors on the console, which ends up in the log. + services.journald.extraConfig = lib.mkAfter "MaxLevelConsole=warning"; + boot.initrd.systemd.contents."/etc/systemd/journald.conf".text = + lib.mkAfter "MaxLevelConsole=warning"; + boot.consoleLogLevel = lib.mkForce 4; + # Neither probing BIOS disk information, nor iPXE option ROMs, which + # network boot would need, nor systemd-oomd, which warns about missing + # swap. + boot.kernelParams = lib.optional pkgs.stdenv.hostPlatform.isx86 "edd=off"; + virtualisation.qemu.options = [ "-global virtio-net-pci.romfile=" ]; + systemd.oomd.enable = false; + virtualisation.memorySize = 2048; + virtualisation.diskSize = 4096; + }; + +in pkgs.testers.runNixOSTest { + name = "krops-${name}"; + + globalTimeout = 4 * 60 * 60; + + nodes.deployer = { pkgs, ... }: { + imports = [ common ]; + environment.systemPackages = [ + pkgs.age pkgs.gnupg pkgs.pass pkgs.passage + ]; + networking.firewall.allowedTCPPorts = [ 8000 9418 ]; + programs.ssh.extraConfig = '' + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + LogLevel ERROR + ''; + systemd.services.fixtures-http = { + wantedBy = [ "multi-user.target" ]; + preStart = "mkdir -p /fixtures/www"; + serviceConfig.ExecStart = + "${pkgs.python3}/bin/python3 -m http.server 8000 --directory /fixtures/www"; + }; + systemd.services.fixtures-git = { + wantedBy = [ "multi-user.target" ]; + preStart = "mkdir -p /fixtures/git"; + serviceConfig.ExecStart = + "${pkgs.git}/bin/git daemon --reuseaddr --export-all --base-path=/fixtures/git"; + }; + }; + + nodes.target = { ... }: { + imports = [ common ]; + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [ sshKeys.snakeOilPublicKey ]; + users.users.deploy = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + openssh.authorizedKeys.keys = [ sshKeys.snakeOilPublicKey ]; + }; + security.sudo.wheelNeedsPassword = false; + }; + + testScript = /* python */ '' + import json + + plan = json.load(open("${plan}")) + nodes = {"deployer": deployer, "target": target} + + + # Runs a command, logging its output only if it fails. + def run(node, command): + status, out = node.execute(f"{command} 2>&1") + if status != 0: + raise Exception(f"{command} fails:\n{out}") + + + start_all() + deployer.wait_for_unit("fixtures-http.service") + deployer.wait_for_unit("fixtures-git.service") + deployer.wait_for_open_port(8000) + deployer.wait_for_open_port(9418) + target.wait_for_unit("sshd.service") + target.wait_for_open_port(22) + + with subtest("setup"): + run(deployer, "install -Dm600 ${sshKeys.snakeOilPrivateKey} /root/.ssh/id_ecdsa") + run(deployer, "ssh root@target true") + run(deployer, "ssh deploy@target sudo true") + run(deployer, "HOME=/root sh ${./fixtures.sh} http://deployer:8000 ${gitHead}") + + failed = [] + for scenario in plan: + node = nodes[scenario["node"]] + try: + with subtest(scenario["name"]): + run(node, f"sh {scenario['prepare']}") + for i, phase in enumerate(scenario["phases"]): + status, out = deployer.execute(f"HOME=/root {phase['populate']} 2>&1") + if phase["expect"] is None: + if status != 0: + raise Exception(f"populate {i} fails:\n{out}") + elif status == 0 or phase["expect"] not in out: + raise Exception( + f"populate {i} doesn't fail with {phase['expect']!r}:\n{out}" + ) + run(node, f"KROPS_TEST_GC=1 sh {phase['check']}") + if phase["mutate"] is not None: + run(node, f"sh {phase['mutate']}") + except Exception as e: + print(f"scenario {scenario['name']} fails: {e}") + failed.append(scenario["name"]) + + if failed: + raise Exception(f"{len(failed)} scenarios fail: {' '.join(failed)}") + ''; +} |
