push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

206
nixos/tests/3proxy.nix Normal file
View File

@@ -0,0 +1,206 @@
{ lib, pkgs, ... }:
{
name = "3proxy";
meta.maintainers = with lib.maintainers; [ misuzu ];
nodes = {
peer0 =
{ lib, ... }:
{
networking.useDHCP = false;
networking.interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.0.1";
prefixLength = 24;
}
{
address = "216.58.211.111";
prefixLength = 24;
}
];
};
};
peer1 =
{ lib, ... }:
{
networking.useDHCP = false;
networking.interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.0.2";
prefixLength = 24;
}
{
address = "216.58.211.112";
prefixLength = 24;
}
];
};
# test that binding to [::] is working when ipv6 is disabled
networking.enableIPv6 = false;
services._3proxy = {
enable = true;
services = [
{
type = "admin";
bindPort = 9999;
auth = [ "none" ];
}
{
type = "proxy";
bindPort = 3128;
auth = [ "none" ];
}
];
};
networking.firewall.allowedTCPPorts = [
3128
9999
];
};
peer2 =
{ lib, ... }:
{
networking.useDHCP = false;
networking.interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.0.3";
prefixLength = 24;
}
{
address = "216.58.211.113";
prefixLength = 24;
}
];
};
services._3proxy = {
enable = true;
services = [
{
type = "admin";
bindPort = 9999;
auth = [ "none" ];
}
{
type = "proxy";
bindPort = 3128;
auth = [ "iponly" ];
acl = [
{
rule = "allow";
}
];
}
];
};
networking.firewall.allowedTCPPorts = [
3128
9999
];
};
peer3 =
{ lib, pkgs, ... }:
{
networking.useDHCP = false;
networking.interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.0.4";
prefixLength = 24;
}
{
address = "216.58.211.114";
prefixLength = 24;
}
];
};
services._3proxy = {
enable = true;
usersFile = pkgs.writeText "3proxy.passwd" ''
admin:CR:$1$.GUV4Wvk$WnEVQtaqutD9.beO5ar1W/
'';
services = [
{
type = "admin";
bindPort = 9999;
auth = [ "none" ];
}
{
type = "proxy";
bindPort = 3128;
auth = [ "strong" ];
acl = [
{
rule = "allow";
}
];
}
];
};
networking.firewall.allowedTCPPorts = [
3128
9999
];
};
};
testScript = ''
start_all()
peer0.systemctl("start network-online.target")
peer0.wait_for_unit("network-online.target")
peer1.wait_for_unit("3proxy.service")
peer1.wait_for_open_port(9999)
# test none auth
peer0.succeed(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://216.58.211.112:9999"
)
peer0.succeed(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://192.168.0.2:9999"
)
peer0.succeed(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://127.0.0.1:9999"
)
peer2.wait_for_unit("3proxy.service")
peer2.wait_for_open_port(9999)
# test iponly auth
peer0.succeed(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://216.58.211.113:9999"
)
peer0.fail(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://192.168.0.3:9999"
)
peer0.fail(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://127.0.0.1:9999"
)
peer3.wait_for_unit("3proxy.service")
peer3.wait_for_open_port(9999)
# test strong auth
peer0.succeed(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999"
)
peer0.fail(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999"
)
peer0.fail(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999"
)
peer0.fail(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999"
)
peer0.fail(
"${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://127.0.0.1:9999"
)
'';
}

30
nixos/tests/aaaaxy.nix Normal file
View File

@@ -0,0 +1,30 @@
{ pkgs, lib, ... }:
{
name = "aaaaxy";
meta.maintainers = with lib.maintainers; [ Luflosi ];
nodes.machine = {
imports = [
./common/x11.nix
];
};
# This starts the game from a known state, feeds it a prerecorded set of button presses
# and then checks if the final game state is identical to the expected state.
# This is also what AAAAXY's CI system does and serves as a good sanity check.
testScript = ''
machine.wait_for_x()
machine.succeed(
# benchmark.dem needs to be in a mutable directory,
# so we can't just refer to the file in the Nix store directly
"mkdir -p '/tmp/aaaaxy/assets/demos/'",
"ln -s '${pkgs.aaaaxy.testing_infra}/assets/demos/benchmark.dem' '/tmp/aaaaxy/assets/demos/'",
"""
'${pkgs.aaaaxy.testing_infra}/scripts/regression-test-demo.sh' \
'aaaaxy' 'on track for Any%, All Paths, All Flipped, No Teleports and No Coil' \
'${pkgs.aaaaxy}/bin/aaaaxy' '/tmp/aaaaxy/assets/demos/benchmark.dem'
""",
)
'';
}

55
nixos/tests/acme-dns.nix Normal file
View File

@@ -0,0 +1,55 @@
{
name = "acme-dns";
nodes.machine =
{ pkgs, ... }:
{
services.acme-dns = {
enable = true;
settings = {
general = rec {
domain = "acme-dns.home.arpa";
nsname = domain;
nsadmin = "admin.home.arpa";
records = [
"${domain}. A 127.0.0.1"
"${domain}. AAAA ::1"
"${domain}. NS ${domain}."
];
};
logconfig.loglevel = "debug";
};
};
environment.systemPackages = with pkgs; [
curl
bind
];
};
testScript = ''
import json
machine.wait_for_unit("acme-dns.service")
machine.wait_for_open_port(53) # dns
machine.wait_for_open_port(8080) # http api
result = machine.succeed("curl --fail -X POST http://localhost:8080/register")
print(result)
registration = json.loads(result)
machine.succeed(f'dig -t TXT @localhost {registration["fulldomain"]} | grep "SOA" | grep "admin.home.arpa"')
# acme-dns exspects a TXT value string length of exactly 43 chars
txt = "___dummy_validation_token_for_txt_record___"
machine.succeed(
"curl --fail -X POST http://localhost:8080/update "
+ f' -H "X-Api-User: {registration["username"]}"'
+ f' -H "X-Api-Key: {registration["password"]}"'
+ f' -d \'{{"subdomain":"{registration["subdomain"]}", "txt":"{txt}"}}\'''
)
assert txt in machine.succeed(f'dig -t TXT +short @localhost {registration["fulldomain"]}')
'';
}

111
nixos/tests/acme/caddy.nix Normal file
View File

@@ -0,0 +1,111 @@
{
config,
lib,
pkgs,
...
}:
let
domain = "example.test";
in
{
# Caddy only supports useACMEHost, hence we use a distinct test suite
name = "caddy";
meta = {
maintainers = lib.teams.acme.members;
# Hard timeout in seconds. Average run time is about 60 seconds.
timeout = 180;
};
nodes = {
# The fake ACME server which will respond to client requests
acme =
{ nodes, ... }:
{
imports = [ ../common/acme/server ];
};
caddy =
{ nodes, config, ... }:
let
fqdn = config.networking.fqdn;
in
{
imports = [ ../common/acme/client ];
networking.domain = domain;
networking.firewall.allowedTCPPorts = [
80
443
];
# Resolve the vhosts the easy way
networking.hosts."127.0.0.1" = [
"caddy-alt.${domain}"
];
# OpenSSL will be used for more thorough certificate validation
environment.systemPackages = [ pkgs.openssl ];
security.acme.certs."${fqdn}" = {
listenHTTP = ":8080";
reloadServices = [ "caddy.service" ];
};
users.users."${config.services.caddy.user}".extraGroups = [ "acme" ];
services.caddy = {
enable = true;
# FIXME reloading caddy is not sufficient to load new certs.
# Restart it manually until this is fixed.
enableReload = false;
globalConfig = ''
auto_https off
'';
virtualHosts."${fqdn}:443" = {
useACMEHost = fqdn;
};
virtualHosts.":80".extraConfig = ''
reverse_proxy localhost:8080
'';
};
specialisation.add_domain.configuration = {
security.acme.certs.${fqdn}.extraDomainNames = [
"caddy-alt.${domain}"
];
};
};
};
testScript =
{ nodes, ... }:
''
${(import ./utils.nix).pythonUtils}
domain = "${domain}"
ca_domain = "${nodes.acme.test-support.acme.caDomain}"
fqdn = "${nodes.caddy.networking.fqdn}"
with subtest("Boot and start with selfsigned certificates"):
caddy.start()
caddy.wait_for_unit("caddy.service")
check_issuer(caddy, fqdn, "minica")
# Check that the web server has picked up the selfsigned cert
check_connection(caddy, fqdn, minica=True)
acme.start()
wait_for_running(acme)
acme.wait_for_open_port(443)
with subtest("Acquire a new cert"):
caddy.succeed(f"systemctl restart acme-{fqdn}.service")
check_issuer(caddy, fqdn, "pebble")
check_domain(caddy, fqdn, fqdn)
download_ca_certs(caddy, ca_domain)
check_connection(caddy, fqdn)
with subtest("security.acme changes reflect on caddy"):
check_connection(caddy, f"caddy-alt.{domain}", fail=True)
switch_to(caddy, "add_domain")
check_connection(caddy, f"caddy-alt.{domain}")
'';
}

View File

@@ -0,0 +1,70 @@
{ runTest }:
let
domain = "example.test";
in
{
http01-builtin = runTest ./http01-builtin.nix;
dns01 = runTest ./dns01.nix;
caddy = runTest ./caddy.nix;
nginx = runTest (
import ./webserver.nix {
inherit domain;
serverName = "nginx";
group = "nginx";
baseModule = {
services.nginx = {
enable = true;
enableReload = true;
logError = "stderr info";
# This tests a number of things at once:
# - Self-signed certs are in place before the webserver startup
# - Nginx is started before acme renewal is attempted
# - useACMEHost behaves as expected
# - acmeFallbackHost behaves as expected
virtualHosts.default = {
default = true;
addSSL = true;
useACMEHost = "proxied.example.test";
acmeFallbackHost = "localhost:8080";
};
};
specialisation.nullroot.configuration = {
services.nginx.virtualHosts."nullroot.${domain}".acmeFallbackHost = "localhost:8081";
};
};
}
);
httpd = runTest (
import ./webserver.nix {
inherit domain;
serverName = "httpd";
group = "wwwrun";
baseModule = {
services.httpd = {
enable = true;
# This is the default by virtue of being the first defined vhost.
virtualHosts.default = {
addSSL = true;
useACMEHost = "proxied.example.test";
locations."/.well-known/acme-challenge" = {
proxyPass = "http://localhost:8080/.well-known/acme-challenge";
extraConfig = ''
ProxyPreserveHost On
'';
};
};
};
specialisation.nullroot.configuration = {
services.httpd.virtualHosts."nullroot.${domain}" = {
locations."/.well-known/acme-challenge" = {
proxyPass = "http://localhost:8081/.well-known/acme-challenge";
extraConfig = ''
ProxyPreserveHost On
'';
};
};
};
};
}
);
}

118
nixos/tests/acme/dns01.nix Normal file
View File

@@ -0,0 +1,118 @@
{
config,
lib,
pkgs,
...
}:
let
domain = "example.test";
dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress;
dnsScript = pkgs.writeShellScript "dns-hook.sh" ''
set -euo pipefail
echo '[INFO]' "[$2]" 'dns-hook.sh' $*
if [ "$1" = "present" ]; then
${pkgs.curl}/bin/curl --data @- http://dnsserver.test:8055/set-txt << EOF
{"host": "$2", "value": "$3"}
EOF
else
${pkgs.curl}/bin/curl --data @- http://dnsserver.test:8055/clear-txt << EOF
{"host": "$2"}
EOF
fi
'';
in
{
name = "dns01";
meta = {
maintainers = lib.teams.acme.members;
# Hard timeout in seconds. Average run time is about 60 seconds.
timeout = 180;
};
nodes = {
# The fake ACME server which will respond to client requests
acme =
{ nodes, ... }:
{
imports = [ ../common/acme/server ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
};
# A fake DNS server which can be configured with records as desired
# Used to test DNS-01 challenge
dnsserver =
{ nodes, ... }:
{
networking = {
firewall.allowedTCPPorts = [
8055
53
];
firewall.allowedUDPPorts = [ 53 ];
# nixos/lib/testing/network.nix will provide name resolution via /etc/hosts
# for all nodes based on their host names and domain
hostName = "dnsserver";
domain = "test";
};
systemd.services.pebble-challtestsrv = {
enable = true;
description = "Pebble ACME challenge test server";
wantedBy = [ "network.target" ];
serviceConfig = {
ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.client.networking.primaryIPAddress}'";
# Required to bind on privileged ports.
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
};
};
};
client =
{ nodes, ... }:
{
imports = [ ../common/acme/client ];
networking.domain = domain;
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
# OpenSSL will be used for more thorough certificate validation
environment.systemPackages = [ pkgs.openssl ];
security.acme.certs."${domain}" = {
domain = "*.${domain}";
dnsProvider = "exec";
dnsPropagationCheck = false;
environmentFile = pkgs.writeText "wildcard.env" ''
EXEC_PATH=${dnsScript}
EXEC_POLLING_INTERVAL=1
EXEC_PROPAGATION_TIMEOUT=1
EXEC_SEQUENCE_INTERVAL=1
'';
};
};
};
testScript = ''
${(import ./utils.nix).pythonUtils}
cert = "${domain}"
dnsserver.start()
acme.start()
wait_for_running(dnsserver)
dnsserver.wait_for_open_port(53)
wait_for_running(acme)
acme.wait_for_open_port(443)
with subtest("Boot and acquire a new cert"):
client.start()
wait_for_running(client)
check_issuer(client, cert, "pebble")
check_domain(client, cert, cert, fail=True)
check_domain(client, cert, f"toodeep.nesting.{cert}", fail=True)
check_domain(client, cert, f"whatever.{cert}")
'';
}

View File

@@ -0,0 +1,320 @@
{
config,
lib,
pkgs,
...
}:
let
domain = "example.test";
in
{
name = "http01-builtin";
meta = {
maintainers = lib.teams.acme.members;
# Hard timeout in seconds. Average run time is about 90 seconds.
timeout = 300;
};
nodes = {
# The fake ACME server which will respond to client requests
acme =
{ nodes, ... }:
{
imports = [ ../common/acme/server ];
};
builtin =
{ nodes, config, ... }:
{
imports = [ ../common/acme/client ];
networking.domain = domain;
networking.firewall.allowedTCPPorts = [ 80 ];
# OpenSSL will be used for more thorough certificate validation
environment.systemPackages = [ pkgs.openssl ];
security.acme.certs."${config.networking.fqdn}" = {
listenHTTP = ":80";
};
systemd.targets."renew-triggered" = {
wantedBy = [ "acme-order-renew-${config.networking.fqdn}.service" ];
after = [ "acme-order-renew-${config.networking.fqdn}.service" ];
unitConfig.RefuseManualStart = true;
};
specialisation = {
renew.configuration = {
# Pebble provides 5 year long certs,
# needs to be higher than that to test renewal
security.acme.certs."${config.networking.fqdn}".validMinDays = 9999;
};
accountchange.configuration = {
security.acme.certs."${config.networking.fqdn}".email = "admin@example.test";
};
keytype.configuration = {
security.acme.certs."${config.networking.fqdn}".keyType = "ec384";
};
# Perform http-01 test again, but using the pre-24.05 account hashing
# (see https://github.com/NixOS/nixpkgs/pull/317257)
# The hash is deterministic in this case - only based on keyType and email.
# Note: This test is making the assumption that the acme module will create
# the account directory regardless of internet connectivity or server reachability.
legacy_account_hash.configuration = {
security.acme.defaults.server = lib.mkForce null;
};
ocsp_stapling.configuration = {
security.acme.certs."${config.networking.fqdn}".ocspMustStaple = true;
};
preservation.configuration = { };
add_cert_and_domain.configuration = {
security.acme.certs = {
"${config.networking.fqdn}" = {
extraDomainNames = [
"builtin-alt.${domain}"
];
};
# We can assume that if renewal succeeds then the account creation leader
# logic is working, since only one service could bind to port 80 at the same time.
"builtin-2.${domain}".listenHTTP = ":80";
};
# To make sure it's the account creation leader that is doing the work.
security.acme.maxConcurrentRenewals = 10;
};
concurrency.configuration = {
# As above, relying on port binding behaviour to assert that concurrency limit
# prevents > 1 service running at a time.
security.acme.maxConcurrentRenewals = 1;
security.acme.certs = {
"${config.networking.fqdn}" = {
extraDomainNames = [
"builtin-alt.${domain}"
];
};
"builtin-2.${domain}" = {
extraDomainNames = [ "builtin-2-alt.${domain}" ];
listenHTTP = ":80";
};
"builtin-3.${domain}".listenHTTP = ":80";
};
};
csr.configuration =
let
conf = pkgs.writeText "openssl.csr.conf" ''
[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
CN = ${config.networking.fqdn}
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = ${config.networking.fqdn}
'';
csrData =
pkgs.runCommandNoCC "csr-and-key"
{
buildInputs = [ pkgs.openssl ];
}
''
mkdir -p $out
openssl req -new -newkey rsa:2048 -nodes \
-keyout $out/key.pem \
-out $out/request.csr \
-config ${conf}
'';
in
{
security.acme.certs."${config.networking.fqdn}" = {
csr = "${csrData}/request.csr";
csrKey = "${csrData}/key.pem";
};
};
};
};
};
testScript =
{ nodes, ... }:
let
certName = nodes.builtin.networking.fqdn;
caDomain = nodes.acme.test-support.acme.caDomain;
in
''
${(import ./utils.nix).pythonUtils}
domain = "${domain}"
cert = "${certName}"
cert2 = "builtin-2." + domain
cert3 = "builtin-3." + domain
legacy_account_dir = "/var/lib/acme/.lego/accounts/1ccf607d9aa280e9af00"
acme.start()
wait_for_running(acme)
acme.wait_for_open_port(443)
with subtest("Boot and acquire a new cert"):
builtin.start()
wait_for_running(builtin)
check_issuer(builtin, cert, "pebble")
check_domain(builtin, cert, cert)
with subtest("Validate permissions"):
check_permissions(builtin, cert, "acme")
with subtest("Check renewal behaviour"):
# First, test no-op behaviour
hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
# old_hash will be used in the preservation tests later
old_hash = hash
builtin.succeed(f"systemctl start acme-{cert}.service")
builtin.succeed(f"systemctl start acme-order-renew-{cert}.service")
builtin.wait_for_unit("renew-triggered.target")
hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
assert hash == hash_after, "Certificate was unexpectedly changed"
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "renew")
builtin.wait_for_unit("renew-triggered.target")
check_issuer(builtin, cert, "pebble")
hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr")
assert hash != hash_after, "Certificate was not renewed"
check_permissions(builtin, cert, "acme")
with subtest("Handles email change correctly"):
hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "accountchange")
builtin.wait_for_unit("renew-triggered.target")
check_issuer(builtin, cert, "pebble")
# Check that there are now 2 account directories
builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2")
hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
# Has to do a full run to register account, which creates new certs.
assert hash != hash_after, "Certificate was not renewed"
# Remove the new account directory
builtin.succeed(
"cd /var/lib/acme/.lego/accounts"
" && ls -1 --sort=time | tee /dev/stderr | head -1 | xargs rm -rf"
)
# old_hash will be used in the preservation tests later
old_hash = hash_after
check_permissions(builtin, cert, "acme")
with subtest("Correctly implements OCSP stapling"):
check_stapling(builtin, cert, "${caDomain}", fail=True)
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "ocsp_stapling")
builtin.wait_for_unit("renew-triggered.target")
check_stapling(builtin, cert, "${caDomain}")
check_permissions(builtin, cert, "acme")
with subtest("Handles keyType change correctly"):
check_key_bits(builtin, cert, 256)
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "keytype")
builtin.wait_for_unit("renew-triggered.target")
check_key_bits(builtin, cert, 384)
# keyType is part of the accountHash, thus a new account will be created
builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2")
check_permissions(builtin, cert, "acme")
with subtest("Reuses generated, valid certs from previous configurations"):
# Right now, the hash should not match due to the previous test
hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr")
assert hash != old_hash, "Expected certificate to differ"
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "preservation")
builtin.wait_for_unit("renew-triggered.target")
hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr")
assert hash == old_hash, "Expected certificate to match from older configuration"
check_permissions(builtin, cert, "acme")
with subtest("Add a new cert, extend existing cert domains"):
check_domain(builtin, cert, f"builtin-alt.{domain}", fail=True)
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "add_cert_and_domain")
builtin.wait_for_unit("renew-triggered.target")
check_issuer(builtin, cert, "pebble")
check_domain(builtin, cert, f"builtin-alt.{domain}")
check_issuer(builtin, cert2, "pebble")
check_domain(builtin, cert2, cert2)
# There should not be a new account folder created
builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2")
check_permissions(builtin, cert, "acme")
check_permissions(builtin, cert2, "acme")
with subtest("Check account hashing compatibility with pre-24.05 settings"):
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "legacy_account_hash"
)
builtin.wait_for_unit("renew-triggered.target")
builtin.succeed(f"stat {legacy_account_dir} > /dev/stderr && rm -rf {legacy_account_dir}")
check_permissions(builtin, cert, "acme")
with subtest("Ensure concurrency limits work"):
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "concurrency")
builtin.wait_for_unit("renew-triggered.target")
check_issuer(builtin, cert3, "pebble")
check_domain(builtin, cert3, cert3)
check_permissions(builtin, cert, "acme")
with subtest("Can renew using a CSR"):
builtin.succeed(f"systemctl stop acme-{cert}.service")
builtin.succeed(f"systemctl clean acme-{cert}.service --what=state")
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "csr")
builtin.wait_for_unit("renew-triggered.target")
check_issuer(builtin, cert, "pebble")
with subtest("Generate self-signed certs"):
acme.shutdown()
check_issuer(builtin, cert, "pebble")
builtin.succeed(f"systemctl stop acme-{cert}.service")
builtin.succeed(f"systemctl clean acme-{cert}.service --what=state")
builtin.succeed(f"systemctl start acme-{cert}.service")
check_issuer(builtin, cert, "minica")
check_domain(builtin, cert, cert)
with subtest("Validate permissions (self-signed)"):
check_permissions(builtin, cert, "acme")
'';
}

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
import time
TOTAL_RETRIES = 20
# BackoffTracker provides a robust system for handling test retries
class BackoffTracker:
delay = 1
increment = 1
def handle_fail(self, retries, message) -> int:
assert retries < TOTAL_RETRIES, message
print(f"Retrying in {self.delay}s, {retries + 1}/{TOTAL_RETRIES}")
time.sleep(self.delay)
# Only increment after the first try
if retries == 0:
self.delay += self.increment
self.increment *= 2
return retries + 1
def protect(self, func):
def wrapper(*args, retries: int = 0, **kwargs):
try:
return func(*args, **kwargs)
except Exception as err:
retries = self.handle_fail(retries, err.args)
return wrapper(*args, retries=retries, **kwargs)
return wrapper
backoff = BackoffTracker()
def run(node, cmd, fail=False):
if fail:
return node.fail(cmd)
else:
return node.succeed(cmd)
# Waits for the system to finish booting or switching configuration
def wait_for_running(node):
node.succeed("systemctl is-system-running --wait")
# On first switch, this will create a symlink to the current system so that we can
# quickly switch between derivations
def switch_to(node, name, fail=False) -> None:
root_specs = "/tmp/specialisation"
node.execute(
f"test -e {root_specs}"
f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}"
)
switcher_path = (
f"/run/current-system/specialisation/{name}/bin/switch-to-configuration"
)
rc, _ = node.execute(f"test -e '{switcher_path}'")
if rc > 0:
switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration"
cmd = f"{switcher_path} test"
run(node, cmd, fail=fail)
if not fail:
wait_for_running(node)
# Ensures the issuer of our cert matches the chain
# and matches the issuer we expect it to be.
# It's a good validation to ensure the cert.pem and fullchain.pem
# are not still selfsigned after verification
@backoff.protect
def check_issuer(node, cert_name, issuer) -> None:
for fname in ("cert.pem", "fullchain.pem"):
actual_issuer = node.succeed(
f"openssl x509 -noout -issuer -in /var/lib/acme/{cert_name}/{fname}"
).partition("=")[2]
assert (
issuer.lower() in actual_issuer.lower()
), f"{fname} issuer mismatch. Expected {issuer} got {actual_issuer}"
# Ensures the provided domain matches with the given cert
def check_domain(node, cert_name, domain, fail=False) -> None:
cmd = f"openssl x509 -noout -checkhost '{domain}' -in /var/lib/acme/{cert_name}/cert.pem"
run(node, cmd, fail=fail)
# Ensures the required values for OCSP stapling are present
# Pebble doesn't provide a full OCSP responder, so just checks the URL
def check_stapling(node, cert_name, ca_domain, fail=False):
rc, _ = node.execute(
f"openssl x509 -noout -ocsp_uri -in /var/lib/acme/{cert_name}/cert.pem"
f" | grep -i 'http://{ca_domain}:4002' 2>&1",
)
assert rc == 0 or fail, "Failed to find OCSP URI in issued certificate"
run(
node,
f"openssl x509 -noout -ext tlsfeature -in /var/lib/acme/{cert_name}/cert.pem"
f" | grep -iv 'no extensions' 2>&1",
fail=fail,
)
# Checks the keyType by validating the number of bits
def check_key_bits(node, cert_name, bits, fail=False):
run(
node,
f"openssl x509 -noout -text -in /var/lib/acme/{cert_name}/cert.pem"
f" | grep -i Public-Key | grep {bits} | tee /dev/stderr",
fail=fail,
)
# Ensure cert comes before chain in fullchain.pem
def check_fullchain(node, cert_name):
cert_file = f"/var/lib/acme/{cert_name}/fullchain.pem"
num_certs = node.succeed(f"grep -o 'END CERTIFICATE' {cert_file}")
assert len(num_certs.strip().split("\n")) > 1, "Insufficient certs in fullchain.pem"
first_cert_data = node.succeed(
f"grep -m1 -B50 'END CERTIFICATE' {cert_file}"
" | openssl x509 -noout -text"
)
for line in first_cert_data.lower().split("\n"):
if "dns:" in line:
print(f"First DNSName in fullchain.pem: {line}")
assert cert_name.lower() in line, f"{cert_name} not found in {line}"
return
assert False
# Checks the permissions in the cert directories are as expected
def check_permissions(node, cert_name, group):
stat = "stat -L -c '%a %U %G' "
node.succeed(
f"test $({stat} /var/lib/acme/{cert_name}/*.pem"
f" | tee /dev/stderr | grep -v '640 acme {group}' | wc -l) -eq 0"
)
node.execute(f"ls -lahR /var/lib/acme/.lego/{cert_name}/* > /dev/stderr")
node.succeed(
f"test $({stat} /var/lib/acme/.lego/{cert_name}/*/{cert_name}*"
f" | tee /dev/stderr | grep -v '640 acme {group}' | wc -l) -eq 0"
)
node.succeed(
f"test $({stat} /var/lib/acme/{cert_name}"
f" | tee /dev/stderr | grep -v '750 acme {group}' | wc -l) -eq 0"
)
node.succeed(
f"test $(find /var/lib/acme/.lego/accounts -type f -exec {stat} {{}} \\;"
f" | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0"
)
@backoff.protect
def download_ca_certs(node, ca_domain):
node.succeed(f"curl https://{ca_domain}:15000/roots/0 > /tmp/ca.crt")
node.succeed(f"curl https://{ca_domain}:15000/intermediate-keys/0 >> /tmp/ca.crt")
@backoff.protect
def check_connection(node, domain, fail=False, minica=False):
cafile = "/tmp/ca.crt"
if minica:
cafile = "/var/lib/acme/.minica/cert.pem"
run(node,
f"openssl s_client -brief -CAfile {cafile}"
f" -verify 2 -verify_return_error -verify_hostname {domain}"
f" -servername {domain} -connect {domain}:443 < /dev/null",
fail=fail,
)

View File

@@ -0,0 +1,4 @@
{
# Helper functions for python
pythonUtils = builtins.readFile ./python-utils.py;
}

View File

@@ -0,0 +1,230 @@
{
serverName,
group,
baseModule,
domain,
}:
{
config,
lib,
pkgs,
...
}:
{
name = serverName;
meta = {
maintainers = lib.teams.acme.members;
# Hard timeout in seconds. Average run time is about 100 seconds.
timeout = 300;
};
interactive.sshBackdoor.enable = true;
nodes = {
# The fake ACME server which will respond to client requests
acme =
{ nodes, ... }:
{
imports = [ ../common/acme/server ];
};
webserver =
{ nodes, ... }:
{
imports = [
../common/acme/client
baseModule
];
networking.domain = domain;
networking.firewall.allowedTCPPorts = [
80
443
];
# Resolve the vhosts the easy way
networking.hosts."127.0.0.1" = [
"proxied.${domain}"
"certchange.${domain}"
"zeroconf.${domain}"
"zeroconf2.${domain}"
"zeroconf3.${domain}"
"nullroot.${domain}"
];
# OpenSSL will be used for more thorough certificate validation
environment.systemPackages = [ pkgs.openssl ];
# Used to determine if service reload was triggered.
# This does not provide a guarantee that the webserver is finished reloading,
# to handle that there is retry logic wrapping any connectivity checks.
systemd.targets."renew-triggered" = {
wantedBy = [ "${serverName}-config-reload.service" ];
after = [ "${serverName}-config-reload.service" ];
unitConfig.RefuseManualStart = true;
};
security.acme.certs."proxied.${domain}" = {
listenHTTP = ":8080";
group = group;
};
specialisation = {
# Test that the web server is correctly reloaded when the cert changes
certchange.configuration = {
security.acme.certs."proxied.${domain}".extraDomainNames = [
"certchange.${domain}"
];
};
# A useful transitional step before other tests, and tests behaviour
# of removing an extra domain from a cert.
certundo.configuration = { };
# Tests these features:
# - enableACME behaves as expected
# - serverAliases are appended to extraDomainNames
# - Correct routing to the specific virtualHost for a cert
# Inherits previous test config
zeroconf.configuration = {
services.${serverName}.virtualHosts."zeroconf.${domain}" = {
addSSL = true;
enableACME = true;
serverAliases = [ "zeroconf2.${domain}" ];
};
};
# Test that serverAliases are correctly removed which triggers
# cert regeneration and service reload.
rmalias.configuration = {
services.${serverName}.virtualHosts."zeroconf.${domain}" = {
addSSL = true;
enableACME = true;
};
};
# Test that "acmeRoot = null" still results in
# valid cert generation by inheriting defaults.
nullroot.configuration = {
# The default.nix has the server-type dependent config statements
# to properly set up the proxying. We need a separate port here to
# avoid hostname issues with the proxy already running on :8080
security.acme.defaults.listenHTTP = ":8081";
services.${serverName}.virtualHosts."nullroot.${domain}" = {
addSSL = true;
enableACME = true;
acmeRoot = null;
};
};
# Test that a adding a second virtual host will not trigger
# other units (account and renewal service for first)
zeroconf3.configuration = {
services.${serverName}.virtualHosts = {
"zeroconf.${domain}" = {
addSSL = true;
enableACME = true;
serverAliases = [ "zeroconf2.${domain}" ];
};
"zeroconf3.${domain}" = {
addSSL = true;
enableACME = true;
};
};
# We're doing something risky with the combination of the service unit being persistent
# that could end up that the timers do not trigger properly. Show that timers have the
# desired effect.
systemd.timers."acme-renew-zeroconf3.${domain}".timerConfig = {
OnCalendar = lib.mkForce "*-*-* *:*:0/5";
AccuracySec = lib.mkForce 0;
# Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
RandomizedDelaySec = lib.mkForce 0;
FixedRandomDelay = lib.mkForce 0;
};
};
};
};
};
testScript =
{ nodes, ... }:
''
${(import ./utils.nix).pythonUtils}
domain = "${domain}"
ca_domain = "${nodes.acme.test-support.acme.caDomain}"
fqdn = f"proxied.{domain}"
webserver.start()
webserver.wait_for_unit("${serverName}.service")
with subtest("Can run on self-signed certificates"):
check_issuer(webserver, fqdn, "minica")
# Check that the web server has picked up the selfsigned cert
check_connection(webserver, fqdn, minica=True)
acme.start()
wait_for_running(acme)
acme.wait_for_open_port(443)
with subtest("Acquire a cert through a proxied lego"):
webserver.succeed(f"systemctl start acme-order-renew-{fqdn}.service")
webserver.wait_for_unit("renew-triggered.target")
download_ca_certs(webserver, ca_domain)
check_issuer(webserver, fqdn, "pebble")
check_connection(webserver, fqdn)
with subtest("security.acme changes reflect on web server part 1"):
check_connection(webserver, f"certchange.{domain}", fail=True)
switch_to(webserver, "certchange")
webserver.wait_for_unit("renew-triggered.target")
check_connection(webserver, f"certchange.{domain}")
check_connection(webserver, fqdn)
with subtest("security.acme changes reflect on web server part 2"):
check_connection(webserver, f"certchange.{domain}")
switch_to(webserver, "certundo")
webserver.wait_for_unit("renew-triggered.target")
check_connection(webserver, f"certchange.{domain}", fail=True)
check_connection(webserver, fqdn)
with subtest("Zero configuration SSL certificates for a vhost"):
check_connection(webserver, f"zeroconf.{domain}", fail=True)
switch_to(webserver, "zeroconf")
webserver.wait_for_unit("renew-triggered.target")
check_connection(webserver, f"zeroconf.{domain}")
check_connection(webserver, f"zeroconf2.{domain}")
check_connection(webserver, fqdn)
with subtest("Removing an alias from a vhost"):
check_connection(webserver, f"zeroconf2.{domain}")
switch_to(webserver, "rmalias")
webserver.wait_for_unit("renew-triggered.target")
check_connection(webserver, f"zeroconf2.{domain}", fail=True)
check_connection(webserver, f"zeroconf.{domain}")
check_connection(webserver, fqdn)
with subtest("Create cert using inherited default validation mechanism"):
check_connection(webserver, f"nullroot.{domain}", fail=True)
switch_to(webserver, "nullroot")
webserver.wait_for_unit("renew-triggered.target")
check_connection(webserver, f"nullroot.{domain}")
with subtest("Ensure that adding a second vhost does not trigger first vhost acme units"):
switch_to(webserver, "zeroconf")
webserver.wait_for_unit("renew-triggered.target")
webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme")
switch_to(webserver, "zeroconf3")
webserver.wait_for_unit("renew-triggered.target")
output = webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme")
# The new certificate unit gets triggered:
t.assertIn(f"acme-zeroconf3.{domain}-start", output)
# The account generation should not be triggered again:
t.assertNotIn("acme-account-d590213ed52603e9128d.target", output)
# The other certificates should also not be triggered:
t.assertNotIn(f"acme-zeroconf.{domain}-start", output)
t.assertNotIn(f"acme-proxied.{domain}-start", output)
# Ensure the timer works, due to our shenanigans with
# RemainAfterExit=true
webserver.wait_until_succeeds(f"journalctl --cursor-file=/tmp/cursor | grep 'Starting Order (and renew) ACME certificate for zeroconf3.{domain}...'")
'';
}

View File

@@ -0,0 +1,104 @@
{ lib, ... }:
{
name = "activation-etc-overlay-immutable";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{ pkgs, ... }:
{
system.etc.overlay.enable = true;
system.etc.overlay.mutable = false;
# Prerequisites
systemd.sysusers.enable = true;
users.mutableUsers = false;
boot.initrd.systemd.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
time.timeZone = "Utc";
# The standard resolvconf service tries to write to /etc and crashes,
# which makes nixos-rebuild exit uncleanly when switching into the new generation
services.resolved.enable = true;
environment.etc = {
"mountpoint/.keep".text = "keep";
"filemount".text = "keep";
};
specialisation.new-generation.configuration = {
environment.etc."newgen".text = "newgen";
};
specialisation.newer-generation.configuration = {
environment.etc."newergen".text = "newergen";
};
};
testScript = # python
''
newergen = machine.succeed("realpath /run/current-system/specialisation/newer-generation/bin/switch-to-configuration").rstrip()
with subtest("/run/nixos-etc-metadata/ is mounted"):
print(machine.succeed("mountpoint /run/nixos-etc-metadata"))
with subtest("No temporary files leaked into stage 2"):
machine.succeed("[ ! -e /etc-metadata-image ]")
machine.succeed("[ ! -e /etc-basedir ]")
with subtest("/etc is mounted as an overlay"):
machine.succeed("findmnt --kernel --type overlay /etc")
with subtest("direct symlinks point to the target without indirection"):
assert machine.succeed("readlink -n /etc/localtime") == "/etc/zoneinfo/Utc"
with subtest("/etc/mtab points to the right file"):
assert "/proc/mounts" == machine.succeed("readlink --no-newline /etc/mtab")
with subtest("Correct mode on the source password files"):
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/passwd") == "644\n"
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/group") == "644\n"
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/shadow") == "0\n"
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/gshadow") == "0\n"
with subtest("Password files are symlinks to /var/lib/nixos/etc"):
assert machine.succeed("readlink -f /etc/passwd") == "/var/lib/nixos/etc/passwd\n"
assert machine.succeed("readlink -f /etc/group") == "/var/lib/nixos/etc/group\n"
assert machine.succeed("readlink -f /etc/shadow") == "/var/lib/nixos/etc/shadow\n"
assert machine.succeed("readlink -f /etc/gshadow") == "/var/lib/nixos/etc/gshadow\n"
with subtest("switching to the same generation"):
machine.succeed("/run/current-system/bin/switch-to-configuration test")
with subtest("the initrd didn't get rebuilt"):
machine.succeed("test /run/current-system/initrd -ef /run/current-system/specialisation/new-generation/initrd")
with subtest("switching to a new generation"):
machine.fail("stat /etc/newgen")
machine.succeed("mount -t tmpfs tmpfs /etc/mountpoint")
machine.succeed("touch /etc/mountpoint/extra-file")
machine.succeed("mount --bind /dev/null /etc/filemount")
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
assert machine.succeed("cat /etc/newgen") == "newgen"
print(machine.succeed("findmnt /etc/mountpoint"))
print(machine.succeed("ls /etc/mountpoint"))
print(machine.succeed("stat /etc/mountpoint/extra-file"))
print(machine.succeed("findmnt /etc/filemount"))
machine.succeed(f"{newergen} switch")
tmpMounts = machine.succeed("find /run -maxdepth 1 -type d -regex '/run/nixos-etc\\..*'").rstrip()
print(tmpMounts)
metaMounts = machine.succeed("find /run -maxdepth 1 -type d -regex '/run/nixos-etc-metadata.*'").rstrip()
print(metaMounts)
numOfTmpMounts = len(tmpMounts.splitlines())
numOfMetaMounts = len(metaMounts.splitlines())
assert numOfTmpMounts == 0, f"Found {numOfTmpMounts} remaining tmpmounts"
assert numOfMetaMounts == 1, f"Found {numOfMetaMounts} remaining metamounts"
'';
}

View File

@@ -0,0 +1,81 @@
{ lib, ... }:
{
name = "activation-etc-overlay-mutable";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{ pkgs, ... }:
{
system.etc.overlay.enable = true;
system.etc.overlay.mutable = true;
# Prerequisites
boot.initrd.systemd.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
specialisation.new-generation.configuration = {
environment.etc."newgen".text = "newgen";
};
specialisation.newer-generation.configuration = {
environment.etc."newergen".text = "newergen";
};
};
testScript = # python
''
newergen = machine.succeed("realpath /run/current-system/specialisation/newer-generation/bin/switch-to-configuration").rstrip()
with subtest("/run/nixos-etc-metadata/ is mounted"):
print(machine.succeed("mountpoint /run/nixos-etc-metadata"))
with subtest("No temporary files leaked into stage 2"):
machine.succeed("[ ! -e /etc-metadata-image ]")
machine.succeed("[ ! -e /etc-basedir ]")
with subtest("/etc is mounted as an overlay"):
machine.succeed("findmnt --kernel --type overlay /etc")
with subtest("switching to the same generation"):
machine.succeed("/run/current-system/bin/switch-to-configuration test")
with subtest("the initrd didn't get rebuilt"):
machine.succeed("test /run/current-system/initrd -ef /run/current-system/specialisation/new-generation/initrd")
with subtest("switching to a new generation"):
machine.fail("stat /etc/newgen")
machine.succeed("echo -n 'mutable' > /etc/mutable")
# Directory
machine.succeed("mkdir /etc/mountpoint")
machine.succeed("mount -t tmpfs tmpfs /etc/mountpoint")
machine.succeed("touch /etc/mountpoint/extra-file")
# File
machine.succeed("touch /etc/filemount")
machine.succeed("mount --bind /dev/null /etc/filemount")
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
assert machine.succeed("cat /etc/newgen") == "newgen"
assert machine.succeed("cat /etc/mutable") == "mutable"
print(machine.succeed("findmnt /etc/mountpoint"))
print(machine.succeed("stat /etc/mountpoint/extra-file"))
print(machine.succeed("findmnt /etc/filemount"))
machine.succeed(f"{newergen} switch")
assert machine.succeed("cat /etc/newergen") == "newergen"
tmpMounts = machine.succeed("find /run -maxdepth 1 -type d -regex '/run/nixos-etc\\..*'").rstrip()
print(tmpMounts)
metaMounts = machine.succeed("find /run -maxdepth 1 -type d -regex '/run/nixos-etc-metadata.*'").rstrip()
print(metaMounts)
numOfTmpMounts = len(tmpMounts.splitlines())
numOfMetaMounts = len(metaMounts.splitlines())
assert numOfTmpMounts == 0, f"Found {numOfTmpMounts} remaining tmpmounts"
assert numOfMetaMounts == 1, f"Found {numOfMetaMounts} remaining metamounts"
'';
}

View File

@@ -0,0 +1,28 @@
{ lib, ... }:
{
name = "activation-nix-channel";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine = {
nix.channel.enable = true;
};
testScript =
{ nodes, ... }:
''
machine.start(allow_reboot=True)
assert machine.succeed("cat /root/.nix-channels") == "${nodes.machine.system.defaultChannel} nixos\n"
nixpkgs_unstable_channel = "https://nixos.org/channels/nixpkgs-unstable nixpkgs"
machine.succeed(f"echo '{nixpkgs_unstable_channel}' > /root/.nix-channels")
machine.reboot()
assert machine.succeed("cat /root/.nix-channels") == f"{nixpkgs_unstable_channel}\n"
'';
}

View File

@@ -0,0 +1,54 @@
{ lib, pkgs, ... }:
{
name = "nixos-init";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{ modulesPath, ... }:
{
imports = [
"${modulesPath}/profiles/perlless.nix"
];
virtualisation.mountHostNixStore = false;
virtualisation.useNixStoreImage = true;
system.nixos-init.enable = true;
# Forcibly set this to only these specific values.
boot.nixStoreMountOpts = lib.mkForce [
"nodev"
"nosuid"
];
};
testScript =
{ nodes, ... }: # python
''
with subtest("init"):
with subtest("/nix/store is mounted with the correct options"):
findmnt_output = machine.succeed("findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store").strip()
print(findmnt_output)
t.assertIn("nodev", findmnt_output)
t.assertIn("nosuid", findmnt_output)
t.assertEqual("${nodes.machine.system.build.toplevel}", machine.succeed("readlink /run/booted-system").strip())
with subtest("activation"):
t.assertEqual("${nodes.machine.system.build.toplevel}", machine.succeed("readlink /run/current-system").strip())
t.assertEqual("${nodes.machine.hardware.firmware}/lib/firmware", machine.succeed("cat /sys/module/firmware_class/parameters/path").strip())
t.assertEqual("${pkgs.kmod}/bin/modprobe", machine.succeed("cat /proc/sys/kernel/modprobe").strip())
t.assertEqual("${nodes.machine.environment.usrbinenv}", machine.succeed("readlink /usr/bin/env").strip())
t.assertEqual("${nodes.machine.environment.binsh}", machine.succeed("readlink /bin/sh").strip())
machine.wait_for_unit("multi-user.target")
with subtest("systemd state passing"):
systemd_analyze_output = machine.succeed("systemd-analyze")
print(systemd_analyze_output)
t.assertIn("(initrd)", systemd_analyze_output, "systemd-analyze has no information about the initrd")
ps_output = machine.succeed("ps ax -o command | grep systemd | head -n 1")
print(ps_output)
t.assertIn("--deserialize", ps_output, "--deserialize flag wasn't passed to systemd")
'';
}

View File

@@ -0,0 +1,26 @@
{ lib, ... }:
{
name = "activation-perlless";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{ pkgs, modulesPath, ... }:
{
imports = [ "${modulesPath}/profiles/perlless.nix" ];
boot.kernelPackages = pkgs.linuxPackages_latest;
virtualisation.mountHostNixStore = false;
virtualisation.useNixStoreImage = true;
};
testScript = ''
perl_store_paths = machine.succeed("ls /nix/store | grep perl || true")
print(perl_store_paths)
assert len(perl_store_paths) == 0
'';
}

View File

@@ -0,0 +1,18 @@
{ lib, ... }:
{
name = "activation-var";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine = { };
testScript = ''
assert machine.succeed("stat -c '%a' /var/tmp") == "1777\n"
assert machine.succeed("stat -c '%a' /var/empty") == "555\n"
assert machine.succeed("stat -c '%U' /var/empty") == "root\n"
assert machine.succeed("stat -c '%G' /var/empty") == "root\n"
assert "i" in machine.succeed("lsattr -d /var/empty")
'';
}

16
nixos/tests/actual.nix Normal file
View File

@@ -0,0 +1,16 @@
{ lib, ... }:
{
name = "actual";
meta.maintainers = [ lib.maintainers.oddlama ];
nodes.machine =
{ ... }:
{
services.actual.enable = true;
};
testScript = ''
machine.wait_for_open_port(3000)
machine.succeed("curl -fvvv -Ls http://localhost:3000/ | grep 'Actual'")
'';
}

148
nixos/tests/adguardhome.nix Normal file
View File

@@ -0,0 +1,148 @@
{
name = "adguardhome";
nodes = {
nullConf = {
services.adguardhome.enable = true;
};
emptyConf = {
services.adguardhome = {
enable = true;
settings = { };
};
};
schemaVersionBefore23 = {
services.adguardhome = {
enable = true;
settings.schema_version = 20;
};
};
declarativeConf = {
services.adguardhome = {
enable = true;
mutableSettings = false;
settings.dns.bootstrap_dns = [ "127.0.0.1" ];
};
};
mixedConf = {
services.adguardhome = {
enable = true;
mutableSettings = true;
settings.dns.bootstrap_dns = [ "127.0.0.1" ];
};
};
dhcpConf =
{ lib, ... }:
{
virtualisation.vlans = [ 1 ];
networking = {
# Configure static IP for DHCP server
useDHCP = false;
interfaces."eth1" = lib.mkForce {
useDHCP = false;
ipv4 = {
addresses = [
{
address = "10.0.10.1";
prefixLength = 24;
}
];
routes = [
{
address = "10.0.10.0";
prefixLength = 24;
}
];
};
};
# Required for DHCP
firewall.allowedUDPPorts = [
67
68
];
};
services.adguardhome = {
enable = true;
allowDHCP = true;
mutableSettings = false;
settings = {
dns.bootstrap_dns = [ "127.0.0.1" ];
dhcp = {
# This implicitly enables CAP_NET_RAW
enabled = true;
interface_name = "eth1";
local_domain_name = "lan";
dhcpv4 = {
gateway_ip = "10.0.10.1";
range_start = "10.0.10.100";
range_end = "10.0.10.101";
subnet_mask = "255.255.255.0";
};
};
};
};
};
client =
{ lib, ... }:
{
virtualisation.vlans = [ 1 ];
networking = {
interfaces.eth1 = {
useDHCP = true;
ipv4.addresses = lib.mkForce [ ];
};
};
};
};
testScript = ''
with subtest("Minimal (settings = null) config test"):
nullConf.wait_for_unit("adguardhome.service")
nullConf.wait_for_open_port(3000)
with subtest("Default config test"):
emptyConf.wait_for_unit("adguardhome.service")
emptyConf.wait_for_open_port(3000)
with subtest("Default schema_version 23 config test"):
schemaVersionBefore23.wait_for_unit("adguardhome.service")
schemaVersionBefore23.wait_for_open_port(3000)
with subtest("Declarative config test, DNS will be reachable"):
declarativeConf.wait_for_unit("adguardhome.service")
declarativeConf.wait_for_open_port(53)
declarativeConf.wait_for_open_port(3000)
with subtest("Mixed config test, check whether merging works"):
mixedConf.wait_for_unit("adguardhome.service")
mixedConf.wait_for_open_port(53)
mixedConf.wait_for_open_port(3000)
# Test whether merging works properly, even if nothing is changed
mixedConf.systemctl("restart adguardhome.service")
mixedConf.wait_for_unit("adguardhome.service")
mixedConf.wait_for_open_port(3000)
with subtest("Testing successful DHCP start"):
dhcpConf.wait_for_unit("adguardhome.service")
client.systemctl("start network-online.target")
client.wait_for_unit("network-online.target")
# Test IP assignment via DHCP
dhcpConf.wait_until_succeeds("ping -c 5 10.0.10.100")
# Test hostname resolution over DHCP-provided DNS
dhcpConf.wait_until_succeeds("ping -c 5 client.lan")
'';
}

111
nixos/tests/aesmd.nix Normal file
View File

@@ -0,0 +1,111 @@
{ pkgs, lib, ... }:
{
name = "aesmd";
meta = {
maintainers = with lib.maintainers; [
trundle
veehaitch
];
};
nodes.machine =
{ lib, ... }:
{
services.aesmd = {
enable = true;
settings = {
defaultQuotingType = "ecdsa_256";
proxyType = "direct";
whitelistUrl = "http://nixos.org";
};
};
# Should have access to the AESM socket
users.users."sgxtest" = {
isNormalUser = true;
extraGroups = [ "sgx" ];
};
# Should NOT have access to the AESM socket
users.users."nosgxtest".isNormalUser = true;
# We don't have a real SGX machine in NixOS tests
systemd.services.aesmd.unitConfig.AssertPathExists = lib.mkForce [ ];
specialisation = {
withQuoteProvider.configuration =
{ ... }:
{
services.aesmd = {
quoteProviderLibrary = pkgs.sgx-azure-dcap-client;
environment = {
AZDCAP_DEBUG_LOG_LEVEL = "INFO";
};
};
};
};
};
testScript =
{ nodes, ... }:
let
specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
in
''
def get_aesmd_pid():
status, main_pid = machine.systemctl("show --property MainPID --value aesmd.service")
assert status == 0, "Could not get MainPID of aesmd.service"
return main_pid.strip()
with subtest("aesmd.service starts"):
machine.wait_for_unit("aesmd.service")
main_pid = get_aesmd_pid()
with subtest("aesmd.service runtime directory permissions"):
runtime_dir = "/run/aesmd";
res = machine.succeed(f"stat -c '%a %U %G' {runtime_dir}").strip()
assert "750 aesmd sgx" == res, f"{runtime_dir} does not have the expected permissions: {res}"
with subtest("aesm.socket available on host"):
socket_path = "/var/run/aesmd/aesm.socket"
machine.wait_until_succeeds(f"test -S {socket_path}")
machine.succeed(f"test 777 -eq $(stat -c '%a' {socket_path})")
for op in [ "-r", "-w", "-x" ]:
machine.succeed(f"sudo -u sgxtest test {op} {socket_path}")
machine.fail(f"sudo -u nosgxtest test {op} {socket_path}")
with subtest("Copies white_list_cert_to_be_verify.bin"):
whitelist_path = "/var/opt/aesmd/data/white_list_cert_to_be_verify.bin"
whitelist_perms = machine.succeed(
f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/stat -c '%a' {whitelist_path}"
).strip()
assert "644" == whitelist_perms, f"white_list_cert_to_be_verify.bin has permissions {whitelist_perms}"
with subtest("Writes and binds aesm.conf in service namespace"):
aesmd_config = machine.succeed(f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/cat /etc/aesmd.conf")
assert aesmd_config == "whitelist url = http://nixos.org\nproxy type = direct\ndefault quoting type = ecdsa_256\n", "aesmd.conf differs"
with subtest("aesmd.service without quote provider library has correct LD_LIBRARY_PATH"):
status, environment = machine.systemctl("show --property Environment --value aesmd.service")
assert status == 0, "Could not get Environment of aesmd.service"
env_by_name = dict(entry.split("=", 1) for entry in environment.split())
assert not env_by_name["LD_LIBRARY_PATH"], "LD_LIBRARY_PATH is not empty"
with subtest("aesmd.service with quote provider library starts"):
machine.succeed('${specialisations}/withQuoteProvider/bin/switch-to-configuration test')
machine.wait_for_unit("aesmd.service")
main_pid = get_aesmd_pid()
with subtest("aesmd.service with quote provider library has correct LD_LIBRARY_PATH"):
ld_library_path = machine.succeed(f"xargs -0 -L1 -a /proc/{main_pid}/environ | grep LD_LIBRARY_PATH")
assert ld_library_path.startswith("LD_LIBRARY_PATH=${pkgs.sgx-azure-dcap-client}/lib:"), \
"LD_LIBRARY_PATH is not set to the configured quote provider library"
with subtest("aesmd.service with quote provider library has set AZDCAP_DEBUG_LOG_LEVEL"):
azdcp_debug_log_level = machine.succeed(f"xargs -0 -L1 -a /proc/{main_pid}/environ | grep AZDCAP_DEBUG_LOG_LEVEL")
assert azdcp_debug_log_level == "AZDCAP_DEBUG_LOG_LEVEL=INFO\n", "AZDCAP_DEBUG_LOG_LEVEL is not set to INFO"
'';
}

47
nixos/tests/agda.nix Normal file
View File

@@ -0,0 +1,47 @@
{ pkgs, ... }:
let
hello-world = pkgs.writeText "hello-world" ''
{-# OPTIONS --guardedness #-}
open import IO
open import Level
main = run {0} (putStrLn "Hello World!")
'';
in
{
name = "agda";
meta = with pkgs.lib.maintainers; {
maintainers = [
alexarice
turion
];
};
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = [
(pkgs.agda.withPackages {
pkgs = p: [ p.standard-library ];
})
];
virtualisation.memorySize = 2000; # Agda uses a lot of memory
};
testScript = ''
# Minimal script that typechecks
machine.succeed("touch TestEmpty.agda")
machine.succeed("agda TestEmpty.agda")
# Hello world
machine.succeed(
"cp ${hello-world} HelloWorld.agda"
)
machine.succeed("agda -l standard-library -i . -c HelloWorld.agda")
# Check execution
assert "Hello World!" in machine.succeed(
"./HelloWorld"
), "HelloWorld does not run properly"
'';
}

View File

@@ -0,0 +1,33 @@
{ pkgs, lib, ... }:
{
name = "age-plugin-tpm-decrypt";
meta = with lib.maintainers; {
maintainers = [
sgo
josh
];
};
nodes.machine =
{ pkgs, ... }:
{
virtualisation.tpm.enable = true;
environment.systemPackages = with pkgs; [
age
age-plugin-tpm
];
};
testScript = ''
machine.start()
machine.succeed("age-plugin-tpm --generate --output identity.txt")
machine.succeed("age-plugin-tpm --convert identity.txt --output recipient.txt")
machine.succeed("echo -n 'Hello World' >data.txt")
machine.succeed("age --encrypt --recipients-file recipient.txt --output data.age data.txt")
data = machine.succeed("age --decrypt --identity identity.txt data.age")
assert data == "Hello World"
'';
}

209
nixos/tests/agnos.nix Normal file
View File

@@ -0,0 +1,209 @@
{
system ? builtins.currentSystem,
pkgs ? import ../.. { inherit system; },
lib ? pkgs.lib,
}:
let
inherit (import ../lib/testing-python.nix { inherit system pkgs; }) makeTest;
nodeIP = n: n.networking.primaryIPAddress;
dnsZone =
nodes:
pkgs.writeText "agnos.test.zone" ''
$TTL 604800
@ IN SOA ns1.agnos.test. root.agnos.test. (
3 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
; name servers - NS records
IN NS ns1.agnos.test.
; name servers - A records
ns1.agnos.test. IN A ${nodeIP nodes.dnsserver}
agnos-ns.agnos.test. IN A ${nodeIP nodes.server}
_acme-challenge.a.agnos.test. IN NS agnos-ns.agnos.test.
_acme-challenge.b.agnos.test. IN NS agnos-ns.agnos.test.
_acme-challenge.c.agnos.test. IN NS agnos-ns.agnos.test.
_acme-challenge.d.agnos.test. IN NS agnos-ns.agnos.test.
'';
mkTest =
{
name,
extraServerConfig ? { },
checkFirewallClosed ? true,
}:
makeTest {
inherit name;
meta = {
maintainers = with lib.maintainers; [ justinas ];
};
nodes = {
# The fake ACME server which will respond to client requests
acme =
{ nodes, pkgs, ... }:
{
imports = [ ./common/acme/server ];
environment.systemPackages = [ pkgs.netcat ];
networking.nameservers = lib.mkForce [ (nodeIP nodes.dnsserver) ];
};
# A fake DNS server which points _acme-challenge subdomains to "server"
dnsserver =
{ nodes, ... }:
{
networking.firewall.allowedTCPPorts = [ 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
services.bind = {
cacheNetworks = [ "192.168.1.0/24" ];
enable = true;
extraOptions = ''
dnssec-validation no;
'';
zones."agnos.test" = {
file = dnsZone nodes;
master = true;
};
};
};
# The server using agnos to request certificates
server =
{ nodes, ... }:
{
imports = [ extraServerConfig ];
networking.extraHosts = ''
${nodeIP nodes.acme} acme.test
'';
security.agnos = {
enable = true;
generateKeys.enable = true;
persistent = false;
server = "https://acme.test/dir";
serverCa = ./common/acme/server/ca.cert.pem;
temporarilyOpenFirewall = true;
settings.accounts = [
{
email = "webmaster@agnos.test";
# account with an existing private key
private_key_path = "${./common/acme/server/acme.test.key.pem}";
certificates = [
{
domains = [ "a.agnos.test" ];
# Absolute paths
fullchain_output_file = "/tmp/a.agnos.test.crt";
key_output_file = "/tmp/a.agnos.test.key";
}
{
domains = [
"b.agnos.test"
"*.b.agnos.test"
];
# Relative paths
fullchain_output_file = "b.agnos.test.crt";
key_output_file = "b.agnos.test.key";
}
];
}
{
email = "webmaster2@agnos.test";
# account with a missing private key, should get generated
private_key_path = "webmaster2.key";
certificates = [
{
domains = [ "c.agnos.test" ];
# Absolute paths
fullchain_output_file = "/tmp/c.agnos.test.crt";
key_output_file = "/tmp/c.agnos.test.key";
}
{
domains = [
"d.agnos.test"
"*.d.agnos.test"
];
# Relative paths
fullchain_output_file = "d.agnos.test.crt";
key_output_file = "d.agnos.test.key";
}
];
}
];
};
};
};
testScript = ''
def check_firewall_closed(caller):
"""
Check that TCP port 53 is closed again.
Since we do not set `networking.firewall.rejectPackets`,
"timed out" indicates a closed port,
while "connection refused" (after agnos has shut down) indicates an open port.
"""
out = caller.fail("nc -v -z -w 1 server 53 2>&1")
assert "Connection timed out" in out
start_all()
acme.wait_for_unit('pebble.service')
server.wait_for_unit('default.target')
# Test that agnos.timer is scheduled
server.succeed("systemctl status agnos.timer")
server.succeed('systemctl start agnos.service')
expected_perms = "640 agnos agnos"
outputs = [
"/tmp/a.agnos.test.crt",
"/tmp/a.agnos.test.key",
"/var/lib/agnos/b.agnos.test.crt",
"/var/lib/agnos/b.agnos.test.key",
"/var/lib/agnos/webmaster2.key",
"/tmp/c.agnos.test.crt",
"/tmp/c.agnos.test.key",
"/var/lib/agnos/d.agnos.test.crt",
"/var/lib/agnos/d.agnos.test.key",
]
for o in outputs:
out = server.succeed(f"stat -c '%a %U %G' {o}").strip()
assert out == expected_perms, \
f"Expected mode/owner/group to be '{expected_perms}', but it was '{out}'"
${lib.optionalString checkFirewallClosed "check_firewall_closed(acme)"}
'';
};
in
{
iptables = mkTest {
name = "iptables";
};
nftables = mkTest {
name = "nftables";
extraServerConfig = {
networking.nftables.enable = true;
};
};
no-firewall = mkTest {
name = "no-firewall";
extraServerConfig = {
networking.firewall.enable = lib.mkForce false;
security.agnos.temporarilyOpenFirewall = lib.mkForce false;
};
checkFirewallClosed = false;
};
}

30
nixos/tests/airsonic.nix Normal file
View File

@@ -0,0 +1,30 @@
{ pkgs, ... }:
{
name = "airsonic";
meta = with pkgs.lib.maintainers; {
maintainers = [ sumnerevans ];
};
nodes.machine =
{ pkgs, ... }:
{
services.airsonic = {
enable = true;
maxMemory = 800;
};
};
testScript = ''
def airsonic_is_up(_) -> bool:
status, _ = machine.execute("curl --fail http://localhost:4040/login")
return status == 0
machine.start()
machine.wait_for_unit("airsonic.service")
machine.wait_for_open_port(4040)
with machine.nested("Waiting for UI to work"):
retry(airsonic_is_up)
'';
}

245
nixos/tests/akkoma.nix Normal file
View File

@@ -0,0 +1,245 @@
# endtoend test for Akkoma
{
lib,
pkgs,
confined ? false,
...
}:
let
inherit ((pkgs.formats.elixirConf { }).lib) mkRaw;
package = pkgs.akkoma;
tlsCert =
names:
pkgs.runCommand "certificates-${lib.head names}"
{
nativeBuildInputs = with pkgs; [ openssl ];
}
''
mkdir -p $out
openssl req -x509 \
-subj '/CN=${lib.head names}/' -days 49710 \
-addext 'subjectAltName = ${lib.concatStringsSep ", " (map (name: "DNS:${name}") names)}' \
-keyout "$out/key.pem" -newkey ed25519 \
-out "$out/cert.pem" -noenc
'';
tlsCertA = tlsCert [
"akkoma-a.nixos.test"
"media.akkoma-a.nixos.test"
];
tlsCertB = tlsCert [
"akkoma-b.nixos.test"
"media.akkoma-b.nixos.test"
];
testMedia = pkgs.runCommand "blank.png" { nativeBuildInputs = with pkgs; [ imagemagick ]; } ''
magick -size 640x480 canvas:transparent "PNG8:$out"
'';
checkFe = pkgs.writeShellApplication {
name = "checkFe";
runtimeInputs = with pkgs; [ curl ];
text = ''
paths=( / /static/{config,styles}.json /pleroma/admin/ )
for path in "''${paths[@]}"; do
diff \
<(curl -f -S -s -o /dev/null -w '%{response_code}' "https://$1$path") \
<(echo -n 200)
done
'';
};
commonConfig =
{ nodes, ... }:
{
security.pki.certificateFiles = [
"${tlsCertA}/cert.pem"
"${tlsCertB}/cert.pem"
];
networking.extraHosts = ''
${nodes.akkoma-a.networking.primaryIPAddress} akkoma-a.nixos.test media.akkoma-a.nixos.test
${nodes.akkoma-b.networking.primaryIPAddress} akkoma-b.nixos.test media.akkoma-b.nixos.test
${nodes.client-a.networking.primaryIPAddress} client-a.nixos.test
${nodes.client-b.networking.primaryIPAddress} client-b.nixos.test
'';
};
clientConfig =
{ pkgs, ... }:
{
environment = {
sessionVariables = {
REQUESTS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
};
systemPackages = with pkgs; [ toot ];
};
};
serverConfig =
{ config, pkgs, ... }:
{
networking = {
domain = "nixos.test";
firewall.allowedTCPPorts = [ 443 ];
};
systemd.services.akkoma.confinement.enable = confined;
services.akkoma = {
enable = true;
inherit package;
config = {
":pleroma" = {
":instance" = {
name = "NixOS test Akkoma server";
description = "NixOS test Akkoma server";
email = "akkoma@nixos.test";
notify_email = "akkoma@nixos.test";
registration_open = true;
};
":media_proxy" = {
enabled = false;
};
"Pleroma.Web.Endpoint" = {
url.host = config.networking.fqdn;
};
"Pleroma.Upload" = {
base_url = "https://media.${config.networking.fqdn}/media/";
};
# disable certificate verification until we figure out how to
# supply our own certificates
":http".adapter.pools = mkRaw "%{default: [conn_opts: [transport_opts: [verify: :verify_none]]]}";
};
};
nginx.addSSL = true;
};
services.nginx.enable = true;
services.postgresql.enable = true;
};
in
{
name = "akkoma";
nodes = {
client-a =
{ ... }:
{
imports = [
clientConfig
commonConfig
];
};
client-b =
{ ... }:
{
imports = [
clientConfig
commonConfig
];
};
akkoma-a =
{ ... }:
{
imports = [
commonConfig
serverConfig
];
services.akkoma.nginx = {
sslCertificate = "${tlsCertA}/cert.pem";
sslCertificateKey = "${tlsCertA}/key.pem";
};
};
akkoma-b =
{ ... }:
{
imports = [
commonConfig
serverConfig
];
services.akkoma.nginx = {
sslCertificate = "${tlsCertB}/cert.pem";
sslCertificateKey = "${tlsCertB}/key.pem";
};
};
};
testScript = ''
import json
import random
import string
from shlex import quote
def randomString(len):
return "".join(random.choice(string.ascii_letters + string.digits) for _ in range(len))
def registerUser(user, password):
return 'pleroma_ctl user new {0} {0}@nixos.test --password {1} -y'.format(
quote(user), quote(password))
def loginUser(instance, user, password):
return 'toot login_cli -i {}.nixos.test -e {}@nixos.test -p {}'.format(
quote(instance), quote(user), quote(password))
userAName = randomString(11)
userBName = randomString(11)
userAPassword = randomString(22)
userBPassword = randomString(22)
testMessage = randomString(22)
testMedia = '${testMedia}'
start_all()
akkoma_a.wait_for_unit('akkoma-initdb.service')
akkoma_b.wait_for_unit('akkoma-initdb.service')
# test repeated initialisation
akkoma_a.systemctl('restart akkoma-initdb.service')
akkoma_a.wait_for_unit('akkoma.service')
akkoma_b.wait_for_unit('akkoma.service')
akkoma_a.wait_for_file('/run/akkoma/socket');
akkoma_b.wait_for_file('/run/akkoma/socket');
akkoma_a.succeed(registerUser(userAName, userAPassword))
akkoma_b.succeed(registerUser(userBName, userBPassword))
akkoma_a.wait_for_unit('nginx.service')
akkoma_b.wait_for_unit('nginx.service')
client_a.succeed(loginUser('akkoma-a', userAName, userAPassword))
client_b.succeed(loginUser('akkoma-b', userBName, userBPassword))
client_b.succeed('toot follow {}@akkoma-a.nixos.test'.format(userAName))
client_a.wait_until_succeeds('toot followers | grep -F -q {}'.format(quote(userBName)))
client_a.succeed('toot post {} --media {} --description "nothing to see here"'.format(
quote(testMessage), quote(testMedia)))
# verify test message
status = json.loads(client_b.wait_until_succeeds(
'toot status --json "$(toot timeline -1 | grep -E -o \'^ID [^ ]+\' | cut -d \' \' -f 2)"'))
assert status['content'] == testMessage
# compare attachment to original
client_b.succeed('cmp {} <(curl -f -S -s {})'.format(quote(testMedia),
quote(status['media_attachments'][0]['url'])))
client_a.succeed('${lib.getExe checkFe} akkoma-a.nixos.test')
client_b.succeed('${lib.getExe checkFe} akkoma-b.nixos.test')
'';
}

40
nixos/tests/alice-lg.nix Normal file
View File

@@ -0,0 +1,40 @@
# This test does a basic functionality check for alice-lg
{
pkgs,
...
}:
{
name = "alice-lg";
nodes = {
host1 = {
environment.systemPackages = with pkgs; [ jq ];
services.alice-lg = {
enable = true;
settings = {
server = {
listen_http = "[::]:7340";
enable_prefix_lookup = true;
asn = 1;
routes_store_refresh_parallelism = 5;
neighbors_store_refresh_parallelism = 10000;
routes_store_refresh_interval = 5;
neighbors_store_refresh_interval = 5;
};
housekeeping = {
interval = 5;
force_release_memory = true;
};
};
};
};
};
testScript = ''
start_all()
host1.wait_for_unit("alice-lg.service")
host1.wait_for_open_port(7340)
host1.succeed("curl http://[::]:7340 | grep 'Alice BGP Looking Glass'")
'';
}

View File

@@ -0,0 +1,52 @@
{ pkgs, ... }:
{
name = "all-terminfo";
meta = with pkgs.lib.maintainers; {
maintainers = [ jkarlson ];
};
nodes.machine =
{
pkgs,
config,
lib,
...
}:
let
# Use derivations instead of attr names to avoid listing missing packages
maskedTerminfos = with pkgs; [
alacritty-graphics # would clobber alacritty terminfo
];
infoFilter =
name: drv:
let
o = builtins.tryEval drv;
in
o.success
&& lib.isDerivation o.value
&& o.value ? outputs
&& builtins.elem "terminfo" o.value.outputs
&& !o.value.meta.broken
&& lib.meta.availableOn pkgs.stdenv.hostPlatform o.value
&& !(builtins.elem o.value maskedTerminfos);
terminfos = lib.filterAttrs infoFilter pkgs;
excludedTerminfos = lib.filterAttrs (
_: drv: !(builtins.elem drv.terminfo config.environment.systemPackages)
) terminfos;
includedOuts = lib.filterAttrs (
_: drv: builtins.elem drv.out config.environment.systemPackages
) terminfos;
in
{
environment = {
enableAllTerminfo = true;
etc."terminfo-missing".text = builtins.concatStringsSep "\n" (builtins.attrNames excludedTerminfos);
etc."terminfo-extra-outs".text = builtins.concatStringsSep "\n" (builtins.attrNames includedOuts);
};
};
testScript = ''
machine.fail("grep . /etc/terminfo-missing >&2")
machine.fail("grep . /etc/terminfo-extra-outs >&2")
'';
}

1659
nixos/tests/all-tests.nix Normal file

File diff suppressed because it is too large Load Diff

35
nixos/tests/alloy.nix Normal file
View File

@@ -0,0 +1,35 @@
{ lib, ... }:
let
nodes = {
machine = {
services.alloy = {
enable = true;
};
environment.etc."alloy/config.alloy".text = "";
};
};
in
{
name = "alloy";
meta = with lib.maintainers; {
maintainers = [
flokli
hbjydev
];
};
inherit nodes;
testScript = ''
start_all()
machine.wait_for_unit("alloy.service")
machine.wait_for_open_port(12345)
machine.succeed(
"curl -sSfN http://127.0.0.1:12345/-/healthy"
)
machine.shutdown()
'';
}

122
nixos/tests/alps.nix Normal file
View File

@@ -0,0 +1,122 @@
let
certs = import ./common/acme/server/snakeoil-certs.nix;
domain = certs.domain;
in
{ pkgs, ... }:
{
name = "alps";
meta = with pkgs.lib.maintainers; {
maintainers = [ hmenke ];
};
nodes = {
server = {
imports = [ ./common/user-account.nix ];
security.pki.certificateFiles = [
certs.ca.cert
];
networking.extraHosts = ''
127.0.0.1 ${domain}
'';
networking.firewall.allowedTCPPorts = [
25
465
993
];
services.postfix = {
enable = true;
enableSubmission = true;
enableSubmissions = true;
settings.main = {
smtp_tls_CAfile = "${certs.ca.cert}";
smtpd_tls_chain_files = [
"${certs.${domain}.key}"
"${certs.${domain}.cert}"
];
};
};
services.dovecot2 = {
enable = true;
enableImap = true;
sslCACert = "${certs.ca.cert}";
sslServerCert = "${certs.${domain}.cert}";
sslServerKey = "${certs.${domain}.key}";
};
};
client =
{ nodes, config, ... }:
{
security.pki.certificateFiles = [
certs.ca.cert
];
networking.extraHosts = ''
${nodes.server.config.networking.primaryIPAddress} ${domain}
'';
services.alps = {
enable = true;
theme = "alps";
imaps = {
host = domain;
port = 993;
};
smtps = {
host = domain;
port = 465;
};
};
environment.systemPackages = [
(pkgs.writers.writePython3Bin "test-alps-login" { } ''
from urllib.request import build_opener, HTTPCookieProcessor, Request
from urllib.parse import urlencode, urljoin
from http.cookiejar import CookieJar
baseurl = "http://localhost:${toString config.services.alps.port}"
username = "alice"
password = "${nodes.server.config.users.users.alice.password}"
cookiejar = CookieJar()
cookieprocessor = HTTPCookieProcessor(cookiejar)
opener = build_opener(cookieprocessor)
data = urlencode({"username": username, "password": password}).encode()
req = Request(urljoin(baseurl, "login"), data=data, method="POST")
with opener.open(req) as ret:
# Check that the alps_session cookie is set
print(cookiejar)
assert any(cookie.name == "alps_session" for cookie in cookiejar)
req = Request(baseurl)
with opener.open(req) as ret:
# Check that the alps_session cookie is still there...
print(cookiejar)
assert any(cookie.name == "alps_session" for cookie in cookiejar)
# ...and that we have not been redirected back to the login page
print(ret.url)
assert ret.url == urljoin(baseurl, "mailbox/INBOX")
req = Request(urljoin(baseurl, "logout"))
with opener.open(req) as ret:
# Check that the alps_session cookie is now gone
print(cookiejar)
assert all(cookie.name != "alps_session" for cookie in cookiejar)
'')
];
};
};
testScript =
{ nodes, ... }:
''
server.start()
server.wait_for_unit("postfix.service")
server.wait_for_unit("dovecot2.service")
server.wait_for_open_port(465)
server.wait_for_open_port(993)
client.start()
client.wait_for_unit("alps.service")
client.wait_for_open_port(${toString nodes.client.config.services.alps.port})
client.succeed("test-alps-login")
'';
}

View File

@@ -0,0 +1,90 @@
{ pkgs, ... }:
let
# See https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html.
iniFormat = pkgs.formats.ini { };
region = "ap-northeast-1";
sharedConfigurationDefaultProfile = "default";
sharedConfigurationFile = iniFormat.generate "config" {
"${sharedConfigurationDefaultProfile}" = {
region = region;
};
};
sharedCredentialsFile = iniFormat.generate "credentials" {
"${sharedConfigurationDefaultProfile}" = {
aws_access_key_id = "placeholder";
aws_secret_access_key = "placeholder";
aws_session_token = "placeholder";
};
};
sharedConfigurationDirectory = pkgs.runCommand ".aws" { } ''
mkdir $out
cp ${sharedConfigurationFile} $out/config
cp ${sharedCredentialsFile} $out/credentials
'';
in
{
name = "amazon-cloudwatch-agent";
nodes.machine =
{ config, pkgs, ... }:
{
services.amazon-cloudwatch-agent = {
enable = true;
commonConfiguration = {
credentials = {
shared_credential_profile = sharedConfigurationDefaultProfile;
shared_credential_file = "${sharedConfigurationDirectory}/credentials";
};
};
configuration = {
agent = {
# Required despite documentation saying the agent ignores it in "onPremise" mode.
region = region;
# Show debug logs and write to a file for interactive debugging.
debug = true;
logfile = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log";
};
logs = {
logs_collected = {
files = {
collect_list = [
{
file_path = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log";
log_group_name = "/var/log/amazon-cloudwatch-agent/amazon-cloudwatch-agent.log";
log_stream_name = "{local_hostname}";
}
];
};
};
};
traces = {
local_mode = true;
traces_collected = {
xray = { };
};
};
};
mode = "onPremise";
};
# Keep the runtime directory for interactive debugging.
systemd.services.amazon-cloudwatch-agent.serviceConfig.RuntimeDirectoryPreserve = true;
};
testScript = ''
start_all()
machine.wait_for_unit("amazon-cloudwatch-agent.service")
machine.wait_for_file("/run/amazon-cloudwatch-agent/amazon-cloudwatch-agent.pid")
machine.wait_for_file("/run/amazon-cloudwatch-agent/amazon-cloudwatch-agent.toml")
# "config-translator" omits this file if no trace configurations are specified.
#
# See https://github.com/aws/amazon-cloudwatch-agent/issues/1320.
machine.wait_for_file("/run/amazon-cloudwatch-agent/amazon-cloudwatch-agent.yaml")
machine.wait_for_file("/run/amazon-cloudwatch-agent/env-config.json")
'';
}

View File

@@ -0,0 +1,44 @@
# This test verifies that the amazon-init service can treat the `user-data` ec2
# metadata file as a shell script. If amazon-init detects that `user-data` is a
# script (based on the presence of the shebang #! line) it executes it and
# exits.
# Note that other tests verify that amazon-init can treat user-data as a nixos
# configuration expression.
{
lib,
...
}:
{
name = "amazon-init";
meta = with lib.maintainers; {
maintainers = [ urbas ];
};
nodes.machine = {
imports = [
../modules/profiles/headless.nix
../modules/virtualisation/amazon-init.nix
];
services.openssh.enable = true;
system.switch.enable = true;
networking.hostName = "";
environment.etc."ec2-metadata/user-data" = {
text = ''
#!/usr/bin/bash
echo successful > /tmp/evidence
# Emulate running nixos-rebuild switch, just without any building.
# https://github.com/nixos/nixpkgs/blob/4c62505847d88f16df11eff3c81bf9a453a4979e/nixos/modules/virtualisation/amazon-init.nix#L55
/run/current-system/bin/switch-to-configuration test
'';
};
};
testScript = ''
# To wait until amazon-init terminates its run
unnamed.wait_for_unit("amazon-init.service")
unnamed.succeed("grep -q successful /tmp/evidence")
'';
}

View File

@@ -0,0 +1,18 @@
{ lib, ... }:
{
name = "amazon-ssm-agent";
meta.maintainers = [ lib.maintainers.anthonyroussel ];
nodes.machine = {
services.amazon-ssm-agent.enable = true;
};
testScript = ''
start_all()
machine.wait_for_file("/etc/amazon/ssm/seelog.xml")
machine.wait_for_file("/etc/amazon/ssm/amazon-ssm-agent.json")
machine.wait_for_unit("amazon-ssm-agent.service")
'';
}

63
nixos/tests/amd-sev.nix Normal file
View File

@@ -0,0 +1,63 @@
{ lib, ... }:
{
name = "amd-sev";
meta = {
maintainers = with lib.maintainers; [
trundle
veehaitch
];
};
nodes.machine =
{ lib, ... }:
{
hardware.cpu.amd.sev.enable = true;
hardware.cpu.amd.sevGuest.enable = true;
specialisation.sevCustomUserGroup.configuration = {
users.groups.sevtest = { };
hardware.cpu.amd.sev = {
enable = true;
group = "root";
mode = "0600";
};
hardware.cpu.amd.sevGuest = {
enable = true;
group = "sevtest";
};
};
};
testScript =
{ nodes, ... }:
let
specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
in
''
machine.wait_for_unit("multi-user.target")
with subtest("Check default settings"):
out = machine.succeed("cat /etc/udev/rules.d/99-local.rules")
assert 'KERNEL=="sev", OWNER="root", GROUP="sev", MODE="0660"' in out
assert 'KERNEL=="sev-guest", OWNER="root", GROUP="sev-guest", MODE="0660"' in out
out = machine.succeed("cat /etc/group")
assert "sev:" in out
assert "sev-guest:" in out
assert "sevtest:" not in out
with subtest("Activate configuration with custom user/group"):
machine.succeed('${specialisations}/sevCustomUserGroup/bin/switch-to-configuration test')
with subtest("Check custom user and group"):
out = machine.succeed("cat /etc/udev/rules.d/99-local.rules")
assert 'KERNEL=="sev", OWNER="root", GROUP="root", MODE="0600"' in out
assert 'KERNEL=="sev-guest", OWNER="root", GROUP="sevtest", MODE="0660"' in out
out = machine.succeed("cat /etc/group")
assert "sev:" not in out
assert "sev-guest:" not in out
assert "sevtest:" in out
'';
}

View File

@@ -0,0 +1,40 @@
{ pkgs, lib, ... }:
let
# Example Android app
demoApp = pkgs.fetchurl {
url = "https://gitlab.com/android_translation_layer/atl_test_apks/-/raw/061e32a3172c8167b1746768d098f0e62d8f564b/demo_app.apk";
hash = "sha256-aXxLZEAMNsL6nL4r2N9rVsbBPmf3+gFGmgo3kZjdo4s=";
};
in
{
name = "android-translation-layer";
meta.maintainers = with pkgs.lib.maintainers; [ onny ];
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
./common/x11.nix
];
services.xserver.enable = true;
environment = {
systemPackages = [ pkgs.android-translation-layer ];
};
};
enableOCR = true;
testScript = ''
machine.wait_for_x()
with subtest("launch android translation layer demo app"):
machine.succeed("android-translation-layer ${demoApp} >&2 &")
machine.sleep(10)
machine.wait_for_text(r"hello PoC world!")
machine.screenshot("atl_demo_app")
machine.succeed("pkill -f android-translation-layer")
'';
}

168
nixos/tests/angie-api.nix Normal file
View File

@@ -0,0 +1,168 @@
{ lib, pkgs, ... }:
let
hosts = ''
192.168.2.101 example.com
192.168.2.101 api.example.com
192.168.2.101 backend.example.com
'';
in
{
name = "angie-api";
meta.maintainers = with pkgs.lib.maintainers; [ izorkin ];
nodes = {
server =
{ pkgs, ... }:
{
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.101";
prefixLength = 24;
}
];
};
extraHosts = hosts;
firewall.allowedTCPPorts = [ 80 ];
};
services.nginx = {
enable = true;
package = pkgs.angie;
upstreams = {
"backend-http" = {
servers = {
"backend.example.com:8080" = {
fail_timeout = "0";
};
};
extraConfig = ''
zone upstream 256k;
'';
};
"backend-socket" = {
servers = {
"unix:/run/example.sock" = {
fail_timeout = "0";
};
};
extraConfig = ''
zone upstream 256k;
'';
};
};
virtualHosts."api.example.com" = {
locations."/console/" = {
extraConfig = ''
api /status/;
allow 192.168.2.201;
deny all;
'';
};
};
virtualHosts."example.com" = {
locations."/test/" = {
root = lib.mkForce (
pkgs.runCommandLocal "testdir" { } ''
mkdir -p "$out/test"
cat > "$out/test/index.html" <<EOF
<html><body>Hello World!</body></html>
EOF
''
);
extraConfig = ''
status_zone test_zone;
allow 192.168.2.201;
deny all;
'';
};
locations."/test/locked/" = {
extraConfig = ''
status_zone test_zone;
deny all;
'';
};
locations."/test/error/" = {
extraConfig = ''
status_zone test_zone;
allow all;
'';
};
locations."/upstream-http/" = {
proxyPass = "http://backend-http";
};
locations."/upstream-socket/" = {
proxyPass = "http://backend-socket";
};
};
};
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.jq ];
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.201";
prefixLength = 24;
}
];
};
extraHosts = hosts;
};
};
};
testScript = ''
start_all()
server.wait_for_unit("nginx")
server.wait_for_open_port(80)
# Check Angie version
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.angie.version' | grep '${pkgs.angie.version}'")
# Check access
client.succeed("curl --verbose --head http://api.example.com/console/ | grep 'HTTP/1.1 200'")
server.succeed("curl --verbose --head http://api.example.com/console/ | grep 'HTTP/1.1 403 Forbidden'")
# Check responses and requests
client.succeed("curl --verbose http://example.com/test/")
client.succeed("curl --verbose http://example.com/test/locked/")
client.succeed("curl --verbose http://example.com/test/locked/")
client.succeed("curl --verbose http://example.com/test/error/")
client.succeed("curl --verbose http://example.com/test/error/")
client.succeed("curl --verbose http://example.com/test/error/")
server.succeed("curl --verbose http://example.com/test/")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.location_zones.test_zone.responses.\"200\"' | grep '1'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.location_zones.test_zone.responses.\"403\"' | grep '3'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.location_zones.test_zone.responses.\"404\"' | grep '3'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.location_zones.test_zone.requests.total' | grep '7'")
# Check upstreams
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.upstreams.\"backend-http\".peers.\"192.168.2.101:8080\".state' | grep 'up'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.upstreams.\"backend-http\".peers.\"192.168.2.101:8080\".health.fails' | grep '0'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.upstreams.\"backend-socket\".peers.\"unix:/run/example.sock\".state' | grep 'up'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.upstreams.\"backend-socket\".peers.\"unix:/run/example.sock\".health.fails' | grep '0'")
client.succeed("curl --verbose http://example.com/upstream-http/")
client.succeed("curl --verbose http://example.com/upstream-socket/")
client.succeed("curl --verbose http://example.com/upstream-socket/")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.upstreams.\"backend-http\".peers.\"192.168.2.101:8080\".health.fails' | grep '1'")
client.succeed("curl --verbose http://api.example.com/console/ | jq -e '.http.upstreams.\"backend-socket\".peers.\"unix:/run/example.sock\".health.fails' | grep '2'")
server.shutdown()
client.shutdown()
'';
}

63
nixos/tests/angrr.nix Normal file
View File

@@ -0,0 +1,63 @@
{ ... }:
{
name = "angrr";
nodes = {
machine = {
services.angrr = {
enable = true;
period = "7d";
};
# `angrr.service` integrates to `nix-gc.service` by default
nix.gc.automatic = true;
# Create a normal nix user for test
users.users.normal.isNormalUser = true;
# For `nix build /run/current-system --out-link`,
# `nix-build` does not support this use case.
nix.settings.experimental-features = [ "nix-command" ];
};
};
testScript = ''
start_all()
machine.wait_for_unit("default.target")
machine.systemctl("stop nix-gc.timer")
# Creates some auto gc roots
# Use /run/current-system so that we do not need to build anything new
machine.succeed("nix build /run/current-system --out-link /tmp/root-auto-gc-root-1")
machine.succeed("nix build /run/current-system --out-link /tmp/root-auto-gc-root-2")
machine.succeed("su normal --command 'nix build /run/current-system --out-link /tmp/user-auto-gc-root-1'")
machine.succeed("su normal --command 'nix build /run/current-system --out-link /tmp/user-auto-gc-root-2'")
machine.systemctl("start nix-gc.service")
# Not auto gc root will be removed
machine.succeed("readlink /tmp/root-auto-gc-root-1")
machine.succeed("readlink /tmp/root-auto-gc-root-2")
machine.succeed("readlink /tmp/user-auto-gc-root-1")
machine.succeed("readlink /tmp/user-auto-gc-root-2")
# Change time to 8 days after (greater than 7d)
machine.succeed("date -s '8 days'")
# Touch GC roots `-2`
machine.succeed("touch /tmp/root-auto-gc-root-2 --no-dereference")
machine.succeed("touch /tmp/user-auto-gc-root-2 --no-dereference")
machine.systemctl("start nix-gc.service")
# Only GC roots `-1` are removed
machine.succeed("test ! -f /tmp/root-auto-gc-root-1")
machine.succeed("readlink /tmp/root-auto-gc-root-2")
machine.succeed("test ! -f /tmp/user-auto-gc-root-1")
machine.succeed("readlink /tmp/user-auto-gc-root-2")
# Change time again
machine.succeed("date -s '8 days'")
machine.systemctl("start nix-gc.service")
# All auto GC roots are removed
machine.succeed("test ! -f /tmp/root-auto-gc-root-2")
machine.succeed("test ! -f /tmp/user-auto-gc-root-2")
'';
}

View File

@@ -0,0 +1,71 @@
{ pkgs, ... }:
let
ankiSyncTest = pkgs.writeScript "anki-sync-test.py" ''
#!${pkgs.python3}/bin/python
import sys
# get site paths from anki itself
from runpy import run_path
run_path("${pkgs.anki}/bin/.anki-wrapped")
import anki
col = anki.collection.Collection('test_collection')
endpoint = 'http://localhost:27701'
# Sanity check: verify bad login fails
try:
col.sync_login('baduser', 'badpass', endpoint)
print("bad user login worked?!")
sys.exit(1)
except anki.errors.SyncError:
pass
# test logging in to users
col.sync_login('user', 'password', endpoint)
col.sync_login('passfileuser', 'passfilepassword', endpoint)
# Test actual sync. login apparently doesn't remember the endpoint...
login = col.sync_login('user', 'password', endpoint)
login.endpoint = endpoint
sync = col.sync_collection(login, False)
assert sync.required == sync.NO_CHANGES
# TODO: create an archive with server content including a test card
# and check we got it?
'';
testPasswordFile = pkgs.writeText "anki-password" "passfilepassword";
in
{
name = "anki-sync-server";
meta = with pkgs.lib.maintainers; {
maintainers = [ martinetd ];
};
nodes.machine = {
services.anki-sync-server = {
enable = true;
users = [
{
username = "user";
password = "password";
}
{
username = "passfileuser";
passwordFile = testPasswordFile;
}
];
};
};
testScript = ''
start_all()
with subtest("Server starts successfully"):
# service won't start without users
machine.wait_for_unit("anki-sync-server.service")
machine.wait_for_open_port(27701)
with subtest("Can sync"):
machine.succeed("${ankiSyncTest}")
'';
}

151
nixos/tests/anubis.nix Normal file
View File

@@ -0,0 +1,151 @@
{ lib, ... }:
{
name = "anubis";
meta.maintainers = with lib.maintainers; [
soopyc
nullcube
ryand56
];
nodes.machine =
{ config, pkgs, ... }:
{
services.anubis = {
defaultOptions = {
# Get default botPolicy
botPolicy = lib.importJSON "${config.services.anubis.package.src}/data/botPolicies.json";
settings = {
DIFFICULTY = 3;
USER_DEFINED_DEFAULT = true;
};
};
instances = {
"".settings = {
TARGET = "http://localhost:8080";
DIFFICULTY = 5;
USER_DEFINED_INSTANCE = true;
};
"tcp" = {
user = "anubis-tcp";
group = "anubis-tcp";
settings = {
TARGET = "http://localhost:8080";
BIND = ":9000";
BIND_NETWORK = "tcp";
METRICS_BIND = ":9001";
METRICS_BIND_NETWORK = "tcp";
};
};
"unix-upstream" = {
group = "nginx";
settings.TARGET = "unix:///run/nginx/nginx.sock";
};
"botPolicy-default" = {
botPolicy = null;
settings.TARGET = "http://localhost:8080";
};
"botPolicy-file" = {
settings = {
TARGET = "http://localhost:8080";
POLICY_FNAME = "/etc/anubis-botPolicy.json";
};
};
};
};
# Empty json for testing
environment.etc."anubis-botPolicy.json".text = lib.generators.toJSON { } {
bots = [
{
name = "allow-all";
user_agent_regex = ".*";
action = "ALLOW";
}
];
};
# support
users.users.nginx.extraGroups = [ config.services.anubis.defaultOptions.group ];
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."basic.localhost".locations = {
"/".proxyPass = "http://unix:${config.services.anubis.instances."".settings.BIND}";
"/metrics".proxyPass = "http://unix:${config.services.anubis.instances."".settings.METRICS_BIND}";
};
virtualHosts."tcp.localhost".locations = {
"/".proxyPass = "http://localhost:9000";
"/metrics".proxyPass = "http://localhost:9001";
};
virtualHosts."unix.localhost".locations = {
"/".proxyPass = "http://unix:${config.services.anubis.instances.unix-upstream.settings.BIND}";
};
# emulate an upstream with nginx, listening on tcp and unix sockets.
virtualHosts."upstream.localhost" = {
default = true; # make nginx match this vhost for `localhost`
listen = [
{ addr = "unix:/run/nginx/nginx.sock"; }
{
addr = "localhost";
port = 8080;
}
];
locations."/" = {
tryFiles = "$uri $uri/index.html =404";
root = pkgs.runCommand "anubis-test-upstream" { } ''
mkdir $out
echo "it works" >> $out/index.html
'';
};
};
};
};
testScript = ''
for unit in ["nginx", "anubis", "anubis-tcp", "anubis-unix-upstream"]:
machine.wait_for_unit(unit + ".service")
for port in [9000, 9001]:
machine.wait_for_open_port(port)
for instance in ["anubis", "anubis-unix-upstream"]:
machine.wait_for_open_unix_socket(f"/run/anubis/{instance}.sock")
machine.wait_for_open_unix_socket(f"/run/anubis/{instance}-metrics.sock")
# Default unix socket mode
machine.succeed('curl -f http://basic.localhost | grep "it works"')
machine.succeed('curl -f http://basic.localhost -H "User-Agent: Mozilla" | grep anubis')
machine.succeed('curl -f http://basic.localhost/metrics | grep anubis_challenges_issued')
# TCP mode
machine.succeed('curl -f http://tcp.localhost -H "User-Agent: Mozilla" | grep anubis')
machine.succeed('curl -f http://tcp.localhost/metrics | grep anubis_challenges_issued')
# Upstream is a unix socket mode
machine.succeed('curl -f http://unix.localhost/index.html | grep "it works"')
# Default user-defined environment variables
machine.succeed('cat /run/current-system/etc/systemd/system/anubis.service | grep "USER_DEFINED_DEFAULT"')
machine.succeed('cat /run/current-system/etc/systemd/system/anubis-tcp.service | grep "USER_DEFINED_DEFAULT"')
# Instance-specific user-specified environment variables
machine.succeed('cat /run/current-system/etc/systemd/system/anubis.service | grep "USER_DEFINED_INSTANCE"')
machine.fail('cat /run/current-system/etc/systemd/system/anubis-tcp.service | grep "USER_DEFINED_INSTANCE"')
# Make sure defaults don't overwrite themselves
machine.succeed('cat /run/current-system/etc/systemd/system/anubis.service | grep "DIFFICULTY=5"')
machine.succeed('cat /run/current-system/etc/systemd/system/anubis-tcp.service | grep "DIFFICULTY=3"')
# Check correct BotPolicy settings are applied
machine.succeed('cat /run/current-system/etc/systemd/system/anubis.service | grep "POLICY_FNAME=/nix/store"')
machine.fail('cat /run/current-system/etc/systemd/system/anubis-botPolicy-default.service | grep "POLICY_FNAME="')
machine.succeed('cat /run/current-system/etc/systemd/system/anubis-botPolicy-file.service | grep "POLICY_FNAME=/etc/anubis-botPolicy.json"')
'';
}

View File

@@ -0,0 +1,18 @@
{ pkgs, ... }:
{
name = "anuko-time-tracker";
meta = {
maintainers = [ ];
};
nodes = {
machine = {
services.anuko-time-tracker.enable = true;
};
};
testScript = ''
start_all()
machine.wait_for_unit("phpfpm-anuko-time-tracker")
machine.wait_for_open_port(80);
machine.wait_until_succeeds("curl -s --fail -L http://localhost/time.php | grep 'Anuko Time Tracker'")
'';
}

44
nixos/tests/apcupsd.nix Normal file
View File

@@ -0,0 +1,44 @@
let
# arbitrary address
ipAddr = "192.168.42.42";
in
{ lib, ... }:
{
name = "apcupsd";
meta.maintainers = with lib.maintainers; [ bjornfor ];
nodes = {
machine = {
services.apcupsd = {
enable = true;
configText = ''
UPSTYPE usb
BATTERYLEVEL 42
# Configure NISIP so that the only way apcaccess can work is to read
# this config.
NISIP ${ipAddr}
'';
};
networking.interfaces.eth1 = {
ipv4.addresses = [
{
address = ipAddr;
prefixLength = 24;
}
];
};
};
};
# Check that the service starts, that the CLI (apcaccess) works and that it
# uses the config (ipAddr) defined in the service config.
testScript = ''
start_all()
machine.wait_for_unit("apcupsd.service")
machine.wait_for_open_port(3551, "${ipAddr}")
res = machine.succeed("apcaccess")
expect_line="MBATTCHG : 42 Percent"
assert "MBATTCHG : 42 Percent" in res, f"expected apcaccess output to contain '{expect_line}' but got '{res}'"
machine.shutdown()
'';
}

70
nixos/tests/apfs.nix Normal file
View File

@@ -0,0 +1,70 @@
{ lib, ... }:
{
name = "apfs";
meta.maintainers = with lib.maintainers; [ Luflosi ];
nodes.machine = {
virtualisation.emptyDiskImages = [ 1024 ];
boot.supportedFilesystems = [ "apfs" ];
};
testScript = ''
machine.wait_for_unit("basic.target")
machine.succeed("mkdir /tmp/mnt")
with subtest("mkapfs refuses to work with a label that is too long"):
machine.fail( "mkapfs -L '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F' /dev/vdb")
with subtest("mkapfs works with the maximum label length"):
machine.succeed("mkapfs -L '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7' /dev/vdb")
with subtest("apfs-label works"):
machine.succeed("mkapfs -L 'myLabel' /dev/vdb")
machine.succeed("apfs-label /dev/vdb | grep -q myLabel")
with subtest("Enable case sensitivity and normalization sensitivity"):
machine.succeed(
"mkapfs -s -z /dev/vdb",
"mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
"echo 'Hello World 1' > /tmp/mnt/test.txt",
"[ ! -f /tmp/mnt/TeSt.TxT ] || false", # Test case sensitivity
"echo 'Hello World 1' | diff - /tmp/mnt/test.txt",
"echo 'Hello World 2' > /tmp/mnt/\u0061\u0301.txt",
"echo 'Hello World 2' | diff - /tmp/mnt/\u0061\u0301.txt",
"[ ! -f /tmp/mnt/\u00e1.txt ] || false", # Test Unicode normalization sensitivity
"umount /tmp/mnt",
"apfsck /dev/vdb",
)
with subtest("Disable case sensitivity and normalization sensitivity"):
machine.succeed(
"mkapfs /dev/vdb",
"mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
"echo 'bla bla bla' > /tmp/mnt/Test.txt",
"echo -n 'Hello World' > /tmp/mnt/test.txt",
"echo ' 1' >> /tmp/mnt/TEST.TXT",
"umount /tmp/mnt",
"apfsck /dev/vdb",
"mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
"echo 'Hello World 1' | diff - /tmp/mnt/TeSt.TxT", # Test case insensitivity
"echo 'Hello World 2' > /tmp/mnt/\u0061\u0301.txt",
"echo 'Hello World 2' | diff - /tmp/mnt/\u0061\u0301.txt",
"echo 'Hello World 2' | diff - /tmp/mnt/\u00e1.txt", # Test Unicode normalization
"umount /tmp/mnt",
"apfsck /dev/vdb",
)
with subtest("Snapshots"):
machine.succeed(
"mkapfs /dev/vdb",
"mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
"echo 'Hello World' > /tmp/mnt/test.txt",
"apfs-snap /tmp/mnt snap-1",
"rm /tmp/mnt/test.txt",
"umount /tmp/mnt",
"mount -o cknodes,readwrite,snap=snap-1 /dev/vdb /tmp/mnt",
"echo 'Hello World' | diff - /tmp/mnt/test.txt",
"umount /tmp/mnt",
"apfsck /dev/vdb",
)
'';
}

View File

@@ -0,0 +1,126 @@
{ pkgs, lib, ... }:
let
helloProfileContents = ''
abi <abi/4.0>,
include <tunables/global>
profile hello ${lib.getExe pkgs.hello} {
include <abstractions/base>
}
'';
in
{
name = "apparmor";
meta.maintainers = with lib.maintainers; [
julm
grimmauld
];
nodes.machine =
{
lib,
...
}:
{
security.apparmor = {
enable = lib.mkDefault true;
policies.hello = {
# test profile enforce and content definition
state = "enforce";
profile = helloProfileContents;
};
policies.sl = {
# test profile complain and path definition
state = "complain";
path = ./sl_profile;
};
policies.hexdump = {
# test profile complain and path definition
state = "enforce";
profile = ''
abi <abi/4.0>,
include <tunables/global>
profile hexdump /nix/store/*/bin/hexdump {
include <abstractions/base>
deny /tmp/** r,
}
'';
};
includes."abstractions/base" = ''
/nix/store/*/bin/** mr,
/nix/store/*/lib/** mr,
/nix/store/** r,
'';
};
};
testScript =
let
inherit (lib) getExe getExe';
in
''
machine.wait_for_unit("multi-user.target")
with subtest("AppArmor profiles are loaded"):
machine.succeed("systemctl status apparmor.service")
# AppArmor securityfs
with subtest("AppArmor securityfs is mounted"):
machine.succeed("mountpoint -q /sys/kernel/security")
machine.succeed("cat /sys/kernel/security/apparmor/profiles")
# Test apparmorRulesFromClosure by:
# 1. Prepending a string of the relevant packages' name and version on each line.
# 2. Sorting according to those strings.
# 3. Removing those prepended strings.
# 4. Using `diff` against the expected output.
with subtest("apparmorRulesFromClosure"):
machine.succeed(
"${getExe' pkgs.diffutils "diff"} -u ${
pkgs.writeText "expected.rules" (import ./makeExpectedPolicies.nix { inherit pkgs; })
} ${
pkgs.runCommand "actual.rules" { preferLocalBuild = true; } ''
${getExe pkgs.gnused} -e 's:^[^ ]* ${builtins.storeDir}/[^,/-]*-\([^/,]*\):\1 \0:' ${
pkgs.apparmorRulesFromClosure {
name = "ping";
additionalRules = [ "x $path/foo/**" ];
} [ pkgs.libcap ]
} |
${getExe' pkgs.coreutils "sort"} -n -k1 |
${getExe pkgs.gnused} -e 's:^[^ ]* ::' >$out
''
}"
)
# Test apparmor profile states by using `diff` against `aa-status`
with subtest("apparmorProfileStates"):
machine.succeed("${getExe' pkgs.diffutils "diff"} -u \
<(${getExe' pkgs.apparmor-bin-utils "aa-status"} --json | ${getExe pkgs.jq} --sort-keys . ) \
<(${getExe pkgs.jq} --sort-keys . ${
pkgs.writers.writeJSON "expectedStates.json" {
version = "2";
processes = { };
profiles = {
hexdump = "enforce";
hello = "enforce";
sl = "complain";
};
}
})")
# Test apparmor profile files in /etc/apparmor.d/<name> to be either a correct symlink (sl) or have the right file contents (hello)
with subtest("apparmorProfileTargets"):
machine.succeed("${getExe' pkgs.diffutils "diff"} -u <(${getExe pkgs.file} /etc/static/apparmor.d/sl) ${pkgs.writeText "expected.link" ''
/etc/static/apparmor.d/sl: symbolic link to ${./sl_profile}
''}")
machine.succeed("${getExe' pkgs.diffutils "diff"} -u /etc/static/apparmor.d/hello ${pkgs.writeText "expected.content" helloProfileContents}")
with subtest("apparmorProfileEnforce"):
machine.succeed("${getExe pkgs.hello} 1> /tmp/test-file")
machine.fail("${lib.getExe' pkgs.util-linux "hexdump"} /tmp/test-file") # no access to /tmp/test-file granted by apparmor
'';
}

View File

@@ -0,0 +1,75 @@
{ pkgs }:
''
ixr ${pkgs.bashNonInteractive}/libexec/**,
mr ${pkgs.bashNonInteractive}/lib/**.so*,
mr ${pkgs.bashNonInteractive}/lib64/**.so*,
mr ${pkgs.bashNonInteractive}/share/**,
r ${pkgs.bashNonInteractive},
r ${pkgs.bashNonInteractive}/etc/**,
r ${pkgs.bashNonInteractive}/lib/**,
r ${pkgs.bashNonInteractive}/lib64/**,
x ${pkgs.bashNonInteractive}/foo/**,
ixr ${pkgs.glibc}/libexec/**,
mr ${pkgs.glibc}/lib/**.so*,
mr ${pkgs.glibc}/lib64/**.so*,
mr ${pkgs.glibc}/share/**,
r ${pkgs.glibc},
r ${pkgs.glibc}/etc/**,
r ${pkgs.glibc}/lib/**,
r ${pkgs.glibc}/lib64/**,
x ${pkgs.glibc}/foo/**,
ixr ${pkgs.libcap}/libexec/**,
mr ${pkgs.libcap}/lib/**.so*,
mr ${pkgs.libcap}/lib64/**.so*,
mr ${pkgs.libcap}/share/**,
r ${pkgs.libcap},
r ${pkgs.libcap}/etc/**,
r ${pkgs.libcap}/lib/**,
r ${pkgs.libcap}/lib64/**,
x ${pkgs.libcap}/foo/**,
ixr ${pkgs.libcap.lib}/libexec/**,
mr ${pkgs.libcap.lib}/lib/**.so*,
mr ${pkgs.libcap.lib}/lib64/**.so*,
mr ${pkgs.libcap.lib}/share/**,
r ${pkgs.libcap.lib},
r ${pkgs.libcap.lib}/etc/**,
r ${pkgs.libcap.lib}/lib/**,
r ${pkgs.libcap.lib}/lib64/**,
x ${pkgs.libcap.lib}/foo/**,
ixr ${pkgs.libidn2.out}/libexec/**,
mr ${pkgs.libidn2.out}/lib/**.so*,
mr ${pkgs.libidn2.out}/lib64/**.so*,
mr ${pkgs.libidn2.out}/share/**,
r ${pkgs.libidn2.out},
r ${pkgs.libidn2.out}/etc/**,
r ${pkgs.libidn2.out}/lib/**,
r ${pkgs.libidn2.out}/lib64/**,
x ${pkgs.libidn2.out}/foo/**,
ixr ${pkgs.libunistring}/libexec/**,
mr ${pkgs.libunistring}/lib/**.so*,
mr ${pkgs.libunistring}/lib64/**.so*,
mr ${pkgs.libunistring}/share/**,
r ${pkgs.libunistring},
r ${pkgs.libunistring}/etc/**,
r ${pkgs.libunistring}/lib/**,
r ${pkgs.libunistring}/lib64/**,
x ${pkgs.libunistring}/foo/**,
ixr ${pkgs.tzdata}/libexec/**,
mr ${pkgs.tzdata}/lib/**.so*,
mr ${pkgs.tzdata}/lib64/**.so*,
mr ${pkgs.tzdata}/share/**,
r ${pkgs.tzdata},
r ${pkgs.tzdata}/etc/**,
r ${pkgs.tzdata}/lib/**,
r ${pkgs.tzdata}/lib64/**,
x ${pkgs.tzdata}/foo/**,
ixr ${pkgs.glibc.libgcc}/libexec/**,
mr ${pkgs.glibc.libgcc}/lib/**.so*,
mr ${pkgs.glibc.libgcc}/lib64/**.so*,
mr ${pkgs.glibc.libgcc}/share/**,
r ${pkgs.glibc.libgcc},
r ${pkgs.glibc.libgcc}/etc/**,
r ${pkgs.glibc.libgcc}/lib/**,
r ${pkgs.glibc.libgcc}/lib64/**,
x ${pkgs.glibc.libgcc}/foo/**,
''

View File

@@ -0,0 +1,5 @@
abi <abi/4.0>,
include <tunables/global>
profile sl /bin/sl {
include <abstractions/base>
}

View File

@@ -0,0 +1,123 @@
# similar to the appliance-repart-image test but with a dm-verity
# protected nix store and tmpfs as rootfs
{ lib, ... }:
{
name = "appliance-repart-image-verity-store";
meta.maintainers = with lib.maintainers; [
nikstur
willibutz
];
nodes.machine =
{
config,
lib,
pkgs,
...
}:
let
inherit (config.image.repart.verityStore) partitionIds;
in
{
imports = [ ../modules/image/repart.nix ];
virtualisation.fileSystems = lib.mkVMOverride {
"/" = {
fsType = "tmpfs";
options = [ "mode=0755" ];
};
# bind-mount the store
"/nix/store" = {
device = "/usr/nix/store";
options = [ "bind" ];
};
};
image.repart = {
verityStore = {
enable = true;
# by default the module works with systemd-boot, for simplicity this test directly boots the UKI
ukiPath = "/EFI/BOOT/BOOT${lib.toUpper config.nixpkgs.hostPlatform.efiArch}.EFI";
};
name = "appliance-verity-store-image";
partitions = {
${partitionIds.esp} = {
# the UKI is injected into this partition by the verityStore module
repartConfig = {
Type = "esp";
Format = "vfat";
SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M";
};
};
${partitionIds.store-verity}.repartConfig = {
Minimize = "best";
};
${partitionIds.store}.repartConfig = {
Minimize = "best";
};
};
};
virtualisation = {
directBoot.enable = false;
mountHostNixStore = false;
useEFIBoot = true;
};
boot = {
loader.grub.enable = false;
initrd.systemd.enable = true;
};
system.image = {
id = "nixos-appliance";
version = "1";
};
# don't create /usr/bin/env
# this would require some extra work on read-only /usr
# and it is not a strict necessity
system.activationScripts.usrbinenv = lib.mkForce "";
};
testScript =
{ nodes, ... }: # python
''
import os
import subprocess
import tempfile
tmp_disk_image = tempfile.NamedTemporaryFile()
subprocess.run([
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
"create",
"-f",
"qcow2",
"-b",
"${nodes.machine.system.build.finalImage}/${nodes.machine.image.repart.imageFile}",
"-F",
"raw",
tmp_disk_image.name,
])
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
machine.wait_for_unit("default.target")
with subtest("Running with volatile root"):
machine.succeed("findmnt --kernel --type tmpfs /")
with subtest("/nix/store is backed by dm-verity protected fs"):
verity_info = machine.succeed("dmsetup info --target verity usr")
assert "ACTIVE" in verity_info,f"unexpected verity info: {verity_info}"
backing_device = machine.succeed("df --output=source /nix/store | tail -n1").strip()
assert "/dev/mapper/usr" == backing_device,"unexpected backing device: {backing_device}"
'';
}

View File

@@ -0,0 +1,128 @@
# Tests building and running a GUID Partition Table (GPT) appliance image.
# "Appliance" here means that the image does not contain the normal NixOS
# infrastructure of a system profile and cannot be re-built via
# `nixos-rebuild`.
{ lib, ... }:
let
rootPartitionLabel = "root";
imageId = "nixos-appliance";
imageVersion = "1-rc1";
in
{
name = "appliance-gpt-image";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine =
{
config,
lib,
pkgs,
...
}:
{
imports = [ ../modules/image/repart.nix ];
virtualisation.directBoot.enable = false;
virtualisation.mountHostNixStore = false;
virtualisation.useEFIBoot = true;
# Disable boot loaders because we install one "manually".
# TODO(raitobezarius): revisit this when #244907 lands
boot.loader.grub.enable = false;
system.image.id = imageId;
system.image.version = imageVersion;
virtualisation.fileSystems = lib.mkForce {
"/" = {
device = "/dev/disk/by-partlabel/${rootPartitionLabel}";
fsType = "ext4";
};
};
image.repart = {
name = "appliance-gpt-image";
# OVMF does not work with the default repart sector size of 4096
sectorSize = 512;
partitions = {
"esp" = {
contents =
let
efiArch = config.nixpkgs.hostPlatform.efiArch;
in
{
"/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source =
"${pkgs.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
"/EFI/Linux/${config.system.boot.loader.ukiFile}".source =
"${config.system.build.uki}/${config.system.boot.loader.ukiFile}";
};
repartConfig = {
Type = "esp";
Format = "vfat";
# Minimize = "guess" seems to not work very well for vfat
# partitions. It's better to set a sensible default instead. The
# aarch64 kernel seems to generally be a little bigger than the
# x86_64 kernel. To stay on the safe side, leave some more slack
# for every platform other than x86_64.
SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M";
};
};
"swap" = {
repartConfig = {
Type = "swap";
Format = "swap";
SizeMinBytes = "10M";
SizeMaxBytes = "10M";
};
};
"root" = {
storePaths = [ config.system.build.toplevel ];
repartConfig = {
Type = "root";
Format = config.fileSystems."/".fsType;
Label = rootPartitionLabel;
Minimize = "guess";
};
};
};
};
};
testScript =
{ nodes, ... }:
''
import os
import subprocess
import tempfile
tmp_disk_image = tempfile.NamedTemporaryFile()
subprocess.run([
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
"create",
"-f",
"qcow2",
"-b",
"${nodes.machine.system.build.image}/${nodes.machine.image.repart.imageFile}",
"-F",
"raw",
tmp_disk_image.name,
])
# Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
os_release = machine.succeed("cat /etc/os-release")
assert 'IMAGE_ID="${imageId}"' in os_release
assert 'IMAGE_VERSION="${imageVersion}"' in os_release
bootctl_status = machine.succeed("bootctl status")
assert "Boot Loader Specification Type #2 (.efi)" in bootctl_status
'';
}

36
nixos/tests/archi.nix Normal file
View File

@@ -0,0 +1,36 @@
{ lib, ... }:
{
name = "archi";
meta.maintainers = with lib.maintainers; [ paumr ];
nodes.machine =
{ pkgs, ... }:
{
imports = [
./common/x11.nix
];
environment.systemPackages = with pkgs; [ archi ];
};
enableOCR = true;
testScript = ''
machine.wait_for_x()
with subtest("createEmptyModel via CLI"):
machine.succeed("Archi -application com.archimatetool.commandline.app -consoleLog -nosplash --createEmptyModel --saveModel smoke.archimate")
machine.copy_from_vm("smoke.archimate", "")
with subtest("UI smoketest"):
machine.succeed("DISPLAY=:0 Archi --createEmptyModel >&2 &")
machine.wait_for_window("Archi")
# wait till main UI is open
# since OCR seems to be buggy wait_for_text was replaced by sleep, issue: #302965
# machine.wait_for_text("Welcome to Archi")
machine.sleep(20)
machine.screenshot("welcome-screen")
'';
}

52
nixos/tests/aria2.nix Normal file
View File

@@ -0,0 +1,52 @@
{ pkgs, ... }:
let
rpcSecret = "supersecret";
rpc-listen-port = 6800;
curlBody = {
jsonrpc = 2.0;
id = 1;
method = "aria2.getVersion";
params = [ "token:${rpcSecret}" ];
};
in
{
name = "aria2";
nodes.machine = {
environment.etc."aria2Rpc".text = rpcSecret;
services.aria2 = {
enable = true;
rpcSecretFile = "/etc/aria2Rpc";
settings = {
inherit rpc-listen-port;
allow-overwrite = false;
check-integrity = true;
console-log-level = "warn";
listen-port = [
{
from = 20000;
to = 20010;
}
{
from = 22222;
to = 22222;
}
];
max-concurrent-downloads = 50;
seed-ratio = 1.2;
summary-interval = 0;
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("aria2.service")
curl_cmd = 'curl --fail-with-body -X POST -H "Content-Type: application/json" \
-d \'${builtins.toJSON curlBody}\' http://localhost:${toString rpc-listen-port}/jsonrpc'
print(machine.wait_until_succeeds(curl_cmd, timeout=10))
machine.shutdown()
'';
meta.maintainers = [ pkgs.lib.maintainers.timhae ];
}

View File

@@ -0,0 +1,278 @@
{
lib,
pkgs,
...
}:
let
user = "alice";
client =
{ pkgs, ... }:
{
imports = [
./common/user-account.nix
./common/x11.nix
];
hardware.graphics.enable = true;
virtualisation.memorySize = 384;
environment = {
systemPackages = [ pkgs.armagetronad ];
variables.XAUTHORITY = "/home/${user}/.Xauthority";
};
test-support.displayManager.auto.user = user;
};
in
{
name = "armagetronad";
meta = with lib.maintainers; {
maintainers = [ numinit ];
};
enableOCR = true;
nodes = {
server = {
services.armagetronad.servers = {
high-rubber = {
enable = true;
name = "Smoke Test High Rubber Server";
port = 4534;
settings = {
SERVER_OPTIONS = "High Rubber server made to run smoke tests.";
CYCLE_RUBBER = 40;
SIZE_FACTOR = 0.5;
};
roundSettings = {
SAY = [
"NixOS Smoke Test Server"
"https://nixos.org"
];
};
};
sty = {
enable = true;
name = "Smoke Test sty+ct+ap Server";
package = pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated;
port = 4535;
settings = {
SERVER_OPTIONS = "sty+ct+ap server made to run smoke tests.";
CYCLE_RUBBER = 20;
SIZE_FACTOR = 0.5;
};
roundSettings = {
SAY = [
"NixOS Smoke Test sty+ct+ap Server"
"https://nixos.org"
];
};
};
trunk = {
enable = true;
name = "Smoke Test trunk Server";
package = pkgs.armagetronad."0.4".dedicated;
port = 4536;
settings = {
SERVER_OPTIONS = "0.4 server made to run smoke tests.";
CYCLE_RUBBER = 20;
SIZE_FACTOR = 0.5;
};
roundSettings = {
SAY = [
"NixOS Smoke Test 0.4 Server"
"https://nixos.org"
];
};
};
};
};
client1 = client;
client2 = client;
};
testScript =
let
xdo =
name: text:
let
xdoScript = pkgs.writeText "${name}.xdo" text;
in
"${pkgs.xdotool}/bin/xdotool ${xdoScript}";
in
''
import shlex
import threading
from collections import namedtuple
class Client(namedtuple('Client', ('node', 'name'))):
def send(self, *keys):
for key in keys:
self.node.send_key(key)
def send_on(self, text, *keys):
self.node.wait_for_text(text)
self.send(*keys)
Server = namedtuple('Server', ('node', 'name', 'address', 'port', 'welcome', 'player1', 'player2'))
# Clients and their in-game names
clients = (
Client(client1, 'Arduino'),
Client(client2, 'SmOoThIcE')
)
# Server configs.
servers = (
Server(server, 'high-rubber', 'server', 4534, 'NixOS Smoke Test Server', 'SmOoThIcE', 'Arduino'),
Server(server, 'sty', 'server', 4535, 'NixOS Smoke Test sty+ct+ap Server', 'Arduino', 'SmOoThIcE'),
Server(server, 'trunk', 'server', 4536, 'NixOS Smoke Test 0.4 Server', 'Arduino', 'SmOoThIcE')
)
"""
Runs a command as the client user.
"""
def run(cmd):
return "su - ${user} -c " + shlex.quote(cmd)
screenshot_idx = 1
"""
Takes screenshots on all clients.
"""
def take_screenshots(screenshot_idx):
for client in clients:
client.node.screenshot(f"screen_{client.name}_{screenshot_idx}")
return screenshot_idx + 1
"""
Sets up a client, waiting for the given barrier on completion.
"""
def client_setup(client, servers, barrier):
client.node.wait_for_x()
# Configure Armagetron so we skip the tutorial.
client.node.succeed(
run("mkdir -p ~/.armagetronad/var"),
run(f"echo 'PLAYER_1 {client.name}' >> ~/.armagetronad/var/autoexec.cfg"),
run("echo 'FIRST_USE 0' >> ~/.armagetronad/var/autoexec.cfg")
)
for idx, srv in enumerate(servers):
client.node.succeed(
run(f"echo 'BOOKMARK_{idx+1}_ADDRESS {srv.address}' >> ~/.armagetronad/var/autoexec.cfg"),
run(f"echo 'BOOKMARK_{idx+1}_NAME {srv.name}' >> ~/.armagetronad/var/autoexec.cfg"),
run(f"echo 'BOOKMARK_{idx+1}_PORT {srv.port}' >> ~/.armagetronad/var/autoexec.cfg")
)
# Start Armagetron. Use the recording mode since it skips the splashscreen.
client.node.succeed(run("cd; ulimit -c unlimited; armagetronad --record test.aarec >&2 & disown"))
client.node.wait_until_succeeds(
run(
"${xdo "create_new_win-select_main_window" ''
search --onlyvisible --name "Armagetron Advanced"
windowfocus --sync
windowactivate --sync
''}"
)
)
# Get into the multiplayer menu.
client.send_on('Armagetron Advanced', 'ret')
client.send_on('Play Game', 'ret')
# Online > LAN > Network Setup > Mates > Server Bookmarks
client.send_on('Multiplayer', 'down', 'down', 'down', 'down', 'ret')
barrier.wait()
# Start everything.
start_all()
# Get to the Server Bookmarks screen on both clients. This takes a while so do it asynchronously.
barrier = threading.Barrier(len(clients) + 1, timeout=600)
for client in clients:
threading.Thread(target=client_setup, args=(client, servers, barrier)).start()
# Wait for the servers to come up.
for srv in servers:
srv.node.wait_for_unit(f"armagetronad-{srv.name}")
srv.node.wait_until_succeeds(f"ss --numeric --udp --listening | grep -q {srv.port}")
# Make sure console commands work through the named pipe we created.
for srv in servers:
srv.node.succeed(
f"echo 'say Testing!' >> /var/lib/armagetronad/{srv.name}/input"
)
srv.node.succeed(
f"echo 'say Testing again!' >> /var/lib/armagetronad/{srv.name}/input"
)
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: Testing!'"
)
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: Testing again!'"
)
# Wait for the client setup to complete.
barrier.wait()
# Main testing loop. Iterates through each server bookmark and connects to them in sequence.
# Assumes that the game is currently on the Server Bookmarks screen.
for srv in servers:
screenshot_idx = take_screenshots(screenshot_idx)
# Connect both clients at once, one second apart.
for client in clients:
client.send('ret')
client.node.sleep(1)
# Wait for clients to connect
for client in clients:
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q '{client.name}.*entered the game'"
)
# Wait for the match to start
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: {srv.welcome}'"
)
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: https://nixos.org'"
)
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Go (round 1 of 10)'"
)
# Wait for the players to die by running into the wall.
player1 = next(client for client in clients if client.name == srv.player1)
player2 = next(client for client in clients if client.name == srv.player2)
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q '{player1.name}.*lost 4 points'"
)
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q '{player2.name}.*lost 4 points'"
)
screenshot_idx = take_screenshots(screenshot_idx)
# Disconnect both clients from the server
for client in clients:
client.send('esc')
client.send_on('Menu', 'up', 'up', 'ret')
srv.node.wait_until_succeeds(
f"journalctl -u armagetronad-{srv.name} -e | grep -q '{client.name}.*left the game'"
)
# Next server.
for client in clients:
client.send_on('Server Bookmarks', 'down')
# Stop the servers
for srv in servers:
srv.node.succeed(
f"systemctl stop armagetronad-{srv.name}"
)
srv.node.wait_until_fails(f"ss --numeric --udp --listening | grep -q {srv.port}")
'';
}

56
nixos/tests/artalk.nix Normal file
View File

@@ -0,0 +1,56 @@
{ lib, pkgs, ... }:
{
name = "artalk";
meta = {
maintainers = with lib.maintainers; [ moraxyc ];
};
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = [
pkgs.curl
pkgs.artalk
pkgs.sudo
];
services.artalk = {
enable = true;
settings = {
cache.enabled = true;
admin_users = [
{
name = "admin";
email = "admin@example.org";
# md5 for 'password'
password = "(md5)5F4DCC3B5AA765D61D8327DEB882CF99";
}
];
};
};
};
testScript = ''
import json
machine.wait_for_unit("artalk.service")
machine.wait_for_open_port(23366)
assert '${pkgs.artalk.version}' in machine.succeed("curl --fail --max-time 10 http://127.0.0.1:23366/api/v2/version")
# Get token
result = json.loads(machine.succeed("""
curl --fail -X POST --json '{
"email": "admin@example.org",
"password": "password"
}' 'http://127.0.0.1:23366/api/v2/auth/email/login'
"""))
token = result['token']
# Test admin
machine.succeed(f"""
curl --fail -X POST --header 'Authorization: {token}' 'http://127.0.0.1:23366/api/v2/cache/flush'
""")
'';
}

34
nixos/tests/atd.nix Normal file
View File

@@ -0,0 +1,34 @@
{ pkgs, ... }:
{
name = "atd";
meta = with pkgs.lib.maintainers; {
maintainers = [ bjornfor ];
};
nodes.machine =
{ ... }:
{
services.atd.enable = true;
users.users.alice = {
isNormalUser = true;
};
};
# "at" has a resolution of 1 minute
testScript = ''
start_all()
machine.wait_for_unit("atd.service") # wait for atd to start
machine.fail("test -f ~root/at-1")
machine.fail("test -f ~alice/at-1")
machine.succeed("echo 'touch ~root/at-1' | at now+1min")
machine.succeed("su - alice -c \"echo 'touch at-1' | at now+1min\"")
machine.succeed("sleep 1.5m")
machine.succeed("test -f ~root/at-1")
machine.succeed("test -f ~alice/at-1")
'';
}

265
nixos/tests/atop.nix Normal file
View File

@@ -0,0 +1,265 @@
{
pkgs,
runTest,
...
}:
let
assertions = rec {
path = program: path: ''
with subtest("The path of ${program} should be ${path}"):
p = machine.succeed("type -p \"${program}\" | head -c -1")
assert p == "${path}", f"${program} is {p}, expected ${path}"
'';
unit = name: state: ''
with subtest("Unit ${name} should be ${state}"):
if "${state}" == "active":
machine.wait_for_unit("${name}")
else:
machine.require_unit_state("${name}", "${state}")
'';
version = ''
import re
with subtest("binary should report the correct version"):
pkgver = "${pkgs.atop.version}"
ver = re.sub(r'(?s)^Version: (\d+\.\d+\.\d+).*', r'\1', machine.succeed("atop -V"))
assert ver == pkgver, f"Version is `{ver}`, expected `{pkgver}`"
'';
atoprc =
contents:
if builtins.stringLength contents > 0 then
''
with subtest("/etc/atoprc should have the correct contents"):
f = machine.succeed("cat /etc/atoprc")
assert f == "${contents}", f"/etc/atoprc contents: '{f}', expected '${contents}'"
''
else
''
with subtest("/etc/atoprc should not be present"):
machine.succeed("test ! -e /etc/atoprc")
'';
wrapper =
present:
if present then
path "atop" "/run/wrappers/bin/atop"
+ ''
with subtest("Wrapper should be setuid root"):
stat = machine.succeed("stat --printf '%a %u' /run/wrappers/bin/atop")
assert stat == "4511 0", f"Wrapper stat is {stat}, expected '4511 0'"
''
else
path "atop" "/run/current-system/sw/bin/atop";
atopService =
present:
if present then
unit "atop.service" "active"
+ ''
with subtest("atop.service should write some data to /var/log/atop"):
def has_data_files(last: bool) -> bool:
files = int(machine.succeed("ls -1 /var/log/atop | wc -l"))
if files == 0:
machine.log("Did not find at least one 1 data file")
if not last:
machine.log("Will retry...")
return False
return True
with machine.nested("Waiting for data files"):
retry(has_data_files)
''
else
unit "atop.service" "inactive";
atopRotateTimer = present: unit "atop-rotate.timer" (if present then "active" else "inactive");
atopacctService =
present:
if present then
unit "atopacct.service" "active"
+ ''
with subtest("atopacct.service should enable process accounting"):
machine.wait_until_succeeds("test -f /run/pacct_source")
with subtest("atopacct.service should write data to /run/pacct_shadow.d"):
def has_data_files(last: bool) -> bool:
files = int(machine.succeed("ls -1 /run/pacct_shadow.d | wc -l"))
if files == 0:
machine.log("Did not find at least one 1 data file")
if not last:
machine.log("Will retry...")
return False
return True
with machine.nested("Waiting for data files"):
retry(has_data_files)
''
else
unit "atopacct.service" "inactive";
netatop =
present:
if present then
unit "netatop.service" "active"
+ ''
with subtest("The netatop kernel module should be loaded"):
out = machine.succeed("modprobe -n -v netatop")
assert out == "", f"Module should be loaded already, but modprobe would have done {out}."
''
else
''
with subtest("The netatop kernel module should be absent"):
machine.fail("modprobe -n -v netatop")
'';
atopgpu =
present:
if present then
(unit "atopgpu.service" "active") + (path "atopgpud" "/run/current-system/sw/bin/atopgpud")
else
(unit "atopgpu.service" "inactive")
+ ''
with subtest("atopgpud should not be present"):
machine.fail("type -p atopgpud")
'';
};
meta = {
timeout = 600;
};
in
{
justThePackage = runTest {
name = "atop-justThePackage";
nodes.machine = {
environment.systemPackages = [ pkgs.atop ];
};
testScript =
with assertions;
builtins.concatStringsSep "\n" [
version
(atoprc "")
(wrapper false)
(atopService false)
(atopRotateTimer false)
(atopacctService false)
(netatop false)
(atopgpu false)
];
inherit meta;
};
defaults = runTest {
name = "atop-defaults";
nodes.machine = {
programs.atop = {
enable = true;
};
};
testScript =
with assertions;
builtins.concatStringsSep "\n" [
version
(atoprc "")
(wrapper false)
(atopService true)
(atopRotateTimer true)
(atopacctService true)
(netatop false)
(atopgpu false)
];
inherit meta;
};
minimal = runTest {
name = "atop-minimal";
nodes.machine = {
programs.atop = {
enable = true;
atopService.enable = false;
atopRotateTimer.enable = false;
atopacctService.enable = false;
};
};
testScript =
with assertions;
builtins.concatStringsSep "\n" [
version
(atoprc "")
(wrapper false)
(atopService false)
(atopRotateTimer false)
(atopacctService false)
(netatop false)
(atopgpu false)
];
inherit meta;
};
netatop = runTest {
name = "atop-netatop";
nodes.machine = {
programs.atop = {
enable = true;
netatop.enable = true;
};
};
testScript =
with assertions;
builtins.concatStringsSep "\n" [
version
(atoprc "")
(wrapper false)
(atopService true)
(atopRotateTimer true)
(atopacctService true)
(netatop true)
(atopgpu false)
];
inherit meta;
};
atopgpu = runTest {
name = "atop-atopgpu";
nodes.machine = {
programs.atop = {
enable = true;
atopgpu.enable = true;
};
};
testScript =
with assertions;
builtins.concatStringsSep "\n" [
version
(atoprc "")
(wrapper false)
(atopService true)
(atopRotateTimer true)
(atopacctService true)
(netatop false)
(atopgpu true)
];
inherit meta;
};
everything = runTest {
name = "atop-everything";
nodes.machine = {
programs.atop = {
enable = true;
settings = {
flags = "faf1";
interval = 2;
};
setuidWrapper.enable = true;
netatop.enable = true;
atopgpu.enable = true;
};
};
testScript =
with assertions;
builtins.concatStringsSep "\n" [
version
(atoprc "flags faf1\\ninterval 2\\n")
(wrapper true)
(atopService true)
(atopRotateTimer true)
(atopacctService true)
(netatop true)
(atopgpu true)
];
inherit meta;
};
}

92
nixos/tests/atticd.nix Normal file
View File

@@ -0,0 +1,92 @@
{ lib, pkgs, ... }:
let
accessKey = "BKIKJAA5BMMU2RHO6IBB";
secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";
minioCredentialsFile = pkgs.writeText "minio-credentials-full" ''
MINIO_ROOT_USER=${accessKey}
MINIO_ROOT_PASSWORD=${secretKey}
'';
environmentFile = pkgs.runCommand "atticd-env" { } ''
echo ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${lib.getExe pkgs.openssl} genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)" > $out
'';
in
{
name = "atticd";
nodes = {
local = {
services.atticd = {
enable = true;
inherit environmentFile;
};
environment.systemPackages = [
pkgs.attic-client
];
};
s3 = {
services.atticd = {
enable = true;
settings = {
storage = {
type = "s3";
bucket = "attic";
region = "us-east-1";
endpoint = "http://127.0.0.1:9000";
credentials = {
access_key_id = accessKey;
secret_access_key = secretKey;
};
};
};
inherit environmentFile;
};
services.minio = {
enable = true;
rootCredentialsFile = minioCredentialsFile;
};
environment.systemPackages = [
pkgs.attic-client
pkgs.minio-client
];
};
};
testScript = # python
''
start_all()
with subtest("local storage push"):
local.wait_for_unit("atticd.service")
token = local.succeed("atticd-atticadm make-token --sub stop --validity 1y --create-cache '*' --pull '*' --push '*' --delete '*' --configure-cache '*' --configure-cache-retention '*'").strip()
local.succeed(f"attic login local http://localhost:8080 {token}")
local.succeed("attic cache create test-cache")
local.succeed("attic push test-cache ${environmentFile}")
with subtest("s3 storage push"):
s3.wait_for_unit("atticd.service")
s3.wait_for_unit("minio.service")
s3.wait_for_open_port(9000)
s3.succeed(
"mc alias set minio "
+ "http://localhost:9000 "
+ "${accessKey} ${secretKey} --api s3v4",
"mc mb minio/attic",
)
token = s3.succeed("atticd-atticadm make-token --sub stop --validity 1y --create-cache '*' --pull '*' --push '*' --delete '*' --configure-cache '*' --configure-cache-retention '*'").strip()
s3.succeed(f"attic login s3 http://localhost:8080 {token}")
s3.succeed("attic cache create test-cache")
s3.succeed("attic push test-cache ${environmentFile}")
'';
}

75
nixos/tests/atuin.nix Normal file
View File

@@ -0,0 +1,75 @@
{ lib, ... }:
let
testPort = 8888;
testUser = "testerman";
testPass = "password";
testEmail = "test.testerman@test.com";
in
{
name = "atuin";
meta.maintainers = with lib.maintainers; [ devusb ];
defaults =
{ pkgs, ... }:
{
environment.systemPackages = [
pkgs.atuin
];
};
nodes = {
server =
{ ... }:
{
services.postgresql.enable = true;
services.atuin = {
enable = true;
port = testPort;
host = "0.0.0.0";
openFirewall = true;
openRegistration = true;
};
};
client = { ... }: { };
};
testScript =
{ nodes, ... }:
#python
''
start_all()
# wait for atuin server startup
server.wait_for_unit("atuin.service")
server.wait_for_open_port(${toString testPort})
# configure atuin client on server node
server.execute("mkdir -p ~/.config/atuin")
server.execute("echo 'sync_address = \"http://localhost:${toString testPort}\"' > ~/.config/atuin/config.toml")
# register with atuin server on server node
server.succeed("atuin register -u ${testUser} -p ${testPass} -e ${testEmail}")
_, key = server.execute("atuin key")
# store test record in atuin server and sync
server.succeed("ATUIN_SESSION=$(atuin uuid) atuin history start 'shazbot'")
server.succeed("ATUIN_SESSION=$(atuin uuid) atuin sync")
# configure atuin client on client node
client.execute("mkdir -p ~/.config/atuin")
client.execute("echo 'sync_address = \"http://server:${toString testPort}\"' > ~/.config/atuin/config.toml")
# log in to atuin server on client node
client.succeed(f"atuin login -u ${testUser} -p ${testPass} -k \"{key}\"")
# pull records from atuin server
client.succeed("atuin sync -f")
# check for test record
client.succeed("ATUIN_SESSION=$(atuin uuid) atuin history list | grep shazbot")
'';
}

View File

@@ -0,0 +1,20 @@
{ lib, ... }:
{
name = "audiobookshelf";
meta.maintainers = with lib.maintainers; [ wietsedv ];
nodes.machine =
{ pkgs, ... }:
{
services.audiobookshelf = {
enable = true;
port = 1234;
};
};
testScript = ''
machine.wait_for_unit("audiobookshelf.service")
machine.wait_for_open_port(1234)
machine.succeed("curl --fail http://localhost:1234/")
'';
}

54
nixos/tests/audit.nix Normal file
View File

@@ -0,0 +1,54 @@
{ lib, ... }:
{
name = "audit";
meta = {
maintainers = with lib.maintainers; [ grimmauld ];
};
nodes = {
machine =
{ lib, pkgs, ... }:
{
security.audit = {
enable = true;
rules = [
"-a always,exit -F exe=${lib.getExe pkgs.hello} -k nixos-test"
];
backlogLimit = 512;
};
security.auditd = {
enable = true;
plugins.af_unix.active = true;
plugins.syslog.active = true;
# plugins.remote.active = true; # needs configuring a remote server for logging
# plugins.filter.active = true; # needs configuring allowlist/denylist
};
environment.systemPackages = [ pkgs.hello ];
};
};
testScript = ''
machine.wait_for_unit("audit-rules-nixos.service")
machine.wait_for_unit("auditd.service")
with subtest("Audit subsystem gets enabled"):
audit_status = machine.succeed("auditctl -s")
t.assertIn("enabled 1", audit_status)
t.assertIn("backlog_limit 512", audit_status)
with subtest("unix socket plugin activated"):
machine.succeed("stat /run/audit/audispd_events")
with subtest("Custom rule produces audit traces"):
machine.succeed("hello")
print(machine.succeed("ausearch -k nixos-test -sc exit_group"))
with subtest("Stopping audit-rules-nixos.service disables the audit subsystem"):
machine.succeed("systemctl stop audit-rules-nixos.service")
t.assertIn("enabled 0", machine.succeed("auditctl -s"))
'';
}

178
nixos/tests/auth-mysql.nix Normal file
View File

@@ -0,0 +1,178 @@
{ pkgs, lib, ... }:
let
dbUser = "nixos_auth";
dbPassword = "topsecret123";
dbName = "auth";
mysqlUsername = "mysqltest";
mysqlPassword = "topsecretmysqluserpassword123";
mysqlGroup = "mysqlusers";
localUsername = "localtest";
localPassword = "topsecretlocaluserpassword123";
mysqlInit = pkgs.writeText "mysqlInit" ''
CREATE USER '${dbUser}'@'localhost' IDENTIFIED BY '${dbPassword}';
CREATE DATABASE ${dbName};
GRANT ALL PRIVILEGES ON ${dbName}.* TO '${dbUser}'@'localhost';
FLUSH PRIVILEGES;
USE ${dbName};
CREATE TABLE `groups` (
rowid int(11) NOT NULL auto_increment,
gid int(11) NOT NULL,
name char(255) NOT NULL,
PRIMARY KEY (rowid)
);
CREATE TABLE `users` (
name varchar(255) NOT NULL,
uid int(11) NOT NULL auto_increment,
gid int(11) NOT NULL,
password varchar(255) NOT NULL,
PRIMARY KEY (uid),
UNIQUE (name)
) AUTO_INCREMENT=5000;
INSERT INTO `users` (name, uid, gid, password) VALUES
('${mysqlUsername}', 5000, 5000, SHA2('${mysqlPassword}', 256));
INSERT INTO `groups` (name, gid) VALUES ('${mysqlGroup}', 5000);
'';
in
{
name = "auth-mysql";
meta.maintainers = with lib.maintainers; [ netali ];
nodes.machine =
{ ... }:
{
services.mysql = {
enable = true;
package = pkgs.mariadb;
settings.mysqld.bind-address = "127.0.0.1";
initialScript = mysqlInit;
};
users.users.${localUsername} = {
isNormalUser = true;
password = localPassword;
};
security.pam.services.login.makeHomeDir = true;
users.mysql = {
enable = true;
host = "127.0.0.1";
user = dbUser;
database = dbName;
passwordFile = "${builtins.toFile "dbPassword" dbPassword}";
pam = {
table = "users";
userColumn = "name";
passwordColumn = "password";
passwordCrypt = "sha256";
disconnectEveryOperation = true;
};
nss = {
getpwnam = ''
SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \
FROM users \
WHERE name='%1$s' \
LIMIT 1
'';
getpwuid = ''
SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \
FROM users \
WHERE uid=%1$u \
LIMIT 1
'';
getspnam = ''
SELECT name, password, 1, 0, 99999, 7, 0, -1, 0 \
FROM users \
WHERE name='%1$s' \
LIMIT 1
'';
getpwent = ''
SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \
FROM users
'';
getspent = ''
SELECT name, password, 1, 0, 99999, 7, 0, -1, 0 \
FROM users
'';
getgrnam = ''
SELECT name, 'x', gid FROM groups WHERE name='%1$s' LIMIT 1
'';
getgrgid = ''
SELECT name, 'x', gid FROM groups WHERE gid='%1$u' LIMIT 1
'';
getgrent = ''
SELECT name, 'x', gid FROM groups
'';
memsbygid = ''
SELECT name FROM users WHERE gid=%1$u
'';
gidsbymem = ''
SELECT gid FROM users WHERE name='%1$s'
'';
};
};
};
testScript = ''
def switch_to_tty(tty_number):
machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'")
machine.send_key(f"alt-f{tty_number}")
machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
machine.wait_for_unit(f"getty@tty{tty_number}.service")
machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
def try_login(tty_number, username, password):
machine.wait_until_tty_matches(tty_number, "login: ")
machine.send_chars(f"{username}\n")
machine.wait_until_tty_matches(tty_number, f"login: {username}")
machine.wait_until_succeeds("pgrep login")
machine.wait_until_tty_matches(tty_number, "Password: ")
machine.send_chars(f"{password}\n")
machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("mysql.service")
machine.wait_until_succeeds("cat /etc/security/pam_mysql.conf | grep users.db_passwd")
machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
with subtest("Local login"):
switch_to_tty("2")
try_login("2", "${localUsername}", "${localPassword}")
machine.wait_until_succeeds("pgrep -u ${localUsername} bash")
machine.send_chars("id > local_id.txt\n")
machine.wait_for_file("/home/${localUsername}/local_id.txt")
machine.succeed("cat /home/${localUsername}/local_id.txt | grep 'uid=1000(${localUsername}) gid=100(users) groups=100(users)'")
with subtest("Local incorrect login"):
switch_to_tty("3")
try_login("3", "${localUsername}", "wrongpassword")
machine.wait_until_tty_matches("3", "Login incorrect")
machine.wait_until_tty_matches("3", "login:")
with subtest("MySQL login"):
switch_to_tty("4")
try_login("4", "${mysqlUsername}", "${mysqlPassword}")
machine.wait_until_succeeds("pgrep -u ${mysqlUsername} bash")
machine.send_chars("id > mysql_id.txt\n")
machine.wait_for_file("/home/${mysqlUsername}/mysql_id.txt")
machine.succeed("cat /home/${mysqlUsername}/mysql_id.txt | grep 'uid=5000(${mysqlUsername}) gid=5000(${mysqlGroup}) groups=5000(${mysqlGroup})'")
with subtest("MySQL incorrect login"):
switch_to_tty("5")
try_login("5", "${mysqlUsername}", "wrongpassword")
machine.wait_until_tty_matches("5", "Login incorrect")
machine.wait_until_tty_matches("5", "login:")
'';
}

183
nixos/tests/authelia.nix Normal file
View File

@@ -0,0 +1,183 @@
# Test Authelia as an auth server for Traefik as a reverse proxy of a local web service
{ lib, ... }:
{
name = "authelia";
meta.maintainers = with lib.maintainers; [ jk ];
nodes = {
authelia =
{
pkgs,
...
}:
{
services.authelia.instances.testing = {
enable = true;
secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile";
secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile";
settings = {
authentication_backend.file.path = "/etc/authelia/users_database.yml";
access_control.default_policy = "one_factor";
session.domain = "example.com";
storage.local.path = "/tmp/db.sqlite3";
notifier.filesystem.filename = "/tmp/notifications.txt";
};
};
# These should not be set from nix but through other means to not leak the secret!
# This is purely for testing purposes!
environment.etc."authelia/storageEncryptionKeyFile" = {
mode = "0400";
user = "authelia-testing";
text = "you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this";
};
environment.etc."authelia/jwtSecretFile" = {
mode = "0400";
user = "authelia-testing";
text = "a_very_important_secret";
};
environment.etc."authelia/users_database.yml" = {
mode = "0400";
user = "authelia-testing";
text = ''
users:
bob:
disabled: false
displayname: bob
# password of password
password: $argon2id$v=19$m=65536,t=3,p=4$2ohUAfh9yetl+utr4tLcCQ$AsXx0VlwjvNnCsa70u4HKZvFkC8Gwajr2pHGKcND/xs
email: bob@jim.com
groups:
- admin
- dev
'';
};
services.traefik = {
enable = true;
dynamicConfigOptions = {
tls.certificates =
let
certDir = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=auth.example.com/CN=static.example.com' -days 36500
mkdir -p $out
cp key.pem cert.pem $out
'';
in
[
{
certFile = "${certDir}/cert.pem";
keyFile = "${certDir}/key.pem";
}
];
http.middlewares.authelia.forwardAuth = {
address = "http://localhost:9091/api/verify?rd=https%3A%2F%2Fauth.example.com%2F";
trustForwardHeader = true;
authResponseHeaders = [
"Remote-User"
"Remote-Groups"
"Remote-Email"
"Remote-Name"
];
};
http.middlewares.authelia-basic.forwardAuth = {
address = "http://localhost:9091/api/verify?auth=basic";
trustForwardHeader = true;
authResponseHeaders = [
"Remote-User"
"Remote-Groups"
"Remote-Email"
"Remote-Name"
];
};
http.routers.simplehttp = {
rule = "Host(`static.example.com`)";
tls = true;
entryPoints = "web";
service = "simplehttp";
};
http.routers.simplehttp-basic-auth = {
rule = "Host(`static-basic-auth.example.com`)";
tls = true;
entryPoints = "web";
service = "simplehttp";
middlewares = [ "authelia-basic@file" ];
};
http.services.simplehttp = {
loadBalancer.servers = [
{
url = "http://localhost:8000";
}
];
};
http.routers.authelia = {
rule = "Host(`auth.example.com`)";
tls = true;
entryPoints = "web";
service = "authelia@file";
};
http.services.authelia = {
loadBalancer.servers = [
{
url = "http://localhost:9091";
}
];
};
};
staticConfigOptions = {
global = {
checkNewVersion = false;
sendAnonymousUsage = false;
};
entryPoints.web.address = ":443";
};
};
systemd.services.simplehttp =
let
fakeWebPageDir = pkgs.writeTextDir "index.html" "hello";
in
{
script = "${pkgs.python3}/bin/python -m http.server --directory ${fakeWebPageDir} 8000";
serviceConfig.Type = "simple";
wantedBy = [ "multi-user.target" ];
};
};
};
testScript = ''
start_all()
authelia.wait_for_unit("simplehttp.service")
authelia.wait_for_unit("traefik.service")
authelia.wait_for_unit("authelia-testing.service")
authelia.wait_for_open_port(443)
authelia.wait_for_unit("multi-user.target")
with subtest("Check for authelia"):
# expect the login page
assert "Login - Authelia", "could not reach authelia" in \
authelia.succeed("curl --insecure -sSf -H Host:auth.example.com https://authelia:443/")
with subtest("Check contacting basic http server via traefik with https works"):
assert "hello", "could not reach raw static site" in \
authelia.succeed("curl --insecure -sSf -H Host:static.example.com https://authelia:443/")
with subtest("Test traefik and authelia"):
with subtest("No details fail"):
authelia.fail("curl --insecure -sSf -H Host:static-basic-auth.example.com https://authelia:443/")
with subtest("Incorrect details fail"):
authelia.fail("curl --insecure -sSf -u 'bob:wordpass' -H Host:static-basic-auth.example.com https://authelia:443/")
authelia.fail("curl --insecure -sSf -u 'alice:password' -H Host:static-basic-auth.example.com https://authelia:443/")
with subtest("Correct details pass"):
assert "hello", "could not reach authed static site with valid credentials" in \
authelia.succeed("curl --insecure -sSf -u 'bob:password' -H Host:static-basic-auth.example.com https://authelia:443/")
'';
}

View File

@@ -0,0 +1,27 @@
{
name = "auto-cpufreq-server";
nodes = {
machine = {
# service will still start but since vm inside qemu cpufreq adjustments
# cannot be made. This will resource in the following error but the service
# remains up:
# ERROR:
# Couldn't find any of the necessary scaling governors.
services.auto-cpufreq = {
enable = true;
settings = {
charger = {
turbo = "auto";
};
};
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("auto-cpufreq.service")
machine.succeed("auto-cpufreq --force reset")
'';
}

23
nixos/tests/autobrr.nix Normal file
View File

@@ -0,0 +1,23 @@
{ lib, ... }:
{
name = "autobrr";
meta.maintainers = with lib.maintainers; [ av-gal ];
nodes.machine =
{ pkgs, ... }:
{
services.autobrr = {
enable = true;
# We create this secret in the Nix store (making it readable by everyone).
# DO NOT DO THIS OUTSIDE OF TESTS!!
secretFile = pkgs.writeText "session_secret" "not-secret";
};
};
testScript = ''
machine.wait_for_unit("autobrr.service")
machine.wait_for_open_port(7474)
machine.succeed("curl --fail http://localhost:7474/")
'';
}

84
nixos/tests/avahi.nix Normal file
View File

@@ -0,0 +1,84 @@
{
pkgs,
# bool: whether to use networkd in the tests
networkd ? false,
...
}:
# Test whether `avahi-daemon' and `libnss-mdns' work as expected.
{
name = "avahi";
meta.maintainers = [ ];
nodes =
let
cfg =
{ ... }:
{
services.avahi = {
enable = true;
nssmdns4 = true;
publish.addresses = true;
publish.domain = true;
publish.enable = true;
publish.userServices = true;
publish.workstation = true;
extraServiceFiles.ssh = "${pkgs.avahi}/etc/avahi/services/ssh.service";
};
}
// pkgs.lib.optionalAttrs networkd {
networking = {
useNetworkd = true;
useDHCP = false;
};
};
in
{
one = cfg;
two = cfg;
};
testScript = ''
start_all()
# mDNS.
one.wait_for_unit("network.target")
two.wait_for_unit("network.target")
one.succeed("avahi-resolve-host-name one.local | tee out >&2")
one.succeed('test "`cut -f1 < out`" = one.local')
one.succeed("avahi-resolve-host-name two.local | tee out >&2")
one.succeed('test "`cut -f1 < out`" = two.local')
two.succeed("avahi-resolve-host-name one.local | tee out >&2")
two.succeed('test "`cut -f1 < out`" = one.local')
two.succeed("avahi-resolve-host-name two.local | tee out >&2")
two.succeed('test "`cut -f1 < out`" = two.local')
# Basic DNS-SD.
one.succeed("avahi-browse -r -t _workstation._tcp | tee out >&2")
one.succeed("test `wc -l < out` -gt 0")
two.succeed("avahi-browse -r -t _workstation._tcp | tee out >&2")
two.succeed("test `wc -l < out` -gt 0")
# More DNS-SD.
one.execute('avahi-publish -s "This is a test" _test._tcp 123 one=1 >&2 &')
one.sleep(5)
two.succeed("avahi-browse -r -t _test._tcp | tee out >&2")
two.succeed("test `wc -l < out` -gt 0")
# NSS-mDNS.
one.succeed("getent hosts one.local >&2")
one.succeed("getent hosts two.local >&2")
two.succeed("getent hosts one.local >&2")
two.succeed("getent hosts two.local >&2")
# extra service definitions
one.succeed("avahi-browse -r -t _ssh._tcp | tee out >&2")
one.succeed("test `wc -l < out` -gt 0")
two.succeed("avahi-browse -r -t _ssh._tcp | tee out >&2")
two.succeed("test `wc -l < out` -gt 0")
one.log(one.execute("systemd-analyze security avahi-daemon.service | grep -v ")[1])
'';
}

127
nixos/tests/ax25.nix Normal file
View File

@@ -0,0 +1,127 @@
{ pkgs, lib, ... }:
let
baud = 57600;
tty = "/dev/ttyACM0";
port = "tnc0";
socatPort = 1234;
createAX25Node = nodeId: {
boot.kernelModules = [ "ax25" ];
networking.firewall.allowedTCPPorts = [ socatPort ];
environment.systemPackages = with pkgs; [
libax25
ax25-tools
ax25-apps
socat
];
services.ax25.axports."${port}" = {
inherit baud tty;
enable = true;
callsign = "NOCALL-${toString nodeId}";
description = "mocked tnc";
};
services.ax25.axlisten = {
enable = true;
};
# All mocks radios will connect back to socat-broker on node 1 in order to get
# all messages that are "broadcasted over the ether"
systemd.services.ax25-mock-hardware = {
description = "mock AX.25 TNC and Radio";
wantedBy = [ "default.target" ];
before = [
"ax25-kissattach-${port}.service"
"axlisten.service"
];
after = [ "network.target" ];
serviceConfig = {
Type = "exec";
ExecStart = "${pkgs.socat}/bin/socat -d -d tcp:192.168.1.1:${toString socatPort} pty,link=${tty},b${toString baud},raw";
};
};
};
in
{
name = "ax25Simple";
nodes = {
node1 = lib.mkMerge [
(createAX25Node 1)
# mimicking radios on the same frequency
{
systemd.services.ax25-mock-ether = {
description = "mock radio ether";
wantedBy = [ "default.target" ];
requires = [ "network.target" ];
before = [ "ax25-mock-hardware.service" ];
# broken needs access to "ss" or "netstat"
path = [ pkgs.iproute2 ];
serviceConfig = {
Type = "exec";
ExecStart = "${pkgs.socat}/bin/socat-broker.sh tcp4-listen:${toString socatPort}";
};
postStart = "${pkgs.coreutils}/bin/sleep 2";
};
}
];
node2 = createAX25Node 2;
node3 = createAX25Node 3;
};
testScript =
{ ... }:
''
def wait_for_machine(m):
m.succeed("lsmod | grep ax25")
m.wait_for_unit("ax25-axports.target")
m.wait_for_unit("axlisten.service")
m.fail("journalctl -o cat -u axlisten.service | grep -i \"no AX.25 port data configured\"")
# start the first node since the socat-broker needs to be running
node1.start()
node1.wait_for_unit("ax25-mock-ether.service")
wait_for_machine(node1)
node2.start()
node3.start()
wait_for_machine(node2)
wait_for_machine(node3)
# Node 1 -> Node 2
node1.succeed("echo hello | ax25_call ${port} NOCALL-1 NOCALL-2")
node2.sleep(1)
node2.succeed("journalctl -o cat -u axlisten.service | grep -A1 \"NOCALL-1 to NOCALL-2 ctl I00\" | grep hello")
# Node 1 -> Node 3
node1.succeed("echo hello | ax25_call ${port} NOCALL-1 NOCALL-3")
node3.sleep(1)
node3.succeed("journalctl -o cat -u axlisten.service | grep -A1 \"NOCALL-1 to NOCALL-3 ctl I00\" | grep hello")
# Node 2 -> Node 1
# must sleep due to previous ax25_call lingering
node2.sleep(5)
node2.succeed("echo hello | ax25_call ${port} NOCALL-2 NOCALL-1")
node1.sleep(1)
node1.succeed("journalctl -o cat -u axlisten.service | grep -A1 \"NOCALL-2 to NOCALL-1 ctl I00\" | grep hello")
# Node 2 -> Node 3
node2.succeed("echo hello | ax25_call ${port} NOCALL-2 NOCALL-3")
node3.sleep(1)
node3.succeed("journalctl -o cat -u axlisten.service | grep -A1 \"NOCALL-2 to NOCALL-3 ctl I00\" | grep hello")
# Node 3 -> Node 1
# must sleep due to previous ax25_call lingering
node3.sleep(5)
node3.succeed("echo hello | ax25_call ${port} NOCALL-3 NOCALL-1")
node1.sleep(1)
node1.succeed("journalctl -o cat -u axlisten.service | grep -A1 \"NOCALL-3 to NOCALL-1 ctl I00\" | grep hello")
# Node 3 -> Node 2
node3.succeed("echo hello | ax25_call ${port} NOCALL-3 NOCALL-2")
node2.sleep(1)
node2.succeed("journalctl -o cat -u axlisten.service | grep -A1 \"NOCALL-3 to NOCALL-2 ctl I00\" | grep hello")
'';
}

View File

@@ -0,0 +1,147 @@
{ pkgs, lib, ... }:
let
user = "alice";
in
{
name = "ayatana-indicators";
meta = {
maintainers = lib.teams.lomiri.members;
};
nodes.machine =
{ config, ... }:
{
imports = [
./common/auto.nix
./common/user-account.nix
];
test-support.displayManager.auto = {
enable = true;
inherit user;
};
services.xserver = {
enable = true;
desktopManager.mate.enable = true;
};
services.displayManager.defaultSession = lib.mkForce "mate";
services.ayatana-indicators = {
enable = true;
packages =
with pkgs;
[
ayatana-indicator-bluetooth
ayatana-indicator-datetime
ayatana-indicator-display
ayatana-indicator-messages
ayatana-indicator-power
ayatana-indicator-session
ayatana-indicator-sound
]
++ (with pkgs.lomiri; [
lomiri-indicator-datetime
lomiri-indicator-network
lomiri-telephony-service
]);
};
# Setup needed by some indicators
services.accounts-daemon.enable = true; # messages
# Lomiri-ish setup for Lomiri indicators
# TODO move into a Lomiri module, once the package set is far enough for the DE to start
networking.networkmanager.enable = true; # lomiri-network-indicator
# TODO potentially urfkill for lomiri-network-indicator?
services.dbus.packages = with pkgs.lomiri; [ libusermetrics ];
environment.systemPackages = with pkgs.lomiri; [ lomiri-schemas ];
services.telepathy.enable = true;
users.users.usermetrics = {
group = "usermetrics";
home = "/var/lib/usermetrics";
createHome = true;
isSystemUser = true;
};
users.groups.usermetrics = { };
};
# TODO session indicator starts up in a semi-broken state, but works fine after a restart. maybe being started before graphical session is truly up & ready?
testScript =
{ nodes, ... }:
let
runCommandOverServiceList = list: command: lib.strings.concatMapStringsSep "\n" command list;
runCommandOverAyatanaIndicators = runCommandOverServiceList nodes.machine.systemd.user.targets.ayatana-indicators.wants;
runCommandOverLomiriIndicators = runCommandOverServiceList nodes.machine.systemd.user.targets.lomiri-indicators.wants;
in
''
start_all()
machine.wait_for_x()
# Desktop environment should reach graphical-session.target
machine.wait_for_unit("graphical-session.target", "${user}")
# MATE relies on XDG autostart to bring up the indicators.
# Not sure *when* XDG autostart fires them up, and awaiting pgrep success seems to misbehave?
machine.sleep(10)
# Now check if all indicators were brought up successfully, and kill them for later
''
+ (runCommandOverAyatanaIndicators (
service:
let
serviceExec = builtins.replaceStrings [ "." ] [ "-" ] service;
in
''
machine.wait_until_succeeds("pgrep -u ${user} -f ${serviceExec}")
machine.succeed("pkill -f ${serviceExec}")
''
))
+ ''
# Ayatana target is the preferred way of starting up indicators on SystemD session, the graphical session is responsible for starting this if it supports them.
# Mate currently doesn't do this, so start it manually for checking (https://github.com/mate-desktop/mate-indicator-applet/issues/63)
machine.systemctl("start ayatana-indicators.target", "${user}")
machine.wait_for_unit("ayatana-indicators.target", "${user}")
# Let all indicator services do their startups, potential post-launch crash & restart cycles so we can properly check for failures
# Not sure if there's a better way of awaiting this without false-positive potential
machine.sleep(10)
# Now check if all indicator services were brought up successfully
''
+ runCommandOverAyatanaIndicators (service: ''
machine.wait_for_unit("${service}", "${user}")
'')
+ ''
# Stop the target
machine.systemctl("stop ayatana-indicators.target", "${user}")
# Let all indicator services do their shutdowns
# Not sure if there's a better way of awaiting this without false-positive potential
machine.sleep(10)
# Lomiri uses a different target, which launches a slightly different set of indicators
machine.systemctl("start lomiri-indicators.target", "${user}")
machine.wait_for_unit("lomiri-indicators.target", "${user}")
# Let all indicator services do their startups, potential post-launch crash & restart cycles so we can properly check for failures
# Not sure if there's a better way of awaiting this without false-positive potential
machine.sleep(10)
# Now check if all indicator services were brought up successfully
''
+ runCommandOverLomiriIndicators (service: ''
machine.wait_for_unit("${service}", "${user}")
'');
}

211
nixos/tests/babeld.nix Normal file
View File

@@ -0,0 +1,211 @@
{
pkgs,
...
}:
{
name = "babeld";
meta = with pkgs.lib.maintainers; {
maintainers = [ hexa ];
};
nodes = {
client =
{ lib, ... }:
{
virtualisation.vlans = [ 10 ];
networking = {
useDHCP = false;
interfaces."eth1" = {
ipv4.addresses = lib.mkForce [
{
address = "192.168.10.2";
prefixLength = 24;
}
];
ipv4.routes = lib.mkForce [
{
address = "0.0.0.0";
prefixLength = 0;
via = "192.168.10.1";
}
];
ipv6.addresses = lib.mkForce [
{
address = "2001:db8:10::2";
prefixLength = 64;
}
];
ipv6.routes = lib.mkForce [
{
address = "::";
prefixLength = 0;
via = "2001:db8:10::1";
}
];
};
};
};
local_router =
{ lib, ... }:
{
virtualisation.vlans = [
10
20
];
networking = {
useDHCP = false;
firewall.enable = false;
interfaces."eth1" = {
ipv4.addresses = lib.mkForce [
{
address = "192.168.10.1";
prefixLength = 24;
}
];
ipv6.addresses = lib.mkForce [
{
address = "2001:db8:10::1";
prefixLength = 64;
}
];
};
interfaces."eth2" = {
ipv4.addresses = lib.mkForce [
{
address = "192.168.20.1";
prefixLength = 24;
}
];
ipv6.addresses = lib.mkForce [
{
address = "2001:db8:20::1";
prefixLength = 64;
}
];
};
};
services.babeld = {
enable = true;
interfaces.eth2 = {
hello-interval = 1;
type = "wired";
};
extraConfig = ''
local-port-readwrite 33123
import-table 254 # main
export-table 254 # main
in ip 192.168.10.0/24 deny
in ip 192.168.20.0/24 deny
in ip 2001:db8:10::/64 deny
in ip 2001:db8:20::/64 deny
in ip 192.168.30.0/24 allow
in ip 2001:db8:30::/64 allow
in deny
redistribute local proto 2
redistribute local deny
'';
};
};
remote_router =
{ lib, ... }:
{
virtualisation.vlans = [
20
30
];
networking = {
useDHCP = false;
firewall.enable = false;
interfaces."eth1" = {
ipv4.addresses = lib.mkForce [
{
address = "192.168.20.2";
prefixLength = 24;
}
];
ipv6.addresses = lib.mkForce [
{
address = "2001:db8:20::2";
prefixLength = 64;
}
];
};
interfaces."eth2" = {
ipv4.addresses = lib.mkForce [
{
address = "192.168.30.1";
prefixLength = 24;
}
];
ipv6.addresses = lib.mkForce [
{
address = "2001:db8:30::1";
prefixLength = 64;
}
];
};
};
services.babeld = {
enable = true;
interfaces.eth1 = {
hello-interval = 1;
type = "wired";
};
extraConfig = ''
local-port-readwrite 33123
import-table 254 # main
export-table 254 # main
in ip 192.168.20.0/24 deny
in ip 192.168.30.0/24 deny
in ip 2001:db8:20::/64 deny
in ip 2001:db8:30::/64 deny
in ip 192.168.10.0/24 allow
in ip 2001:db8:10::/64 allow
in deny
redistribute local proto 2
redistribute local deny
'';
};
};
};
testScript = ''
start_all()
local_router.wait_for_unit("babeld.service")
remote_router.wait_for_unit("babeld.service")
local_router.wait_until_succeeds("ip route get 192.168.30.1")
local_router.wait_until_succeeds("ip route get 2001:db8:30::1")
remote_router.wait_until_succeeds("ip route get 192.168.10.1")
remote_router.wait_until_succeeds("ip route get 2001:db8:10::1")
client.succeed("ping -c1 192.168.30.1")
client.succeed("ping -c1 2001:db8:30::1")
remote_router.succeed("ping -c1 192.168.10.2")
remote_router.succeed("ping -c1 2001:db8:10::2")
'';
}

23
nixos/tests/bazarr.nix Normal file
View File

@@ -0,0 +1,23 @@
{ lib, ... }:
let
port = 42069;
in
{
name = "bazarr";
nodes.machine =
{ pkgs, ... }:
{
services.bazarr = {
enable = true;
listenPort = port;
};
};
testScript = ''
machine.wait_for_unit("bazarr.service")
machine.wait_for_open_port(${toString port})
machine.succeed("curl --fail http://localhost:${toString port}/")
'';
}

40
nixos/tests/bcache.nix Normal file
View File

@@ -0,0 +1,40 @@
{ pkgs, ... }:
{
name = "bcache";
meta.maintainers = with pkgs.lib.maintainers; [ pineapplehunter ];
nodes.machine =
{ pkgs, ... }:
{
virtualisation.emptyDiskImages = [ 4096 ];
networking.hostId = "deadbeef";
boot.supportedFilesystems = [ "ext4" ];
environment.systemPackages = [ pkgs.parted ];
};
testScript = ''
machine.succeed("modprobe bcache")
machine.succeed("bcache version")
machine.succeed("ls /dev")
machine.succeed(
"mkdir /tmp/mnt",
"udevadm settle",
"parted --script /dev/vdb mklabel gpt",
"parted --script /dev/vdb mkpart primary 0% 50% mkpart primary 50% 100%",
"udevadm settle",
"bcache make -C /dev/vdb1",
"bcache make -B /dev/vdb2",
"udevadm settle",
"bcache attach /dev/vdb1 /dev/vdb2",
"bcache set-cachemode /dev/vdb2 writeback",
"udevadm settle",
"bcache show",
"ls /sys/fs/bcache",
"mkfs.ext4 /dev/bcache0",
"mount /dev/bcache0 /tmp/mnt",
"umount /tmp/mnt",
"udevadm settle",
)
'';
}

40
nixos/tests/bcachefs.nix Normal file
View File

@@ -0,0 +1,40 @@
{ pkgs, ... }:
{
name = "bcachefs";
meta = {
inherit (pkgs.bcachefs-tools.meta) maintainers;
};
nodes.machine =
{ pkgs, ... }:
{
virtualisation.emptyDiskImages = [ 4096 ];
networking.hostId = "deadbeef";
boot.supportedFilesystems = [ "bcachefs" ];
environment.systemPackages = with pkgs; [
parted
keyutils
];
};
testScript = ''
machine.succeed("modprobe bcachefs")
machine.succeed("bcachefs version")
machine.succeed("ls /dev")
machine.succeed(
"mkdir /tmp/mnt",
"udevadm settle",
"parted --script /dev/vdb mklabel msdos",
"parted --script /dev/vdb -- mkpart primary 1024M 50% mkpart primary 50% -1s",
"udevadm settle",
"echo password | bcachefs format --encrypted --metadata_replicas 2 --label vtest /dev/vdb1 /dev/vdb2",
"echo password | bcachefs unlock -k session /dev/vdb1",
"echo password | mount -t bcachefs /dev/vdb1:/dev/vdb2 /tmp/mnt",
"udevadm settle",
"bcachefs fs usage /tmp/mnt",
"umount /tmp/mnt",
"udevadm settle",
)
'';
}

View File

@@ -0,0 +1,50 @@
{ pkgs, lib, ... }:
let
pythonEnv = pkgs.python3.withPackages (p: [ p.beanstalkc ]);
produce = pkgs.writeScript "produce.py" ''
#!${pythonEnv.interpreter}
import beanstalkc
queue = beanstalkc.Connection(host='localhost', port=11300, parse_yaml=False);
queue.put(b'this is a job')
queue.put(b'this is another job')
'';
consume = pkgs.writeScript "consume.py" ''
#!${pythonEnv.interpreter}
import beanstalkc
queue = beanstalkc.Connection(host='localhost', port=11300, parse_yaml=False);
job = queue.reserve(timeout=0)
print(job.body.decode('utf-8'))
job.delete()
'';
in
{
name = "beanstalkd";
meta.maintainers = [ lib.maintainers.aanderse ];
nodes.machine =
{ ... }:
{
services.beanstalkd.enable = true;
};
testScript = ''
start_all()
machine.wait_for_unit("beanstalkd.service")
machine.succeed("${produce}")
assert "this is a job\n" == machine.succeed(
"${consume}"
)
assert "this is another job\n" == machine.succeed(
"${consume}"
)
'';
}

76
nixos/tests/bees.nix Normal file
View File

@@ -0,0 +1,76 @@
{ lib, pkgs, ... }:
{
name = "bees";
nodes.machine =
{ config, pkgs, ... }:
{
virtualisation.emptyDiskImages = [
{
size = 4096;
driveConfig.deviceExtraOpts.serial = "aux1";
}
{
size = 4096;
driveConfig.deviceExtraOpts.serial = "aux2";
}
];
virtualisation.fileSystems = {
"/aux1" = {
# filesystem configured to be deduplicated
device = "/dev/disk/by-id/virtio-aux1";
fsType = "btrfs";
autoFormat = true;
};
"/aux2" = {
# filesystem not configured to be deduplicated
device = "/dev/disk/by-id/virtio-aux2";
fsType = "btrfs";
autoFormat = true;
};
};
services.beesd.filesystems = {
aux1 = {
spec = "/dev/disk/by-id/virtio-aux1";
hashTableSizeMB = 16;
verbosity = "debug";
};
};
};
testScript =
let
someContentIsShared =
loc:
pkgs.writeShellScript "some-content-is-shared" ''
[[ $(btrfs fi du -s --raw ${lib.escapeShellArg loc}/dedup-me-{1,2} | awk 'BEGIN { count=0; } NR>1 && $3 == 0 { count++ } END { print count }') -eq 0 ]]
'';
in
''
# shut down the instance started by systemd at boot, so we can test our test procedure
machine.succeed("systemctl stop beesd@aux1.service")
machine.succeed(
"dd if=/dev/urandom of=/aux1/dedup-me-1 bs=1M count=8",
"cp --reflink=never /aux1/dedup-me-1 /aux1/dedup-me-2",
"cp --reflink=never /aux1/* /aux2/",
"sync",
)
machine.fail(
"${someContentIsShared "/aux1"}",
"${someContentIsShared "/aux2"}",
)
machine.succeed("systemctl start beesd@aux1.service")
# assert that "Set Shared" column is nonzero
machine.wait_until_succeeds(
"${someContentIsShared "/aux1"}",
)
machine.fail("${someContentIsShared "/aux2"}")
# assert that 16MB hash table size requested was honored
machine.succeed(
"[[ $(stat -c %s /aux1/.beeshome/beeshash.dat) = $(( 16 * 1024 * 1024)) ]]"
)
'';
}

62
nixos/tests/benchexec.nix Normal file
View File

@@ -0,0 +1,62 @@
{ pkgs, lib, ... }:
let
user = "alice";
in
{
name = "benchexec";
nodes.benchexec = {
imports = [ ./common/user-account.nix ];
programs.benchexec = {
enable = true;
users = [ user ];
};
};
testScript =
{ ... }:
let
runexec = lib.getExe' pkgs.benchexec "runexec";
echo = builtins.toString pkgs.benchexec;
test = lib.getExe (
pkgs.writeShellApplication rec {
name = "test";
meta.mainProgram = name;
text = "echo '${echo}'";
}
);
wd = "/tmp";
stdout = "${wd}/runexec.out";
stderr = "${wd}/runexec.err";
in
''
start_all()
machine.wait_for_unit("multi-user.target")
benchexec.succeed(''''\
systemd-run \
--property='StandardOutput=file:${stdout}' \
--property='StandardError=file:${stderr}' \
--unit=runexec --wait --user --machine='${user}@' \
--working-directory ${wd} \
'${runexec}' \
--debug \
--read-only-dir / \
--hidden-dir /home \
'${test}' \
'''')
benchexec.succeed("grep -s '${echo}' ${wd}/output.log")
benchexec.succeed("test \"$(grep -Ec '((start|wall|cpu)time|memory)=' ${stdout})\" = 4")
benchexec.succeed("! grep -E '(WARNING|ERROR)' ${stderr}")
'';
interactive.nodes.benchexec.services.kmscon = {
enable = true;
fonts = [
{
name = "Fira Code";
package = pkgs.fira-code;
}
];
};
}

View File

@@ -0,0 +1,83 @@
{ lib, compression, ... }:
{
name = "binary-cache-" + compression;
meta.maintainers = with lib.maintainers; [ thomasjm ];
nodes.machine =
{ pkgs, ... }:
{
imports = [ ../modules/installer/cd-dvd/channel.nix ];
environment.systemPackages = with pkgs; [
openssl
python3
];
# We encrypt the binary cache before putting it on the machine so Nix
# doesn't bring any references along.
environment.etc."binary-cache.tar.gz.encrypted".source =
with pkgs;
runCommand "binary-cache.tar.gz.encrypted"
{
allowReferences = [ ];
nativeBuildInputs = [ openssl ];
}
''
tar -czf tmp.tar.gz -C "${
mkBinaryCache {
rootPaths = [ hello ];
inherit compression;
}
}" .
openssl enc -aes-256-cbc -salt -in tmp.tar.gz -out $out -k mysecretpassword
'';
nix.extraOptions = ''
experimental-features = nix-command
'';
};
testScript = ''
# Decrypt the cache into /tmp/binary-cache.tar.gz
machine.succeed("openssl enc -d -aes-256-cbc -in /etc/binary-cache.tar.gz.encrypted -out /tmp/binary-cache.tar.gz -k mysecretpassword")
# Untar the cache into /tmp/cache
machine.succeed("mkdir /tmp/cache")
machine.succeed("tar -C /tmp/cache -xf /tmp/binary-cache.tar.gz")
# Sanity test of cache structure
status, stdout = machine.execute("ls /tmp/cache")
cache_files = stdout.split()
assert ("nix-cache-info" in cache_files)
assert ("nar" in cache_files)
# Nix store ping should work
machine.succeed("nix store ping --store file:///tmp/cache")
# Cache should contain a .narinfo referring to "hello"
grepLogs = machine.succeed("grep -l 'StorePath: /nix/store/[[:alnum:]]*-hello-.*' /tmp/cache/*.narinfo")
# Get the store path referenced by the .narinfo
narInfoFile = grepLogs.strip()
narInfoContents = machine.succeed("cat " + narInfoFile)
import re
match = re.match(r"^StorePath: (/nix/store/[a-z0-9]*-hello-.*)$", narInfoContents, re.MULTILINE)
if not match: raise Exception("Couldn't find hello store path in cache")
storePath = match[1]
# Make sure the store path doesn't exist yet
machine.succeed("[ ! -d %s ] || exit 1" % storePath)
# Should be able to build hello using the cache
logs = machine.succeed("nix-build -A hello '<nixpkgs>' --option require-sigs false --option trusted-substituters file:///tmp/cache --option substituters file:///tmp/cache 2>&1")
logLines = logs.split("\n")
if not "this path will be fetched" in logLines[0]: raise Exception("Unexpected first log line")
def shouldBe(got, desired):
if got != desired: raise Exception("Expected '%s' but got '%s'" % (desired, got))
shouldBe(logLines[1], " " + storePath)
shouldBe(logLines[2], "copying path '%s' from 'file:///tmp/cache'..." % storePath)
shouldBe(logLines[3], storePath)
# Store path should exist in the store now
machine.succeed("[ -d %s ] || exit 1" % storePath)
'';
}

30
nixos/tests/bind.nix Normal file
View File

@@ -0,0 +1,30 @@
{ ... }:
{
name = "bind";
nodes.machine =
{ pkgs, lib, ... }:
{
services.bind.enable = true;
services.bind.extraOptions = "empty-zones-enable no;";
services.bind.zones = lib.singleton {
name = ".";
master = true;
file = pkgs.writeText "root.zone" ''
$TTL 3600
. IN SOA ns.example.org. admin.example.org. ( 1 3h 1h 1w 1d )
. IN NS ns.example.org.
ns.example.org. IN A 192.168.0.1
ns.example.org. IN AAAA abcd::1
1.0.168.192.in-addr.arpa IN PTR ns.example.org.
'';
};
};
testScript = ''
machine.wait_for_unit("bind.service")
machine.succeed("host 192.168.0.1 127.0.0.1 | grep -qF ns.example.org")
'';
}

135
nixos/tests/bird.nix Normal file
View File

@@ -0,0 +1,135 @@
{
runTest,
package,
}:
let
makeBirdHost =
hostId:
{ pkgs, ... }:
{
virtualisation.vlans = [ 1 ];
environment.systemPackages = with pkgs; [ jq ];
networking = {
useNetworkd = true;
useDHCP = false;
firewall.enable = false;
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.${hostId}/24";
};
services.bird = {
inherit package;
enable = true;
config = ''
log syslog all;
debug protocols all;
router id 10.0.0.${hostId};
protocol device {
}
protocol kernel kernel4 {
ipv4 {
import none;
export all;
};
}
protocol static static4 {
ipv4;
include "static4.conf";
}
protocol ospf v2 ospf4 {
ipv4 {
export all;
};
area 0 {
interface "eth1" {
hello 5;
wait 5;
};
};
}
protocol kernel kernel6 {
ipv6 {
import none;
export all;
};
}
protocol static static6 {
ipv6;
include "static6.conf";
}
protocol ospf v3 ospf6 {
ipv6 {
export all;
};
area 0 {
interface "eth1" {
hello 5;
wait 5;
};
};
}
'';
preCheckConfig = ''
echo "route 1.2.3.4/32 blackhole;" > static4.conf
echo "route fd00::/128 blackhole;" > static6.conf
'';
};
systemd.tmpfiles.rules = [
"f /etc/bird/static4.conf - - - - route 10.10.0.${hostId}/32 blackhole;"
"f /etc/bird/static6.conf - - - - route fdff::${hostId}/128 blackhole;"
];
};
in
{
twoNodeOSPF = runTest {
name = "bird-twoNodeOSPF";
nodes.host1 = makeBirdHost "1";
nodes.host2 = makeBirdHost "2";
testScript = ''
start_all()
host1.wait_for_unit("bird.service")
host2.wait_for_unit("bird.service")
host1.succeed("bird --version")
host2.succeed("bird --version")
host1.succeed("systemctl reload bird.service")
with subtest("Waiting for advertised IPv4 routes"):
host1.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.2\")) | any'")
host2.wait_until_succeeds("ip --json r | jq -e 'map(select(.dst == \"10.10.0.1\")) | any'")
with subtest("Waiting for advertised IPv6 routes"):
host1.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::2\")) | any'")
host2.wait_until_succeeds("ip --json -6 r | jq -e 'map(select(.dst == \"fdff::1\")) | any'")
with subtest("Check fake routes in preCheckConfig do not exist"):
host1.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")
host2.fail("ip --json r | jq -e 'map(select(.dst == \"1.2.3.4\")) | any'")
host1.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")
host2.fail("ip --json -6 r | jq -e 'map(select(.dst == \"fd00::\")) | any'")
'';
};
}

View File

@@ -0,0 +1,86 @@
# This test does a basic functionality check for birdwatcher
{
name = "birdwatcher";
nodes.host1 =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [ jq ];
services.bird = {
enable = true;
config = ''
log syslog all;
debug protocols all;
router id 10.0.0.1;
protocol device {
}
protocol kernel kernel4 {
ipv4 {
import none;
export all;
};
}
protocol kernel kernel6 {
ipv6 {
import none;
export all;
};
}
'';
};
services.birdwatcher = {
enable = true;
settings = ''
[server]
allow_from = []
allow_uncached = false
modules_enabled = ["status",
"protocols",
"protocols_bgp",
"protocols_short",
"routes_protocol",
"routes_peer",
"routes_table",
"routes_table_filtered",
"routes_table_peer",
"routes_filtered",
"routes_prefixed",
"routes_noexport",
"routes_pipe_filtered_count",
"routes_pipe_filtered"
]
[status]
reconfig_timestamp_source = "bird"
reconfig_timestamp_match = "# created: (.*)"
filter_fields = []
[bird]
listen = "0.0.0.0:29184"
config = "/etc/bird/bird.conf"
birdc = "${pkgs.bird2}/bin/birdc"
ttl = 5 # time to live (in minutes) for caching of cli output
[parser]
filter_fields = []
[cache]
use_redis = false # if not using redis cache, activate housekeeping to save memory!
[housekeeping]
interval = 5
force_release_memory = true
'';
};
};
testScript = ''
start_all()
host1.wait_for_unit("bird.service")
host1.wait_for_unit("birdwatcher.service")
host1.wait_for_open_port(29184)
host1.succeed("curl http://[::]:29184/status | jq -r .status.message | grep 'Daemon is up and running'")
host1.succeed("curl http://[::]:29184/protocols | jq -r .protocols.device1.state | grep 'up'")
'';
}

View File

@@ -0,0 +1,27 @@
{ lib, ... }:
let
testPort = 8179;
in
{
name = "bitbox-bridge";
meta.maintainers = with lib.maintainers; [
izelnakri
tensor5
];
nodes.machine = {
services.bitbox-bridge = {
enable = true;
port = testPort;
runOnMount = false;
};
};
testScript = ''
start_all()
machine.wait_for_unit("bitbox-bridge.service")
machine.wait_for_open_port(${toString testPort})
machine.wait_until_succeeds("curl -fL http://localhost:${toString testPort}/api/info | grep version")
'';
}

55
nixos/tests/bitcoind.nix Normal file
View File

@@ -0,0 +1,55 @@
{ pkgs, ... }:
{
name = "bitcoind";
meta = with pkgs.lib; {
maintainers = with maintainers; [ _1000101 ];
};
nodes.machine =
{ ... }:
{
services.bitcoind."mainnet" = {
enable = true;
rpc = {
port = 8332;
users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
users.rpc2.passwordHMAC = "1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225";
};
};
environment.etc."test.blank".text = "";
services.bitcoind."testnet" = {
enable = true;
configFile = "/etc/test.blank";
testnet = true;
rpc = {
port = 18332;
};
extraCmdlineOptions = [
"-rpcuser=rpc"
"-rpcpassword=rpc"
"-rpcauth=rpc2:1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225"
];
};
};
testScript = ''
start_all()
machine.wait_for_unit("bitcoind-mainnet.service")
machine.wait_for_unit("bitcoind-testnet.service")
machine.wait_until_succeeds(
'curl --fail --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 | grep \'"chain":"main"\' '
)
machine.wait_until_succeeds(
'curl --fail --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:8332 | grep \'"chain":"main"\' '
)
machine.wait_until_succeeds(
'curl --fail --user rpc:rpc --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 | grep \'"chain":"test"\' '
)
machine.wait_until_succeeds(
'curl --fail --user rpc2:rpc2 --data-binary \'{"jsonrpc": "1.0", "id":"curltest", "method": "getblockchaininfo", "params": [] }\' -H \'content-type: text/plain;\' localhost:18332 | grep \'"chain":"test"\' '
)
'';
}

201
nixos/tests/bittorrent.nix Normal file
View File

@@ -0,0 +1,201 @@
# This test runs a Bittorrent tracker on one machine, and verifies
# that two client machines can download the torrent using
# `transmission'. The first client (behind a NAT router) downloads
# from the initial seeder running on the tracker. Then we kill the
# initial seeder. The second client downloads from the first client,
# which only works if the first client successfully uses the UPnP-IGD
# protocol to poke a hole in the NAT.
{ pkgs, ... }:
let
# Some random file to serve.
file = pkgs.hello.src;
internalRouterAddress = "192.168.3.1";
internalClient1Address = "192.168.3.2";
externalRouterAddress = "80.100.100.1";
externalClient2Address = "80.100.100.2";
externalTrackerAddress = "80.100.100.3";
download-dir = "/var/lib/transmission/Downloads";
transmissionConfig =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.transmission_3 ];
services.transmission = {
enable = true;
settings = {
dht-enabled = false;
message-level = 2;
inherit download-dir;
};
};
};
in
{
name = "bittorrent";
meta = with pkgs.lib.maintainers; {
maintainers = [
rob
bobvanderlinden
];
};
nodes = {
tracker =
{ pkgs, ... }:
{
imports = [ transmissionConfig ];
virtualisation.vlans = [ 1 ];
networking.firewall.enable = false;
networking.interfaces.eth1.ipv4.addresses = [
{
address = externalTrackerAddress;
prefixLength = 24;
}
];
# We need Apache on the tracker to serve the torrents.
services.httpd = {
enable = true;
virtualHosts = {
"torrentserver.org" = {
adminAddr = "foo@example.org";
documentRoot = "/tmp";
};
};
};
services.opentracker.enable = true;
};
router =
{ pkgs, nodes, ... }:
{
virtualisation.vlans = [
1
2
];
networking.nat.enable = true;
networking.nat.internalInterfaces = [ "eth2" ];
networking.nat.externalInterface = "eth1";
networking.firewall.enable = true;
networking.firewall.trustedInterfaces = [ "eth2" ];
networking.interfaces.eth0.ipv4.addresses = [ ];
networking.interfaces.eth1.ipv4.addresses = [
{
address = externalRouterAddress;
prefixLength = 24;
}
];
networking.interfaces.eth2.ipv4.addresses = [
{
address = internalRouterAddress;
prefixLength = 24;
}
];
services.miniupnpd = {
enable = true;
externalInterface = "eth1";
internalIPs = [ "eth2" ];
appendConfig = ''
ext_ip=${externalRouterAddress}
'';
};
};
client1 =
{ pkgs, nodes, ... }:
{
imports = [ transmissionConfig ];
environment.systemPackages = [ pkgs.miniupnpc ];
virtualisation.vlans = [ 2 ];
networking.interfaces.eth0.ipv4.addresses = [ ];
networking.interfaces.eth1.ipv4.addresses = [
{
address = internalClient1Address;
prefixLength = 24;
}
];
networking.defaultGateway = internalRouterAddress;
networking.firewall.enable = false;
};
client2 =
{ pkgs, ... }:
{
imports = [ transmissionConfig ];
virtualisation.vlans = [ 1 ];
networking.interfaces.eth0.ipv4.addresses = [ ];
networking.interfaces.eth1.ipv4.addresses = [
{
address = externalClient2Address;
prefixLength = 24;
}
];
networking.firewall.enable = false;
};
};
testScript =
{ nodes, ... }:
''
start_all()
# Wait for network and miniupnpd.
router.systemctl("start network-online.target")
router.wait_for_unit("network-online.target")
router.wait_for_unit("miniupnpd")
# Create the torrent.
tracker.succeed("mkdir ${download-dir}/data")
tracker.succeed(
"cp ${file} ${download-dir}/data/test.tar.bz2"
)
tracker.succeed(
"transmission-create ${download-dir}/data/test.tar.bz2 --private --tracker http://${externalTrackerAddress}:6969/announce --outfile /tmp/test.torrent"
)
tracker.succeed("chmod 644 /tmp/test.torrent")
# Start the tracker. !!! use a less crappy tracker
tracker.systemctl("start network-online.target")
tracker.wait_for_unit("network-online.target")
tracker.wait_for_unit("opentracker.service")
tracker.wait_for_open_port(6969)
# Start the initial seeder.
tracker.succeed(
"transmission-remote --add /tmp/test.torrent --no-portmap --no-dht --download-dir ${download-dir}/data"
)
# Now we should be able to download from the client behind the NAT.
tracker.wait_for_unit("httpd")
client1.systemctl("start network-online.target")
client1.wait_for_unit("network-online.target")
client1.succeed("transmission-remote --add http://${externalTrackerAddress}/test.torrent >&2 &")
client1.wait_for_file("${download-dir}/test.tar.bz2")
client1.succeed(
"cmp ${download-dir}/test.tar.bz2 ${file}"
)
# Bring down the initial seeder.
tracker.stop_job("transmission")
# Now download from the second client. This can only succeed if
# the first client created a NAT hole in the router.
client2.systemctl("start network-online.target")
client2.wait_for_unit("network-online.target")
client2.succeed(
"transmission-remote --add http://${externalTrackerAddress}/test.torrent --no-portmap --no-dht >&2 &"
)
client2.wait_for_file("${download-dir}/test.tar.bz2")
client2.succeed(
"cmp ${download-dir}/test.tar.bz2 ${file}"
)
'';
}

30
nixos/tests/blint.nix Normal file
View File

@@ -0,0 +1,30 @@
{
lib,
pkgs,
...
}:
{
name = "owasp blint test";
meta.maintainers = with lib; [
maintainers.ethancedwards8
teams.ngi
];
nodes.machine = {
environment.systemPackages = with pkgs; [
blint
jq
];
};
testScript =
{ nodes, ... }:
''
start_all()
machine.succeed('blint -i ${lib.getExe pkgs.ripgrep} -o /tmp/ripgrep')
machine.succeed('jq . /tmp/ripgrep/*.json')
'';
}

View File

@@ -0,0 +1,31 @@
{ pkgs, ... }:
{
name = "blockbook-frontend";
meta = with pkgs.lib; {
maintainers = with maintainers; [ _1000101 ];
};
nodes.machine =
{ ... }:
{
services.blockbook-frontend."test" = {
enable = true;
};
services.bitcoind.mainnet = {
enable = true;
rpc = {
port = 8030;
users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("blockbook-frontend-test.service")
machine.wait_for_open_port(9030)
machine.succeed("curl -sSfL http://localhost:9030 | grep 'Blockbook'")
'';
}

39
nixos/tests/blocky.nix Normal file
View File

@@ -0,0 +1,39 @@
{
name = "blocky";
nodes = {
server =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.dnsutils ];
services.blocky = {
enable = true;
settings = {
customDNS = {
mapping = {
"printer.lan" = "192.168.178.3,2001:0db8:85a3:08d3:1319:8a2e:0370:7344";
};
};
upstream = {
default = [
"8.8.8.8"
"1.1.1.1"
];
};
port = 53;
httpPort = 5000;
logLevel = "info";
};
};
};
};
testScript = ''
with subtest("Service test"):
server.wait_for_unit("blocky.service")
server.wait_for_open_port(53)
server.wait_for_open_port(5000)
server.succeed("dig @127.0.0.1 +short -x 192.168.178.3 | grep -qF printer.lan")
'';
}

View File

@@ -0,0 +1,27 @@
{ lib, ... }:
{
name = "PDS";
nodes.machine = {
services.bluesky-pds = {
enable = true;
settings = {
PDS_PORT = 3000;
PDS_HOSTNAME = "example.com";
# Snake oil testing credentials
PDS_JWT_SECRET = "7b93fee53be046bf59c27a32a0fb2069";
PDS_ADMIN_PASSWORD = "3a4077bc0d5f04eca945ef0509f7e809";
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX = "ae4f5028d04c833ba630f29debd5ff80b7700e43e9f4bf70f729a88cd6a6ce35";
};
};
};
testScript = ''
machine.wait_for_unit("bluesky-pds.service")
machine.wait_for_open_port(3000)
machine.succeed("curl --fail http://localhost:3000")
'';
meta.maintainers = with lib.maintainers; [ t4ccer ];
}

52
nixos/tests/bookstack.nix Normal file
View File

@@ -0,0 +1,52 @@
{ pkgs, ... }:
let
app-key = "TestTestTestTestTestTestTestTest";
in
{
name = "bookstack";
meta = {
maintainers = [ pkgs.lib.maintainers.savyajha ];
platforms = pkgs.lib.platforms.linux;
};
nodes.bookstackMysql = {
services.bookstack = {
enable = true;
hostname = "localhost";
nginx.onlySSL = false;
settings = {
APP_KEY_FILE = pkgs.writeText "bookstack-appkey" app-key;
LOG_CHANNEL = "stdout";
SITE_OWNER = "mail@example.com";
DB_DATABASE = "bookstack";
DB_USERNAME = "bookstack";
DB_SOCKET = "/run/mysqld/mysqld.sock";
};
};
services.mysql = {
enable = true;
package = pkgs.mariadb;
settings.mysqld.character-set-server = "utf8mb4";
ensureDatabases = [
"bookstack"
];
ensureUsers = [
{
name = "bookstack";
ensurePermissions = {
"bookstack.*" = "ALL PRIVILEGES";
};
}
];
};
};
testScript = ''
bookstackMysql.wait_for_unit("phpfpm-bookstack.service")
bookstackMysql.wait_for_unit("nginx.service")
bookstackMysql.wait_for_unit("mysql.service")
bookstackMysql.succeed("curl -fvvv -Ls http://localhost/ | grep 'Log In'")
'';
}

191
nixos/tests/boot-stage1.nix Normal file
View File

@@ -0,0 +1,191 @@
{ pkgs, ... }:
{
name = "boot-stage1";
nodes.machine =
{
config,
pkgs,
lib,
...
}:
{
boot.extraModulePackages =
let
compileKernelModule =
name: source:
pkgs.runCommandCC name
rec {
inherit source;
kdev = config.boot.kernelPackages.kernel.dev;
kver = config.boot.kernelPackages.kernel.modDirVersion;
ksrc = "${kdev}/lib/modules/${kver}/build";
hardeningDisable = [ "pic" ];
nativeBuildInputs = kdev.moduleBuildDependencies;
}
''
echo "obj-m += $name.o" > Makefile
echo "$source" > "$name.c"
make -C "$ksrc" M=$(pwd) modules
install -vD "$name.ko" "$out/lib/modules/$kver/$name.ko"
'';
# This spawns a kthread which just waits until it gets a signal and
# terminates if that is the case. We want to make sure that nothing during
# the boot process kills any kthread by accident, like what happened in
# issue #15226.
kcanary = compileKernelModule "kcanary" ''
#include <linux/version.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/signal.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 10, 0)
#include <linux/sched/signal.h>
#endif
MODULE_LICENSE("GPL");
struct task_struct *canaryTask;
static int kcanary(void *nothing)
{
allow_signal(SIGINT);
allow_signal(SIGTERM);
allow_signal(SIGKILL);
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout_interruptible(msecs_to_jiffies(100));
if (signal_pending(current)) break;
}
return 0;
}
static int kcanaryInit(void)
{
kthread_run(&kcanary, NULL, "kcanary");
return 0;
}
static void kcanaryExit(void)
{
kthread_stop(canaryTask);
}
module_init(kcanaryInit);
module_exit(kcanaryExit);
'';
in
lib.singleton kcanary;
boot.initrd.kernelModules = [ "kcanary" ];
boot.initrd.extraUtilsCommands =
let
compile =
name: source:
pkgs.runCommandCC name { inherit source; } ''
mkdir -p "$out/bin"
echo "$source" | gcc -Wall -o "$out/bin/$name" -xc -
'';
daemonize =
name: source:
compile name ''
#include <stdio.h>
#include <unistd.h>
void runSource(void) {
${source}
}
int main(void) {
if (fork() > 0) return 0;
setsid();
runSource();
return 1;
}
'';
mkCmdlineCanary =
{
name,
cmdline ? "",
source ? "",
}:
(daemonize name ''
char *argv[] = {"${cmdline}", NULL};
execvp("${name}-child", argv);
'')
// {
child = compile "${name}-child" ''
#include <stdio.h>
#include <unistd.h>
int main(void) {
${source}
while (1) sleep(1);
return 1;
}
'';
};
copyCanaries = lib.concatMapStrings (canary: ''
${lib.optionalString (canary ? child) ''
copy_bin_and_libs "${canary.child}/bin/${canary.child.name}"
''}
copy_bin_and_libs "${canary}/bin/${canary.name}"
'');
in
copyCanaries [
# Simple canary process which just sleeps forever and should be killed by
# stage 2.
(daemonize "canary1" "while (1) sleep(1);")
# We want this canary process to try mimicking a kthread using a cmdline
# with a zero length so we can make sure that the process is properly
# killed in stage 1.
(mkCmdlineCanary {
name = "canary2";
source = ''
FILE *f;
f = fopen("/run/canary2.pid", "w");
fprintf(f, "%d\n", getpid());
fclose(f);
'';
})
# This canary process mimics a storage daemon, which we do NOT want to be
# killed before going into stage 2. For more on root storage daemons, see:
# https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/
(mkCmdlineCanary {
name = "canary3";
cmdline = "@canary3";
})
];
boot.initrd.postMountCommands = ''
canary1
canary2
canary3
# Make sure the pidfile of canary 2 is created so that we still can get
# its former pid after the killing spree starts next within stage 1.
while [ ! -s /run/canary2.pid ]; do sleep 0.1; done
'';
};
testScript = ''
machine.wait_for_unit("multi-user.target")
machine.succeed("test -s /run/canary2.pid")
machine.fail("pgrep -a canary1")
machine.fail("kill -0 $(< /run/canary2.pid)")
machine.succeed('pgrep -a -f "^@canary3$"')
machine.succeed('pgrep -a -f "^\\[kcanary\\]$"')
'';
meta.maintainers = with pkgs.lib.maintainers; [ aszlig ];
}

131
nixos/tests/boot-stage2.nix Normal file
View File

@@ -0,0 +1,131 @@
{ pkgs, ... }:
{
name = "boot-stage2";
nodes.machine =
{
config,
pkgs,
lib,
...
}:
let
# Prints the user's UID. Can't just do a shell script
# because setuid is ignored for interpreted programs.
uid = pkgs.writeCBin "uid" ''
#include <unistd.h>
#include <stdio.h>
int main(void) {
printf("%d\n", geteuid());
return 0;
}
'';
in
{
users.users.alice = {
isNormalUser = true;
uid = 1000;
};
virtualisation = {
emptyDiskImages = [ 256 ];
# Mount an ext4 as the upper layer of the Nix store.
fileSystems = {
"/nix/store" = lib.mkForce {
device = "/dev/vdb"; # the above disk image
fsType = "ext4";
# data=journal always displays after errors=remount-ro; this is only needed because of the overlay
# and #375257 will trigger with `errors=remount-ro` on a non-overlaid store:
# see ordering in https://github.com/torvalds/linux/blob/v6.12/fs/ext4/super.c#L2974
options = [
"defaults"
"errors=remount-ro"
"data=journal"
];
};
};
};
environment.systemPackages = [ pkgs.xxd ];
system.extraDependencies = [ uid ];
boot = {
initrd = {
# Format the upper Nix store.
postDeviceCommands = ''
${pkgs.e2fsprogs}/bin/mkfs.ext4 /dev/vdb
'';
# Overlay the RO store onto it.
# Note that bug #375257 can be triggered without an overlay,
# using the errors=remount-ro option (or similar) or with an overlay where any of the
# paths ends in 'ro'. The offending mountpoint also has to be the last (top) one
# if an option ending in 'ro' is the last in the list, so test both cases here.
postMountCommands = ''
mkdir -p /mnt-root/nix/store/ro /mnt-root/nix/store/rw /mnt-root/nix/store/work
mount --bind /mnt-root/nix/.ro-store /mnt-root/nix/store/ro
mount -t overlay overlay \
-o lowerdir=/mnt-root/nix/store/ro,upperdir=/mnt-root/nix/store/rw,workdir=/mnt-root/nix/store/work \
/mnt-root/nix/store
# Be very rude and try to put suid files and/or devices into the store.
evil=/mnt-root/nix/store/evil
mkdir -p $evil/bin $evil/dev
echo "making evil suid..." >&2
cp /mnt-root/${builtins.unsafeDiscardStringContext "${uid}"}/bin/uid $evil/bin/suid
chmod 4755 $evil/bin/suid
[ -u $evil/bin/suid ] || exit 1
echo "making evil devzero..." >&2
mknod -m 666 $evil/dev/zero c 1 5
[ -c $evil/dev/zero ] || exit 1
'';
kernelModules = [ "overlay" ];
};
postBootCommands = ''
touch /etc/post-boot-ran
mount
'';
};
};
testScript = ''
machine.wait_for_unit("multi-user.target")
machine.succeed("test /etc/post-boot-ran")
machine.fail("touch /nix/store/should-not-work");
for opt in ["ro", "nosuid", "nodev"]:
with subtest(f"testing store mount option: {opt}"):
machine.succeed(f'[[ "$(findmnt --direction backward --first-only --noheadings --output OPTIONS /nix/store)" =~ (^|,){opt}(,|$) ]]')
# should still be suid
machine.succeed('[ -u /nix/store/evil/bin/suid ]')
# runs as alice and is not root
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
# can be remounted and runs as root
machine.succeed('mount -o remount,suid,bind /nix/store && mount >&2')
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 0 ]')
# double checking we can undo it
machine.succeed('mount -o remount,nosuid,bind /nix/store && mount >&2')
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]')
# should still be a character device
machine.succeed('[ -c /nix/store/evil/dev/zero ]')
# should not work
machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
# can be remounted and works
machine.succeed('mount -o remount,dev,bind /nix/store && mount >&2')
machine.succeed('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
# double checking we can undo it
machine.succeed('mount -o remount,nodev,bind /nix/store && mount >&2')
machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]')
'';
meta.maintainers = with pkgs.lib.maintainers; [ numinit ];
}

223
nixos/tests/boot.nix Normal file
View File

@@ -0,0 +1,223 @@
{
system ? builtins.currentSystem,
config ? { },
pkgs ? import ../.. { inherit system config; },
}:
with import ../lib/testing-python.nix { inherit system pkgs; };
let
lib = pkgs.lib;
qemu-common = import ../lib/qemu-common.nix { inherit lib pkgs; };
mkStartCommand =
{
memory ? 2048,
cdrom ? null,
usb ? null,
pxe ? null,
uboot ? false,
uefi ? false,
extraFlags ? [ ],
}:
let
qemu = qemu-common.qemuBinary pkgs.qemu_test;
flags = [
"-m"
(toString memory)
"-netdev"
("user,id=net0" + (lib.optionalString (pxe != null) ",tftp=${pxe},bootfile=netboot.ipxe"))
"-device"
(
"virtio-net-pci,netdev=net0"
+ (lib.optionalString (pxe != null && uefi) ",romfile=${pkgs.ipxe}/ipxe.efirom")
)
]
++ lib.optionals (cdrom != null) [
"-cdrom"
cdrom
]
++ lib.optionals (usb != null) [
"-device"
"usb-ehci"
"-drive"
"id=usbdisk,file=${usb},if=none,readonly"
"-device"
"usb-storage,drive=usbdisk"
]
++ lib.optionals (pxe != null) [
"-boot"
"order=n"
]
++ lib.optionals uefi [
"-drive"
"if=pflash,format=raw,unit=0,readonly=on,file=${pkgs.OVMF.firmware}"
"-drive"
"if=pflash,format=raw,unit=1,readonly=on,file=${pkgs.OVMF.variables}"
]
++ extraFlags;
flagsStr = lib.concatStringsSep " " flags;
in
"${qemu} ${flagsStr}";
iso =
(import ../lib/eval-config.nix {
system = null;
modules = [
../modules/installer/cd-dvd/installation-cd-minimal.nix
../modules/testing/test-instrumentation.nix
{ nixpkgs.pkgs = pkgs; }
];
}).config.system.build.isoImage;
sd =
(import ../lib/eval-config.nix {
system = null;
modules = [
../modules/installer/sd-card/sd-image-x86_64.nix
../modules/testing/test-instrumentation.nix
{
sdImage.compressImage = false;
nixpkgs.pkgs = pkgs;
}
];
}).config.system.build.sdImage;
makeBootTest =
name: config:
let
startCommand = mkStartCommand config;
in
makeTest {
name = "boot-" + name;
nodes = { };
testScript = ''
machine = create_machine("${startCommand}")
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("nix store verify --no-trust -r --option experimental-features nix-command /run/current-system")
with subtest("Check whether the channel got installed correctly"):
machine.succeed("nix-instantiate --dry-run '<nixpkgs>' -A hello")
machine.succeed("nix-env --dry-run -iA nixos.procps")
machine.shutdown()
'';
};
makeNetbootTest =
name: extraConfig:
let
config =
(import ../lib/eval-config.nix {
system = null;
modules = [
../modules/installer/netboot/netboot.nix
../modules/testing/test-instrumentation.nix
{
boot.kernelParams = [
"serial"
"live.nixos.passwordHash=$6$jnwR50SkbLYEq/Vp$wmggwioAkfmwuYqd5hIfatZWS/bO6hewzNIwIrWcgdh7k/fhUzZT29Vil3ioMo94sdji/nipbzwEpxecLZw0d0" # "password"
];
nixpkgs.pkgs = pkgs;
}
{
key = "serial";
}
];
}).config;
ipxeBootDir = pkgs.symlinkJoin {
name = "ipxeBootDir";
paths = [
config.system.build.netbootRamdisk
config.system.build.kernel
config.system.build.netbootIpxeScript
];
};
startCommand = mkStartCommand (
{
pxe = ipxeBootDir;
}
// extraConfig
);
in
makeTest {
name = "boot-netboot-" + name;
nodes = { };
testScript = ''
machine = create_machine("${startCommand}")
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("grep 'serial' /proc/cmdline")
machine.succeed("grep 'live.nixos.passwordHash' /proc/cmdline")
machine.succeed("grep '$6$jnwR50SkbLYEq/Vp$wmggwioAkfmwuYqd5hIfatZWS/bO6hewzNIwIrWcgdh7k/fhUzZT29Vil3ioMo94sdji/nipbzwEpxecLZw0d0' /etc/shadow")
machine.shutdown()
'';
};
in
{
uefiCdrom = makeBootTest "uefi-cdrom" {
uefi = true;
cdrom = "${iso}/iso/${iso.isoName}";
};
uefiUsb = makeBootTest "uefi-usb" {
uefi = true;
usb = "${iso}/iso/${iso.isoName}";
};
uefiNetboot = makeNetbootTest "uefi" {
uefi = true;
};
}
// lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
biosCdrom = makeBootTest "bios-cdrom" {
cdrom = "${iso}/iso/${iso.isoName}";
};
biosUsb = makeBootTest "bios-usb" {
usb = "${iso}/iso/${iso.isoName}";
};
biosNetboot = makeNetbootTest "bios" { };
ubootExtlinux =
let
sdImage = "${sd}/sd-image/${sd.imageName}";
mutableImage = "/tmp/linked-image.qcow2";
startCommand = mkStartCommand {
extraFlags = [
"-bios"
"${pkgs.ubootQemuX86}/u-boot.rom"
"-machine"
"type=pc,accel=tcg"
"-drive"
"file=${mutableImage},if=virtio"
];
};
in
makeTest {
name = "boot-uboot-extlinux";
nodes = { };
testScript = ''
import os
# Create a mutable linked image backed by the read-only SD image
if os.system("qemu-img create -f qcow2 -F raw -b ${sdImage} ${mutableImage}") != 0:
raise RuntimeError("Could not create mutable linked image")
machine = create_machine("${startCommand}")
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("nix store verify -r --no-trust --option experimental-features nix-command /run/current-system")
machine.shutdown()
'';
# kernel can't find rootfs after boot - investigate?
meta.broken = true;
};
}

203
nixos/tests/bootspec.nix Normal file
View File

@@ -0,0 +1,203 @@
{
system ? builtins.currentSystem,
config ? { },
pkgs ? import ../.. { inherit system config; },
}:
with import ../lib/testing-python.nix { inherit system pkgs; };
with pkgs.lib;
let
baseline = {
virtualisation.useBootLoader = true;
};
grub = {
boot.loader.grub.enable = true;
};
systemd-boot = {
boot.loader.systemd-boot.enable = true;
};
uefi = {
virtualisation.useEFIBoot = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.grub.efiSupport = true;
environment.systemPackages = [ pkgs.efibootmgr ];
};
standard = {
boot.bootspec.enable = true;
imports = [
baseline
systemd-boot
uefi
];
};
in
{
basic = makeTest {
name = "systemd-boot-with-bootspec";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine = standard;
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /run/current-system/boot.json")
'';
};
grub = makeTest {
name = "grub-with-bootspec";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine = {
boot.bootspec.enable = true;
imports = [
baseline
grub
uefi
];
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /run/current-system/boot.json")
'';
};
legacy-boot = makeTest {
name = "legacy-boot-with-bootspec";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine = {
boot.bootspec.enable = true;
imports = [
baseline
grub
];
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /run/current-system/boot.json")
'';
};
# Check that initrd create corresponding entries in bootspec.
initrd = makeTest {
name = "bootspec-with-initrd";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine = {
imports = [ standard ];
environment.systemPackages = [ pkgs.jq ];
# It's probably the case, but we want to make it explicit here.
boot.initrd.enable = true;
};
testScript = ''
import json
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /run/current-system/boot.json")
bootspec = json.loads(machine.succeed("jq -r '.\"org.nixos.bootspec.v1\"' /run/current-system/boot.json"))
assert 'initrd' in bootspec, "Bootspec should contain initrd field when initrd is enabled"
assert 'initrdSecrets' not in bootspec, "Bootspec should not contain initrdSecrets when there's no initrdSecrets"
'';
};
# Check that initrd secrets create corresponding entries in bootspec.
initrd-secrets = makeTest {
name = "bootspec-with-initrd-secrets";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine = {
imports = [ standard ];
environment.systemPackages = [ pkgs.jq ];
# It's probably the case, but we want to make it explicit here.
boot.initrd.enable = true;
boot.initrd.secrets."/some/example" = pkgs.writeText "example-secret" "test";
};
testScript = ''
import json
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /run/current-system/boot.json")
bootspec = json.loads(machine.succeed("jq -r '.\"org.nixos.bootspec.v1\"' /run/current-system/boot.json"))
assert 'initrdSecrets' in bootspec, "Bootspec should contain an 'initrdSecrets' field given there's an initrd secret"
'';
};
# Check that specialisations create corresponding entries in bootspec.
specialisation = makeTest {
name = "bootspec-with-specialisation";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine = {
imports = [ standard ];
environment.systemPackages = [ pkgs.jq ];
specialisation.something.configuration = { };
};
testScript = ''
import json
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("test -e /run/current-system/boot.json")
machine.succeed("test -e /run/current-system/specialisation/something/boot.json")
sp_in_parent = json.loads(machine.succeed("jq -r '.\"org.nixos.specialisation.v1\".something' /run/current-system/boot.json"))
sp_in_fs = json.loads(machine.succeed("cat /run/current-system/specialisation/something/boot.json"))
assert sp_in_parent['org.nixos.bootspec.v1'] == sp_in_fs['org.nixos.bootspec.v1'], "Bootspecs of the same specialisation are different!"
'';
};
# Check that extensions are propagated.
extensions = makeTest {
name = "bootspec-with-extensions";
meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
nodes.machine =
{ config, ... }:
{
imports = [ standard ];
environment.systemPackages = [ pkgs.jq ];
boot.bootspec.extensions = {
"org.nix-tests.product" = {
osRelease = config.environment.etc."os-release".source;
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
current_os_release = machine.succeed("cat /etc/os-release")
bootspec_os_release = machine.succeed("cat $(jq -r '.\"org.nix-tests.product\".osRelease' /run/current-system/boot.json)")
assert current_os_release == bootspec_os_release, "Filename referenced by extension has unexpected contents"
'';
};
}

286
nixos/tests/borgbackup.nix Normal file
View File

@@ -0,0 +1,286 @@
{ pkgs, ... }:
let
passphrase = "supersecret";
dataDir = "/ran:dom/data";
subDir = "not_anything_here";
excludedSubDirFile = "not_this_file_either";
excludeFile = "not_this_file";
keepFile = "important_file";
keepFileData = "important_data";
localRepo = "/root/back:up";
# a repository on a file system which is not mounted automatically
localRepoMount = "/noAutoMount";
archiveName = "my_archive";
remoteRepo = "borg@server:."; # No need to specify path
privateKey = pkgs.writeText "id_ed25519" ''
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrwAAAJB+cF5HfnBe
RwAAAAtzc2gtZWQyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrw
AAAEBN75NsJZSpt63faCuaD75Unko0JjlSDxMhYHAPJk2/xXHxQHThDpD9/AMWNqQer3Tg
9gXMb2lTZMn0pelo8xyvAAAADXJzY2h1ZXR6QGt1cnQ=
-----END OPENSSH PRIVATE KEY-----
'';
publicKey = ''
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHxQHThDpD9/AMWNqQer3Tg9gXMb2lTZMn0pelo8xyv root@client
'';
privateKeyAppendOnly = pkgs.writeText "id_ed25519" ''
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBacZuz1ELGQdhI7PF6dGFafCDlvh8pSEc4cHjkW0QjLwAAAJC9YTxxvWE8
cQAAAAtzc2gtZWQyNTUxOQAAACBacZuz1ELGQdhI7PF6dGFafCDlvh8pSEc4cHjkW0QjLw
AAAEAAhV7wTl5dL/lz+PF/d4PnZXuG1Id6L/mFEiGT1tZsuFpxm7PUQsZB2Ejs8Xp0YVp8
IOW+HylIRzhweORbRCMvAAAADXJzY2h1ZXR6QGt1cnQ=
-----END OPENSSH PRIVATE KEY-----
'';
publicKeyAppendOnly = ''
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFpxm7PUQsZB2Ejs8Xp0YVp8IOW+HylIRzhweORbRCMv root@client
'';
in
{
name = "borgbackup";
meta = with pkgs.lib; {
maintainers = with maintainers; [ dotlambda ];
};
nodes = {
client =
{ ... }:
{
virtualisation.fileSystems.${localRepoMount} = {
device = "tmpfs";
fsType = "tmpfs";
options = [ "noauto" ];
};
services.borgbackup.jobs = {
local = {
paths = dataDir;
repo = localRepo;
preHook = ''
# Don't append a timestamp
archiveName="${archiveName}"
'';
encryption = {
mode = "repokey";
inherit passphrase;
};
compression = "auto,zlib,9";
prune.keep = {
within = "1y";
yearly = 5;
};
exclude = [ "*/${excludeFile}" ];
extraCreateArgs = [
"--exclude-caches"
"--exclude-if-present"
".dont backup"
];
wrapper = "borg-main";
postHook = "echo post";
startAt = [ ]; # Do not run automatically
};
localMount = {
paths = dataDir;
repo = localRepoMount;
encryption.mode = "none";
wrapper = null;
startAt = [ ];
};
remote = {
paths = dataDir;
repo = remoteRepo;
encryption.mode = "none";
startAt = [ ];
environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
};
remoteAppendOnly = {
paths = dataDir;
repo = remoteRepo;
encryption.mode = "none";
startAt = [ ];
environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly";
};
commandSuccess = {
dumpCommand = pkgs.writeScript "commandSuccess" ''
echo -n test
'';
repo = remoteRepo;
encryption.mode = "none";
startAt = [ ];
environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
};
commandFail = {
dumpCommand = "${pkgs.coreutils}/bin/false";
repo = remoteRepo;
encryption.mode = "none";
startAt = [ ];
environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
};
sleepInhibited = {
inhibitsSleep = true;
# Blocks indefinitely while "backing up" so that we can try to suspend the local system while it's hung
dumpCommand = pkgs.writeScript "sleepInhibited" ''
cat /dev/zero
'';
repo = remoteRepo;
encryption.mode = "none";
startAt = [ ];
environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
};
};
};
server =
{ ... }:
{
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
};
};
services.borgbackup.repos.repo1 = {
authorizedKeys = [ publicKey ];
path = "/data/borgbackup";
};
# Second repo to make sure the authorizedKeys options are merged correctly
services.borgbackup.repos.repo2 = {
authorizedKeysAppendOnly = [ publicKeyAppendOnly ];
path = "/data/borgbackup";
quota = ".5G";
};
};
};
testScript = ''
start_all()
client.fail('test -d "${remoteRepo}"')
client.succeed(
"cp ${privateKey} /root/id_ed25519"
)
client.succeed("chmod 0600 /root/id_ed25519")
client.succeed(
"cp ${privateKeyAppendOnly} /root/id_ed25519.appendOnly"
)
client.succeed("chmod 0600 /root/id_ed25519.appendOnly")
client.succeed("mkdir -p ${dataDir}/${subDir}")
client.succeed("touch ${dataDir}/${excludeFile}")
client.succeed("touch '${dataDir}/${subDir}/.dont backup'")
client.succeed("touch ${dataDir}/${subDir}/${excludedSubDirFile}")
client.succeed("echo '${keepFileData}' > ${dataDir}/${keepFile}")
with subtest("local"):
borg = "BORG_PASSPHRASE='${passphrase}' borg"
client.systemctl("start --wait borgbackup-job-local")
client.fail("systemctl is-failed borgbackup-job-local")
# Make sure exactly one archive has been created
assert int(client.succeed("{} list '${localRepo}' | wc -l".format(borg))) > 0
# Make sure excludeFile has been excluded
client.fail(
"{} list '${localRepo}::${archiveName}' | grep -qF '${excludeFile}'".format(borg)
)
# Make sure excludedSubDirFile has been excluded
client.fail(
"{} list '${localRepo}::${archiveName}' | grep -qF '${subDir}/${excludedSubDirFile}".format(borg)
)
# Make sure keepFile has the correct content
client.succeed("{} extract '${localRepo}::${archiveName}'".format(borg))
assert "${keepFileData}" in client.succeed("cat ${dataDir}/${keepFile}")
# Make sure the same is true when using `borg mount`
client.succeed(
"mkdir -p /mnt/borg && {} mount '${localRepo}::${archiveName}' /mnt/borg".format(
borg
)
)
assert "${keepFileData}" in client.succeed(
"cat /mnt/borg/${dataDir}/${keepFile}"
)
# Make sure custom wrapper name works
client.succeed("command -v borg-main")
with subtest("localMount"):
# the file system for the repo should not be already mounted
client.fail("mount | grep ${localRepoMount}")
# ensure trying to write to the mountpoint before the fs is mounted fails
client.succeed("chattr +i ${localRepoMount}")
borg = "borg"
client.systemctl("start --wait borgbackup-job-localMount")
client.fail("systemctl is-failed borgbackup-job-localMount")
# Make sure exactly one archive has been created
assert int(client.succeed("{} list '${localRepoMount}' | wc -l".format(borg))) > 0
# Make sure disabling wrapper works
client.fail("command -v borg-job-localMount")
with subtest("remote"):
borg = "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519' borg"
server.wait_for_unit("sshd.service")
client.wait_for_unit("network.target")
client.systemctl("start --wait borgbackup-job-remote")
client.fail("systemctl is-failed borgbackup-job-remote")
# Make sure we can't access repos other than the specified one
client.fail("{} list borg\@server:wrong".format(borg))
# Make sure default wrapper works
client.succeed("command -v borg-job-remote")
# TODO: Make sure that data is actually deleted
with subtest("remoteAppendOnly"):
borg = (
"BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly' borg"
)
server.wait_for_unit("sshd.service")
client.wait_for_unit("network.target")
client.systemctl("start --wait borgbackup-job-remoteAppendOnly")
client.fail("systemctl is-failed borgbackup-job-remoteAppendOnly")
# Make sure we can't access repos other than the specified one
client.fail("{} list borg\@server:wrong".format(borg))
# TODO: Make sure that data is not actually deleted
with subtest("commandSuccess"):
server.wait_for_unit("sshd.service")
client.wait_for_unit("network.target")
client.systemctl("start --wait borgbackup-job-commandSuccess")
client.fail("systemctl is-failed borgbackup-job-commandSuccess")
id = client.succeed("borg-job-commandSuccess list | tail -n1 | cut -d' ' -f1").strip()
client.succeed(f"borg-job-commandSuccess extract ::{id} stdin")
assert "test" == client.succeed("cat stdin")
with subtest("commandFail"):
server.wait_for_unit("sshd.service")
client.wait_for_unit("network.target")
client.systemctl("start --wait borgbackup-job-commandFail")
client.succeed("systemctl is-failed borgbackup-job-commandFail")
with subtest("sleepInhibited"):
server.wait_for_unit("sshd.service")
client.wait_for_unit("network.target")
client.fail("systemd-inhibit --list | grep -q borgbackup")
client.systemctl("start borgbackup-job-sleepInhibited")
client.wait_until_succeeds("systemd-inhibit --list | grep -q borgbackup")
client.systemctl("stop borgbackup-job-sleepInhibited")
'';
}

26
nixos/tests/borgmatic.nix Normal file
View File

@@ -0,0 +1,26 @@
{ pkgs, ... }:
{
name = "borgmatic";
nodes.machine =
{ ... }:
{
services.borgmatic = {
enable = true;
settings = {
source_directories = [ "/home" ];
repositories = [
{
label = "local";
path = "/var/backup";
}
];
keep_daily = 7;
};
};
};
testScript = ''
machine.succeed("borgmatic rcreate -e none")
machine.succeed("borgmatic")
'';
}

View File

@@ -0,0 +1,57 @@
{
pkgs,
lib,
...
}:
{
name = "botamusique";
meta.maintainers = with lib.maintainers; [ hexa ];
nodes = {
machine =
{ config, ... }:
{
networking.extraHosts = ''
127.0.0.1 all.api.radio-browser.info
'';
services.murmur = {
enable = true;
registerName = "NixOS tests";
};
services.botamusique = {
enable = true;
settings = {
server = {
channel = "NixOS tests";
};
bot = {
version = false;
auto_check_update = false;
};
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("murmur.service")
machine.wait_for_unit("botamusique.service")
machine.sleep(10)
machine.wait_until_succeeds(
"journalctl -u murmur.service -e | grep -q '<1:botamusique(-1)> Authenticated'"
)
with subtest("Check systemd hardening"):
output = machine.execute("systemctl show botamusique.service")[1]
machine.log(output)
output = machine.execute("systemd-analyze security botamusique.service")[1]
machine.log(output)
'';
}

42
nixos/tests/bpf.nix Normal file
View File

@@ -0,0 +1,42 @@
{ lib, ... }:
{
name = "bpf";
meta.maintainers = with lib.maintainers; [ martinetd ];
nodes.machine =
{ pkgs, ... }:
{
programs.bcc.enable = true;
environment.systemPackages = with pkgs; [ bpftrace ];
};
testScript = ''
## bcc
# syscount -d 1 stops 1s after probe started so is good for that
print(machine.succeed("syscount -d 1"))
## bpftrace
# list probes
machine.succeed("bpftrace -l")
# simple BEGIN probe (user probe on bpftrace itself)
print(machine.succeed("bpftrace -e 'BEGIN { print(\"ok\\n\"); exit(); }'"))
# tracepoint
print(machine.succeed("bpftrace -e 'tracepoint:syscalls:sys_enter_* { print(probe); exit() }'"))
# kprobe
print(machine.succeed("bpftrace -e 'kprobe:schedule { print(probe); exit() }'"))
# BTF
print(machine.succeed("bpftrace -e 'kprobe:schedule { "
" printf(\"tgid: %d\\n\", ((struct task_struct*) curtask)->tgid); exit() "
"}'"))
# module BTF (bpftrace >= 0.17)
# test is currently disabled on aarch64 as kfunc does not work there yet
# https://github.com/iovisor/bpftrace/issues/2496
print(machine.succeed("uname -m | grep aarch64 || "
"bpftrace -e 'kfunc:nft_trans_alloc_gfp { "
" printf(\"portid: %d\\n\", args->ctx->portid); "
"} BEGIN { exit() }'"))
# glibc includes
print(machine.succeed("bpftrace -e '#include <errno.h>\n"
"BEGIN { printf(\"ok %d\\n\", EINVAL); exit(); }'"))
'';
}

23
nixos/tests/bpftune.nix Normal file
View File

@@ -0,0 +1,23 @@
{ lib, pkgs, ... }:
{
name = "bpftune";
meta = {
maintainers = with lib.maintainers; [ nickcao ];
};
nodes = {
machine =
{ pkgs, ... }:
{
services.bpftune.enable = true;
};
};
testScript = ''
machine.wait_for_unit("bpftune.service")
machine.wait_for_console_text("bpftune works")
'';
}

View File

@@ -0,0 +1,41 @@
{ lib, ... }:
{
name = "breitbandmessung";
meta.maintainers = with lib.maintainers; [ b4dm4n ];
node.pkgsReadOnly = false;
nodes.machine =
{ pkgs, ... }:
{
imports = [
./common/user-account.nix
./common/x11.nix
];
# increase screen size to make the whole program visible
virtualisation.resolution = {
x = 1280;
y = 1024;
};
test-support.displayManager.auto.user = "alice";
environment.systemPackages = with pkgs; [ breitbandmessung ];
environment.variables.XAUTHORITY = "/home/alice/.Xauthority";
# breitbandmessung is unfree
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "breitbandmessung" ];
};
enableOCR = true;
testScript = ''
machine.wait_for_x()
machine.execute("su - alice -c breitbandmessung >&2 &")
machine.wait_for_window("Breitbandmessung")
machine.wait_for_text("Breitbandmessung")
machine.wait_for_text("Datenschutz")
machine.screenshot("breitbandmessung")
'';
}

View File

@@ -0,0 +1,21 @@
{ pkgs, ... }:
{
name = "broadcast-box";
meta = { inherit (pkgs.broadcast-box.meta) maintainers; };
nodes.machine = {
services.broadcast-box = {
enable = true;
web = {
host = "127.0.0.1";
port = 8080;
};
};
};
testScript = ''
machine.wait_for_unit("broadcast-box.service")
machine.wait_for_open_port(8080)
machine.succeed("curl --fail http://localhost:8080/")
'';
}

47
nixos/tests/brscan5.nix Normal file
View File

@@ -0,0 +1,47 @@
# integration tests for brscan5 sane driver
{ lib, ... }:
{
name = "brscan5";
meta.maintainers = with lib.maintainers; [ mattchrist ];
node.pkgsReadOnly = false;
nodes.machine = {
nixpkgs.config.allowUnfree = true;
hardware.sane = {
enable = true;
brscan5 = {
enable = true;
netDevices = {
"a" = {
model = "ADS-1200";
nodename = "BRW0080927AFBCE";
};
"b" = {
model = "ADS-1200";
ip = "192.168.1.2";
};
};
};
};
};
testScript = ''
import re
# sane loads libsane-brother5.so.1 successfully, and scanimage doesn't die
strace = machine.succeed('strace scanimage -L 2>&1').split("\n")
regexp = 'openat\(.*libsane-brother5.so.1", O_RDONLY|O_CLOEXEC\) = \d\d*$'
assert len([x for x in strace if re.match(regexp,x)]) > 0
# module creates a config
cfg = machine.succeed('cat /etc/opt/brother/scanner/brscan5/brsanenetdevice.cfg')
assert 'DEVICE=a , "ADS-1200" , 0x4f9:0x459 , NODENAME=BRW0080927AFBCE' in cfg
assert 'DEVICE=b , "ADS-1200" , 0x4f9:0x459 , IP-ADDRESS=192.168.1.2' in cfg
# scanimage lists the two network scanners
scanimage = machine.succeed("scanimage -L")
print(scanimage)
assert """device `brother5:net1;dev0' is a Brother b ADS-1200""" in scanimage
assert """device `brother5:net1;dev1' is a Brother a ADS-1200""" in scanimage
'';
}

126
nixos/tests/btrbk-doas.nix Normal file
View File

@@ -0,0 +1,126 @@
{ pkgs, ... }:
let
privateKey = ''
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrwAAAJB+cF5HfnBe
RwAAAAtzc2gtZWQyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrw
AAAEBN75NsJZSpt63faCuaD75Unko0JjlSDxMhYHAPJk2/xXHxQHThDpD9/AMWNqQer3Tg
9gXMb2lTZMn0pelo8xyvAAAADXJzY2h1ZXR6QGt1cnQ=
-----END OPENSSH PRIVATE KEY-----
'';
publicKey = ''
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHxQHThDpD9/AMWNqQer3Tg9gXMb2lTZMn0pelo8xyv
'';
in
{
name = "btrbk-doas";
meta = with pkgs.lib; {
maintainers = with maintainers; [
symphorien
tu-maurice
];
};
nodes = {
archive =
{ ... }:
{
security.sudo.enable = false;
security.doas.enable = true;
environment.systemPackages = with pkgs; [ btrfs-progs ];
# note: this makes the privateKey world readable.
# don't do it with real ssh keys.
environment.etc."btrbk_key".text = privateKey;
services.btrbk = {
extraPackages = [ pkgs.lz4 ];
instances = {
remote = {
onCalendar = "minutely";
settings = {
ssh_identity = "/etc/btrbk_key";
ssh_user = "btrbk";
stream_compress = "lz4";
volume = {
"ssh://main/mnt" = {
target = "/mnt";
snapshot_dir = "btrbk/remote";
subvolume = "to_backup";
};
};
};
};
};
};
};
main =
{ ... }:
{
security.sudo.enable = false;
security.doas.enable = true;
environment.systemPackages = with pkgs; [ btrfs-progs ];
services.openssh = {
enable = true;
passwordAuthentication = false;
kbdInteractiveAuthentication = false;
};
services.btrbk = {
extraPackages = [ pkgs.lz4 ];
sshAccess = [
{
key = publicKey;
roles = [
"source"
"send"
"info"
"delete"
];
}
];
instances = {
local = {
onCalendar = "minutely";
settings = {
volume = {
"/mnt" = {
snapshot_dir = "btrbk/local";
subvolume = "to_backup";
};
};
};
};
};
};
};
};
testScript = ''
start_all()
# create btrfs partition at /mnt
for machine in (archive, main):
machine.succeed("dd if=/dev/zero of=/data_fs bs=120M count=1")
machine.succeed("mkfs.btrfs /data_fs")
machine.succeed("mkdir /mnt")
machine.succeed("mount /data_fs /mnt")
# what to backup and where
main.succeed("btrfs subvolume create /mnt/to_backup")
main.succeed("mkdir -p /mnt/btrbk/{local,remote}")
# check that local snapshots work
with subtest("local"):
main.succeed("echo foo > /mnt/to_backup/bar")
main.wait_until_succeeds("cat /mnt/btrbk/local/*/bar | grep foo")
main.succeed("echo bar > /mnt/to_backup/bar")
main.succeed("cat /mnt/btrbk/local/*/bar | grep foo")
# check that btrfs send/receive works and ssh access works
with subtest("remote"):
archive.wait_until_succeeds("cat /mnt/*/bar | grep bar")
main.succeed("echo baz > /mnt/to_backup/bar")
archive.succeed("cat /mnt/*/bar | grep bar")
'';
}

View File

@@ -0,0 +1,39 @@
{ lib, pkgs, ... }:
{
name = "btrbk-no-timer";
meta.maintainers = with lib.maintainers; [ oxalica ];
nodes.machine =
{ ... }:
{
environment.systemPackages = with pkgs; [ btrfs-progs ];
services.btrbk.instances.local = {
onCalendar = null;
settings.volume."/mnt" = {
snapshot_dir = "btrbk/local";
subvolume = "to_backup";
};
};
};
testScript = ''
start_all()
# Create btrfs partition at /mnt
machine.succeed("truncate --size=128M /data_fs")
machine.succeed("mkfs.btrfs /data_fs")
machine.succeed("mkdir /mnt")
machine.succeed("mount /data_fs /mnt")
machine.succeed("btrfs subvolume create /mnt/to_backup")
machine.succeed("mkdir -p /mnt/btrbk/local")
# The service should not have any triggering timer.
unit = machine.get_unit_info('btrbk-local.service')
assert "TriggeredBy" not in unit
# Manually starting the service should still work.
machine.succeed("echo foo > /mnt/to_backup/bar")
machine.start_job("btrbk-local.service")
machine.wait_until_succeeds("cat /mnt/btrbk/local/*/bar | grep foo")
'';
}

View File

@@ -0,0 +1,59 @@
# This tests validates the order of generated sections that may contain
# other sections.
# When a `volume` section has both `subvolume` and `target` children,
# `target` must go before `subvolume`. Otherwise, `target` will become
# a child of the last `subvolume` instead of `volume`, due to the
# order-sensitive config format.
#
# Issue: https://github.com/NixOS/nixpkgs/issues/195660
{ lib, pkgs, ... }:
{
name = "btrbk-section-order";
meta.maintainers = with lib.maintainers; [ oxalica ];
nodes.machine =
{ ... }:
{
services.btrbk.instances.local = {
onCalendar = null;
settings = {
timestamp_format = "long";
target."ssh://global-target/".ssh_user = "root";
volume."/btrfs" = {
snapshot_dir = "/volume-snapshots";
target."ssh://volume-target/".ssh_user = "root";
subvolume."@subvolume" = {
snapshot_dir = "/subvolume-snapshots";
target."ssh://subvolume-target/".ssh_user = "root";
};
};
};
};
};
testScript = ''
import difflib
machine.wait_for_unit("basic.target")
got = machine.succeed("cat /etc/btrbk/local.conf").strip()
expect = """
backend btrfs-progs-sudo
stream_compress no
timestamp_format long
target ssh://global-target/
ssh_user root
volume /btrfs
snapshot_dir /volume-snapshots
target ssh://volume-target/
ssh_user root
subvolume @subvolume
snapshot_dir /subvolume-snapshots
target ssh://subvolume-target/
ssh_user root
""".strip()
print(got)
if got != expect:
diff = difflib.unified_diff(expect.splitlines(keepends=True), got.splitlines(keepends=True), fromfile="expected", tofile="got")
print("".join(diff))
assert got == expect
'';
}

120
nixos/tests/btrbk.nix Normal file
View File

@@ -0,0 +1,120 @@
{ pkgs, ... }:
let
privateKey = ''
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrwAAAJB+cF5HfnBe
RwAAAAtzc2gtZWQyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrw
AAAEBN75NsJZSpt63faCuaD75Unko0JjlSDxMhYHAPJk2/xXHxQHThDpD9/AMWNqQer3Tg
9gXMb2lTZMn0pelo8xyvAAAADXJzY2h1ZXR6QGt1cnQ=
-----END OPENSSH PRIVATE KEY-----
'';
publicKey = ''
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHxQHThDpD9/AMWNqQer3Tg9gXMb2lTZMn0pelo8xyv
'';
in
{
name = "btrbk";
meta = with pkgs.lib; {
maintainers = with maintainers; [ symphorien ];
};
nodes = {
archive =
{ ... }:
{
environment.systemPackages = with pkgs; [ btrfs-progs ];
# note: this makes the privateKey world readable.
# don't do it with real ssh keys.
environment.etc."btrbk_key".text = privateKey;
services.btrbk = {
instances = {
remote = {
onCalendar = "minutely";
settings = {
ssh_identity = "/etc/btrbk_key";
ssh_user = "btrbk";
stream_compress = "lz4";
volume = {
"ssh://main/mnt" = {
target = "/mnt";
snapshot_dir = "btrbk/remote";
subvolume = "to_backup";
};
};
};
};
};
};
};
main =
{ ... }:
{
environment.systemPackages = with pkgs; [ btrfs-progs ];
services.openssh = {
enable = true;
settings = {
KbdInteractiveAuthentication = false;
PasswordAuthentication = false;
};
};
services.btrbk = {
extraPackages = [ pkgs.lz4 ];
sshAccess = [
{
key = publicKey;
roles = [
"source"
"send"
"info"
"delete"
];
}
];
instances = {
local = {
onCalendar = "minutely";
settings = {
volume = {
"/mnt" = {
snapshot_dir = "btrbk/local";
subvolume = "to_backup";
};
};
};
};
};
};
};
};
testScript = ''
start_all()
# create btrfs partition at /mnt
for machine in (archive, main):
machine.succeed("dd if=/dev/zero of=/data_fs bs=120M count=1")
machine.succeed("mkfs.btrfs /data_fs")
machine.succeed("mkdir /mnt")
machine.succeed("mount /data_fs /mnt")
# what to backup and where
main.succeed("btrfs subvolume create /mnt/to_backup")
main.succeed("mkdir -p /mnt/btrbk/{local,remote}")
# check that local snapshots work
with subtest("local"):
main.succeed("echo foo > /mnt/to_backup/bar")
main.wait_until_succeeds("cat /mnt/btrbk/local/*/bar | grep foo")
main.succeed("echo bar > /mnt/to_backup/bar")
main.succeed("cat /mnt/btrbk/local/*/bar | grep foo")
# check that btrfs send/receive works and ssh access works
with subtest("remote"):
archive.wait_until_succeeds("cat /mnt/*/bar | grep bar")
main.succeed("echo baz > /mnt/to_backup/bar")
archive.succeed("cat /mnt/*/bar | grep bar")
'';
}

104
nixos/tests/budgie.nix Normal file
View File

@@ -0,0 +1,104 @@
{ pkgs, lib, ... }:
{
name = "budgie";
meta.maintainers = lib.teams.budgie.members;
nodes.machine =
{ ... }:
{
imports = [
./common/user-account.nix
];
services.xserver.enable = true;
services.xserver.displayManager = {
lightdm.enable = true;
autoLogin = {
enable = true;
user = "alice";
};
};
# We don't ship gnome-text-editor in Budgie module, we add this line mainly
# to catch eval issues related to this option.
environment.budgie.excludePackages = [ pkgs.gnome-text-editor ];
services.xserver.desktopManager.budgie = {
enable = true;
extraPlugins = [
pkgs.budgie-analogue-clock-applet
];
};
};
testScript =
{ nodes, ... }:
let
user = nodes.machine.users.users.alice;
env = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus DISPLAY=:0";
su = command: "su - ${user.name} -c '${env} ${command}'";
in
''
with subtest("Wait for login"):
# wait_for_x() checks graphical-session.target, which is expected to be
# inactive on Budgie before Budgie manages user session with systemd.
# https://github.com/BuddiesOfBudgie/budgie-desktop/blob/39e9f0895c978f76/src/session/budgie-desktop.in#L16
#
# Previously this was unconditionally touched by xsessionWrapper but was
# changed in #233981 (we have Budgie:GNOME in XDG_CURRENT_DESKTOP).
# machine.wait_for_x()
machine.wait_until_succeeds('journalctl -t budgie-session-binary --grep "Entering running state"')
machine.wait_for_file("${user.home}/.Xauthority")
machine.succeed("xauth merge ${user.home}/.Xauthority")
with subtest("Check that logging in has given the user ownership of devices"):
machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
with subtest("Check if Budgie session components actually start"):
for i in ["budgie-daemon", "budgie-panel", "budgie-wm", "budgie-desktop-view", "gsd-media-keys"]:
machine.wait_until_succeeds(f"pgrep -f {i}")
# We don't check xwininfo for budgie-wm.
# See https://github.com/NixOS/nixpkgs/pull/216737#discussion_r1155312754
machine.wait_for_window("budgie-daemon")
machine.wait_for_window("budgie-panel")
with subtest("Check if various environment variables are set"):
cmd = "xargs --null --max-args=1 echo < /proc/$(pgrep -xf /run/current-system/sw/bin/budgie-wm)/environ"
machine.succeed(f"{cmd} | grep 'XDG_CURRENT_DESKTOP' | grep 'Budgie:GNOME'")
machine.succeed(f"{cmd} | grep 'BUDGIE_PLUGIN_DATADIR' | grep '${pkgs.budgie-desktop-with-plugins.pname}'")
# From the nixos/budgie module
machine.succeed(f"{cmd} | grep 'SSH_AUTH_SOCK' | grep 'gcr'")
with subtest("Open run dialog"):
machine.send_key("alt-f2")
machine.wait_for_window("budgie-run-dialog")
machine.sleep(2)
machine.screenshot("run_dialog")
machine.send_key("esc")
with subtest("Open Budgie Control Center"):
machine.succeed("${su "budgie-control-center >&2 &"}")
machine.wait_for_window("Budgie Control Center")
with subtest("Lock the screen"):
machine.succeed("${su "budgie-screensaver-command -l >&2 &"}")
machine.wait_until_succeeds("${su "budgie-screensaver-command -q"} | grep 'The screensaver is active'")
machine.sleep(2)
machine.send_chars("${user.password}", delay=0.5)
machine.screenshot("budgie_screensaver")
machine.send_chars("\n")
machine.wait_until_succeeds("${su "budgie-screensaver-command -q"} | grep 'The screensaver is inactive'")
machine.sleep(2)
with subtest("Open GNOME terminal"):
machine.succeed("${su "gnome-terminal"}")
machine.wait_for_window("${user.name}@machine: ~")
with subtest("Check if Budgie has ever coredumped"):
machine.fail("coredumpctl --json=short | grep budgie")
machine.sleep(10)
machine.screenshot("screen")
'';
}

138
nixos/tests/buildbot.nix Normal file
View File

@@ -0,0 +1,138 @@
# Test ensures buildbot master comes up correctly and workers can connect
{ pkgs, ... }:
{
name = "buildbot";
nodes = {
bbmaster =
{ pkgs, ... }:
{
services.buildbot-master = {
enable = true;
# NOTE: use fake repo due to no internet in hydra ci
factorySteps = [
"steps.Git(repourl='git://gitrepo/fakerepo.git', mode='incremental')"
"steps.ShellCommand(command=['bash', 'fakerepo.sh'])"
];
changeSource = [
"changes.GitPoller('git://gitrepo/fakerepo.git', workdir='gitpoller-workdir', branch='master', pollInterval=300)"
];
};
networking.firewall.allowedTCPPorts = [
8010
8011
9989
];
environment.systemPackages = with pkgs; [
git
buildbot-full
];
};
bbworker =
{ pkgs, ... }:
{
services.buildbot-worker = {
enable = true;
masterUrl = "bbmaster:9989";
};
environment.systemPackages = with pkgs; [
git
buildbot-worker
];
};
gitrepo =
{ pkgs, ... }:
{
services.openssh.enable = true;
networking.firewall.allowedTCPPorts = [
22
9418
];
environment.systemPackages = with pkgs; [ git ];
systemd.services.git-daemon = {
description = "Git daemon for the test";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"sshd.service"
];
serviceConfig.Restart = "always";
path = with pkgs; [
coreutils
git
openssh
];
environment = {
HOME = "/root";
};
preStart = ''
git config --global user.name 'Nobody Fakeuser'
git config --global user.email 'nobody\@fakerepo.com'
rm -rvf /srv/repos/fakerepo.git /tmp/fakerepo
mkdir -pv /srv/repos/fakerepo ~/.ssh
ssh-keyscan -H gitrepo > ~/.ssh/known_hosts
cat ~/.ssh/known_hosts
mkdir -p /src/repos/fakerepo
cd /srv/repos/fakerepo
rm -rf *
git init
echo -e '#!/bin/sh\necho fakerepo' > fakerepo.sh
cat fakerepo.sh
touch .git/git-daemon-export-ok
git add fakerepo.sh .git/git-daemon-export-ok
git commit -m fakerepo
'';
script = ''
git daemon --verbose --export-all --base-path=/srv/repos --reuseaddr
'';
};
};
};
testScript = ''
gitrepo.wait_for_unit("git-daemon.service")
gitrepo.wait_for_unit("multi-user.target")
with subtest("Repo is accessible via git daemon"):
bbmaster.systemctl("start network-online.target")
bbmaster.wait_for_unit("network-online.target")
bbmaster.succeed("rm -rfv /tmp/fakerepo")
bbmaster.succeed("git clone git://gitrepo/fakerepo /tmp/fakerepo")
with subtest("Master service and worker successfully connect"):
bbmaster.wait_for_unit("buildbot-master.service")
bbmaster.wait_until_succeeds("curl --fail -s --head http://bbmaster:8010")
bbworker.systemctl("start network-online.target")
bbworker.wait_for_unit("network-online.target")
bbworker.succeed("nc -z bbmaster 8010")
bbworker.succeed("nc -z bbmaster 9989")
bbworker.wait_for_unit("buildbot-worker.service")
with subtest("Stop buildbot worker"):
bbmaster.succeed("systemctl -l --no-pager status buildbot-master")
bbmaster.succeed("systemctl stop buildbot-master")
bbworker.fail("nc -z bbmaster 8010")
bbworker.fail("nc -z bbmaster 9989")
bbworker.succeed("systemctl -l --no-pager status buildbot-worker")
bbworker.succeed("systemctl stop buildbot-worker")
with subtest("Buildbot daemon mode works"):
bbmaster.succeed(
"buildbot create-master /tmp",
"mv -fv /tmp/master.cfg.sample /tmp/master.cfg",
"sed -i 's/8010/8011/' /tmp/master.cfg",
"buildbot start /tmp",
"nc -z bbmaster 8011",
)
bbworker.wait_until_succeeds("curl --fail -s --head http://bbmaster:8011")
bbmaster.wait_until_succeeds("buildbot stop /tmp")
bbworker.fail("nc -z bbmaster 8011")
'';
meta.maintainers = pkgs.lib.teams.buildbot.members;
}

Some files were not shown because too many files have changed in this diff Show More