summaryrefslogtreecommitdiffstats
path: root/nixos/modules/testing/test-instrumentation.nix
blob: 47890479a4c93bf4f3f36ad9523dda7302ff8858 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# This module allows the test driver to connect to the virtual machine
# via a root shell attached to a virtio console.

{
  options,
  config,
  lib,
  pkgs,
  ...
}:

with lib;

let
  cfg = config.testing;

  qemu-common = import ../../lib/qemu-common.nix { inherit (pkgs) lib stdenv; };

  backdoorService = {
    requires = [
      "dev-hvc0.device"
      "dev-${qemu-common.qemuSerialDevice}.device"
    ];
    after = [
      "dev-hvc0.device"
      "dev-${qemu-common.qemuSerialDevice}.device"
    ];
    script = ''
      export USER=root
      export HOME=/root
      export DISPLAY=:0.0

      # Determine if this script is ran with nounset
      strict="false"
      if set -o | grep --quiet --perl-regexp "nounset\s+on"; then
          strict="true"
      fi

      if [[ -e /etc/profile ]]; then
          # TODO: Currently shell profiles are not checked at build time,
          # so we need to unset stricter options to source them
          set +o nounset
          # shellcheck disable=SC1091
          source /etc/profile
          [ "$strict" = "true" ] && set -o nounset
      fi

      # Don't use a pager when executing backdoor
      # actions. Because we use a tty, commands like systemctl
      # or nix-store get confused into thinking they're running
      # interactively.
      export PAGER=

      cd /tmp
      exec < /dev/hvc0 > /dev/hvc0
      while ! exec 2> /dev/${qemu-common.qemuSerialDevice}; do sleep 0.1; done
      echo "connecting to host..." >&2
      stty -F /dev/hvc0 raw -echo # prevent nl -> cr/nl conversion
      # The following line is essential since it signals to
      # the test driver that the shell is ready.
      # See: the connect method in the Machine class.
      echo "Spawning backdoor root shell..."
      # Passing the terminal device makes bash run non-interactively.
      # Otherwise we get errors on the terminal because bash tries to
      # setup things like job control.
      # Note: calling bash explicitly here instead of sh makes sure that
      # we can also run non-NixOS guests during tests. This, however, is
      # mostly futureproofing as the test instrumentation is still very
      # tightly coupled to NixOS.
      PS1="" exec ${pkgs.bashNonInteractive}/bin/bash --norc /dev/hvc0
    '';
    serviceConfig.KillSignal = "SIGHUP";
  };

  managerSettings = {
    # Don't clobber the console with duplicate systemd messages.
    ShowStatus = false;
    # Allow very slow start
    DefaultTimeoutStartSec = 300;
    DefaultDeviceTimeoutSec = 300;
    # Don't enforce a minimum uptime before shutting down.
    MinimumUptimeSec = 0;
  };

in

{

  options.testing = {
    backdoor = lib.mkEnableOption "backdoor service in stage 2" // {
      # See assertion below for why the backdoor doesn't work with containers.
      default = !config.boot.isContainer;
    };

    initrdBackdoor = lib.mkEnableOption ''
      backdoor.service in initrd. Requires
      boot.initrd.systemd.enable to be enabled. Boot will pause in
      stage 1 at initrd.target, and will listen for commands from the
      Machine python interface, just like stage 2 normally does. This
      enables commands to be sent to test and debug stage 1. Use
      machine.switch_root() to leave stage 1 and proceed to stage 2
    '';
  };

  config = {

    assertions = [
      {
        assertion = cfg.initrdBackdoor -> config.boot.initrd.systemd.enable;
        message = ''
          `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.
        '';
      }
    ];

    systemd.services.backdoor = lib.mkIf cfg.backdoor (
      lib.mkMerge [
        backdoorService
        {
          wantedBy = [ "multi-user.target" ];
        }
      ]
    );

    boot.initrd.systemd = lib.mkMerge [
      {
        contents."/etc/systemd/journald.conf".text = ''
          [Journal]
          ForwardToConsole=yes
          TTYPath=/dev/${qemu-common.qemuSerialDevice}
          MaxLevelConsole=debug
        '';

        settings.Manager = managerSettings;
      }

      (lib.mkIf cfg.initrdBackdoor {
        # Implemented in machine.switch_root(). Suppress the unit by
        # making it a noop without removing it, which would break
        # initrd-parse-etc.service
        services.initrd-cleanup.serviceConfig.ExecStart = [
          # Reset
          ""
          # noop
          "/bin/true"
        ];

        services.backdoor = lib.mkMerge [
          backdoorService
          {
            # TODO: Both stage 1 and stage 2 should use these same
            # settings. But a lot of existing tests rely on
            # backdoor.service having default orderings,
            # e.g. systemd-boot.update relies on /boot being mounted
            # as soon as backdoor starts. But it can be useful for
            # backdoor to start even earlier.
            wantedBy = [ "sysinit.target" ];
            unitConfig.DefaultDependencies = false;
            conflicts = [
              "shutdown.target"
              "initrd-switch-root.target"
            ];
            before = [
              "shutdown.target"
              "initrd-switch-root.target"
            ];
          }
        ];

        storePaths = [
          "${pkgs.coreutils}/bin/env"
        ];
      })
    ];

    # Prevent agetty from being instantiated on the serial device, since it
    # interferes with the backdoor (writes to it will randomly fail
    # with EIO).  Likewise for hvc0.
    systemd.services."serial-getty@${qemu-common.qemuSerialDevice}".enable = false;
    systemd.services."serial-getty@hvc0".enable = false;

    # Only set these settings when the options exist. Some tests (e.g. those
    # that do not specify any nodes, or an empty attr set as nodes) will not
    # have the QEMU module loaded and thuse these options can't and should not
    # be set.
    virtualisation = lib.optionalAttrs (options ? virtualisation.qemu.package) {
      qemu = {
        # NOTE: optionalAttrs
        #       test-instrumentation.nix appears to be used without qemu-vm.nix, so
        #       we avoid defining attributes if not possible.
        # TODO: refactor such that test-instrumentation can import qemu-vm
        package = lib.mkDefault pkgs.qemu_test;
      };
    };

    boot.kernel.sysctl = {
      "kernel.hung_task_timeout_secs" = 600;
      # Panic on out-of-memory conditions rather than letting the
      # OOM killer randomly get rid of processes, since this leads
      # to failures that are hard to diagnose.
      "vm.panic_on_oom" = lib.mkDefault 2;
    };

    boot.kernelParams = [
      "console=${qemu-common.qemuSerialDevice}"
      "console=tty0"
      # Panic if an error occurs in stage 1 (rather than waiting for
      # user intervention).
      "panic=1"
      "boot.panic_on_fail"
      # Using acpi_pm as a clock source causes the guest clock to
      # slow down under high host load.  This is usually a bad
      # thing, but for VM tests it should provide a bit more
      # determinism (e.g. if the VM runs at lower speed, then
      # timeouts in the VM should also be delayed).
      "clocksource=acpi_pm"
    ];

    # `xwininfo' is used by the test driver to query open windows.
    environment.systemPackages = [ pkgs.xwininfo ];

    # Log everything to the serial console.
    services.journald.settings.Journal = {
      ForwardToConsole = true;
      TTYPath = "/dev/${qemu-common.qemuSerialDevice}";
      MaxLevelConsole = "debug";
    };

    systemd.settings.Manager = managerSettings;
    systemd.user.settings.Manager = {
      # Allow very slow start
      DefaultTimeoutStartSec = 300;
      DefaultDeviceTimeoutSec = 300;
    };

    boot.consoleLogLevel = 7;

    # Prevent tests from accessing the Internet.
    networking.defaultGateway = mkOverride 150 null;
    networking.nameservers = mkOverride 150 [ ];

    system.requiredKernelConfig = with config.lib.kernelConfig; [
      (isYes "SERIAL_8250_CONSOLE")
      (isYes "SERIAL_8250")
      (isEnabled "VIRTIO_CONSOLE")
    ];

    networking.usePredictableInterfaceNames = false;

    # Make it easy to log in as root when running the test interactively.
    # This needs to be a file because of a quirk in systemd credentials,
    # where you cannot specify an empty string as a value. systemd-sysusers
    # uses credentials to set passwords on users.
    users.users.root.hashedPasswordFile = mkOverride 150 "${pkgs.writeText "hashed-password.root" ""}";

    services.displayManager.logToJournal = true;

    services.logrotate.enable = mkOverride 150 false;

    # Make sure we use the Guest Agent from the QEMU package for testing
    # to reduce the closure size required for the tests.
    services.qemuGuest.package = pkgs.qemu_test.ga;

    # Squelch warning about unset system.stateVersion
    system.stateVersion = (lib.mkOverride 1200) lib.trivial.release;
  };

}