staging-nixos merge for 2026-04-18 (#511133)

This commit is contained in:
zowoq
2026-04-18 10:53:03 +00:00
committed by GitHub
44 changed files with 1697 additions and 594 deletions
@@ -88,52 +88,34 @@ An SSH-based backdoor to log into machines can be enabled with
}
```
::: {.warning}
Make sure to only enable the backdoor for interactive tests
(i.e. by using `interactive.sshBackdoor.enable`)! This is the only
supported configuration.
Running a test in a sandbox with this will fail because `/dev/vhost-vsock` isn't available
in the sandbox.
:::
This creates a [vsock socket](https://man7.org/linux/man-pages/man7/vsock.7.html)
for each VM to log in with SSH. This configures root login with an empty password.
When the VMs get started interactively with the test-driver, it's possible to
connect to `machine` with
On the host-side a UNIX domain-socket is used with
[vhost-device-vsock](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-vsock/README.md).
That way, it's not necessary to assign system-wide unique vsock numbers.
```
$ ssh vsock/3 -o User=root
$ ssh vsock-mux//tmp/path/to/host -o User=root
```
The socket numbers correspond to the node number of the test VM, but start
at three instead of one because that's the lowest possible
vsock number. The exact SSH commands are also printed out when starting
`nixos-test-driver`.
The socket paths are printed when starting the test driver:
```
Note: this requires systemd-ssh-proxy(1) to be enabled (default on NixOS 25.05 and newer).
machine: ssh -o User=root vsock-mux//tmp/tmpg1rp9nti/machine_host.socket
```
On non-NixOS systems you'll probably need to enable
the SSH config from {manpage}`systemd-ssh-proxy(1)` yourself.
If starting VM fails with an error like
During a test-run, it's possible to print the SSH commands again by running
```
qemu-system-x86_64: -device vhost-vsock-pci,guest-cid=3: vhost-vsock: unable to set guest cid: Address already in use
```
it means that the vsock numbers for the VMs are already in use. This can happen
if another interactive test with SSH backdoor enabled is running on the machine.
In that case, you need to assign another range of vsock numbers. You can pick another
offset with
```nix
{
sshBackdoor = {
enable = true;
vsockOffset = 23542;
};
}
In [2]: dump_machine_ssh()
SSH backdoor enabled, the machines can be accessed like this:
Note: this requires systemd-ssh-proxy(1) to be enabled (default on NixOS 25.05 and newer).
machine: ssh -o User=root vsock-mux//tmp/tmpg1rp9nti/machine_host.socket
```
## Port forwarding to NixOS test VMs {#sec-nixos-test-port-forwarding}
@@ -512,19 +512,11 @@ Once you are in the sandbox shell, you can access the VMs (for example, `machine
with SSH over vsock:
```
bash# ssh -F ./ssh_config vsock/3
bash# ssh -F ./ssh_config -o User=root vsock-mux//tmp/.../machine_host.socket
```
For the AF_VSOCK feature to work, `/dev/vhost-vsock` is needed in the sandbox
which can be done with e.g.
```
nix-build -A nixosTests.foo --option sandbox-paths /dev/vhost-vsock
```
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`.
The socket paths are printed at the beginning of the test. See
[](#sec-nixos-test-ssh-access) for more context.
### SSH access to test containers {#sec-test-container-ssh-access}
+6 -3
View File
@@ -2189,9 +2189,6 @@
"test-opt-sshBackdoor.enable": [
"index.html#test-opt-sshBackdoor.enable"
],
"test-opt-sshBackdoor.vsockOffset": [
"index.html#test-opt-sshBackdoor.vsockOffset"
],
"test-opt-enableDebugHook": [
"index.html#test-opt-enableDebugHook"
],
@@ -2222,6 +2219,9 @@
"test-opt-interactive": [
"index.html#test-opt-interactive"
],
"test-opt-logLevel": [
"index.html#test-opt-logLevel"
],
"test-opt-meta": [
"index.html#test-opt-meta"
],
@@ -2258,6 +2258,9 @@
"test-opt-passthru": [
"index.html#test-opt-passthru"
],
"test-opt-qemu.forceAccel": [
"index.html#test-opt-qemu.forceAccel"
],
"test-opt-qemu.package": [
"index.html#test-opt-qemu.package"
],
@@ -336,6 +336,8 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.
- Budgie has been updated to 10.10, please check the [upstream announcement](https://buddiesofbudgie.org/blog/budgie-10-10-released) for more details.
- `fonts.fontconfig.useEmbeddedBitmaps` is now set to `true` by default.
- `stestrCheckHook` was added: This test hook runs `stestr run`. You can disable tests with `disabledTests` and `disabledTestsRegex`.
- `services.frp` now supports multiple instances through `services.frp.instances` to make it possible to run multiple frp clients or servers at the same time.
+15 -8
View File
@@ -24,30 +24,37 @@ rec {
else
throw "Unknown QEMU serial device for system '${stdenv.hostPlatform.system}'";
qemuBinary =
qemuPkg:
qemuBinary = qemuPkg: qemuBinaryWith { inherit qemuPkg; };
qemuBinaryWith =
{
qemuPkg,
forceAccel ? false,
}:
let
hostStdenv = qemuPkg.stdenv;
hostSystem = hostStdenv.system;
guestSystem = stdenv.hostPlatform.system;
accel = accelName: if forceAccel then accelName else "${accelName}:tcg";
linuxHostGuestMatrix = {
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=kvm:tcg -cpu max";
armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=kvm:tcg -cpu max";
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=kvm:tcg -cpu max";
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=${accel "kvm"} -cpu max";
armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=${accel "kvm"} -cpu max";
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=${accel "kvm"} -cpu max";
powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
powerpc64-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
riscv32-linux = "${qemuPkg}/bin/qemu-system-riscv32 -machine virt";
riscv64-linux = "${qemuPkg}/bin/qemu-system-riscv64 -machine virt";
x86_64-darwin = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=kvm:tcg -cpu max";
x86_64-darwin = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=${accel "kvm"} -cpu max";
};
otherHostGuestMatrix = {
aarch64-darwin = {
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=2,accel=hvf:tcg -cpu max";
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=2,accel=${accel "hvf"} -cpu max";
inherit (otherHostGuestMatrix.x86_64-darwin) x86_64-linux;
};
x86_64-darwin = {
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine type=q35,accel=hvf:tcg -cpu max";
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine type=q35,accel=${accel "hvf"} -cpu max";
};
};
+11 -8
View File
@@ -7,13 +7,15 @@
imagemagick_light,
ipython,
junit-xml,
mypy,
ptpython,
pydantic,
python,
ruff,
remote-pdb,
ruff,
ty,
netpbm,
vhost-device-vsock,
nixosTests,
qemu_pkg ? qemu_test,
qemu_test,
@@ -45,6 +47,7 @@ buildPythonApplication {
ipython
junit-xml
ptpython
pydantic
remote-pdb
]
++ extraPythonPackages python.pkgs;
@@ -56,6 +59,7 @@ buildPythonApplication {
socat
util-linux
vde2
vhost-device-vsock
]
++ lib.optionals enableNspawn [
systemd
@@ -65,20 +69,19 @@ buildPythonApplication {
tesseract4
];
passthru.tests = {
inherit (nixosTests.nixos-test-driver) driver-timeout;
};
# containers test requires extra nix features that are not available in ofborg.
passthru.tests = removeAttrs nixosTests.nixos-test-driver [ "containers" ];
doCheck = true;
nativeCheckInputs = [
mypy
ruff
ty
];
checkPhase = ''
echo -e "\x1b[32m## run mypy\x1b[0m"
mypy test_driver extract-docstrings.py
echo -e "\x1b[32m## run ty\x1b[0m"
ty check --error-on-warning test_driver extract-docstrings.py
echo -e "\x1b[32m## run ruff check\x1b[0m"
ruff check .
echo -e "\x1b[32m## run ruff format\x1b[0m"
@@ -66,17 +66,20 @@ def function_docstrings(functions: list[ast.FunctionDef]) -> str | None:
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.
"""
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
methods = [node for node in machine_class.body if isinstance(node, ast.FunctionDef)]
methods.sort(key=lambda x: x.name)
# Do not document internal functions prefixed with underscore
methods = [m for m in methods if not m.name.startswith("_")]
return methods
def main() -> None:
+1 -20
View File
@@ -17,27 +17,8 @@ find = {}
test_driver = ["py.typed"]
[tool.ruff]
target-version = "py312"
target-version = "py313"
line-length = 88
lint.select = ["E", "F", "I", "U", "N"]
lint.ignore = ["E501", "N818"]
# xxx: we can import https://pypi.org/project/types-colorama/ here
[[tool.mypy.overrides]]
module = "colorama.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "ptpython.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "junit_xml.*"
ignore_missing_imports = true
[tool.mypy]
warn_redundant_casts = true
disallow_untyped_calls = true
disallow_untyped_defs = true
no_implicit_optional = true
@@ -8,10 +8,11 @@ from pathlib import Path
import ptpython.ipython
from test_driver.debug import Debug, DebugAbstract, DebugNop
from test_driver.driver import Driver
from test_driver.driver import Driver, DriverConfiguration, load_driver_configuration
from test_driver.logger import (
CompositeLogger,
JunitXMLLogger,
LogLevel,
TerminalLogger,
XMLLogger,
)
@@ -22,7 +23,7 @@ class EnvDefault(argparse.Action):
environment variable as the flags default value.
"""
def __init__(self, envvar, required=False, default=None, nargs=None, **kwargs): # type: ignore
def __init__(self, envvar, required=False, default=None, nargs=None, **kwargs):
if not default and envvar:
if envvar in os.environ:
if nargs is not None and (nargs.isdigit() or nargs in ["*", "+"]):
@@ -36,7 +37,7 @@ class EnvDefault(argparse.Action):
required = False
super().__init__(default=default, required=required, nargs=nargs, **kwargs)
def __call__(self, parser, namespace, values, option_string=None): # type: ignore
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
@@ -56,6 +57,13 @@ def writeable_dir(arg: str) -> Path:
def main() -> None:
arg_parser = argparse.ArgumentParser(prog="nixos-test-driver")
arg_parser.add_argument(
"-c",
"--config",
help="the test driver configuration file",
type=Path,
required=True,
)
arg_parser.add_argument(
"--keep-vm-state",
help=argparse.SUPPRESS,
@@ -78,54 +86,6 @@ def main() -> None:
"--debug-hook-attach",
help="Enable interactive debugging breakpoints for sandboxed runs",
)
arg_parser.add_argument(
"--vm-names",
metavar="VM-NAME",
action=EnvDefault,
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",
action=EnvDefault,
envvar="vlans",
nargs="*",
help="vlans to span by the driver",
)
arg_parser.add_argument(
"--global-timeout",
type=int,
metavar="GLOBAL_TIMEOUT",
action=EnvDefault,
envvar="globalTimeout",
help="Timeout in seconds for the whole test",
)
arg_parser.add_argument(
"-o",
"--output_directory",
@@ -139,17 +99,14 @@ def main() -> None:
help="Enable JunitXML report generation to the given path",
type=Path,
)
log_level_map = {level.name.lower(): level for level in LogLevel}
arg_parser.add_argument(
"testscript",
"--log-level",
metavar="LOG_LEVEL",
action=EnvDefault,
envvar="testScript",
help="the test script to run",
type=Path,
)
arg_parser.add_argument(
"--dump-vsocks",
help="indicates that the interactive SSH backdoor is active and dumps information about it on start",
type=int,
envvar="logLevel",
choices=log_level_map,
help="Set the log level",
)
args = arg_parser.parse_args()
@@ -169,6 +126,9 @@ def main() -> None:
if args.junit_xml:
logger.add_logger(JunitXMLLogger(output_directory / args.junit_xml))
if args.log_level:
logger.set_log_level(log_level_map[args.log_level])
if not args.keep_machine_state:
logger.info(
"Machine state will be reset. To keep it, pass --keep-machine-state"
@@ -178,35 +138,22 @@ def main() -> None:
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(
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(),
config=load_driver_configuration(args.config),
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:
driver.dump_machine_ssh(offset)
if driver.config.enable_ssh_backdoor:
driver.dump_machine_ssh()
if args.interactive:
history_dir = os.getcwd()
history_path = os.path.join(history_dir, ".nixos-test-history")
ptpython.ipython.embed(
user_ns=driver.test_symbols(),
history_filename=history_path,
) # type:ignore
)
else:
tic = time.time()
driver.run_tests()
@@ -221,12 +168,14 @@ def generate_driver_symbols() -> None:
scripts.
"""
d = Driver(
vm_names=[],
vm_start_scripts=[],
container_names=[],
container_start_scripts=[],
vlans=[],
tests="",
config=DriverConfiguration(
vms=dict(),
containers=dict(),
vlans=[],
global_timeout=0,
enable_ssh_backdoor=False,
test_script=Path("testScriptWithTypes"),
),
out_dir=Path(),
logger=CompositeLogger([]),
)
@@ -6,7 +6,7 @@ import subprocess
import sys
from abc import ABC, abstractmethod
from remote_pdb import RemotePdb # type:ignore
from remote_pdb import RemotePdb
from test_driver.logger import AbstractLogger
@@ -42,12 +42,12 @@ class Debug(DebugAbstract):
self.logger.log_test_error(
f"Breakpoint reached, run 'sudo {self.attach} {pattern}'"
)
os.environ["bashInteractive"] = shutil.which("bash") # type:ignore
os.environ["bashInteractive"] = shutil.which("bash") # ty: ignore[invalid-assignment]
if os.fork() == 0:
subprocess.run(["sleep", pattern])
else:
# RemotePdb writes log messages to both stderr AND the logger,
# which is the same here. Hence, disabling the remote_pdb logger
# to avoid duplicate messages in the build log.
logging.root.manager.loggerDict["remote_pdb"].disabled = True # type:ignore
logging.root.manager.loggerDict["remote_pdb"].disabled = True # ty: ignore[invalid-assignment]
RemotePdb(host=host, port=port).set_trace(sys._getframe().f_back)
+165 -44
View File
@@ -1,3 +1,4 @@
import json
import os
import re
import signal
@@ -8,11 +9,13 @@ import threading
import traceback
from collections.abc import Callable, Iterator
from contextlib import AbstractContextManager, contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from unittest import TestCase
from colorama import Style
from pydantic import BaseModel
from test_driver.debug import DebugAbstract, DebugNop
from test_driver.errors import MachineError, RequestedAssertionFailed
@@ -26,7 +29,25 @@ from test_driver.machine import (
from test_driver.polling_condition import PollingCondition
from test_driver.vlan import VLan
SENTINEL = object()
class NodeConfiguration(BaseModel):
name: str
start_script: Path
class DriverConfiguration(BaseModel):
vms: dict[str, NodeConfiguration]
containers: dict[str, NodeConfiguration]
vlans: list[int]
global_timeout: int
enable_ssh_backdoor: bool
test_script: Path
def load_driver_configuration(file_path: str) -> DriverConfiguration:
with open(file_path) as f:
data = json.load(f)
return DriverConfiguration.model_validate(data)
class AssertionTester(TestCase):
@@ -63,81 +84,143 @@ def pythonize_name(name: str) -> str:
return re.sub(r"^[^A-Za-z_]|[^A-Za-z0-9_]", "_", name)
def in_nix_sandbox() -> bool:
# There seems to be no better method at the time
typical_nix_env_vars = "NIX_BUILD_TOP" in os.environ
nix_shell_marker = "IN_NIX_SHELL" in os.environ
return typical_nix_env_vars and not nix_shell_marker
@dataclass
class VsockPair:
guest: Path
host: Path
cid: int
class VHostDeviceVsock:
def __init__(self, tmp_dir: Path, machines: list[str]):
self.temp_dir_handle = tempfile.TemporaryDirectory(dir=tmp_dir)
self.temp_dir = Path(self.temp_dir_handle.name)
self.sockets = {
machine: VsockPair(
self.temp_dir / f"{machine}_guest.socket",
self.temp_dir / f"{machine}_host.socket",
cid,
)
for cid, machine in enumerate(machines, start=3)
}
self.vhost_proc = subprocess.Popen(
[
"vhost-device-vsock",
*(
arg
for vsock_pair in self.sockets.values()
for arg in (
"--vm",
f"guest-cid={vsock_pair.cid},socket={vsock_pair.guest},uds-path={vsock_pair.host}",
)
),
]
)
def __del__(self) -> None:
self.vhost_proc.kill()
self.temp_dir_handle.cleanup()
class Driver:
"""A handle to the driver that sets up the environment
and runs the tests"""
config: DriverConfiguration
tests: str
vlans: list[VLan]
machines_qemu: list[QemuMachine]
machines_nspawn: list[NspawnMachine]
vlans: list[VLan] = []
machines_qemu: list[QemuMachine] = []
machines_nspawn: list[NspawnMachine] = []
polling_conditions: list[PollingCondition]
global_timeout: int
race_timer: threading.Timer
keep_machine_state: bool
logger: AbstractLogger
debug: DebugAbstract
vhost_vsock: VHostDeviceVsock | None = None
def __init__(
self,
vm_names: list[str],
vm_start_scripts: list[str],
container_names: list[str],
container_start_scripts: list[str],
vlans: list[int],
tests: str,
config: DriverConfiguration,
out_dir: Path,
logger: AbstractLogger,
keep_machine_state: bool = False,
global_timeout: int = 24 * 60 * 60 * 7,
debug: DebugAbstract = DebugNop(),
):
self.tests = tests
self.config = config
self.tests = config.test_script.read_text()
self.out_dir = out_dir
self.global_timeout = global_timeout
self.race_timer = threading.Timer(global_timeout, self.terminate_test)
self.logger = logger
self.debug = debug
self.polling_conditions = []
self.keep_machine_state = keep_machine_state
def __enter__(self) -> "Driver":
self.race_timer = threading.Timer(
self.config.global_timeout, self.terminate_test
)
tmp_dir = get_tmp_dir()
with self.logger.nested("start all VLans"):
vlans = list(set(vlans))
self.vlans = [VLan(nr, tmp_dir, self.logger) for nr in vlans]
self.vlans = [VLan(nr, tmp_dir, self.logger) for nr in self.config.vlans]
self.polling_conditions = []
if self.config.enable_ssh_backdoor and self.config.vms:
with self.logger.nested("start vhost-device-vsock"):
self.vhost_vsock = VHostDeviceVsock(
tmp_dir, list(self.config.vms.keys())
)
self.machines_qemu = [
QemuMachine(
name=name,
start_command=vm_start_script,
keep_machine_state=keep_machine_state,
start_command=vm_config.start_script.as_posix(),
keep_machine_state=self.keep_machine_state,
tmp_dir=tmp_dir,
callbacks=[self.check_polling_conditions],
out_dir=self.out_dir,
logger=self.logger,
vsock_host=(
self.vhost_vsock.sockets[name].host
if self.vhost_vsock is not None
else None
),
vsock_guest=(
self.vhost_vsock.sockets[name].guest
if self.vhost_vsock is not None
else None
),
)
for name, vm_start_script in zip(vm_names, vm_start_scripts)
for name, vm_config in self.config.vms.items()
]
if len(container_start_scripts) > 0:
if self.config.containers and in_nix_sandbox():
self._init_nspawn_environment()
self.machines_nspawn = [
NspawnMachine(
name=name,
start_command=container_start_script,
start_command=container_config.start_script.as_posix(),
tmp_dir=tmp_dir,
logger=self.logger,
keep_machine_state=keep_machine_state,
keep_machine_state=self.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,
)
for name, container_config in self.config.containers.items()
]
return self
def _init_nspawn_environment(self) -> None:
assert os.geteuid() == 0, (
f"systemd-nspawn requires root to work. You are {os.geteuid()}"
@@ -193,9 +276,6 @@ class Driver:
machines.sort(key=lambda machine: machine.name)
return machines
def __enter__(self) -> "Driver":
return self
def __exit__(self, *_: Any) -> None:
with self.logger.nested("cleanup"):
self.race_timer.cancel()
@@ -211,6 +291,14 @@ class Driver:
except Exception as e:
self.logger.error(f"Error during cleanup of vlan{vlan.nr}: {e}")
if self.config.enable_ssh_backdoor:
try:
del self.vhost_vsock
except Exception as e:
self.logger.error(
f"Error during cleanup of vhost-device-vsock process: {e}"
)
def subtest(self, name: str) -> Iterator[None]:
"""Group logs under a given test name"""
with self.logger.subtest(name):
@@ -227,7 +315,7 @@ class Driver:
general_symbols = dict(
start_all=self.start_all,
test_script=self.test_script,
test_script=self.config.test_script,
machines=self.machines,
machines_qemu=self.machines_qemu,
machines_nspawn=self.machines_nspawn,
@@ -248,6 +336,7 @@ class Driver:
NspawnMachine=NspawnMachine, # for typing
t=AssertionTester(),
debug=self.debug,
dump_machine_ssh=self.dump_machine_ssh,
)
machine_symbols = {pythonize_name(m.name): m for m in self.machines}
# If there's exactly one machine, make it available under the name
@@ -267,18 +356,28 @@ class Driver:
)
return {**general_symbols, **machine_symbols, **vlan_symbols}
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} vsocks require {Style.BRIGHT}systemd-ssh-proxy(1){Style.RESET_ALL} to be enabled (default on NixOS 25.05 and newer)."
)
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)
def dump_machine_ssh(self) -> None:
if not self.config.enable_ssh_backdoor:
return
assert self.vhost_vsock is not None
if self.machines:
print("SSH backdoor enabled, the machines can be accessed like this:")
print(
f" {name}:{spaces}{Style.BRIGHT}{machine.ssh_backdoor_command(index)}{Style.RESET_ALL}"
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)."
)
longest_name = len(
max((machine.name for machine in self.machines), key=len)
)
for index, machine in enumerate(self.machines):
name = machine.name
spaces = " " * (longest_name - len(name) + 2)
print(
f" {name}:{spaces}{Style.BRIGHT}{machine.ssh_backdoor_command()}{Style.RESET_ALL}"
)
else:
print("SSH backdoor enabled, but no machines defined")
def test_script(self) -> None:
"""Run the test script"""
@@ -324,7 +423,7 @@ class Driver:
def run_tests(self) -> None:
"""Run the test script (for non-interactive test runs)"""
self.logger.info(
f"Test will time out and terminate in {self.global_timeout} seconds"
f"Test will time out and terminate in {self.config.global_timeout} seconds"
)
self.race_timer.start()
self.test_script()
@@ -336,10 +435,22 @@ class Driver:
def start_all(self) -> None:
"""Start all machines"""
with self.logger.nested("start all VMs"):
errors: list[tuple[str, BaseException]] = []
def start_machine(machine: BaseMachine) -> None:
try:
machine.start()
except Exception as e:
errors.append((machine.name, e))
threads = []
for machine in self.machines:
# Create a thread for each machine's start method
t = threading.Thread(target=machine.start, name=f"start-{machine.name}")
t = threading.Thread(
target=start_machine,
args=(machine,),
name=f"start-{machine.name}",
)
threads.append(t)
t.start()
@@ -347,6 +458,12 @@ class Driver:
for t in threads:
t.join()
if errors:
messages = [f"{name}: {e}" for name, e in errors]
raise MachineError(
"Failed to start the following machines:\n" + "\n".join(messages)
)
def join_all(self) -> None:
"""Wait for all machines to shut down"""
with self.logger.nested("wait for all VMs to finish"):
@@ -378,6 +495,10 @@ class Driver:
"""
tmp_dir = get_tmp_dir()
if self.config.enable_ssh_backdoor:
self.logger.warning(
f"create_machine({name}): not enabling SSH backdoor, this is not supported for VMs created with create_machine!"
)
return QemuMachine(
tmp_dir=tmp_dir,
out_dir=self.out_dir,
@@ -418,7 +539,7 @@ class Driver:
def __enter__(self) -> None:
driver.polling_conditions.append(self.condition)
def __exit__(self, a, b, c) -> None: # type: ignore
def __exit__(self, a, b, c) -> None:
res = driver.polling_conditions.pop()
assert res is self.condition
+89 -33
View File
@@ -1,5 +1,4 @@
import atexit
import codecs
import os
import sys
import time
@@ -7,6 +6,7 @@ import unicodedata
from abc import ABC, abstractmethod
from collections.abc import Iterator
from contextlib import ExitStack, contextmanager
from enum import IntEnum
from pathlib import Path
from queue import Empty, Queue
from typing import Any
@@ -17,6 +17,13 @@ from colorama import Fore, Style
from junit_xml import TestCase, TestSuite
class LogLevel(IntEnum):
DEBUG = 1
INFO = 2
WARNING = 3
ERROR = 4
class AbstractLogger(ABC):
@abstractmethod
def log(self, message: str, attributes: dict[str, str] = {}) -> None:
@@ -33,19 +40,23 @@ class AbstractLogger(ABC):
pass
@abstractmethod
def info(self, *args, **kwargs) -> None: # type: ignore
def debug(self, *args, **kwargs) -> None:
pass
@abstractmethod
def warning(self, *args, **kwargs) -> None: # type: ignore
def info(self, *args, **kwargs) -> None:
pass
@abstractmethod
def error(self, *args, **kwargs) -> None: # type: ignore
def warning(self, *args, **kwargs) -> None:
pass
@abstractmethod
def log_test_error(self, *args, **kwargs) -> None: # type:ignore
def error(self, *args, **kwargs) -> None:
pass
@abstractmethod
def log_test_error(self, *args, **kwargs) -> None:
pass
@abstractmethod
@@ -56,6 +67,10 @@ class AbstractLogger(ABC):
def print_serial_logs(self, enable: bool) -> None:
pass
@abstractmethod
def set_log_level(self, level: LogLevel) -> None:
pass
class JunitXMLLogger(AbstractLogger):
class TestCaseState:
@@ -71,6 +86,7 @@ class JunitXMLLogger(AbstractLogger):
self.currentSubtest = "main"
self.outfile: Path = outfile
self._print_serial_logs = True
self._log_level = LogLevel.INFO
atexit.register(self.close)
def log(self, message: str, attributes: dict[str, str] = {}) -> None:
@@ -91,17 +107,23 @@ class JunitXMLLogger(AbstractLogger):
self.log(message)
yield
def info(self, *args, **kwargs) -> None: # type: ignore
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
def debug(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.DEBUG:
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
def warning(self, *args, **kwargs) -> None: # type: ignore
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
def info(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.INFO:
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
def error(self, *args, **kwargs) -> None: # type: ignore
def warning(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.WARNING:
self.tests[self.currentSubtest].stdout += args[0] + os.linesep
def error(self, *args, **kwargs) -> None:
self.tests[self.currentSubtest].stderr += args[0] + os.linesep
self.tests[self.currentSubtest].failure = True
def log_test_error(self, *args, **kwargs) -> None: # type: ignore
def log_test_error(self, *args, **kwargs) -> None:
self.error(*args, **kwargs)
def log_serial(self, message: str, machine: str) -> None:
@@ -113,6 +135,9 @@ class JunitXMLLogger(AbstractLogger):
def print_serial_logs(self, enable: bool) -> None:
self._print_serial_logs = enable
def set_log_level(self, level: LogLevel) -> None:
self._log_level = level
def close(self) -> None:
with open(self.outfile, "w") as f:
test_cases = []
@@ -155,19 +180,23 @@ class CompositeLogger(AbstractLogger):
stack.enter_context(logger.nested(message, attributes))
yield
def info(self, *args, **kwargs) -> None: # type: ignore
def debug(self, *args, **kwargs) -> None:
for logger in self.logger_list:
logger.debug(*args, **kwargs)
def info(self, *args, **kwargs) -> None:
for logger in self.logger_list:
logger.info(*args, **kwargs)
def warning(self, *args, **kwargs) -> None: # type: ignore
def warning(self, *args, **kwargs) -> None:
for logger in self.logger_list:
logger.warning(*args, **kwargs)
def log_test_error(self, *args, **kwargs) -> None: # type: ignore
def log_test_error(self, *args, **kwargs) -> None:
for logger in self.logger_list:
logger.log_test_error(*args, **kwargs)
def error(self, *args, **kwargs) -> None: # type: ignore
def error(self, *args, **kwargs) -> None:
for logger in self.logger_list:
logger.error(*args, **kwargs)
sys.exit(1)
@@ -180,10 +209,15 @@ class CompositeLogger(AbstractLogger):
for logger in self.logger_list:
logger.log_serial(message, machine)
def set_log_level(self, level: LogLevel) -> None:
for logger in self.logger_list:
logger.set_log_level(level)
class TerminalLogger(AbstractLogger):
def __init__(self) -> None:
self._print_serial_logs = True
self._log_level = LogLevel.INFO
def maybe_prefix(self, message: str, attributes: dict[str, str]) -> str:
if "machine" in attributes:
@@ -206,7 +240,8 @@ class TerminalLogger(AbstractLogger):
def nested(self, message: str, attributes: dict[str, str] = {}) -> Iterator[None]:
self._eprint(
self.maybe_prefix(
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, attributes
Style.BRIGHT + Fore.GREEN + message + Style.RESET_ALL, # ty: ignore[unsupported-operator]
attributes,
)
)
@@ -215,37 +250,49 @@ class TerminalLogger(AbstractLogger):
toc = time.time()
self.log(f"(finished: {message}, in {toc - tic:.2f} seconds)", attributes)
def info(self, *args, **kwargs) -> None: # type: ignore
self.log(*args, **kwargs)
def debug(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.DEBUG:
self._eprint(
Style.DIM + self.maybe_prefix(args[0], kwargs) + Style.RESET_ALL # ty: ignore[unsupported-operator]
)
def warning(self, *args, **kwargs) -> None: # type: ignore
self.log(*args, **kwargs)
def info(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.INFO:
self.log(*args, **kwargs)
def error(self, *args, **kwargs) -> None: # type: ignore
def warning(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.WARNING:
self.log(*args, **kwargs)
def error(self, *args, **kwargs) -> None:
self.log(*args, **kwargs)
def print_serial_logs(self, enable: bool) -> None:
self._print_serial_logs = enable
def set_log_level(self, level: LogLevel) -> None:
self._log_level = level
def log_serial(self, message: str, machine: str) -> None:
if not self._print_serial_logs:
return
self._eprint(Style.DIM + f"{machine} # {message}" + Style.RESET_ALL)
self._eprint(Style.DIM + f"{machine} # {message}" + Style.RESET_ALL) # ty: ignore[unsupported-operator]
def log_test_error(self, *args, **kwargs) -> None: # type: ignore
prefix = Fore.RED + "!!! " + Style.RESET_ALL
def log_test_error(self, *args, **kwargs) -> None:
prefix = Fore.RED + "!!! " + Style.RESET_ALL # ty: ignore[unsupported-operator]
# NOTE: using `warning` instead of `error` to ensure it does not exit after printing the first log
self.warning(f"{prefix}{args[0]}", *args[1:], **kwargs)
class XMLLogger(AbstractLogger):
def __init__(self, outfile: str) -> None:
self.logfile_handle = codecs.open(outfile, "wb")
self.logfile_handle = open(outfile, "wb")
self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
self.queue: Queue[dict[str, str]] = Queue()
self._print_serial_logs = True
self._log_level = LogLevel.INFO
self.xml.startDocument()
self.xml.startElement("logfile", attrs=AttributesImpl({}))
@@ -268,16 +315,22 @@ class XMLLogger(AbstractLogger):
self.xml.characters(message)
self.xml.endElement("line")
def info(self, *args, **kwargs) -> None: # type: ignore
def debug(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.DEBUG:
self.log(*args, **kwargs)
def info(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.INFO:
self.log(*args, **kwargs)
def warning(self, *args, **kwargs) -> None:
if self._log_level <= LogLevel.WARNING:
self.log(*args, **kwargs)
def error(self, *args, **kwargs) -> None:
self.log(*args, **kwargs)
def warning(self, *args, **kwargs) -> None: # type: ignore
self.log(*args, **kwargs)
def error(self, *args, **kwargs) -> None: # type: ignore
self.log(*args, **kwargs)
def log_test_error(self, *args, **kwargs) -> None: # type: ignore
def log_test_error(self, *args, **kwargs) -> None:
self.log(*args, **kwargs)
def log(self, message: str, attributes: dict[str, str] = {}) -> None:
@@ -287,6 +340,9 @@ class XMLLogger(AbstractLogger):
def print_serial_logs(self, enable: bool) -> None:
self._print_serial_logs = enable
def set_log_level(self, level: LogLevel) -> None:
self._log_level = level
def log_serial(self, message: str, machine: str) -> None:
if not self._print_serial_logs:
return
@@ -24,9 +24,11 @@ from typing import Any
from test_driver.errors import MachineError, RequestedAssertionFailed
from test_driver.logger import AbstractLogger
from .ocr import perform_ocr_on_screenshot, perform_ocr_variants_on_screenshot
from .qmp import QMPSession
from test_driver.machine.ocr import (
perform_ocr_on_screenshot,
perform_ocr_variants_on_screenshot,
)
from test_driver.machine.qmp import QMPSession
CHAR_TO_KEY = {
"A": "shift-a",
@@ -147,6 +149,7 @@ class QemuStartCommand:
qmp_socket_path: Path,
shell_socket_path: Path,
allow_reboot: bool = False,
vsock_guest: Path | None = None,
) -> str:
display_opts = ""
@@ -170,6 +173,12 @@ class QemuStartCommand:
if not allow_reboot:
qemu_opts += " -no-reboot"
if vsock_guest is not None:
qemu_opts += (
f" -chardev socket,id=vsock_ssh,path={vsock_guest} "
f"-device vhost-user-vsock-pci,chardev=vsock_ssh "
)
return (
f"{self._cmd}"
f" -qmp unix:{qmp_socket_path},server=on,wait=off"
@@ -203,13 +212,19 @@ class QemuStartCommand:
qmp_socket_path: Path,
shell_socket_path: Path,
allow_reboot: bool,
vsock_guest: Path | None = None,
) -> subprocess.Popen:
return subprocess.Popen(
self.cmd(
monitor_socket_path, qmp_socket_path, shell_socket_path, allow_reboot
monitor_socket_path,
qmp_socket_path,
shell_socket_path,
allow_reboot,
vsock_guest,
),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
cwd=state_dir,
env=self.build_environment(state_dir, shared_dir),
@@ -724,6 +739,9 @@ class QemuMachine(BaseMachine):
shell: socket.socket | None
serial_thread: threading.Thread | None
vsock_guest: Path | None
vsock_host: Path | None
booted: bool
connected: bool
# Store last serial console lines for use
@@ -741,6 +759,8 @@ class QemuMachine(BaseMachine):
name: str | None = None,
keep_machine_state: bool = False,
callbacks: list[Callable] | None = None,
vsock_guest: Path | None = None,
vsock_host: Path | None = None,
) -> None:
self.start_command = QemuStartCommand(start_command)
super().__init__(
@@ -753,6 +773,8 @@ class QemuMachine(BaseMachine):
)
self.full_console_log = []
self.vsock_guest = vsock_guest
self.vsock_host = vsock_host
# set up directories
self.monitor_path = self.state_dir / "monitor"
@@ -769,8 +791,9 @@ class QemuMachine(BaseMachine):
self.booted = False
self.connected = False
def ssh_backdoor_command(self, index: int) -> str:
return f"ssh -o User=root vsock/{index}"
def ssh_backdoor_command(self) -> str:
assert self.vsock_host is not None
return f"ssh -o User=root vsock-mux/{self.vsock_host}"
def is_up(self) -> bool:
return self.booted and self.connected
@@ -1224,9 +1247,31 @@ class QemuMachine(BaseMachine):
self.qmp_path,
self.shell_path,
allow_reboot,
self.vsock_guest,
)
self.monitor, _ = monitor_socket.accept()
self.shell, _ = shell_socket.accept()
def accept_or_fail(sock: socket.socket, name: str) -> socket.socket:
"""Accept a connection on a socket, polling the status to check
if the QEMU process is still alive. Without this, socket.accept()
would block forever if QEMU exits before connecting.
"""
assert self.process
while True:
readable, _, _ = select.select([sock], [], [], 1.0)
if readable:
conn, _ = sock.accept()
return conn
rc = self.process.poll()
if rc is not None:
output = ""
if self.process.stdout:
output = self.process.stdout.read().decode(errors="ignore")
raise MachineError(
f"QEMU process exited with code {rc} before connecting to {name} socket.\n{output}"
)
self.monitor = accept_or_fail(monitor_socket, "monitor")
self.shell = accept_or_fail(shell_socket, "shell")
self.qmp_client = QMPSession.from_path(self.qmp_path)
# Store last serial console lines for use
@@ -1434,7 +1479,7 @@ class NspawnMachine(BaseMachine):
self.machine_sock_path = self.tmp_dir / f"{self.name}-nspawn.sock"
def ssh_backdoor_command(self, index: int) -> str:
def ssh_backdoor_command(self) -> str:
# documented in systemd-ssh-generator(8) and https://systemd.io/CONTAINER_INTERFACE/
socket_path = f"/run/systemd/nspawn/unix-export/{self.name}/ssh"
proxy_cmd = f"socat - UNIX-CLIENT:{socket_path}"
@@ -25,7 +25,7 @@ class PollingCondition:
seconds_interval: float = 2.0,
description: str | None = None,
):
self.condition = condition # type: ignore
self.condition = condition # ty: ignore[invalid-assignment]
self.seconds_interval = seconds_interval
self.logger = logger
@@ -33,7 +33,7 @@ class PollingCondition:
if condition.__doc__:
self.description = condition.__doc__
else:
self.description = condition.__name__
self.description = condition.__name__ # ty: ignore[unresolved-attribute]
else:
self.description = str(description)
@@ -54,7 +54,7 @@ class PollingCondition:
self.logger.info(last_message)
try:
res = self.condition() # type: ignore
res = self.condition()
except Exception:
res = False
res = res is None or res
@@ -89,7 +89,7 @@ class PollingCondition:
def __enter__(self) -> None:
self.entry_count += 1
def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore
def __exit__(self, exc_type, exc_value, traceback) -> None:
assert self.entered
self.entry_count -= 1
self.last_called = time.monotonic()
+29 -5
View File
@@ -4,6 +4,7 @@ import io
import os
import select
import subprocess
import threading
import typing
from pathlib import Path
@@ -57,6 +58,11 @@ class VLan:
def __repr__(self) -> str:
return f"<Vlan Nr. {self.nr}>"
def _log_stream(self, stream: typing.IO[str], prefix: str) -> None:
"""Read lines from a stream and log via debug()."""
for line in stream:
self.logger.debug(f"{prefix}: {line.rstrip()}")
def __init__(self, nr: int, tmp_dir: Path, logger: AbstractLogger):
self.nr = nr
self.socket_dir = tmp_dir / f"vde{self.nr}.ctl"
@@ -66,7 +72,7 @@ class VLan:
# TODO: don't side-effect environment here
os.environ[f"QEMU_VDE_SOCKET_{self.nr}"] = str(self.socket_dir)
self.logger.info("start vlan")
self.logger.debug("start vlan")
self.process = subprocess.Popen(
[
@@ -84,7 +90,7 @@ class VLan:
bufsize=1, # Line buffered.
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=None, # Do not swallow stderr.
stderr=subprocess.STDOUT,
text=True,
)
self.pid = self.process.pid
@@ -117,19 +123,37 @@ class VLan:
if "1000 Success" in line:
break
self._stdout_thread = threading.Thread(
target=self._log_stream,
args=(self.process.stdout, f"vde_switch[{self.nr}]"),
daemon=True,
)
self._stdout_thread.start()
# 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.logger.debug(f"creating tap interface {self.tap_name}")
self.plug_process = subprocess.Popen(
["vde_plug2tap", "-s", self.socket_dir, self.tap_name],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
assert self.plug_process.stdout is not None
self._plug_stdout_thread = threading.Thread(
target=self._log_stream,
args=(self.plug_process.stdout, f"vde_plug2tap[{self.nr}]"),
daemon=True,
)
self._plug_stdout_thread.start()
assert (self.socket_dir / "ctl").exists(), "cannot start vde_switch"
self.logger.info(f"running vlan (pid {self.pid}; ctl {self.socket_dir})")
self.logger.debug(f"running vlan (pid {self.pid}; ctl {self.socket_dir})")
def stop(self) -> None:
self.logger.info(f"kill vlan (pid {self.pid})")
self.logger.debug(f"kill vlan (pid {self.pid})")
assert self.process.stdin is not None
self.process.stdin.close()
if self.plug_process:
+1
View File
@@ -58,3 +58,4 @@ serial_stdout_on: Callable[[], None]
polling_condition: PollingConditionProtocol
debug: DebugAbstract
t: TestCase
dump_machine_ssh: Callable[[], None]
+1
View File
@@ -20,6 +20,7 @@ let
testModules = [
./call-test.nix
./driver.nix
./driver-configuration.nix
./interactive.nix
./legacy.nix
./meta.nix
@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
nodeConfigurationAttrs = lib.mkOption {
internal = true;
type = types.attrsOf (
types.submodule {
options = {
name = lib.mkOption {
internal = true;
type = types.str;
};
start_script = lib.mkOption {
internal = true;
type = types.path;
};
};
}
);
};
in
{
options = {
driverConfiguration = lib.mkOption {
description = "Configuration attribute set for test driver invocation";
internal = true;
type = types.submodule {
options = {
vms = nodeConfigurationAttrs;
containers = nodeConfigurationAttrs;
vlans = lib.mkOption {
internal = true;
type = types.listOf types.ints.unsigned;
};
global_timeout = lib.mkOption {
internal = true;
type = types.ints.unsigned;
};
enable_ssh_backdoor = lib.mkOption {
internal = true;
type = types.bool;
};
test_script = lib.mkOption {
internal = true;
type = types.path;
};
};
};
};
driverConfigurationFile = lib.mkOption {
internal = true;
type = types.path;
};
};
config = {
driverConfiguration = {
vms = lib.mapAttrs (name: value: {
inherit name;
start_script = lib.getExe value.system.build.vm;
}) config.nodes;
containers = lib.mapAttrs (name: value: {
inherit name;
start_script = lib.getExe value.system.build.nspawn;
}) config.containers;
vlans = lib.unique (
lib.concatMap (
m: (m.virtualisation.vlans ++ (lib.mapAttrsToList (_: v: v.vlan) m.virtualisation.interfaces))
) (lib.attrValues config.nodes ++ lib.attrValues config.containers)
);
global_timeout = config.globalTimeout;
test_script = pkgs.writeText "test-script" config.testScriptString;
enable_ssh_backdoor = config.sshBackdoor.enable;
};
driverConfigurationFile = pkgs.writers.writeJSON "driverConfiguration.json" config.driverConfiguration;
};
}
+56 -26
View File
@@ -7,6 +7,8 @@
let
inherit (lib) mkOption types literalMD;
inherit (config) sshBackdoor;
# Reifies and correctly wraps the python test driver for
# the respective qemu version and with or without ocr support
testDriver = config.pythonTestDriverPackage.override {
@@ -15,12 +17,6 @@ let
enableNspawn = config.containers != { };
};
vlans = map (
m: (m.virtualisation.vlans ++ (lib.mapAttrsToList (_: v: v.vlan) m.virtualisation.interfaces))
) ((lib.attrValues config.nodes) ++ (lib.attrValues config.containers));
vms = map (m: m.system.build.vm) (lib.attrValues config.nodes);
containers = map (m: m.system.build.nspawn) (lib.attrValues config.containers);
pythonizeName =
name:
let
@@ -30,11 +26,12 @@ let
(if builtins.match "[A-z_]" head == null then "_" else head)
+ lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail;
uniqueVlans = lib.unique (builtins.concatLists vlans);
vlanNames = map (i: "vlan${toString i}: VLan;") uniqueVlans;
vlanTypeHints = lib.strings.concatMapStringsSep "\n" (
i: "vlan${toString i}: VLan"
) config.driverConfiguration.vlans;
vmMachineNames = map (c: c.system.name) (lib.attrValues config.nodes);
containerMachineNames = map (c: c.system.name) (lib.attrValues config.containers);
vmMachineNames = lib.attrNames config.driverConfiguration.vms;
containerMachineNames = lib.attrNames config.driverConfiguration.containers;
theOnlyMachine =
let
@@ -70,17 +67,13 @@ let
''
mkdir -p $out/bin
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 vmMachineTypeHints}" >> testScriptWithTypes
echo "${toString containerMachineTypeHints}" >> testScriptWithTypes
echo "${toString vlanNames}" >> testScriptWithTypes
echo "${toString vlanTypeHints}" >> testScriptWithTypes
echo -n "$testScript" >> testScriptWithTypes
echo "Running type check (enable/disable: config.skipTypeCheck)"
@@ -92,7 +85,7 @@ let
testScriptWithTypes
''}
echo -n "$testScript" >> $out/test-script
cp "${config.driverConfiguration.test_script}" $out/test-script
ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver
@@ -109,16 +102,9 @@ let
)" ${hostPkgs.python3Packages.pyflakes}/bin/pyflakes $out/test-script
''}
# set defaults through environment
# see: ./test-driver/test-driver.py argparse implementation
wrapProgram $out/bin/nixos-test-driver \
--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}' \
--add-flags "--config ${config.driverConfigurationFile}" \
--add-flags "--log-level ${config.logLevel}" \
${lib.escapeShellArgs (
lib.concatMap (arg: [
"--add-flags"
@@ -158,6 +144,16 @@ in
defaultText = "hostPkgs.qemu_test";
};
qemu.forceAccel = mkOption {
description = ''
Whether to force the use of hardware-accelerated virtualisation.
When enabled, QEMU will not fall back to the slower software emulation
(TCG) and will instead error out if the accelerator is not available.
'';
type = types.bool;
default = false;
};
globalTimeout = mkOption {
description = ''
A global timeout for the complete test, expressed in seconds.
@@ -219,6 +215,18 @@ in
This may speed up your iteration cycle, unless you're working on the [{option}`testScript`](#test-opt-testScript).
'';
};
logLevel = mkOption {
description = "Log level for the test driver.";
type = types.enum [
"debug"
"info"
"warning"
"error"
];
default = "info";
example = "warning";
};
};
config = {
@@ -232,5 +240,27 @@ in
# make available on the test runner
passthru.driver = config.driver;
nodeDefaults =
{ config, ... }:
{
# This is needed for the SSH backdoor to function.
# Set this to `true` by default to not change essential QEMU flags
# depending on whether debugging is enabled.
#
# If needed, this can still be turned off.
virtualisation.qemu.enableSharedMemory = lib.mkDefault true;
assertions = [
{
assertion = sshBackdoor.enable -> config.virtualisation.qemu.enableSharedMemory;
message = ''
When turning on the SSH backdoor of the NixOS test-framework,
`virtualisation.qemu.enableSharedMemory` MUST be `true`
(affected: ${config.networking.hostName}).
'';
}
];
};
};
}
+10 -35
View File
@@ -13,6 +13,7 @@ let
mapAttrs
mkIf
mkMerge
mkRemovedOptionModule
mkOption
optionalAttrs
types
@@ -71,7 +72,9 @@ let
config.nodeDefaults
{
key = "base-qemu";
virtualisation.qemu.package = testModuleArgs.config.qemu.package;
virtualisation.qemu = {
inherit (testModuleArgs.config.qemu) package forceAccel;
};
virtualisation.host.pkgs = hostPkgs;
}
testModuleArgs.config.extraBaseNodeModules
@@ -128,6 +131,12 @@ let
in
{
imports = [
(mkRemovedOptionModule [ "sshBackdoor" "vsockOffset" ] ''
The option `sshBackdoor.vsockOffset` has been removed from the testing framework.
The functionality provided by it is not needed anymore.
'')
];
options = {
sshBackdoor = {
@@ -137,22 +146,6 @@ in
type = types.bool;
description = "Whether to turn on the VSOCK-based access to all VMs. This provides an unauthenticated access intended for debugging.";
};
vsockOffset = mkOption {
default = 2;
type = types.ints.between 2 4294967296;
description = ''
This field is only relevant when multiple users run the (interactive)
driver outside the sandbox and with the SSH backdoor activated.
The typical symptom for this being a problem are error messages like this:
`vhost-vsock: unable to set guest cid: Address already in use`
This option allows to assign an offset to each vsock number to
resolve this.
This is a 32bit number. The lowest possible vsock number is `3`
(i.e. with the lowest node number being `1`, this is 2+1).
'';
};
};
node.type = mkOption {
@@ -317,10 +310,6 @@ in
passthru.nodes = config.nodesCompat;
passthru.containers = config.containers;
extraDriverArgs = mkIf config.sshBackdoor.enable [
"--dump-vsocks=${toString config.sshBackdoor.vsockOffset}"
];
defaults = mkMerge [
(mkIf config.node.pkgsReadOnly {
nixpkgs.pkgs = config.node.pkgs;
@@ -341,20 +330,6 @@ in
})
];
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
+5 -1
View File
@@ -80,6 +80,10 @@ in
};
};
imports = [
../../modules/misc/assertions.nix
];
config = {
rawTestDerivation = hostPkgs.stdenv.mkDerivation config.rawTestDerivationArg;
rawTestDerivationArg =
@@ -131,7 +135,7 @@ in
};
test = lib.lazyDerivation {
# lazyDerivation improves performance when only passthru items and/or meta are used.
derivation = config.rawTestDerivation;
derivation = lib.asserts.checkAssertWarn config.assertions config.warnings config.rawTestDerivation;
inherit (config) passthru meta;
};
+27
View File
@@ -541,6 +541,33 @@ let
- https://github.com/qemu/qemu/blob/master/scripts/qemu-binfmt-conf.sh
*/
binfmtMagics = import ./binfmt-magics.nix;
# Utilities for working with the security.pam module (pam.nix)
pam = {
/*
Set up the ordering for a set of PAM rules using an ordered list of rules.
The input is an ordered list of PAM rules. Each rule is an attrset similar to the options
in `security.pam.services.<service>.rules.<rule>`, with two modifications:
1. The `order` option may not be given.
2. The `name` option is required.
The output is an attrset of rules suitable for `security.pam.services.<service>.rules`.
The `order` option on the resulting rules will automatically be configured according to the
(implied) ordering of the input rules.
*/
autoOrderRules = lib.flip lib.pipe [
(lib.imap1 (
index: rule:
assert lib.assertMsg (!rule ? order) "the 'order' option may not be set when using autoOrderRules";
rule // { order = lib.mkDefault (10000 + index * 100); }
))
(map (rule: lib.nameValuePair rule.name (removeAttrs rule [ "name" ])))
lib.listToAttrs
];
};
};
in
utils
+2 -1
View File
@@ -518,7 +518,8 @@ in
useEmbeddedBitmaps = lib.mkOption {
type = lib.types.bool;
default = false;
default = lib.versionAtLeast config.system.stateVersion "26.05";
defaultText = lib.literalExpression "lib.versionAtLeast config.system.stateVersion \"26.05\"";
description = "Use embedded bitmaps in fonts like Calibri.";
};
+55 -24
View File
@@ -3,6 +3,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -173,6 +174,28 @@ let
};
};
useDefaultRules = lib.mkOption {
# This option is experimental and subject to breaking changes without notice.
visible = false;
default = true;
type = lib.types.bool;
description = ''
Whether to set up the default NixOS rule stack for this service.
Set this to `false` if you want to define the entire rule stack for this service.
::: {.warning}
This option is experimental and subject to breaking changes without notice.
If you use this option in your system configuration, you will need to manually monitor this module for any changes. Otherwise, failure to adjust your configuration properly could lead to you being locked out of your system, or worse, your system could be left wide open to attackers.
If you share configuration examples that use this option, you MUST include this warning so that users are informed.
You may freely use this option within `nixpkgs`, and future changes will account for those use sites.
:::
'';
};
unixAuth = lib.mkOption {
default = true;
type = lib.types.bool;
@@ -899,16 +922,9 @@ let
# !!! TODO: move the LDAP stuff to the LDAP module, and the
# Samba stuff to the Samba module. This requires that the PAM
# module provides the right hooks.
rules =
let
autoOrderRules = lib.flip lib.pipe [
(lib.imap1 (index: rule: rule // { order = lib.mkDefault (10000 + index * 100); }))
(map (rule: lib.nameValuePair rule.name (removeAttrs rule [ "name" ])))
lib.listToAttrs
];
in
{
account = autoOrderRules [
rules = (
lib.optionalAttrs cfg.useDefaultRules {
account = utils.pam.autoOrderRules [
{
name = "ldap";
enable = use_ldap;
@@ -989,7 +1005,7 @@ let
];
auth = autoOrderRules (
auth = utils.pam.autoOrderRules (
[
{
name = "oslogin_login";
@@ -1362,7 +1378,7 @@ let
]
);
password = autoOrderRules [
password = utils.pam.autoOrderRules [
{
name = "systemd_home";
enable = config.services.homed.enable;
@@ -1447,7 +1463,7 @@ let
}
];
session = autoOrderRules [
session = utils.pam.autoOrderRules [
{
name = "env";
enable = cfg.setEnvironment;
@@ -1679,7 +1695,8 @@ let
modulePath = "${pkgs.intune-portal}/lib/security/pam_intune.so";
}
];
};
}
);
};
};
@@ -2554,16 +2571,30 @@ in
};
security.pam.services = {
other.text = ''
auth required pam_warn.so
auth required pam_deny.so
account required pam_warn.so
account required pam_deny.so
password required pam_warn.so
password required pam_deny.so
session required pam_warn.so
session required pam_deny.so
'';
other = {
useDefaultRules = false;
rules =
let
rules = utils.pam.autoOrderRules [
{
name = "warn";
control = "required";
modulePath = "${package}/lib/security/pam_warn.so";
}
{
name = "deny";
control = "required";
modulePath = "${package}/lib/security/pam_deny.so";
}
];
in
{
auth = rules;
account = rules;
password = rules;
session = rules;
};
};
# Most of these should be moved to specific modules.
i3lock.enable = lib.mkDefault config.programs.i3lock.enable;
+259 -45
View File
@@ -1,6 +1,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -383,67 +384,280 @@ in
# GDM LFS PAM modules, adapted somehow to NixOS
security.pam.services = {
gdm-launch-environment.text = ''
auth required pam_succeed_if.so audit quiet_success user ingroup gdm
auth optional pam_permit.so
gdm-launch-environment = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "gdm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"ingroup"
"gdm"
];
}
{
name = "permit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
account required pam_succeed_if.so audit quiet_success user ingroup gdm
account sufficient pam_unix.so
account = utils.pam.autoOrderRules [
{
name = "gdm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"ingroup"
"gdm"
];
}
{
name = "unix";
control = "sufficient";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
];
password required pam_deny.so
password = utils.pam.autoOrderRules [
{
name = "deny";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_deny.so";
}
];
session required pam_succeed_if.so audit quiet_success user ingroup gdm
session required pam_env.so conffile=/etc/pam/environment readenv=0
session optional ${config.systemd.package}/lib/security/pam_systemd.so
session optional pam_keyinit.so force revoke
session optional pam_permit.so
'';
session = utils.pam.autoOrderRules [
{
name = "gdm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"ingroup"
"gdm"
];
}
{
name = "env";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
settings.conffile = "/etc/pam/environment";
settings.readenv = 0;
}
{
name = "systemd";
control = "optional";
modulePath = "${config.systemd.package}/lib/security/pam_systemd.so";
}
{
name = "keyinit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
settings.force = true;
settings.revoke = true;
}
{
name = "permit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
};
};
gdm-password.text = ''
auth substack login
account include login
password substack login
session include login
'';
gdm-password = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
account = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
password = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
session = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
gdm-autologin.text = ''
auth requisite pam_nologin.so
auth required pam_succeed_if.so uid >= 1000 quiet
${lib.optionalString (pamLogin.enable && pamLogin.enableGnomeKeyring) ''
auth [success=ok default=1] ${gdm}/lib/security/pam_gdm.so
auth optional ${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so
''}
auth required pam_permit.so
gdm-autologin = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "nologin";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_nologin.so";
}
{
name = "gdm-normal-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.quiet = true;
args = lib.mkBefore [
"uid"
">="
"1000"
];
}
{
name = "gdm";
enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
control = "[success=ok default=1]";
modulePath = "${gdm}/lib/security/pam_gdm.so";
}
{
name = "gnome_keyring";
enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
control = "optional";
modulePath = "${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so";
}
{
name = "permit";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
account sufficient pam_unix.so
account = utils.pam.autoOrderRules [
{
name = "unix";
control = "sufficient";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
];
password requisite pam_unix.so nullok yescrypt
password = utils.pam.autoOrderRules [
{
name = "unix";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
settings.nullok = true;
settings.yescrypt = true;
}
];
session optional pam_keyinit.so revoke
session include login
'';
session = utils.pam.autoOrderRules [
{
name = "keyinit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
settings.revoke = true;
}
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
# This would block password prompt when included by gdm-password.
# GDM will instead run gdm-fingerprint in parallel.
login.fprintAuth = lib.mkIf config.services.fprintd.enable false;
gdm-fingerprint.text = lib.mkIf config.services.fprintd.enable ''
auth required pam_shells.so
auth requisite pam_nologin.so
auth requisite pam_faillock.so preauth
auth required ${pkgs.fprintd}/lib/security/pam_fprintd.so
auth required pam_env.so conffile=/etc/pam/environment readenv=0
${lib.optionalString (pamLogin.enable && pamLogin.enableGnomeKeyring) ''
auth [success=ok default=1] ${gdm}/lib/security/pam_gdm.so
auth optional ${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so
''}
gdm-fingerprint = lib.mkIf config.services.fprintd.enable {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "shells";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_shells.so";
}
{
name = "nologin";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_nologin.so";
}
{
name = "faillock";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_faillock.so";
settings.preauth = true;
}
{
name = "fprintd";
control = "required";
modulePath = "${pkgs.fprintd}/lib/security/pam_fprintd.so";
}
{
name = "env";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
settings.conffile = "/etc/pam/environment";
settings.readenv = 0;
}
{
name = "gdm";
enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
control = "[success=ok default=1]";
modulePath = "${gdm}/lib/security/pam_gdm.so";
}
{
name = "gnome_keyring";
enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
control = "optional";
modulePath = "${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so";
}
];
account include login
account = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
password required pam_deny.so
password = utils.pam.autoOrderRules [
{
name = "deny";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_deny.so";
}
];
session include login
'';
session = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
};
};
+50 -8
View File
@@ -1,6 +1,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -105,17 +106,58 @@ in
};
}
// optionalAttrs dmcfg.autoLogin.enable {
ly-autologin.text = ''
auth requisite pam_nologin.so
auth required pam_succeed_if.so uid >= 1000 quiet
auth required pam_permit.so
ly-autologin = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "nologin";
control = "requisite";
modulePath = "pam_nologin.so";
}
{
name = "ly-normal-user";
control = "required";
modulePath = "pam_succeed_if.so";
settings.quiet = true;
args = lib.mkBefore [
"uid"
">="
"1000"
];
}
{
name = "permit";
control = "required";
modulePath = "pam_permit.so";
}
];
account include ly
account = utils.pam.autoOrderRules [
{
name = "ly";
control = "include";
modulePath = "ly";
}
];
password include ly
password = utils.pam.autoOrderRules [
{
name = "ly";
control = "include";
modulePath = "ly";
}
];
session include ly
'';
session = utils.pam.autoOrderRules [
{
name = "ly";
control = "include";
modulePath = "ly";
}
];
};
};
};
environment = {
@@ -1,6 +1,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -83,39 +84,133 @@ in
environment.etc."plasmalogin.conf.d/99-user.conf".source = userConfigFile;
security.pam.services = {
plasmalogin.text = ''
auth substack login
account include login
password substack login
session include login
'';
plasmalogin = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
account = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
password = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
session = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
plasmalogin-autologin.text = ''
auth requisite pam_nologin.so
auth required pam_permit.so
plasmalogin-autologin = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "nologin";
control = "requisite";
modulePath = "pam_nologin.so";
}
{
name = "permit";
control = "required";
modulePath = "pam_permit.so";
}
];
account include plasmalogin
password include plasmalogin
session include plasmalogin
'';
account = utils.pam.autoOrderRules [
{
name = "plasmalogin";
control = "include";
modulePath = "plasmalogin";
}
];
password = utils.pam.autoOrderRules [
{
name = "plasmalogin";
control = "include";
modulePath = "plasmalogin";
}
];
session = utils.pam.autoOrderRules [
{
name = "plasmalogin";
control = "include";
modulePath = "plasmalogin";
}
];
};
};
plasmalogin-greeter.text = ''
# Load environment from /etc/environment and ~/.pam_environment
auth required pam_env.so conffile=/etc/pam/environment readenv=0
plasmalogin-greeter = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
# Load environment from /etc/environment and ~/.pam_environment
name = "env";
control = "required";
modulePath = "pam_env.so";
settings.conffile = "/etc/pam/environment";
settings.readenv = 0;
}
{
# Always let the greeter start without authentication
name = "permit";
control = "required";
modulePath = "pam_permit.so";
}
];
# Always let the greeter start without authentication
auth required pam_permit.so
account = utils.pam.autoOrderRules [
{
# No action required for account management
name = "permit";
control = "required";
modulePath = "pam_permit.so";
}
];
# No action required for account management
account required pam_permit.so
password = utils.pam.autoOrderRules [
{
# Can't change password
name = "deny";
control = "required";
modulePath = "pam_deny.so";
}
];
# Can't change password
password required pam_deny.so
# Setup session
session required pam_unix.so
session optional ${config.systemd.package}/lib/security/pam_systemd.so
'';
session = utils.pam.autoOrderRules [
{
# Setup session
name = "unix";
control = "required";
modulePath = "pam_unix.so";
}
{
name = "systemd";
control = "optional";
modulePath = "${config.systemd.package}/lib/security/pam_systemd.so";
}
];
};
};
};
# FIXME: use upstream sysusers
+171 -26
View File
@@ -1,6 +1,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -368,40 +369,184 @@ in
};
security.pam.services = {
sddm.text = ''
auth substack login
account include login
password substack login
session include login
'';
sddm = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
account = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
password = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
session = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
sddm-greeter.text = ''
auth required pam_succeed_if.so audit quiet_success user = sddm
auth optional pam_permit.so
sddm-greeter = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "sddm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"="
"sddm"
];
}
{
name = "permit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
account required pam_succeed_if.so audit quiet_success user = sddm
account sufficient pam_unix.so
account = utils.pam.autoOrderRules [
{
name = "sddm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"="
"sddm"
];
}
{
name = "unix";
control = "sufficient";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
];
password required pam_deny.so
password = utils.pam.autoOrderRules [
{
name = "deny";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_deny.so";
}
];
session required pam_succeed_if.so audit quiet_success user = sddm
session required pam_env.so conffile=/etc/pam/environment readenv=0
session optional ${config.systemd.package}/lib/security/pam_systemd.so
session optional pam_keyinit.so force revoke
session optional pam_permit.so
'';
session = utils.pam.autoOrderRules [
{
name = "sddm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"="
"sddm"
];
}
{
name = "env";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
settings.conffile = "/etc/pam/environment";
settings.readenv = 0;
}
{
name = "systemd";
control = "optional";
modulePath = "${config.systemd.package}/lib/security/pam_systemd.so";
}
{
name = "keyinit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
settings.force = true;
settings.revoke = true;
}
{
name = "permit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
};
};
sddm-autologin.text = ''
auth requisite pam_nologin.so
auth required pam_succeed_if.so uid >= ${toString cfg.autoLogin.minimumUid} quiet
auth required pam_permit.so
sddm-autologin = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "nologin";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_nologin.so";
}
{
name = "sddm-autologin-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.quiet = true;
args = lib.mkBefore [
"uid"
">="
(toString cfg.autoLogin.minimumUid)
];
}
{
name = "permit";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
account include sddm
account = utils.pam.autoOrderRules [
{
name = "sddm";
control = "include";
modulePath = "sddm";
}
];
password include sddm
password = utils.pam.autoOrderRules [
{
name = "sddm";
control = "include";
modulePath = "sddm";
}
];
session include sddm
'';
session = utils.pam.autoOrderRules [
{
name = "sddm";
control = "include";
modulePath = "sddm";
}
];
};
};
};
users.users.sddm = {
+19 -4
View File
@@ -1,6 +1,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -333,9 +334,23 @@ in
};
};
security.pam.services.vsftpd.text = mkIf (cfg.enableVirtualUsers && cfg.userDbPath != null) ''
auth required pam_userdb.so db=${cfg.userDbPath}
account required pam_userdb.so db=${cfg.userDbPath}
'';
security.pam.services.vsftpd = mkIf (cfg.enableVirtualUsers && cfg.userDbPath != null) {
useDefaultRules = false;
rules =
let
rules = utils.pam.autoOrderRules [
{
name = "userdb";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_userdb.so";
settings.db = cfg.userDbPath;
}
];
in
{
auth = rules;
account = rules;
};
};
};
}
+40 -7
View File
@@ -2,6 +2,7 @@
config,
pkgs,
lib,
utils,
...
}:
@@ -105,13 +106,45 @@ in
security.polkit.enable = true;
security.pam.services.cage.text = ''
auth required pam_unix.so nullok
account required pam_unix.so
session required pam_unix.so
session required pam_env.so conffile=/etc/pam/environment readenv=0
session required ${config.systemd.package}/lib/security/pam_systemd.so
'';
security.pam.services.cage = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "unix";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
settings.nullok = true;
}
];
account = utils.pam.autoOrderRules [
{
name = "unix";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
];
session = utils.pam.autoOrderRules [
{
name = "unix";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
{
name = "env";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
settings.conffile = "/etc/pam/environment";
settings.readenv = 0;
}
{
name = "systemd";
control = "required";
modulePath = "${config.systemd.package}/lib/security/pam_systemd.so";
}
];
};
};
hardware.graphics.enable = mkDefault true;
@@ -1,6 +1,7 @@
{
config,
lib,
utils,
pkgs,
...
}:
@@ -279,47 +280,201 @@ in
security.polkit.enable = true;
security.pam.services.lightdm.text = ''
auth substack login
account include login
password substack login
session include login
''
# https://github.com/elementary/switchboard-plug-parental-controls/blob/8.0.1/src/daemon/Server.vala#L325
# Must specify conffile since pam_time defaults to ${linux-pam}/etc/security/time.conf.
+ lib.optionalString config.services.pantheon.parental-controls.enable ''
account required pam_time.so conffile=/etc/security/time.conf
'';
security.pam.services.lightdm = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
account = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
{
name = "time";
# https://github.com/elementary/switchboard-plug-parental-controls/blob/8.0.1/src/daemon/Server.vala#L325
enable = config.services.pantheon.parental-controls.enable;
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_time.so";
# Must specify conffile since pam_time defaults to ${linux-pam}/etc/security/time.conf.
settings.conffile = "/etc/security/time.conf";
}
];
password = utils.pam.autoOrderRules [
{
name = "login";
control = "substack";
modulePath = "login";
}
];
session = utils.pam.autoOrderRules [
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
security.pam.services.lightdm-greeter.text = ''
auth required pam_succeed_if.so audit quiet_success user = lightdm
auth optional pam_permit.so
security.pam.services.lightdm-greeter = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "lightdm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"="
"lightdm"
];
}
{
name = "permit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
account required pam_succeed_if.so audit quiet_success user = lightdm
account sufficient pam_unix.so
account = utils.pam.autoOrderRules [
{
name = "lightdm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"="
"lightdm"
];
}
{
name = "unix";
control = "sufficient";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
];
password required pam_deny.so
password = utils.pam.autoOrderRules [
{
name = "deny";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_deny.so";
}
];
session required pam_succeed_if.so audit quiet_success user = lightdm
session required pam_env.so conffile=/etc/pam/environment readenv=0
session optional ${config.systemd.package}/lib/security/pam_systemd.so
session optional pam_keyinit.so force revoke
session optional pam_permit.so
'';
session = utils.pam.autoOrderRules [
{
name = "lightdm-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.audit = true;
settings.quiet_success = true;
args = lib.mkAfter [
"user"
"="
"lightdm"
];
}
{
name = "env";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
settings.conffile = "/etc/pam/environment";
settings.readenv = 0;
}
{
name = "systemd";
control = "optional";
modulePath = "${config.systemd.package}/lib/security/pam_systemd.so";
}
{
name = "keyinit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
settings.force = true;
settings.revoke = true;
}
{
name = "permit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
};
};
security.pam.services.lightdm-autologin.text = ''
auth requisite pam_nologin.so
security.pam.services.lightdm-autologin = {
useDefaultRules = false;
rules = {
auth = utils.pam.autoOrderRules [
{
name = "nologin";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_nologin.so";
}
{
name = "lightdm-normal-user";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
settings.quiet = true;
args = lib.mkBefore [
"uid"
">="
"1000"
];
}
{
name = "permit";
control = "required";
modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
}
];
auth required pam_succeed_if.so uid >= 1000 quiet
auth required pam_permit.so
account = utils.pam.autoOrderRules [
{
name = "unix";
control = "sufficient";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
}
];
account sufficient pam_unix.so
password = utils.pam.autoOrderRules [
{
name = "unix";
control = "requisite";
modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
settings.nullok = true;
settings.yescrypt = true;
}
];
password requisite pam_unix.so nullok yescrypt
session optional pam_keyinit.so revoke
session include login
'';
session = utils.pam.autoOrderRules [
{
name = "keyinit";
control = "optional";
modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
settings.revoke = true;
}
{
name = "login";
control = "include";
modulePath = "login";
}
];
};
};
users.users.lightdm = {
home = "/var/lib/lightdm";
+3 -1
View File
@@ -1780,7 +1780,9 @@ in
optionalString hasBonds "options bonding max_bonds=0";
boot.kernel.sysctl = {
"net.ipv4.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces);
# Only set when proxyARP needs it; never write =0 (the kernel default),
# which would race with systemd-networkd's IPv4Forwarding= on switch.
"net.ipv4.conf.all.forwarding" = mkIf (any (i: i.proxyARP) interfaces) (mkDefault true);
"net.ipv6.conf.all.disable_ipv6" = mkDefault (!cfg.enableIPv6);
"net.ipv6.conf.default.disable_ipv6" = mkDefault (!cfg.enableIPv6);
# allow all users to do ICMP echo requests (ping)
+52 -1
View File
@@ -306,8 +306,42 @@ let
(builtins.concatStringsSep "")
]}
${lib.optionalString cfg.qemu.forceAccel (
if hostPkgs.stdenv.hostPlatform.isLinux then
''
# Check for hardware-accelerated virtualisation support (KVM)
if [ ! -e /dev/kvm ]; then
echo "forceAccel is enabled but /dev/kvm does not exist." >&2
echo "Hardware-accelerated virtualisation (KVM) is not available on this system." >&2
exit 1
elif [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
echo "forceAccel is enabled but /dev/kvm is not accessible (permission denied)." >&2
echo "Check that the nix build user is in the 'kvm' group or that /dev/kvm has the correct permissions." >&2
exit 1
fi
''
else if hostPkgs.stdenv.hostPlatform.isDarwin then
''
# Check for hardware-accelerated virtualisation support (HVF)
if ! sysctl -n kern.hv_support 2>/dev/null | grep -q 1; then
echo "forceAccel is enabled but Hypervisor.framework is not available on this system." >&2
exit 1
fi
''
else
''
echo "forceAccel is enabled but no known accelerator is available for this platform." >&2
exit 1
''
)}
# Start QEMU.
exec ${qemu-common.qemuBinary qemu} \
exec ${
qemu-common.qemuBinaryWith {
qemuPkg = qemu;
forceAccel = cfg.qemu.forceAccel;
}
} \
-name ${config.system.name} \
-m ${toString config.virtualisation.memorySize} \
-smp ${toString config.virtualisation.cores} \
@@ -716,6 +750,8 @@ in
};
virtualisation.qemu = {
enableSharedMemory = mkEnableOption "shared memory";
package = mkOption {
type = types.package;
default =
@@ -728,6 +764,17 @@ in
description = "QEMU package to use.";
};
forceAccel = mkOption {
type = types.bool;
default = false;
description = ''
Whether to force the use of hardware-accelerated virtualisation.
When enabled, QEMU will not fall back to the slower software
emulation (TCG) and will instead error out if the accelerator is not
available.
'';
};
options = mkOption {
type = types.listOf types.str;
default = [ ];
@@ -1338,6 +1385,10 @@ in
"-device usb-kbd"
"-device usb-tablet"
])
(mkIf cfg.qemu.enableSharedMemory [
"-object memory-backend-memfd,id=mem0,size=${toString config.virtualisation.memorySize}M,share=on"
"-machine memory-backend=mem0"
])
(
let
alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9));
+1
View File
@@ -149,6 +149,7 @@ in
lib-extend = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./nixos-test-driver/lib-extend.nix { };
node-name = runTest ./nixos-test-driver/node-name.nix;
busybox = runTest ./nixos-test-driver/busybox.nix;
ssh-backdoor = runTestOn [ "x86_64-linux" ] ./nixos-test-driver/ssh-backdoor.nix;
console-log = runTest ./nixos-test-driver/console-log.nix;
containers = runTest ./nixos-test-driver/containers.nix;
driver-timeout =
@@ -0,0 +1,36 @@
{ pkgs, lib, ... }:
{
name = "ssh-backdoor";
sshBackdoor.enable = true;
nodes.machine = { };
testScript = ''
import subprocess
start_all()
machine.wait_for_unit("multi-user.target")
assert driver.vhost_vsock is not None
host_socket = driver.vhost_vsock.sockets["machine"].host
with subtest("ssh from the host via systemd-ssh-proxy"):
subprocess.run(
[
"${lib.getExe pkgs.openssh}",
"-vvv",
f"vsock-mux/{host_socket}",
"-o",
"User=root",
"-F",
# The backdoor feature of the driver copies this into NIX_BUILD_TOP.
# We can't do this here since `enableDebugHook=true;` would halt
# instead of terminating the test execution if it fails.
"${pkgs.systemd}/lib/systemd/ssh_config.d/20-systemd-ssh-proxy.conf",
"--",
"true"
],
check=True
)
'';
}
+2 -2
View File
@@ -12,13 +12,13 @@ let
in
stdenv.mkDerivation (finalAttrs: {
pname = "mimalloc";
version = "3.1.6";
version = "3.3.0";
src = fetchFromGitHub {
owner = "microsoft";
repo = "mimalloc";
tag = "v${finalAttrs.version}";
hash = "sha256-7zG0Sqloanz/b+fkJ4wzO86uBmtf9fdYNAT9ixLouyY=";
hash = "sha256-xy9gPihw3xvhnd6BrCYfMnnRp5dPSodynKRToYwxuzg=";
};
doCheck = !stdenv.hostPlatform.isStatic;
@@ -342,6 +342,9 @@ def execute(argv: list[str]) -> None:
build_attr = BuildAttr.from_arg(args.attr, args.file)
flake = Flake.from_arg(args.flake, target_host)
if flake and (args.upgrade or args.upgrade_all):
logger.warning("'--upgrade(-all)' flag has no effect for flake-based systems")
if can_run and not flake and not args.store_path:
services.write_version_suffix(grouped_nix_args)
@@ -738,6 +738,7 @@ def upgrade_channels(all_channels: bool = False, sudo: bool = False) -> None:
"also pass '--sudo' or run the command as root (e.g., with sudo)"
)
channel_updated = False
for channel_path in Path("/nix/var/nix/profiles/per-user/root/channels/").glob("*"):
if channel_path.is_dir() and (
all_channels
@@ -749,3 +750,7 @@ def upgrade_channels(all_channels: bool = False, sudo: bool = False) -> None:
check=False,
sudo=sudo,
)
channel_updated = True
if not channel_updated:
logger.warning("'--upgrade(-all)' flag passed but no channels to update")
+5 -3
View File
@@ -16,18 +16,20 @@
rustPlatform.buildRustPackage (finalAttrs: {
pname = "ruff";
version = "0.15.10";
version = "0.15.11";
__structuredAttrs = true;
src = fetchFromGitHub {
owner = "astral-sh";
repo = "ruff";
tag = finalAttrs.version;
hash = "sha256-x+zqgIJATDWimxbh/VGt94HFiUSD4H9QD/49hnHgfuQ=";
hash = "sha256-hFKUbgYrwiSPTqNZD7HlDaoHueZrJxbrL1g/v1WD6GA=";
};
cargoBuildFlags = [ "--package=ruff" ];
cargoHash = "sha256-EQERi5NSMUK7qfFYWilzhacYlK4ShQl9Koz8t/8I6+U=";
cargoHash = "sha256-gj0Ks9uyRE1Z8LELHmnpElHLCdP6lf/bE5ji+7qD9aA=";
nativeBuildInputs = [ installShellFiles ];
+3 -11
View File
@@ -6,21 +6,13 @@
stdenv.mkDerivation (finalAttrs: {
pname = "time";
version = "1.9";
version = "1.10";
src = fetchurl {
url = "mirror://gnu/time/time-${finalAttrs.version}.tar.gz";
hash = "sha256-+6zwyB5iQp3z4zvaTO44dWYE8Y4B2XczjiMwaj47Uh4=";
url = "mirror://gnu/time/time-${finalAttrs.version}.tar.xz";
hash = "sha256-cGv3uERMqeuQN+ntoY4dDrfCMnrn2MLOOkgjxfgMexE=";
};
patches = [
# fixes cross-compilation to riscv64-linux
./time-1.9-implicit-func-decl-clang.patch
# https://lists.gnu.org/archive/html/bug-time/2025-10/msg00000.html
# fix compilation with gcc15
./time-1.9-fix-sighandler-prototype-for-c23.patch
];
outputs = [
"out"
"info"
@@ -1,30 +0,0 @@
In C23 functions with empty argument list in the prototype are treated
as taking no arguments. This means that the `int` argument of the
sighandler must be specified explicitly or the code will fail to
compile due to mismatched function type.
Signed-off-by: Marcin Serwin <marcin@serwin.dev>
---
This fixes the same issue as
<https://lists.gnu.org/archive/html/bug-time/2025-01/msg00000.html> and
<https://lists.gnu.org/archive/html/bug-time/2025-03/msg00000.html> but does not
rely on `sighandler_t` which is a GNU extension.
src/time.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/time.c b/src/time.c
index 7b401bc..88287dd 100644
--- a/src/time.c
+++ b/src/time.c
@@ -77,7 +77,7 @@ enum
/* A Pointer to a signal handler. */
-typedef RETSIGTYPE (*sighandler) ();
+typedef RETSIGTYPE (*sighandler) (int);
/* msec = milliseconds = 1/1,000 (1*10e-3) second.
usec = microseconds = 1/1,000,000 (1*10e-6) second. */
--
2.51.0
@@ -1,24 +0,0 @@
https://lists.gnu.org/archive/html/bug-time/2022-08/msg00001.html
From c8deae54f92d636878097063b411af9fb5262ad3 Mon Sep 17 00:00:00 2001
From: Khem Raj <raj.khem@gmail.com>
Date: Mon, 15 Aug 2022 07:24:24 -0700
Subject: [PATCH] include string.h for memset()
Fixes implicit function declaration warning e.g.
resuse.c:103:3: error: call to undeclared library function 'memset' with type 'void *(void *, int, unsigned long)'
Upstream-Status: Submitted [https://lists.gnu.org/archive/html/bug-time/2022-08/msg00001.html]
Signed-off-by: Khem Raj <raj.khem@gmail.com>
--- a/src/resuse.c
+++ b/src/resuse.c
@@ -22,6 +22,7 @@
*/
#include "config.h"
+#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/resource.h>
@@ -0,0 +1,45 @@
{
lib,
fetchFromGitHub,
rustPlatform,
installShellFiles,
}:
rustPlatform.buildRustPackage (finalAttrs: {
pname = "vhost-device-vsock";
version = "0.3.0";
src = fetchFromGitHub {
owner = "rust-vmm";
repo = "vhost-device";
tag = "vhost-device-vsock-v${finalAttrs.version}";
hash = "sha256-g+u6WBJtizIgQwC0kkWdAcTiYCM1zMI4YBLVRU4MOrs=";
};
__structuredAttrs = true;
outputs = [
"out"
"man"
];
cargoBuildFlags = "-p vhost-device-vsock";
cargoTestFlags = "-p vhost-device-vsock";
cargoHash = "sha256-mtORRCY/TNeIEgRCQ1ZbjpsykteRm2FHRveKaQxD/Pw=";
nativeBuildInputs = [ installShellFiles ];
postInstall = ''
installManPage vhost-device-vsock/*.1
'';
meta = {
homepage = "https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-vsock/README.md";
maintainers = with lib.maintainers; [ ma27 ];
license = with lib.licenses; [
asl20
bsd3
];
platforms = lib.platforms.linux;
};
})
+14 -14
View File
@@ -5,38 +5,38 @@
"lts": false
},
"6.1": {
"version": "6.1.168",
"hash": "sha256:0vkp75sfnjvfqxjh6gqcx24h2m6qj6xkwlw6b118cja43vjnz1g0",
"version": "6.1.169",
"hash": "sha256:0b7g7awbn1zryrh0pnjsh00d7j7ivda8i380jddhfj8ph1sfdjz0",
"lts": true
},
"5.15": {
"version": "5.15.202",
"hash": "sha256:1m6d53qx1ah4jwpa8hwjdmq0jn2hf7xmz1li6rwpdqjp97vvvh8b",
"version": "5.15.203",
"hash": "sha256:0r6w6glfpzp6qz0kbxzpmabxwgw1y5k9a407lj98gsap5bcfgsqb",
"lts": true
},
"5.10": {
"version": "5.10.252",
"hash": "sha256:1yqa4zmvi5ihf50kxcff06abfi6xw0b9ajzagvy6gdzfr7igpcrl",
"version": "5.10.253",
"hash": "sha256:1j2sszv8j9s6qlrvbnyj1qf9aapl0srbps3g4bvf5s2hh29281zc",
"lts": true
},
"6.6": {
"version": "6.6.134",
"hash": "sha256:1grp1wqgzjsk6xyl0nvd2hxlxjj0wgz04x544zkz8srp6rxnjy33",
"version": "6.6.135",
"hash": "sha256:0ahklx827y6rnh77a77bf4qr3sbp2z5z12l98avfv78nwznkilnk",
"lts": true
},
"6.12": {
"version": "6.12.81",
"hash": "sha256:0iw84bqdbh9dlaqd1bqgldg50riw2b5is7ipqnbp0sll8cv9rc62",
"version": "6.12.82",
"hash": "sha256:1a8r1wzfssrnqbf4yvbcfynf5w6la4vy1w5wlns1p63krl2hnmqf",
"lts": true
},
"6.18": {
"version": "6.18.22",
"hash": "sha256:0nazlm6j5blyd4qgl0z6xc3qk00vz3cfvx5mqv18awv5ygx94g52",
"version": "6.18.23",
"hash": "sha256:0d2ihdz5hdy1ywhck76y9rnzzvkl2lhrb5xvc6w5l4ydpxv8wb9a",
"lts": true
},
"6.19": {
"version": "6.19.12",
"hash": "sha256:1md8b270pdyk9d8cq0qyr8qmymcijmj3gc39nn394wpr0l94yp6f",
"version": "6.19.13",
"hash": "sha256:0j2ncikwi4mkx9v33ahzmi2qq2bx5f82701nnha1grs0lzzb2n85",
"lts": false
},
"7.0": {