staging-nixos merge for 2026-04-18 (#511133)
This commit is contained in:
@@ -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}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -58,3 +58,4 @@ serial_stdout_on: Callable[[], None]
|
||||
polling_condition: PollingConditionProtocol
|
||||
debug: DebugAbstract
|
||||
t: TestCase
|
||||
dump_machine_ssh: Callable[[], None]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.";
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
)
|
||||
'';
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ];
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
})
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user