shadow: enable running upstream system test suite

This commit is contained in:
João Santos Reis
2026-05-06 10:09:37 +01:00
parent 963c03f40c
commit 6d97e51b7c
3 changed files with 201 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
{ runTest }:
{
login = runTest ./login.nix;
system = runTest ./system.nix;
}

View File

@@ -0,0 +1,147 @@
{ pkgs, ... }:
let
# Create a Python environment for the controller with all necessary test framework dependencies
controllerPython = pkgs.python3.withPackages (ps: [
ps.flaky
ps.jc
ps.pytest
ps.pytest-mh
ps.pytest-ticket
]);
shadowHostName = "shadowhost";
in
{
name = "shadow-system-tests";
meta.maintainers = with pkgs.lib.maintainers; [ joaosreis ];
nodes = {
# The target host: runs sshd, has shadow and other test dependencies installed, mutable users, and some specific settings to match the expectations of the test suite
shadowhost =
{ pkgs, ... }:
{
networking.hostName = shadowHostName;
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "yes";
PasswordAuthentication = false;
};
};
users.mutableUsers = true;
environment.systemPackages = with pkgs; [
shadow
expect
];
users.defaultUserShell = "/bin/sh";
security.loginDefs.settings = {
PASS_MAX_DAYS = 99999;
PASS_MIN_DAYS = 0;
PASS_WARN_AGE = 7;
USERGROUPS_ENAB = "yes";
CREATE_HOME = "yes";
UID_MIN = 1001;
GID_MIN = 1001;
};
security.pam.services = {
newusers.text = ''
auth required pam_permit.so
account required pam_permit.so
password required pam_permit.so
session required pam_permit.so
'';
};
services.envfs.enable = true;
};
# The controller: runs pytest-mh against the shadow host
controller =
{ pkgs, ... }:
{
environment.systemPackages = [
controllerPython
pkgs.openssh
];
};
};
testScript = ''
import textwrap
start_all()
# ------------------------------------------------------------------
# 1. Generate an SSH keypair on the controller and authorise it
# on the shadow host
# ------------------------------------------------------------------
controller.succeed("mkdir -p /root/.ssh && chmod 700 /root/.ssh")
controller.succeed(
"ssh-keygen -t ed25519 -N \'\' -f /root/.ssh/id_ed25519 2>&1"
)
pub_key = controller.succeed("cat /root/.ssh/id_ed25519.pub").strip()
# Inject the generated public key into the shadow host at runtime
shadowhost.succeed("mkdir -p /root/.ssh && chmod 700 /root/.ssh")
shadowhost.succeed(
f"echo '{pub_key}' >> /root/.ssh/authorized_keys && "
"chmod 600 /root/.ssh/authorized_keys"
)
# ------------------------------------------------------------------
# 2. Make sure the shadow host has a writable /etc/login.defs,
# since the test framework expects to be able to write to it.
# ------------------------------------------------------------------
shadowhost.succeed(
"cp --remove-destination $(readlink -f /etc/login.defs) /etc/login.defs && "
"chmod 644 /etc/login.defs"
)
# ------------------------------------------------------------------
# 3. Copy the upstream test suite onto the controller
# ------------------------------------------------------------------
controller.succeed(
"cp -r ${pkgs.shadow.passthru.testFramework} /root/shadow-tests && "
"chmod -R u+w /root/shadow-tests"
)
# ------------------------------------------------------------------
# 4. Write the mhc.yaml topology config
# This tells pytest-mh where the shadow host is and which role
# it plays. The hostname must match the NixOS node name.
# ------------------------------------------------------------------
controller.succeed(textwrap.dedent("""
cat > /root/shadow-tests/mhc.yaml << 'EOF'
domains:
- id: shadow
hosts:
- hostname: ${shadowHostName}
role: shadow
ssh:
user: root
private_key: /root/.ssh/id_ed25519
EOF
"""))
# ------------------------------------------------------------------
# 5. Run the upstream pytest-mh test suite from the controller
# ------------------------------------------------------------------
shadowhost.wait_for_unit("sshd.service")
# gpasswd tests are disabled, since they rely on specific behavior of the gpasswd command that is not applicable to NixOS
controller.succeed(
"cd /root/shadow-tests && "
"${controllerPython}/bin/pytest "
"--mh-config=mhc.yaml "
"--deselect=tests/test_gpasswd.py "
"-v tests/"
)
'';
}

View File

@@ -20,6 +20,7 @@
withTcb ? lib.meta.availableOn stdenv.hostPlatform tcb,
tcb,
cmocka,
fetchpatch,
}:
let
glibc' =
@@ -139,7 +140,58 @@ stdenv.mkDerivation (finalAttrs: {
passthru = {
shellPath = "/bin/nologin";
# TODO: Run system tests: https://github.com/shadow-maint/shadow/blob/master/doc/contributions/tests.md#system-tests
tests = { inherit (nixosTests) shadow; };
# Package the upstream system test framework for use in nixosTests
testFramework = stdenv.mkDerivation {
name = "shadow-test-framework";
inherit (finalAttrs) version;
src = "${finalAttrs.src}/tests/system";
installPhase = ''
cp -r . $out/
'';
dontBuild = true;
patches = [
# tests: update useradd tests to expect ID 1001
(fetchpatch {
name = "update-useradd-tests.patch";
url = "https://github.com/shadow-maint/shadow/commit/59fbe8415dab17f1e702fbdee96956886c86c737.patch";
hash = "sha256-U0+NSCd4AfTuHMddTx9+wNtpdJt9t8+D5ApW0OCNgsY=";
stripLen = 2;
})
# tests: update usermod tests to expect ID 1001
(fetchpatch {
name = "update-usermod-tests.patch";
url = "https://github.com/shadow-maint/shadow/commit/91c2ad44ababca2e32cdb71152b0f7f2a7c546be.patch";
hash = "sha256-v1EEvMfUYoE/ZnBM0k/+kUBK3W1dXr588OQzvFmnXLI=";
stripLen = 2;
})
# tests: update groupadd tests to expect GID 1001
(fetchpatch {
name = "update-groupadd-tests.patch";
url = "https://github.com/shadow-maint/shadow/commit/60568eaec13d1e417f56f0e59ac9573c0e3b9a83.patch";
hash = "sha256-tLmGeW4Y7UcZqMhW3IXgygKPhCmbZkkW5XTJ7CFz9vg=";
stripLen = 2;
})
# tests: update newgrp tests to expect GID 1002
(fetchpatch {
name = "update-newgrp-tests.patch";
url = "https://github.com/shadow-maint/shadow/commit/49ff9bf33a7b6af57cb26688c3139f014302c9d9.patch";
hash = "sha256-1760t4Ezd5Ke4PFZ70Njb+k+1kp17aT/7uqu1PswVss=";
stripLen = 2;
})
];
postPatch = ''
# Replace the gshadow existence check in the test framework with a more NixOS-friendly one, since NixOS does not have /etc/gshadow as a regular file
substituteInPlace framework/hosts/shadow.py \
--replace-fail 'getent gshadow > /dev/null 2>&1' 'test -f /etc/gshadow'
# Remove the backup entry for gshadow, since it's not being used in the tests running on NixOS
sed -i '/{"origin": "\/etc\/gshadow", "backup": "gshadow"}/d' framework/hosts/shadow.py
# Replace the Debian-specific check in the useradd test with a NixOS-specific one
substituteInPlace tests/test_useradd.py \
--replace-fail 'if "Debian" in shadow.host.distro_name:' 'if "NixOS" in shadow.host.distro_name:'
'';
};
};
})