Merge remote-tracking branch 'origin/staging-next' into staging

This commit is contained in:
K900 2025-05-28 11:42:18 +03:00
commit a3612e2210
807 changed files with 39720 additions and 38888 deletions

View File

@ -115,8 +115,8 @@ All new projects should use the CUDA redistributables available in [`cudaPackage
### Updating supported compilers and GPUs {#updating-supported-compilers-and-gpus} ### Updating supported compilers and GPUs {#updating-supported-compilers-and-gpus}
1. Update `nvcc-compatibilities.nix` in `pkgs/development/cuda-modules/` to include the newest release of NVCC, as well as any newly supported host compilers. 1. Update `nvccCompatibilities` in `pkgs/development/cuda-modules/_cuda/data/nvcc.nix` to include the newest release of NVCC, as well as any newly supported host compilers.
2. Update `gpus.nix` in `pkgs/development/cuda-modules/` to include any new GPUs supported by the new release of CUDA. 2. Update `cudaCapabilityToInfo` in `pkgs/development/cuda-modules/_cuda/data/cuda.nix` to include any new GPUs supported by the new release of CUDA.
### Updating the CUDA Toolkit runfile installer {#updating-the-cuda-toolkit} ### Updating the CUDA Toolkit runfile installer {#updating-the-cuda-toolkit}

View File

@ -583,6 +583,14 @@ rec {
# https://github.com/llvm/llvm-project/pull/132173 # https://github.com/llvm/llvm-project/pull/132173
cmodel = "medium"; cmodel = "medium";
}; };
linux-kernel = {
name = "loongarch-multiplatform";
target = "vmlinuz.efi";
autoModules = true;
preferBuiltin = true;
baseConfig = "defconfig";
DTB = true;
};
}; };
# This function takes a minimally-valid "platform" and returns an # This function takes a minimally-valid "platform" and returns an
@ -611,6 +619,9 @@ rec {
else if platform.isAarch64 then else if platform.isAarch64 then
if platform.isDarwin then apple-m1 else aarch64-multiplatform if platform.isDarwin then apple-m1 else aarch64-multiplatform
else if platform.isLoongArch64 then
loongarch64-multiplatform
else if platform.isRiscV then else if platform.isRiscV then
riscv-multiplatform riscv-multiplatform

View File

@ -13037,12 +13037,6 @@
githubId = 843652; githubId = 843652;
name = "Kim Burgess"; name = "Kim Burgess";
}; };
kindrowboat = {
email = "hello@kindrobot.ca";
github = "kindrowboat";
githubId = 777773;
name = "Stef Dunlap";
};
kini = { kini = {
email = "keshav.kini@gmail.com"; email = "keshav.kini@gmail.com";
github = "kini"; github = "kini";
@ -17609,7 +17603,7 @@
}; };
nicoo = { nicoo = {
email = "nicoo@debian.org"; email = "nicoo@debian.org";
github = "nbraud"; github = "nicoonoclaste";
githubId = 1155801; githubId = 1155801;
name = "nicoo"; name = "nicoo";
keys = [ { fingerprint = "E44E 9EA5 4B8E 256A FB73 49D3 EC9D 3708 72BC 7A8C"; } ]; keys = [ { fingerprint = "E44E 9EA5 4B8E 256A FB73 49D3 EC9D 3708 72BC 7A8C"; } ];
@ -22924,6 +22918,11 @@
matrix = "@c3n21:matrix.org"; matrix = "@c3n21:matrix.org";
githubId = 37077738; githubId = 37077738;
}; };
sinjin2300 = {
name = "Sinjin";
github = "Sinjin2300";
githubId = 35543336;
};
sioodmy = { sioodmy = {
name = "Antoni Sokołowski"; name = "Antoni Sokołowski";
github = "sioodmy"; github = "sioodmy";

View File

@ -1,16 +1,23 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Find alleged cherry-picks # Find alleged cherry-picks
set -e set -eo pipefail
if [ $# != "2" ] ; then if [ $# != "2" ] ; then
echo "usage: check-cherry-picks.sh base_rev head_rev" echo "usage: check-cherry-picks.sh base_rev head_rev"
exit 2 exit 2
fi fi
# Make sure we are inside the nixpkgs repo, even when called from outside
cd "$(dirname "${BASH_SOURCE[0]}")"
PICKABLE_BRANCHES=${PICKABLE_BRANCHES:-master staging release-??.?? staging-??.??} PICKABLE_BRANCHES=${PICKABLE_BRANCHES:-master staging release-??.?? staging-??.??}
problem=0 problem=0
commits="$(git rev-list \
-E -i --grep="cherry.*[0-9a-f]{40}" --reverse \
"$1..$2")"
while read new_commit_sha ; do while read new_commit_sha ; do
if [ -z "$new_commit_sha" ] ; then if [ -z "$new_commit_sha" ] ; then
continue # skip empty lines continue # skip empty lines
@ -88,10 +95,6 @@ while read new_commit_sha ; do
echo "$original_commit_sha not found in any pickable branch" echo "$original_commit_sha not found in any pickable branch"
problem=1 problem=1
done <<< "$( done <<< "$commits"
git rev-list \
-E -i --grep="cherry.*[0-9a-f]{40}" --reverse \
"$1..$2"
)"
exit $problem exit $problem

View File

@ -27,3 +27,6 @@
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
- `services.clamsmtp` is unmaintained and was removed from Nixpkgs. - `services.clamsmtp` is unmaintained and was removed from Nixpkgs.
- `amdgpu` kernel driver overdrive mode can now be enabled by setting [hardware.amdgpu.overdrive.enable](#opt-hardware.amdgpu.overdrive.enable) and customized through [hardware.amdgpu.overdrive.ppfeaturemask](#opt-hardware.amdgpu.overdrive.ppfeaturemask).
This allows for fine-grained control over the GPU's performance and maybe required by overclocking softwares like Corectrl and Lact. These new options replace old options such as {option}`programs.corectrl.gpuOverclock.enable` and {option}`programs.tuxclocker.enableAMD`.

View File

@ -8,13 +8,23 @@ let
inherit (lib) inherit (lib)
mkEnableOption mkEnableOption
mkIf mkIf
mkOption
mkPackageOption mkPackageOption
; ;
cfg = config.programs.corectrl; cfg = config.programs.corectrl;
in in
{ {
imports = [
(lib.mkRenamedOptionModule
[ "programs" "corectrl" "gpuOverclock" "enable" ]
[ "hardware" "amdgpu" "overdrive" "enable" ]
)
(lib.mkRenamedOptionModule
[ "programs" "corectrl" "gpuOverclock" "ppfeaturemask" ]
[ "hardware" "amdgpu" "overdrive" "ppfeaturemask" ]
)
];
options.programs.corectrl = { options.programs.corectrl = {
enable = mkEnableOption '' enable = mkEnableOption ''
CoreCtrl, a tool to overclock amd graphics cards and processors. CoreCtrl, a tool to overclock amd graphics cards and processors.
@ -24,23 +34,6 @@ in
package = mkPackageOption pkgs "corectrl" { package = mkPackageOption pkgs "corectrl" {
extraDescription = "Useful for overriding the configuration options used for the package."; extraDescription = "Useful for overriding the configuration options used for the package.";
}; };
gpuOverclock = {
enable = mkEnableOption ''
GPU overclocking
'';
ppfeaturemask = mkOption {
type = lib.types.str;
default = "0xfffd7fff";
example = "0xffffffff";
description = ''
Sets the `amdgpu.ppfeaturemask` kernel option.
In particular, it is used here to set the overdrive bit.
Default is `0xfffd7fff` as it is less likely to cause flicker issues.
Setting it to `0xffffffff` enables all features.
'';
};
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
@ -61,12 +54,6 @@ in
} }
}); });
''; '';
# https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/gpu/drm/amd/include/amd_shared.h#n169
# The overdrive bit
boot.kernelParams = mkIf cfg.gpuOverclock.enable [
"amdgpu.ppfeaturemask=${cfg.gpuOverclock.ppfeaturemask}"
];
}; };
meta.maintainers = with lib.maintainers; [ meta.maintainers = with lib.maintainers; [

View File

@ -8,7 +8,7 @@
{ {
meta = { meta = {
maintainers = lib.teams.gnome.members; maintainers = [ ];
}; };
###### interface ###### interface
@ -37,11 +37,6 @@
environment.systemPackages = [ pkgs.telepathy-mission-control ]; environment.systemPackages = [ pkgs.telepathy-mission-control ];
services.dbus.packages = [ pkgs.telepathy-mission-control ]; services.dbus.packages = [ pkgs.telepathy-mission-control ];
# Enable runtime optional telepathy in gnome-shell
services.xserver.desktopManager.gnome.sessionPath = with pkgs; [
telepathy-glib
];
}; };
} }

View File

@ -16,21 +16,44 @@ in
series cards. Note: this removes support for analog video outputs, series cards. Note: this removes support for analog video outputs,
which is only available in the `radeon` driver which is only available in the `radeon` driver
''; '';
initrd.enable = lib.mkEnableOption '' initrd.enable = lib.mkEnableOption ''
loading `amdgpu` kernelModule in stage 1. loading `amdgpu` kernelModule in stage 1.
Can fix lower resolution in boot screen during initramfs phase Can fix lower resolution in boot screen during initramfs phase
''; '';
overdrive = {
enable = lib.mkEnableOption ''`amdgpu` overdrive mode for overclocking'';
ppfeaturemask = lib.mkOption {
type = lib.types.str;
default = "0xfffd7fff";
example = "0xffffffff";
description = ''
Sets the `amdgpu.ppfeaturemask` kernel option. It can be used to enable the overdrive bit.
Default is `0xfffd7fff` as it is less likely to cause flicker issues. Setting it to
`0xffffffff` enables all features, but also can be unstable. See
[the kernel documentation](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/gpu/drm/amd/include/amd_shared.h#n169)
for more information.
'';
};
};
opencl.enable = lib.mkEnableOption ''OpenCL support using ROCM runtime library''; opencl.enable = lib.mkEnableOption ''OpenCL support using ROCM runtime library'';
# cfg.amdvlk option is defined in ./amdvlk.nix module # cfg.amdvlk option is defined in ./amdvlk.nix module
}; };
config = { config = {
boot.kernelParams = lib.optionals cfg.legacySupport.enable [ boot.kernelParams =
"amdgpu.si_support=1" lib.optionals cfg.legacySupport.enable [
"amdgpu.cik_support=1" "amdgpu.si_support=1"
"radeon.si_support=0" "amdgpu.cik_support=1"
"radeon.cik_support=0" "radeon.si_support=0"
]; "radeon.cik_support=0"
]
++ lib.optionals cfg.overdrive.enable [
"amdgpu.ppfeaturemask=${cfg.overdrive.ppfeaturemask}"
];
boot.initrd.kernelModules = lib.optionals cfg.initrd.enable [ "amdgpu" ]; boot.initrd.kernelModules = lib.optionals cfg.initrd.enable [ "amdgpu" ];

View File

@ -13,7 +13,7 @@ let
haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" || cfg.extraAliases != ""; haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" || cfg.extraAliases != "";
haveCanonical = cfg.canonical != ""; haveCanonical = cfg.canonical != "";
haveTransport = cfg.transport != ""; haveTransport = cfg.transport != "" || (cfg.enableSlowDomains && cfg.slowDomains != [ ]);
haveVirtual = cfg.virtual != ""; haveVirtual = cfg.virtual != "";
haveLocalRecipients = cfg.localRecipients != null; haveLocalRecipients = cfg.localRecipients != null;
@ -319,13 +319,20 @@ let
aliasesFile = pkgs.writeText "postfix-aliases" aliases; aliasesFile = pkgs.writeText "postfix-aliases" aliases;
canonicalFile = pkgs.writeText "postfix-canonical" cfg.canonical; canonicalFile = pkgs.writeText "postfix-canonical" cfg.canonical;
virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual; virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
transportFile = pkgs.writeText "postfix-transport" (
lib.optionalString (cfg.enableSlowDomains && cfg.slowDomains != [ ]) (
lib.concatMapStrings (domain: ''
${domain} slow:
'') cfg.slowDomains
)
+ cfg.transport
);
localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" ( localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" (
lib.concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients lib.concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients
); );
checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides; checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
mainCfFile = pkgs.writeText "postfix-main.cf" mainCf; mainCfFile = pkgs.writeText "postfix-main.cf" mainCf;
masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent; masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent;
transportFile = pkgs.writeText "postfix-transport" cfg.transport;
headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks; headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks;
in in
@ -550,6 +557,32 @@ in
''; '';
}; };
enableSlowDomains = lib.mkEnableOption "slow domains feature for rate limiting specific domains";
slowDomains = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"orange.fr"
"gmail.com"
];
description = "List of domains to be rate-limited using the slow transport.";
};
slowDomainsConfig = {
defaultDestinationRateDelay = lib.mkOption {
type = lib.types.str;
default = "5s";
description = "Default rate delay for destinations.";
};
defaultDestinationConcurrencyLimit = lib.mkOption {
type = lib.types.int;
default = 3;
description = "Concurrency limit for slow destinations.";
};
};
aliasMapType = lib.mkOption { aliasMapType = lib.mkOption {
type = type =
with lib.types; with lib.types;
@ -985,7 +1018,10 @@ in
smtpd_tls_key_file = cfg.sslKey; smtpd_tls_key_file = cfg.sslKey;
smtpd_tls_security_level = lib.mkDefault "may"; smtpd_tls_security_level = lib.mkDefault "may";
}
// lib.optionalAttrs cfg.enableSlowDomains {
default_destination_rate_delay = cfg.slowDomainsConfig.defaultDestinationRateDelay;
default_destination_concurrency_limit = cfg.slowDomainsConfig.defaultDestinationConcurrencyLimit;
}; };
services.postfix.masterConfig = services.postfix.masterConfig =
@ -1077,6 +1113,14 @@ in
lib.concatLists (lib.mapAttrsToList mkKeyVal cfg.submissionOptions); lib.concatLists (lib.mapAttrsToList mkKeyVal cfg.submissionOptions);
}; };
} }
// lib.optionalAttrs cfg.enableSlowDomains {
slow = {
command = "smtp";
type = "unix";
private = true;
maxproc = 2;
};
}
// lib.optionalAttrs cfg.enableSmtp { // lib.optionalAttrs cfg.enableSmtp {
smtp_inet = { smtp_inet = {
name = "smtp"; name = "smtp";
@ -1128,7 +1172,7 @@ in
(lib.mkIf haveCanonical { (lib.mkIf haveCanonical {
services.postfix.mapFiles.canonical = canonicalFile; services.postfix.mapFiles.canonical = canonicalFile;
}) })
(lib.mkIf haveTransport { (lib.mkIf (haveTransport || (cfg.enableSlowDomains && cfg.slowDomains != [ ])) {
services.postfix.mapFiles.transport = transportFile; services.postfix.mapFiles.transport = transportFile;
}) })
(lib.mkIf haveVirtual { (lib.mkIf haveVirtual {

View File

@ -132,6 +132,21 @@ in
"@system-service" "@system-service"
"~@privileged" "~@privileged"
]; ];
SupplementaryGroups = [ "render" ]; # for rocm to access /dev/dri/renderD* devices
DeviceAllow = [
# CUDA
# https://docs.nvidia.com/dgx/pdf/dgx-os-5-user-guide.pdf
"char-nvidiactl"
"char-nvidia-caps"
"char-nvidia-frontend"
"char-nvidia-uvm"
# ROCm
"char-drm"
"char-fb"
"char-kfd"
# WSL (Windows Subsystem for Linux)
"/dev/dxg"
];
}; };
}; };

View File

@ -8,16 +8,18 @@ let
cfg = config.programs.tuxclocker; cfg = config.programs.tuxclocker;
in in
{ {
imports = [
(lib.mkRenamedOptionModule
[ "programs" "tuxclocker" "enableAMD" ]
[ "hardware" "amdgpu" "overdrive" "enable" ]
)
];
options.programs.tuxclocker = { options.programs.tuxclocker = {
enable = lib.mkEnableOption '' enable = lib.mkEnableOption ''
TuxClocker, a hardware control and monitoring program TuxClocker, a hardware control and monitoring program
''; '';
enableAMD = lib.mkEnableOption ''
AMD GPU controls.
Sets the `amdgpu.ppfeaturemask` kernel parameter to 0xfffd7fff to enable all TuxClocker controls
'';
enabledNVIDIADevices = lib.mkOption { enabledNVIDIADevices = lib.mkOption {
type = lib.types.listOf lib.types.int; type = lib.types.listOf lib.types.int;
default = [ ]; default = [ ];
@ -72,9 +74,5 @@ in
); );
in in
lib.concatStrings (map configSection cfg.enabledNVIDIADevices); lib.concatStrings (map configSection cfg.enabledNVIDIADevices);
# https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/gpu/drm/amd/include/amd_shared.h#n207
# Enable everything modifiable in TuxClocker
boot.kernelParams = lib.mkIf cfg.enableAMD [ "amdgpu.ppfeaturemask=0xfffd7fff" ];
}; };
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,64 +1,62 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let user = "alice";
user = "alice"; in
in {
{ name = "benchexec";
name = "benchexec";
nodes.benchexec = { nodes.benchexec = {
imports = [ ./common/user-account.nix ]; imports = [ ./common/user-account.nix ];
programs.benchexec = { 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; enable = true;
fonts = [ users = [ user ];
{
name = "Fira Code";
package = pkgs.fira-code;
}
];
}; };
} };
)
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

@ -1,57 +1,55 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "bitcoind";
name = "bitcoind"; meta = with pkgs.lib; {
meta = with pkgs.lib; { maintainers = with maintainers; [ _1000101 ];
maintainers = with maintainers; [ _1000101 ]; };
};
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
services.bitcoind."mainnet" = { services.bitcoind."mainnet" = {
enable = true; enable = true;
rpc = { rpc = {
port = 8332; port = 8332;
users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033"; users.rpc.passwordHMAC = "acc2374e5f9ba9e62a5204d3686616cf$53abdba5e67a9005be6a27ca03a93ce09e58854bc2b871523a0d239a72968033";
users.rpc2.passwordHMAC = "1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225"; 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 = '' environment.etc."test.blank".text = "";
start_all() services.bitcoind."testnet" = {
enable = true;
configFile = "/etc/test.blank";
testnet = true;
rpc = {
port = 18332;
};
extraCmdlineOptions = [
"-rpcuser=rpc"
"-rpcpassword=rpc"
"-rpcauth=rpc2:1495e4a3ad108187576c68f7f9b5ddc5$accce0881c74aa01bb8960ff3bdbd39f607fd33178147679e055a4ac35f53225"
];
};
};
machine.wait_for_unit("bitcoind-mainnet.service") testScript = ''
machine.wait_for_unit("bitcoind-testnet.service") start_all()
machine.wait_until_succeeds( machine.wait_for_unit("bitcoind-mainnet.service")
'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_for_unit("bitcoind-testnet.service")
)
machine.wait_until_succeeds( 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"\' ' '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( 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"\' ' '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( 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"\' ' '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"\' '
) )
'';
}

View File

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

View File

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

View File

@ -1,193 +1,191 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "boot-stage1";
name = "boot-stage1";
nodes.machine = nodes.machine =
{ {
config, config,
pkgs, pkgs,
lib, lib,
... ...
}: }:
{ {
boot.extraModulePackages = boot.extraModulePackages =
let let
compileKernelModule = compileKernelModule =
name: source: name: source:
pkgs.runCommandCC name pkgs.runCommandCC name
rec { rec {
inherit source; inherit source;
kdev = config.boot.kernelPackages.kernel.dev; kdev = config.boot.kernelPackages.kernel.dev;
kver = config.boot.kernelPackages.kernel.modDirVersion; kver = config.boot.kernelPackages.kernel.modDirVersion;
ksrc = "${kdev}/lib/modules/${kver}/build"; ksrc = "${kdev}/lib/modules/${kver}/build";
hardeningDisable = [ "pic" ]; hardeningDisable = [ "pic" ];
nativeBuildInputs = kdev.moduleBuildDependencies; 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) echo "obj-m += $name.o" > Makefile
{ echo "$source" > "$name.c"
kthread_run(&kcanary, NULL, "kcanary"); make -C "$ksrc" M=$(pwd) modules
return 0; install -vD "$name.ko" "$out/lib/modules/$kver/$name.ko"
}
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 = # This spawns a kthread which just waits until it gets a signal and
name: source: # terminates if that is the case. We want to make sure that nothing during
compile name '' # 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 <stdio.h>
#include <unistd.h> #include <unistd.h>
void runSource(void) {
${source}
}
int main(void) { int main(void) {
if (fork() > 0) return 0; ${source}
setsid(); while (1) sleep(1);
runSource();
return 1; return 1;
} }
''; '';
};
mkCmdlineCanary = copyCanaries = lib.concatMapStrings (canary: ''
{ ${lib.optionalString (canary ? child) ''
name, copy_bin_and_libs "${canary.child}/bin/${canary.child.name}"
cmdline ? "", ''}
source ? "", copy_bin_and_libs "${canary}/bin/${canary.name}"
}: '');
(daemonize name ''
char *argv[] = {"${cmdline}", NULL};
execvp("${name}-child", argv);
'')
// {
child = compile "${name}-child" ''
#include <stdio.h>
#include <unistd.h>
int main(void) { in
${source} copyCanaries [
while (1) sleep(1); # Simple canary process which just sleeps forever and should be killed by
return 1; # stage 2.
} (daemonize "canary1" "while (1) sleep(1);")
'';
};
copyCanaries = lib.concatMapStrings (canary: '' # We want this canary process to try mimicking a kthread using a cmdline
${lib.optionalString (canary ? child) '' # with a zero length so we can make sure that the process is properly
copy_bin_and_libs "${canary.child}/bin/${canary.child.name}" # killed in stage 1.
''} (mkCmdlineCanary {
copy_bin_and_libs "${canary}/bin/${canary.name}" name = "canary2";
''); source = ''
FILE *f;
f = fopen("/run/canary2.pid", "w");
fprintf(f, "%d\n", getpid());
fclose(f);
'';
})
in # This canary process mimics a storage daemon, which we do NOT want to be
copyCanaries [ # killed before going into stage 2. For more on root storage daemons, see:
# Simple canary process which just sleeps forever and should be killed by # https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/
# stage 2. (mkCmdlineCanary {
(daemonize "canary1" "while (1) sleep(1);") name = "canary3";
cmdline = "@canary3";
})
];
# We want this canary process to try mimicking a kthread using a cmdline boot.initrd.postMountCommands = ''
# with a zero length so we can make sure that the process is properly canary1
# killed in stage 1. canary2
(mkCmdlineCanary { canary3
name = "canary2"; # Make sure the pidfile of canary 2 is created so that we still can get
source = '' # its former pid after the killing spree starts next within stage 1.
FILE *f; while [ ! -s /run/canary2.pid ]; do sleep 0.1; done
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 testScript = ''
# killed before going into stage 2. For more on root storage daemons, see: machine.wait_for_unit("multi-user.target")
# https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/ machine.succeed("test -s /run/canary2.pid")
(mkCmdlineCanary { machine.fail("pgrep -a canary1")
name = "canary3"; machine.fail("kill -0 $(< /run/canary2.pid)")
cmdline = "@canary3"; machine.succeed('pgrep -a -f "^@canary3$"')
}) machine.succeed('pgrep -a -f "^\\[kcanary\\]$"')
]; '';
boot.initrd.postMountCommands = '' meta.maintainers = with pkgs.lib.maintainers; [ aszlig ];
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 ];
}
)

View File

@ -1,133 +1,131 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "boot-stage2";
name = "boot-stage2";
nodes.machine = nodes.machine =
{ {
config, config,
pkgs, pkgs,
lib, lib,
... ...
}: }:
let let
# Prints the user's UID. Can't just do a shell script # Prints the user's UID. Can't just do a shell script
# because setuid is ignored for interpreted programs. # because setuid is ignored for interpreted programs.
uid = pkgs.writeCBin "uid" '' uid = pkgs.writeCBin "uid" ''
#include <unistd.h> #include <unistd.h>
#include <stdio.h> #include <stdio.h>
int main(void) { int main(void) {
printf("%d\n", geteuid()); printf("%d\n", geteuid());
return 0; return 0;
} }
''; '';
in in
{ {
users.users.alice = { users.users.alice = {
isNormalUser = true; isNormalUser = true;
uid = 1000; uid = 1000;
}; };
virtualisation = { virtualisation = {
emptyDiskImages = [ 256 ]; emptyDiskImages = [ 256 ];
# Mount an ext4 as the upper layer of the Nix store. # Mount an ext4 as the upper layer of the Nix store.
fileSystems = { fileSystems = {
"/nix/store" = lib.mkForce { "/nix/store" = lib.mkForce {
device = "/dev/vdb"; # the above disk image device = "/dev/vdb"; # the above disk image
fsType = "ext4"; fsType = "ext4";
# data=journal always displays after errors=remount-ro; this is only needed because of the overlay # 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: # 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 # see ordering in https://github.com/torvalds/linux/blob/v6.12/fs/ext4/super.c#L2974
options = [ options = [
"defaults" "defaults"
"errors=remount-ro" "errors=remount-ro"
"data=journal" "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 = '' environment.systemPackages = [ pkgs.xxd ];
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"]: system.extraDependencies = [ uid ];
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 boot = {
machine.succeed('[ -u /nix/store/evil/bin/suid ]') initrd = {
# runs as alice and is not root # Format the upper Nix store.
machine.succeed('[ "$(sudo -u alice /nix/store/evil/bin/suid)" == 1000 ]') postDeviceCommands = ''
# can be remounted and runs as root ${pkgs.e2fsprogs}/bin/mkfs.ext4 /dev/vdb
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 # Overlay the RO store onto it.
machine.succeed('[ -c /nix/store/evil/dev/zero ]') # Note that bug #375257 can be triggered without an overlay,
# should not work # using the errors=remount-ro option (or similar) or with an overlay where any of the
machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]') # paths ends in 'ro'. The offending mountpoint also has to be the last (top) one
# can be remounted and works # if an option ending in 'ro' is the last in the list, so test both cases here.
machine.succeed('mount -o remount,dev,bind /nix/store && mount >&2') postMountCommands = ''
machine.succeed('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]') mkdir -p /mnt-root/nix/store/ro /mnt-root/nix/store/rw /mnt-root/nix/store/work
# double checking we can undo it mount --bind /mnt-root/nix/.ro-store /mnt-root/nix/store/ro
machine.succeed('mount -o remount,nodev,bind /nix/store && mount >&2') mount -t overlay overlay \
machine.fail('[ "$(dd if=/nix/store/evil/dev/zero bs=1 count=1 | xxd -pl1)" == 00 ]') -o lowerdir=/mnt-root/nix/store/ro,upperdir=/mnt-root/nix/store/rw,workdir=/mnt-root/nix/store/work \
''; /mnt-root/nix/store
meta.maintainers = with pkgs.lib.maintainers; [ numinit ]; # 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 ];
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -6,56 +6,54 @@
# order-sensitive config format. # order-sensitive config format.
# #
# Issue: https://github.com/NixOS/nixpkgs/issues/195660 # Issue: https://github.com/NixOS/nixpkgs/issues/195660
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: {
{ name = "btrbk-section-order";
name = "btrbk-section-order"; meta.maintainers = with lib.maintainers; [ oxalica ];
meta.maintainers = with lib.maintainers; [ oxalica ];
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
services.btrbk.instances.local = { services.btrbk.instances.local = {
onCalendar = null; onCalendar = null;
settings = { settings = {
timestamp_format = "long"; timestamp_format = "long";
target."ssh://global-target/".ssh_user = "root"; target."ssh://global-target/".ssh_user = "root";
volume."/btrfs" = { volume."/btrfs" = {
snapshot_dir = "/volume-snapshots"; snapshot_dir = "/volume-snapshots";
target."ssh://volume-target/".ssh_user = "root"; target."ssh://volume-target/".ssh_user = "root";
subvolume."@subvolume" = { subvolume."@subvolume" = {
snapshot_dir = "/subvolume-snapshots"; snapshot_dir = "/subvolume-snapshots";
target."ssh://subvolume-target/".ssh_user = "root"; target."ssh://subvolume-target/".ssh_user = "root";
};
}; };
}; };
}; };
}; };
};
testScript = '' testScript = ''
import difflib import difflib
machine.wait_for_unit("basic.target") machine.wait_for_unit("basic.target")
got = machine.succeed("cat /etc/btrbk/local.conf").strip() got = machine.succeed("cat /etc/btrbk/local.conf").strip()
expect = """ expect = """
backend btrfs-progs-sudo backend btrfs-progs-sudo
stream_compress no stream_compress no
timestamp_format long timestamp_format long
target ssh://global-target/ 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 ssh_user root
volume /btrfs """.strip()
snapshot_dir /volume-snapshots print(got)
target ssh://volume-target/ if got != expect:
ssh_user root diff = difflib.unified_diff(expect.splitlines(keepends=True), got.splitlines(keepends=True), fromfile="expected", tofile="got")
subvolume @subvolume print("".join(diff))
snapshot_dir /subvolume-snapshots assert got == expect
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
'';
}
)

View File

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

View File

@ -1,104 +1,102 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "budgie";
name = "budgie";
meta.maintainers = lib.teams.budgie.members; meta.maintainers = lib.teams.budgie.members;
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
imports = [ imports = [
./common/user-account.nix ./common/user-account.nix
]; ];
services.xserver.enable = true; services.xserver.enable = true;
services.xserver.displayManager = { services.xserver.displayManager = {
lightdm.enable = true; lightdm.enable = true;
autoLogin = { 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; enable = true;
extraPlugins = [ user = "alice";
pkgs.budgie-analogue-clock-applet
];
}; };
}; };
testScript = # We don't ship gnome-text-editor in Budgie module, we add this line mainly
{ nodes, ... }: # to catch eval issues related to this option.
let environment.budgie.excludePackages = [ pkgs.gnome-text-editor ];
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"): services.xserver.desktopManager.budgie = {
machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}") enable = true;
extraPlugins = [
pkgs.budgie-analogue-clock-applet
];
};
};
with subtest("Check if Budgie session components actually start"): testScript =
for i in ["budgie-daemon", "budgie-panel", "budgie-wm", "budgie-desktop-view", "gsd-media-keys"]: { nodes, ... }:
machine.wait_until_succeeds(f"pgrep -f {i}") let
# We don't check xwininfo for budgie-wm. user = nodes.machine.users.users.alice;
# See https://github.com/NixOS/nixpkgs/pull/216737#discussion_r1155312754 env = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus DISPLAY=:0";
machine.wait_for_window("budgie-daemon") su = command: "su - ${user.name} -c '${env} ${command}'";
machine.wait_for_window("budgie-panel") 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 if various environment variables are set"): with subtest("Check that logging in has given the user ownership of devices"):
cmd = "xargs --null --max-args=1 echo < /proc/$(pgrep -xf /run/current-system/sw/bin/budgie-wm)/environ" machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
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}'")
with subtest("Open run dialog"): with subtest("Check if Budgie session components actually start"):
machine.send_key("alt-f2") for i in ["budgie-daemon", "budgie-panel", "budgie-wm", "budgie-desktop-view", "gsd-media-keys"]:
machine.wait_for_window("budgie-run-dialog") machine.wait_until_succeeds(f"pgrep -f {i}")
machine.sleep(2) # We don't check xwininfo for budgie-wm.
machine.screenshot("run_dialog") # See https://github.com/NixOS/nixpkgs/pull/216737#discussion_r1155312754
machine.send_key("esc") machine.wait_for_window("budgie-daemon")
machine.wait_for_window("budgie-panel")
with subtest("Open Budgie Control Center"): with subtest("Check if various environment variables are set"):
machine.succeed("${su "budgie-control-center >&2 &"}") cmd = "xargs --null --max-args=1 echo < /proc/$(pgrep -xf /run/current-system/sw/bin/budgie-wm)/environ"
machine.wait_for_window("Budgie Control Center") 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}'")
with subtest("Lock the screen"): with subtest("Open run dialog"):
machine.succeed("${su "budgie-screensaver-command -l >&2 &"}") machine.send_key("alt-f2")
machine.wait_until_succeeds("${su "budgie-screensaver-command -q"} | grep 'The screensaver is active'") machine.wait_for_window("budgie-run-dialog")
machine.sleep(2) machine.sleep(2)
machine.send_chars("${user.password}", delay=0.5) machine.screenshot("run_dialog")
machine.screenshot("budgie_screensaver") machine.send_key("esc")
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"): with subtest("Open Budgie Control Center"):
machine.succeed("${su "gnome-terminal"}") machine.succeed("${su "budgie-control-center >&2 &"}")
machine.wait_for_window("${user.name}@machine: ~") machine.wait_for_window("Budgie Control Center")
with subtest("Check if Budgie has ever coredumped"): with subtest("Lock the screen"):
machine.fail("coredumpctl --json=short | grep budgie") machine.succeed("${su "budgie-screensaver-command -l >&2 &"}")
machine.sleep(10) machine.wait_until_succeeds("${su "budgie-screensaver-command -q"} | grep 'The screensaver is active'")
machine.screenshot("screen") 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")
'';
}

View File

@ -1,33 +1,31 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }:
{ {
name = "buildkite-agent"; name = "buildkite-agent";
meta.maintainers = with lib.maintainers; [ flokli ]; meta.maintainers = with lib.maintainers; [ flokli ];
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
services.buildkite-agents = { services.buildkite-agents = {
one = { one = {
privateSshKeyPath = (import ./ssh-keys.nix pkgs).snakeOilPrivateKey; privateSshKeyPath = (import ./ssh-keys.nix pkgs).snakeOilPrivateKey;
tokenPath = (pkgs.writeText "my-token" "5678"); tokenPath = (pkgs.writeText "my-token" "5678");
}; };
two = { two = {
tokenPath = (pkgs.writeText "my-token" "1234"); tokenPath = (pkgs.writeText "my-token" "1234");
};
}; };
}; };
};
testScript = '' testScript = ''
start_all() start_all()
# we can't wait on the unit to start up, as we obviously can't connect to buildkite, # we can't wait on the unit to start up, as we obviously can't connect to buildkite,
# but we can look whether files are set up correctly # but we can look whether files are set up correctly
machine.wait_for_file("/var/lib/buildkite-agent-one/buildkite-agent.cfg") machine.wait_for_file("/var/lib/buildkite-agent-one/buildkite-agent.cfg")
machine.wait_for_file("/var/lib/buildkite-agent-one/.ssh/id_rsa") machine.wait_for_file("/var/lib/buildkite-agent-one/.ssh/id_rsa")
machine.wait_for_file("/var/lib/buildkite-agent-two/buildkite-agent.cfg") machine.wait_for_file("/var/lib/buildkite-agent-two/buildkite-agent.cfg")
''; '';
} }
)

View File

@ -1,87 +1,85 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "c2FmZQ";
name = "c2FmZQ"; meta.maintainers = with lib.maintainers; [ hmenke ];
meta.maintainers = with lib.maintainers; [ hmenke ];
nodes.machine = { nodes.machine = {
services.c2fmzq-server = { services.c2fmzq-server = {
enable = true; enable = true;
port = 8080; port = 8080;
passphraseFile = builtins.toFile "pwfile" "hunter2"; # don't do this on real deployments passphraseFile = builtins.toFile "pwfile" "hunter2"; # don't do this on real deployments
settings = { settings = {
verbose = 3; # debug verbose = 3; # debug
# make sure multiple freeform options evaluate # make sure multiple freeform options evaluate
allow-new-accounts = true; allow-new-accounts = true;
auto-approve-new-accounts = true; auto-approve-new-accounts = true;
licenses = false; licenses = false;
};
};
environment = {
sessionVariables = {
C2FMZQ_PASSPHRASE = "lol";
C2FMZQ_API_SERVER = "http://localhost:8080";
};
systemPackages = [
pkgs.c2fmzq
(pkgs.writeScriptBin "c2FmZQ-client-wrapper" ''
#!${pkgs.expect}/bin/expect -f
spawn c2FmZQ-client {*}$argv
expect {
"Enter password:" { send "$env(PASSWORD)\r" }
"Type YES to confirm:" { send "YES\r" }
timeout { exit 1 }
eof { exit 0 }
}
interact
'')
];
}; };
}; };
environment = {
sessionVariables = {
C2FMZQ_PASSPHRASE = "lol";
C2FMZQ_API_SERVER = "http://localhost:8080";
};
systemPackages = [
pkgs.c2fmzq
(pkgs.writeScriptBin "c2FmZQ-client-wrapper" ''
#!${pkgs.expect}/bin/expect -f
spawn c2FmZQ-client {*}$argv
expect {
"Enter password:" { send "$env(PASSWORD)\r" }
"Type YES to confirm:" { send "YES\r" }
timeout { exit 1 }
eof { exit 0 }
}
interact
'')
];
};
};
testScript = testScript =
{ nodes, ... }: { nodes, ... }:
'' ''
machine.start() machine.start()
machine.wait_for_unit("c2fmzq-server.service") machine.wait_for_unit("c2fmzq-server.service")
machine.wait_for_open_port(8080) machine.wait_for_open_port(8080)
with subtest("Create accounts for alice and bob"): with subtest("Create accounts for alice and bob"):
machine.succeed("PASSWORD=foobar c2FmZQ-client-wrapper -- -v 3 create-account alice@example.com") machine.succeed("PASSWORD=foobar c2FmZQ-client-wrapper -- -v 3 create-account alice@example.com")
machine.succeed("PASSWORD=fizzbuzz c2FmZQ-client-wrapper -- -v 3 create-account bob@example.com") machine.succeed("PASSWORD=fizzbuzz c2FmZQ-client-wrapper -- -v 3 create-account bob@example.com")
with subtest("Log in as alice"): with subtest("Log in as alice"):
machine.succeed("PASSWORD=foobar c2FmZQ-client-wrapper -- -v 3 login alice@example.com") machine.succeed("PASSWORD=foobar c2FmZQ-client-wrapper -- -v 3 login alice@example.com")
msg = machine.succeed("c2FmZQ-client -v 3 status") msg = machine.succeed("c2FmZQ-client -v 3 status")
assert "Logged in as alice@example.com" in msg, f"ERROR: Not logged in as alice:\n{msg}" assert "Logged in as alice@example.com" in msg, f"ERROR: Not logged in as alice:\n{msg}"
with subtest("Create a new album, upload a file, and delete the uploaded file"): with subtest("Create a new album, upload a file, and delete the uploaded file"):
machine.succeed("c2FmZQ-client -v 3 create-album 'Rarest Memes'") machine.succeed("c2FmZQ-client -v 3 create-album 'Rarest Memes'")
machine.succeed("echo 'pls do not steal' > meme.txt") machine.succeed("echo 'pls do not steal' > meme.txt")
machine.succeed("c2FmZQ-client -v 3 import meme.txt 'Rarest Memes'") machine.succeed("c2FmZQ-client -v 3 import meme.txt 'Rarest Memes'")
machine.succeed("c2FmZQ-client -v 3 sync") machine.succeed("c2FmZQ-client -v 3 sync")
machine.succeed("rm meme.txt") machine.succeed("rm meme.txt")
with subtest("Share the album with bob"): with subtest("Share the album with bob"):
machine.succeed("c2FmZQ-client-wrapper -- -v 3 share 'Rarest Memes' bob@example.com") machine.succeed("c2FmZQ-client-wrapper -- -v 3 share 'Rarest Memes' bob@example.com")
with subtest("Log in as bob"): with subtest("Log in as bob"):
machine.succeed("PASSWORD=fizzbuzz c2FmZQ-client-wrapper -- -v 3 login bob@example.com") machine.succeed("PASSWORD=fizzbuzz c2FmZQ-client-wrapper -- -v 3 login bob@example.com")
msg = machine.succeed("c2FmZQ-client -v 3 status") msg = machine.succeed("c2FmZQ-client -v 3 status")
assert "Logged in as bob@example.com" in msg, f"ERROR: Not logged in as bob:\n{msg}" assert "Logged in as bob@example.com" in msg, f"ERROR: Not logged in as bob:\n{msg}"
with subtest("Download the shared file"): with subtest("Download the shared file"):
machine.succeed("c2FmZQ-client -v 3 download 'shared/Rarest Memes/meme.txt'") machine.succeed("c2FmZQ-client -v 3 download 'shared/Rarest Memes/meme.txt'")
machine.succeed("c2FmZQ-client -v 3 export 'shared/Rarest Memes/meme.txt' .") machine.succeed("c2FmZQ-client -v 3 export 'shared/Rarest Memes/meme.txt' .")
msg = machine.succeed("cat meme.txt") msg = machine.succeed("cat meme.txt")
assert "pls do not steal\n" == msg, f"File content is not the same:\n{msg}" assert "pls do not steal\n" == msg, f"File content is not the same:\n{msg}"
with subtest("Test that PWA is served"): with subtest("Test that PWA is served"):
msg = machine.succeed("curl -sSfL http://localhost:8080") msg = machine.succeed("curl -sSfL http://localhost:8080")
assert "c2FmZQ" in msg, f"Could not find 'c2FmZQ' in the output:\n{msg}" assert "c2FmZQ" in msg, f"Could not find 'c2FmZQ' in the output:\n{msg}"
with subtest("A setting with false value is properly passed"): with subtest("A setting with false value is properly passed"):
machine.succeed("systemctl show -p ExecStart --value c2fmzq-server.service | grep -F -- '--licenses=false'"); machine.succeed("systemctl show -p ExecStart --value c2fmzq-server.service | grep -F -- '--licenses=false'");
''; '';
} }
)

View File

@ -1,44 +1,42 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }:
{ {
name = "cage"; name = "cage";
meta = with pkgs.lib.maintainers; { meta = with pkgs.lib.maintainers; {
maintainers = [ matthewbauer ]; maintainers = [ matthewbauer ];
}; };
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
imports = [ ./common/user-account.nix ]; imports = [ ./common/user-account.nix ];
fonts.packages = with pkgs; [ dejavu_fonts ]; fonts.packages = with pkgs; [ dejavu_fonts ];
services.cage = { services.cage = {
enable = true; enable = true;
user = "alice"; user = "alice";
program = "${pkgs.xterm}/bin/xterm"; program = "${pkgs.xterm}/bin/xterm";
};
# Need to switch to a different GPU driver than the default one (-vga std) so that Cage can launch:
virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
}; };
enableOCR = true; # Need to switch to a different GPU driver than the default one (-vga std) so that Cage can launch:
virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
};
testScript = enableOCR = true;
{ nodes, ... }:
let testScript =
user = nodes.machine.config.users.users.alice; { nodes, ... }:
in let
'' user = nodes.machine.config.users.users.alice;
with subtest("Wait for cage to boot up"): in
start_all() ''
machine.wait_for_file("/run/user/${toString user.uid}/wayland-0.lock") with subtest("Wait for cage to boot up"):
machine.wait_until_succeeds("pgrep xterm") start_all()
machine.wait_for_text("alice@machine") machine.wait_for_file("/run/user/${toString user.uid}/wayland-0.lock")
machine.screenshot("screen") machine.wait_until_succeeds("pgrep xterm")
''; machine.wait_for_text("alice@machine")
} machine.screenshot("screen")
) '';
}

View File

@ -1,72 +1,70 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }:
let let
cagebreakConfigfile = pkgs.writeText "config" '' cagebreakConfigfile = pkgs.writeText "config" ''
workspaces 1 workspaces 1
escape C-t escape C-t
bind t exec env DISPLAY=:0 ${pkgs.xterm}/bin/xterm -cm -pc bind t exec env DISPLAY=:0 ${pkgs.xterm}/bin/xterm -cm -pc
''; '';
in in
{ {
name = "cagebreak"; name = "cagebreak";
meta = with pkgs.lib.maintainers; { meta = with pkgs.lib.maintainers; {
maintainers = [ berbiche ]; maintainers = [ berbiche ];
};
nodes.machine =
{ config, ... }:
{
# Automatically login on tty1 as a normal user:
imports = [ ./common/user-account.nix ];
services.getty.autologinUser = "alice";
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/cagebreak
cp -f ${cagebreakConfigfile} ~/.config/cagebreak/config
cagebreak
fi
'';
hardware.graphics.enable = true;
programs.xwayland.enable = true;
security.polkit.enable = true;
environment.systemPackages = [
pkgs.cagebreak
pkgs.wayland-utils
];
# Need to switch to a different GPU driver than the default one (-vga std) so that Cagebreak can launch:
virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ];
}; };
nodes.machine = enableOCR = true;
{ config, ... }:
{
# Automatically login on tty1 as a normal user:
imports = [ ./common/user-account.nix ];
services.getty.autologinUser = "alice";
programs.bash.loginShellInit = ''
if [ "$(tty)" = "/dev/tty1" ]; then
set -e
mkdir -p ~/.config/cagebreak testScript =
cp -f ${cagebreakConfigfile} ~/.config/cagebreak/config { nodes, ... }:
let
user = nodes.machine.config.users.users.alice;
XDG_RUNTIME_DIR = "/run/user/${toString user.uid}";
in
''
start_all()
machine.wait_for_unit("multi-user.target")
machine.wait_for_file("${XDG_RUNTIME_DIR}/wayland-0")
cagebreak with subtest("ensure wayland works with wayinfo from wallutils"):
fi print(machine.succeed("env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} wayland-info"))
'';
hardware.graphics.enable = true; # TODO: Fix the XWayland test (log the cagebreak output to debug):
programs.xwayland.enable = true; # with subtest("ensure xwayland works with xterm"):
security.polkit.enable = true; # machine.send_key("ctrl-t")
environment.systemPackages = [ # machine.send_key("t")
pkgs.cagebreak # machine.wait_until_succeeds("pgrep xterm")
pkgs.wayland-utils # machine.wait_for_text("${user.name}@machine")
]; # machine.screenshot("screen")
# machine.send_key("ctrl-d")
# Need to switch to a different GPU driver than the default one (-vga std) so that Cagebreak can launch: '';
virtualisation.qemu.options = [ "-vga none -device virtio-gpu-pci" ]; }
};
enableOCR = true;
testScript =
{ nodes, ... }:
let
user = nodes.machine.config.users.users.alice;
XDG_RUNTIME_DIR = "/run/user/${toString user.uid}";
in
''
start_all()
machine.wait_for_unit("multi-user.target")
machine.wait_for_file("${XDG_RUNTIME_DIR}/wayland-0")
with subtest("ensure wayland works with wayinfo from wallutils"):
print(machine.succeed("env XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} wayland-info"))
# TODO: Fix the XWayland test (log the cagebreak output to debug):
# with subtest("ensure xwayland works with xterm"):
# machine.send_key("ctrl-t")
# machine.send_key("t")
# machine.wait_until_succeeds("pgrep xterm")
# machine.wait_for_text("${user.name}@machine")
# machine.screenshot("screen")
# machine.send_key("ctrl-d")
'';
}
)

View File

@ -1,62 +1,60 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: let
let certs = import ./common/acme/server/snakeoil-certs.nix;
certs = import ./common/acme/server/snakeoil-certs.nix; inherit (certs) domain;
inherit (certs) domain; in
in {
{ name = "canaille";
name = "canaille"; meta.maintainers = with pkgs.lib.maintainers; [ erictapen ];
meta.maintainers = with pkgs.lib.maintainers; [ erictapen ];
nodes.server = nodes.server =
{ pkgs, lib, ... }: { pkgs, lib, ... }:
{ {
services.canaille = { services.canaille = {
enable = true; enable = true;
secretKeyFile = pkgs.writeText "canaille-secret-key" '' secretKeyFile = pkgs.writeText "canaille-secret-key" ''
this is not a secret key this is not a secret key
''; '';
settings = { settings = {
SERVER_NAME = domain; SERVER_NAME = domain;
};
}; };
services.nginx.virtualHosts."${domain}" = {
enableACME = lib.mkForce false;
sslCertificate = certs."${domain}".cert;
sslCertificateKey = certs."${domain}".key;
};
networking.hosts."::1" = [ "${domain}" ];
networking.firewall.allowedTCPPorts = [
80
443
];
users.users.canaille.shell = pkgs.bashInteractive;
security.pki.certificateFiles = [ certs.ca.cert ];
}; };
nodes.client = services.nginx.virtualHosts."${domain}" = {
{ nodes, ... }: enableACME = lib.mkForce false;
{ sslCertificate = certs."${domain}".cert;
networking.hosts."${nodes.server.networking.primaryIPAddress}" = [ "${domain}" ]; sslCertificateKey = certs."${domain}".key;
security.pki.certificateFiles = [ certs.ca.cert ];
}; };
testScript = networking.hosts."::1" = [ "${domain}" ];
{ ... }: networking.firewall.allowedTCPPorts = [
'' 80
import json 443
];
start_all() users.users.canaille.shell = pkgs.bashInteractive;
server.wait_for_unit("canaille.socket")
server.wait_until_succeeds("curl -f https://${domain}") security.pki.certificateFiles = [ certs.ca.cert ];
server.succeed("sudo -iu canaille -- canaille create user --user-name admin --password adminpass --emails admin@${domain}") };
json_str = server.succeed("sudo -iu canaille -- canaille get user")
assert json.loads(json_str)[0]["user_name"] == "admin" nodes.client =
server.succeed("sudo -iu canaille -- canaille config check") { nodes, ... }:
''; {
} networking.hosts."${nodes.server.networking.primaryIPAddress}" = [ "${domain}" ];
) security.pki.certificateFiles = [ certs.ca.cert ];
};
testScript =
{ ... }:
''
import json
start_all()
server.wait_for_unit("canaille.socket")
server.wait_until_succeeds("curl -f https://${domain}")
server.succeed("sudo -iu canaille -- canaille create user --user-name admin --password adminpass --emails admin@${domain}")
json_str = server.succeed("sudo -iu canaille -- canaille get user")
assert json.loads(json_str)[0]["user_name"] == "admin"
server.succeed("sudo -iu canaille -- canaille config check")
'';
}

View File

@ -1,250 +1,248 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "castopod";
name = "castopod"; meta = with lib.maintainers; {
meta = with lib.maintainers; { maintainers = [ alexoundos ];
maintainers = [ alexoundos ]; };
nodes.castopod =
{ nodes, ... }:
{
# otherwise 500 MiB file upload fails!
virtualisation.diskSize = 512 + 3 * 512;
networking.firewall.allowedTCPPorts = [ 80 ];
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.attrsets.mapAttrsToList (
name: _: "127.0.0.1 ${name}"
) nodes.castopod.services.nginx.virtualHosts
);
services.castopod = {
enable = true;
database.createLocally = true;
localDomain = "castopod.example.com";
maxUploadSize = "512M";
};
}; };
nodes.castopod = nodes.client =
{ nodes, ... }: {
{ nodes,
# otherwise 500 MiB file upload fails! pkgs,
virtualisation.diskSize = 512 + 3 * 512; lib,
...
}:
let
domain = nodes.castopod.services.castopod.localDomain;
networking.firewall.allowedTCPPorts = [ 80 ]; getIP = node: (builtins.head node.networking.interfaces.eth1.ipv4.addresses).address;
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.attrsets.mapAttrsToList (
name: _: "127.0.0.1 ${name}"
) nodes.castopod.services.nginx.virtualHosts
);
services.castopod = { targetPodcastSize = 500 * 1024 * 1024;
enable = true; lameMp3Bitrate = 348300;
database.createLocally = true; lameMp3FileAdjust = -800;
localDomain = "castopod.example.com"; targetPodcastDuration = toString ((targetPodcastSize + lameMp3FileAdjust) / (lameMp3Bitrate / 8));
maxUploadSize = "512M"; bannerWidth = 3000;
}; banner = pkgs.runCommand "gen-castopod-cover.jpg" { } ''
}; ${pkgs.imagemagick}/bin/magick `
`-background green -bordercolor white -gravity northwest xc:black `
`-duplicate 99 `
`-seed 1 -resize "%[fx:rand()*72+24]" `
`-seed 0 -rotate "%[fx:rand()*360]" -border 6x6 -splice 16x36 `
`-seed 0 -rotate "%[fx:floor(rand()*4)*90]" -resize "150x50!" `
`+append -crop 10x1@ +repage -roll "+%[fx:(t%2)*72]+0" -append `
`-resize ${toString bannerWidth} -quality 1 $out
'';
nodes.client = coverWidth = toString 3000;
{ cover = pkgs.runCommand "gen-castopod-banner.jpg" { } ''
nodes, ${pkgs.imagemagick}/bin/magick `
pkgs, `-background white -bordercolor white -gravity northwest xc:black `
lib, `-duplicate 99 `
... `-seed 1 -resize "%[fx:rand()*72+24]" `
}: `-seed 0 -rotate "%[fx:rand()*360]" -border 6x6 -splice 36x36 `
let `-seed 0 -rotate "%[fx:floor(rand()*4)*90]" -resize "144x144!" `
domain = nodes.castopod.services.castopod.localDomain; `+append -crop 10x1@ +repage -roll "+%[fx:(t%2)*72]+0" -append `
`-resize ${coverWidth} -quality 1 $out
'';
in
{
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.attrsets.mapAttrsToList (
name: _: "${getIP nodes.castopod} ${name}"
) nodes.castopod.services.nginx.virtualHosts
);
getIP = node: (builtins.head node.networking.interfaces.eth1.ipv4.addresses).address; environment.systemPackages =
let
username = "admin";
email = "admin@${domain}";
password = "Abcd1234";
podcastTitle = "Some Title";
episodeTitle = "Episode Title";
browser-test =
pkgs.writers.writePython3Bin "browser-test"
{
libraries = [ pkgs.python3Packages.selenium ];
flakeIgnore = [
"E124"
"E501"
];
}
''
from selenium.webdriver.common.by import By
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from subprocess import STDOUT
import logging
targetPodcastSize = 500 * 1024 * 1024; selenium_logger = logging.getLogger("selenium")
lameMp3Bitrate = 348300; selenium_logger.setLevel(logging.DEBUG)
lameMp3FileAdjust = -800; selenium_logger.addHandler(logging.StreamHandler())
targetPodcastDuration = toString ((targetPodcastSize + lameMp3FileAdjust) / (lameMp3Bitrate / 8));
bannerWidth = 3000;
banner = pkgs.runCommand "gen-castopod-cover.jpg" { } ''
${pkgs.imagemagick}/bin/magick `
`-background green -bordercolor white -gravity northwest xc:black `
`-duplicate 99 `
`-seed 1 -resize "%[fx:rand()*72+24]" `
`-seed 0 -rotate "%[fx:rand()*360]" -border 6x6 -splice 16x36 `
`-seed 0 -rotate "%[fx:floor(rand()*4)*90]" -resize "150x50!" `
`+append -crop 10x1@ +repage -roll "+%[fx:(t%2)*72]+0" -append `
`-resize ${toString bannerWidth} -quality 1 $out
'';
coverWidth = toString 3000; options = Options()
cover = pkgs.runCommand "gen-castopod-banner.jpg" { } '' options.add_argument('--headless')
${pkgs.imagemagick}/bin/magick ` service = Service(log_output=STDOUT)
`-background white -bordercolor white -gravity northwest xc:black ` driver = Firefox(options=options, service=service)
`-duplicate 99 ` driver = Firefox(options=options)
`-seed 1 -resize "%[fx:rand()*72+24]" ` driver.implicitly_wait(30)
`-seed 0 -rotate "%[fx:rand()*360]" -border 6x6 -splice 36x36 ` driver.set_page_load_timeout(60)
`-seed 0 -rotate "%[fx:floor(rand()*4)*90]" -resize "144x144!" `
`+append -crop 10x1@ +repage -roll "+%[fx:(t%2)*72]+0" -append `
`-resize ${coverWidth} -quality 1 $out
'';
in
{
networking.extraHosts = lib.strings.concatStringsSep "\n" (
lib.attrsets.mapAttrsToList (
name: _: "${getIP nodes.castopod} ${name}"
) nodes.castopod.services.nginx.virtualHosts
);
environment.systemPackages = # install ##########################################################
let
username = "admin";
email = "admin@${domain}";
password = "Abcd1234";
podcastTitle = "Some Title";
episodeTitle = "Episode Title";
browser-test =
pkgs.writers.writePython3Bin "browser-test"
{
libraries = [ pkgs.python3Packages.selenium ];
flakeIgnore = [
"E124"
"E501"
];
}
''
from selenium.webdriver.common.by import By
from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from subprocess import STDOUT
import logging
selenium_logger = logging.getLogger("selenium") driver.get('http://${domain}/cp-install')
selenium_logger.setLevel(logging.DEBUG)
selenium_logger.addHandler(logging.StreamHandler())
options = Options() wait = WebDriverWait(driver, 20)
options.add_argument('--headless')
service = Service(log_output=STDOUT)
driver = Firefox(options=options, service=service)
driver = Firefox(options=options)
driver.implicitly_wait(30)
driver.set_page_load_timeout(60)
# install ########################################################## wait.until(EC.title_contains("installer"))
driver.get('http://${domain}/cp-install') driver.find_element(By.CSS_SELECTOR, '#username').send_keys(
'${username}'
)
driver.find_element(By.CSS_SELECTOR, '#email').send_keys(
'${email}'
)
driver.find_element(By.CSS_SELECTOR, '#password').send_keys(
'${password}'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Finish install')]"
).click()
wait = WebDriverWait(driver, 20) wait.until(EC.title_contains("Auth"))
wait.until(EC.title_contains("installer")) driver.find_element(By.CSS_SELECTOR, '#email').send_keys(
'${email}'
)
driver.find_element(By.CSS_SELECTOR, '#password').send_keys(
'${password}'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Login')]"
).click()
driver.find_element(By.CSS_SELECTOR, '#username').send_keys( wait.until(EC.title_contains("Admin dashboard"))
'${username}'
)
driver.find_element(By.CSS_SELECTOR, '#email').send_keys(
'${email}'
)
driver.find_element(By.CSS_SELECTOR, '#password').send_keys(
'${password}'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Finish install')]"
).click()
wait.until(EC.title_contains("Auth")) # create podcast ###################################################
driver.find_element(By.CSS_SELECTOR, '#email').send_keys( driver.get('http://${domain}/admin/podcasts/new')
'${email}'
)
driver.find_element(By.CSS_SELECTOR, '#password').send_keys(
'${password}'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Login')]"
).click()
wait.until(EC.title_contains("Admin dashboard")) wait.until(EC.title_contains("Create podcast"))
# create podcast ################################################### driver.find_element(By.CSS_SELECTOR, '#cover').send_keys(
'${cover}'
)
driver.find_element(By.CSS_SELECTOR, '#banner').send_keys(
'${banner}'
)
driver.find_element(By.CSS_SELECTOR, '#title').send_keys(
'${podcastTitle}'
)
driver.find_element(By.CSS_SELECTOR, '#handle').send_keys(
'some_handle'
)
driver.find_element(By.CSS_SELECTOR, '#description').send_keys(
'Some description'
)
driver.find_element(By.CSS_SELECTOR, '#owner_name').send_keys(
'Owner Name'
)
driver.find_element(By.CSS_SELECTOR, '#owner_email').send_keys(
'owner@email.xyz'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Create podcast')]"
).click()
driver.get('http://${domain}/admin/podcasts/new') wait.until(EC.title_contains("${podcastTitle}"))
wait.until(EC.title_contains("Create podcast")) driver.find_element(By.XPATH,
"//span[contains(., 'Add an episode')]"
).click()
driver.find_element(By.CSS_SELECTOR, '#cover').send_keys( wait.until(EC.title_contains("Add an episode"))
'${cover}'
)
driver.find_element(By.CSS_SELECTOR, '#banner').send_keys(
'${banner}'
)
driver.find_element(By.CSS_SELECTOR, '#title').send_keys(
'${podcastTitle}'
)
driver.find_element(By.CSS_SELECTOR, '#handle').send_keys(
'some_handle'
)
driver.find_element(By.CSS_SELECTOR, '#description').send_keys(
'Some description'
)
driver.find_element(By.CSS_SELECTOR, '#owner_name').send_keys(
'Owner Name'
)
driver.find_element(By.CSS_SELECTOR, '#owner_email').send_keys(
'owner@email.xyz'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Create podcast')]"
).click()
wait.until(EC.title_contains("${podcastTitle}")) # upload podcast ###################################################
driver.find_element(By.XPATH, driver.find_element(By.CSS_SELECTOR, '#audio_file').send_keys(
"//span[contains(., 'Add an episode')]" '/tmp/podcast.mp3'
).click() )
driver.find_element(By.CSS_SELECTOR, '#cover').send_keys(
'${cover}'
)
driver.find_element(By.CSS_SELECTOR, '#description').send_keys(
'Episode description'
)
driver.find_element(By.CSS_SELECTOR, '#title').send_keys(
'${episodeTitle}'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Create episode')]"
).click()
wait.until(EC.title_contains("Add an episode")) wait.until(EC.title_contains("${episodeTitle}"))
# upload podcast ################################################### driver.close()
driver.quit()
driver.find_element(By.CSS_SELECTOR, '#audio_file').send_keys(
'/tmp/podcast.mp3'
)
driver.find_element(By.CSS_SELECTOR, '#cover').send_keys(
'${cover}'
)
driver.find_element(By.CSS_SELECTOR, '#description').send_keys(
'Episode description'
)
driver.find_element(By.CSS_SELECTOR, '#title').send_keys(
'${episodeTitle}'
)
driver.find_element(By.XPATH,
"//button[contains(., 'Create episode')]"
).click()
wait.until(EC.title_contains("${episodeTitle}"))
driver.close()
driver.quit()
'';
in
[
pkgs.firefox-unwrapped
pkgs.geckodriver
browser-test
(pkgs.writeShellApplication {
name = "build-mp3";
runtimeInputs = with pkgs; [
sox
lame
];
text = ''
out=/tmp/podcast.mp3
sox -n -r 48000 -t wav - synth ${targetPodcastDuration} sine 440 `
`| lame --noreplaygain --cbr -q 9 -b 320 - $out
FILESIZE="$(stat -c%s $out)"
[ "$FILESIZE" -gt 0 ]
[ "$FILESIZE" -le "${toString targetPodcastSize}" ]
''; '';
}) in
]; [
}; pkgs.firefox-unwrapped
pkgs.geckodriver
browser-test
(pkgs.writeShellApplication {
name = "build-mp3";
runtimeInputs = with pkgs; [
sox
lame
];
text = ''
out=/tmp/podcast.mp3
sox -n -r 48000 -t wav - synth ${targetPodcastDuration} sine 440 `
`| lame --noreplaygain --cbr -q 9 -b 320 - $out
FILESIZE="$(stat -c%s $out)"
[ "$FILESIZE" -gt 0 ]
[ "$FILESIZE" -le "${toString targetPodcastSize}" ]
'';
})
];
};
testScript = '' testScript = ''
start_all() start_all()
castopod.wait_for_unit("castopod-setup.service") castopod.wait_for_unit("castopod-setup.service")
castopod.wait_for_file("/run/phpfpm/castopod.sock") castopod.wait_for_file("/run/phpfpm/castopod.sock")
castopod.wait_for_unit("nginx.service") castopod.wait_for_unit("nginx.service")
castopod.wait_for_open_port(80) castopod.wait_for_open_port(80)
castopod.wait_until_succeeds("curl -sS -f http://castopod.example.com") castopod.wait_until_succeeds("curl -sS -f http://castopod.example.com")
client.succeed("build-mp3") client.succeed("build-mp3")
with subtest("Create superadmin, log in, create and upload a podcast"): with subtest("Create superadmin, log in, create and upload a podcast"):
client.succeed(\ client.succeed(\
"PYTHONUNBUFFERED=1 systemd-cat -t browser-test browser-test") "PYTHONUNBUFFERED=1 systemd-cat -t browser-test browser-test")
''; '';
} }
)

View File

@ -1,49 +1,47 @@
# This test checks charliecloud image construction and run # This test checks charliecloud image construction and run
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: let
let
dockerfile = pkgs.writeText "Dockerfile" '' dockerfile = pkgs.writeText "Dockerfile" ''
FROM nix FROM nix
RUN mkdir /home /tmp RUN mkdir /home /tmp
RUN touch /etc/passwd /etc/group RUN touch /etc/passwd /etc/group
CMD ["true"] CMD ["true"]
''; '';
in in
{ {
name = "charliecloud"; name = "charliecloud";
meta = with pkgs.lib.maintainers; { meta = with pkgs.lib.maintainers; {
maintainers = [ bzizou ]; maintainers = [ bzizou ];
}; };
nodes = { nodes = {
host = host =
{ ... }: { ... }:
{ {
environment.systemPackages = [ pkgs.charliecloud ]; environment.systemPackages = [ pkgs.charliecloud ];
virtualisation.docker.enable = true; virtualisation.docker.enable = true;
users.users.alice = { users.users.alice = {
isNormalUser = true; isNormalUser = true;
extraGroups = [ "docker" ]; extraGroups = [ "docker" ];
};
}; };
}; };
};
testScript = '' testScript = ''
host.start() host.start()
host.wait_for_unit("docker.service") host.wait_for_unit("docker.service")
host.succeed( host.succeed(
'su - alice -c "docker load --input=${pkgs.dockerTools.examples.nix}"' 'su - alice -c "docker load --input=${pkgs.dockerTools.examples.nix}"'
) )
host.succeed( host.succeed(
"cp ${dockerfile} /home/alice/Dockerfile" "cp ${dockerfile} /home/alice/Dockerfile"
) )
host.succeed('su - alice -c "ch-build -t hello ."') host.succeed('su - alice -c "ch-build -t hello ."')
host.succeed('su - alice -c "ch-builder2tar hello /var/tmp"') host.succeed('su - alice -c "ch-builder2tar hello /var/tmp"')
host.succeed('su - alice -c "ch-tar2dir /var/tmp/hello.tar.gz /var/tmp"') host.succeed('su - alice -c "ch-tar2dir /var/tmp/hello.tar.gz /var/tmp"')
host.succeed('su - alice -c "ch-run /var/tmp/hello -- echo Running_From_Container_OK"') host.succeed('su - alice -c "ch-run /var/tmp/hello -- echo Running_From_Container_OK"')
''; '';
} }
)

View File

@ -1,84 +1,82 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "cinnamon-wayland";
name = "cinnamon-wayland";
meta.maintainers = lib.teams.cinnamon.members; meta.maintainers = lib.teams.cinnamon.members;
nodes.machine = nodes.machine =
{ nodes, ... }: { nodes, ... }:
{ {
imports = [ ./common/user-account.nix ]; imports = [ ./common/user-account.nix ];
services.xserver.enable = true; services.xserver.enable = true;
services.xserver.desktopManager.cinnamon.enable = true; services.xserver.desktopManager.cinnamon.enable = true;
services.displayManager = { services.displayManager = {
autoLogin.enable = true; autoLogin.enable = true;
autoLogin.user = nodes.machine.users.users.alice.name; autoLogin.user = nodes.machine.users.users.alice.name;
defaultSession = "cinnamon-wayland"; defaultSession = "cinnamon-wayland";
};
# For the sessionPath subtest.
services.xserver.desktopManager.cinnamon.sessionPath = [ pkgs.gpaste ];
}; };
enableOCR = true; # For the sessionPath subtest.
services.xserver.desktopManager.cinnamon.sessionPath = [ pkgs.gpaste ];
};
testScript = enableOCR = true;
{ nodes, ... }:
let
user = nodes.machine.users.users.alice;
env = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus";
su = command: "su - ${user.name} -c '${env} ${command}'";
# Call javascript in cinnamon (the shell), returns a tuple (success, output), testScript =
# where `success` is true if the dbus call was successful and `output` is what { nodes, ... }:
# the javascript evaluates to. let
eval = user = nodes.machine.users.users.alice;
name: su "gdbus call --session -d org.Cinnamon -o /org/Cinnamon -m org.Cinnamon.Eval ${name}"; env = "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${toString user.uid}/bus";
in su = command: "su - ${user.name} -c '${env} ${command}'";
''
machine.wait_for_unit("display-manager.service")
with subtest("Wait for wayland server"): # Call javascript in cinnamon (the shell), returns a tuple (success, output),
machine.wait_for_file("/run/user/${toString user.uid}/wayland-0") # where `success` is true if the dbus call was successful and `output` is what
# the javascript evaluates to.
eval =
name: su "gdbus call --session -d org.Cinnamon -o /org/Cinnamon -m org.Cinnamon.Eval ${name}";
in
''
machine.wait_for_unit("display-manager.service")
with subtest("Check that logging in has given the user ownership of devices"): with subtest("Wait for wayland server"):
machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}") machine.wait_for_file("/run/user/${toString user.uid}/wayland-0")
with subtest("Wait for the Cinnamon shell"): with subtest("Check that logging in has given the user ownership of devices"):
# Correct output should be (true, '2') machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
# https://github.com/linuxmint/cinnamon/blob/5.4.0/js/ui/main.js#L183-L187
machine.wait_until_succeeds("${eval "Main.runState"} | grep -q 'true,..2'")
with subtest("Check if Cinnamon components actually start"): with subtest("Wait for the Cinnamon shell"):
for i in ["csd-media-keys", "xapp-sn-watcher", "nemo-desktop"]: # Correct output should be (true, '2')
machine.wait_until_succeeds(f"pgrep -f {i}") # https://github.com/linuxmint/cinnamon/blob/5.4.0/js/ui/main.js#L183-L187
machine.wait_until_succeeds("journalctl -b --grep 'Loaded applet menu@cinnamon.org'") machine.wait_until_succeeds("${eval "Main.runState"} | grep -q 'true,..2'")
machine.wait_until_succeeds("journalctl -b --grep 'calendar@cinnamon.org: Calendar events supported'")
with subtest("Check if sessionPath option actually works"): with subtest("Check if Cinnamon components actually start"):
machine.succeed("${eval "imports.gi.GIRepository.Repository.get_search_path\\(\\)"} | grep gpaste") for i in ["csd-media-keys", "xapp-sn-watcher", "nemo-desktop"]:
machine.wait_until_succeeds(f"pgrep -f {i}")
machine.wait_until_succeeds("journalctl -b --grep 'Loaded applet menu@cinnamon.org'")
machine.wait_until_succeeds("journalctl -b --grep 'calendar@cinnamon.org: Calendar events supported'")
with subtest("Open Cinnamon Settings"): with subtest("Check if sessionPath option actually works"):
machine.succeed("${su "cinnamon-settings themes >&2 &"}") machine.succeed("${eval "imports.gi.GIRepository.Repository.get_search_path\\(\\)"} | grep gpaste")
machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'cinnamon-settings'")
machine.wait_for_text('(Style|Appearance|Color)')
machine.sleep(2)
machine.screenshot("cinnamon_settings")
with subtest("Check if screensaver works"): with subtest("Open Cinnamon Settings"):
# This is not supported at the moment. machine.succeed("${su "cinnamon-settings themes >&2 &"}")
# https://trello.com/b/HHs01Pab/cinnamon-wayland machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'cinnamon-settings'")
machine.execute("${su "cinnamon-screensaver-command -l >&2 &"}") machine.wait_for_text('(Style|Appearance|Color)')
machine.wait_until_succeeds("journalctl -b --grep 'cinnamon-screensaver is disabled in wayland sessions'") machine.sleep(2)
machine.screenshot("cinnamon_settings")
with subtest("Open GNOME Terminal"): with subtest("Check if screensaver works"):
machine.succeed("${su "dbus-launch gnome-terminal"}") # This is not supported at the moment.
machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'gnome-terminal'") # https://trello.com/b/HHs01Pab/cinnamon-wayland
machine.sleep(2) machine.execute("${su "cinnamon-screensaver-command -l >&2 &"}")
machine.wait_until_succeeds("journalctl -b --grep 'cinnamon-screensaver is disabled in wayland sessions'")
with subtest("Check if Cinnamon has ever coredumped"): with subtest("Open GNOME Terminal"):
machine.fail("coredumpctl --json=short | grep -E 'cinnamon|nemo'") machine.succeed("${su "dbus-launch gnome-terminal"}")
''; machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'gnome-terminal'")
} machine.sleep(2)
)
with subtest("Check if Cinnamon has ever coredumped"):
machine.fail("coredumpctl --json=short | grep -E 'cinnamon|nemo'")
'';
}

View File

@ -1,104 +1,102 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "cinnamon";
name = "cinnamon";
meta.maintainers = lib.teams.cinnamon.members; meta.maintainers = lib.teams.cinnamon.members;
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
imports = [ ./common/user-account.nix ]; imports = [ ./common/user-account.nix ];
services.xserver.enable = true; services.xserver.enable = true;
services.xserver.desktopManager.cinnamon.enable = true; services.xserver.desktopManager.cinnamon.enable = true;
# We don't ship gnome-text-editor in Cinnamon module, we add this line mainly # We don't ship gnome-text-editor in Cinnamon module, we add this line mainly
# to catch eval issues related to this option. # to catch eval issues related to this option.
environment.cinnamon.excludePackages = [ pkgs.gnome-text-editor ]; environment.cinnamon.excludePackages = [ pkgs.gnome-text-editor ];
# For the sessionPath subtest. # For the sessionPath subtest.
services.xserver.desktopManager.cinnamon.sessionPath = [ pkgs.gpaste ]; services.xserver.desktopManager.cinnamon.sessionPath = [ pkgs.gpaste ];
# For OCR test. # For OCR test.
services.xserver.displayManager.lightdm.greeters.slick.extraConfig = '' services.xserver.displayManager.lightdm.greeters.slick.extraConfig = ''
enable-hidpi = on enable-hidpi = on
'';
};
enableOCR = true;
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}'";
# Call javascript in cinnamon (the shell), returns a tuple (success, output),
# where `success` is true if the dbus call was successful and `output` is what
# the javascript evaluates to.
eval =
name: su "gdbus call --session -d org.Cinnamon -o /org/Cinnamon -m org.Cinnamon.Eval ${name}";
in
''
machine.wait_for_unit("display-manager.service")
with subtest("Test if we can see username in slick-greeter"):
machine.wait_for_text("${user.description}")
machine.screenshot("slick_greeter_lightdm")
with subtest("Login with slick-greeter"):
machine.send_chars("${user.password}\n")
machine.wait_for_x()
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("Wait for the Cinnamon shell"):
# Correct output should be (true, '2')
# https://github.com/linuxmint/cinnamon/blob/5.4.0/js/ui/main.js#L183-L187
machine.wait_until_succeeds("${eval "Main.runState"} | grep -q 'true,..2'")
with subtest("Check if Cinnamon components actually start"):
for i in ["csd-media-keys", "cinnamon-killer-daemon", "xapp-sn-watcher", "nemo-desktop"]:
machine.wait_until_succeeds(f"pgrep -f {i}")
machine.wait_until_succeeds("journalctl -b --grep 'Loaded applet menu@cinnamon.org'")
machine.wait_until_succeeds("journalctl -b --grep 'calendar@cinnamon.org: Calendar events supported'")
with subtest("Check if sessionPath option actually works"):
machine.succeed("${eval "imports.gi.GIRepository.Repository.get_search_path\\(\\)"} | grep gpaste")
with subtest("Open Cinnamon Settings"):
machine.succeed("${su "cinnamon-settings themes >&2 &"}")
machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'cinnamon-settings'")
machine.wait_for_text('(Style|Appearance|Color)')
machine.sleep(2)
machine.screenshot("cinnamon_settings")
with subtest("Lock the screen"):
machine.succeed("${su "cinnamon-screensaver-command -l >&2 &"}")
machine.wait_until_succeeds("${su "cinnamon-screensaver-command -q"} | grep 'The screensaver is active'")
machine.sleep(2)
machine.screenshot("cinnamon_screensaver")
machine.send_chars("${user.password}\n", delay=0.2)
machine.wait_until_succeeds("${su "cinnamon-screensaver-command -q"} | grep 'The screensaver is inactive'")
machine.sleep(2)
with subtest("Open GNOME Terminal"):
machine.succeed("${su "gnome-terminal"}")
machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'gnome-terminal'")
machine.sleep(2)
with subtest("Open virtual keyboard"):
machine.succeed("${su "dbus-send --print-reply --dest=org.Cinnamon /org/Cinnamon org.Cinnamon.ToggleKeyboard"}")
machine.wait_for_text('(Ctrl|Alt)')
machine.sleep(2)
machine.screenshot("cinnamon_virtual_keyboard")
with subtest("Check if Cinnamon has ever coredumped"):
machine.fail("coredumpctl --json=short | grep -E 'cinnamon|nemo'")
''; '';
} };
)
enableOCR = true;
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}'";
# Call javascript in cinnamon (the shell), returns a tuple (success, output),
# where `success` is true if the dbus call was successful and `output` is what
# the javascript evaluates to.
eval =
name: su "gdbus call --session -d org.Cinnamon -o /org/Cinnamon -m org.Cinnamon.Eval ${name}";
in
''
machine.wait_for_unit("display-manager.service")
with subtest("Test if we can see username in slick-greeter"):
machine.wait_for_text("${user.description}")
machine.screenshot("slick_greeter_lightdm")
with subtest("Login with slick-greeter"):
machine.send_chars("${user.password}\n")
machine.wait_for_x()
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("Wait for the Cinnamon shell"):
# Correct output should be (true, '2')
# https://github.com/linuxmint/cinnamon/blob/5.4.0/js/ui/main.js#L183-L187
machine.wait_until_succeeds("${eval "Main.runState"} | grep -q 'true,..2'")
with subtest("Check if Cinnamon components actually start"):
for i in ["csd-media-keys", "cinnamon-killer-daemon", "xapp-sn-watcher", "nemo-desktop"]:
machine.wait_until_succeeds(f"pgrep -f {i}")
machine.wait_until_succeeds("journalctl -b --grep 'Loaded applet menu@cinnamon.org'")
machine.wait_until_succeeds("journalctl -b --grep 'calendar@cinnamon.org: Calendar events supported'")
with subtest("Check if sessionPath option actually works"):
machine.succeed("${eval "imports.gi.GIRepository.Repository.get_search_path\\(\\)"} | grep gpaste")
with subtest("Open Cinnamon Settings"):
machine.succeed("${su "cinnamon-settings themes >&2 &"}")
machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'cinnamon-settings'")
machine.wait_for_text('(Style|Appearance|Color)')
machine.sleep(2)
machine.screenshot("cinnamon_settings")
with subtest("Lock the screen"):
machine.succeed("${su "cinnamon-screensaver-command -l >&2 &"}")
machine.wait_until_succeeds("${su "cinnamon-screensaver-command -q"} | grep 'The screensaver is active'")
machine.sleep(2)
machine.screenshot("cinnamon_screensaver")
machine.send_chars("${user.password}\n", delay=0.2)
machine.wait_until_succeeds("${su "cinnamon-screensaver-command -q"} | grep 'The screensaver is inactive'")
machine.sleep(2)
with subtest("Open GNOME Terminal"):
machine.succeed("${su "gnome-terminal"}")
machine.wait_until_succeeds("${eval "global.display.focus_window.wm_class"} | grep -i 'gnome-terminal'")
machine.sleep(2)
with subtest("Open virtual keyboard"):
machine.succeed("${su "dbus-send --print-reply --dest=org.Cinnamon /org/Cinnamon org.Cinnamon.ToggleKeyboard"}")
machine.wait_for_text('(Ctrl|Alt)')
machine.sleep(2)
machine.screenshot("cinnamon_virtual_keyboard")
with subtest("Check if Cinnamon has ever coredumped"):
machine.fail("coredumpctl --json=short | grep -E 'cinnamon|nemo'")
'';
}

View File

@ -18,117 +18,115 @@ let
in in
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "cjdns";
name = "cjdns"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ ehmry ];
maintainers = [ ehmry ]; };
};
nodes = { nodes = {
# Alice finds peers over over ETHInterface. # Alice finds peers over over ETHInterface.
alice = alice =
{ ... }: { ... }:
{ {
imports = [ basicConfig ]; imports = [ basicConfig ];
services.cjdns.ETHInterface.bind = "eth1"; services.cjdns.ETHInterface.bind = "eth1";
services.httpd.enable = true; services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org"; services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ]; networking.firewall.allowedTCPPorts = [ 80 ];
}; };
# Bob explicitly connects to Carol over UDPInterface. # Bob explicitly connects to Carol over UDPInterface.
bob = bob =
{ ... }: { ... }:
{ {
imports = [ basicConfig ]; imports = [ basicConfig ];
networking.interfaces.eth1.ipv4.addresses = [ networking.interfaces.eth1.ipv4.addresses = [
{ {
address = "192.168.0.2"; address = "192.168.0.2";
prefixLength = 24; prefixLength = 24;
} }
]; ];
services.cjdns = { services.cjdns = {
UDPInterface = { UDPInterface = {
bind = "0.0.0.0:1024"; bind = "0.0.0.0:1024";
connectTo."192.168.0.1:1024" = { connectTo."192.168.0.1:1024" = {
password = carolPassword; password = carolPassword;
publicKey = carolPubKey; publicKey = carolPubKey;
};
}; };
}; };
}; };
};
# Carol listens on ETHInterface and UDPInterface, # Carol listens on ETHInterface and UDPInterface,
# but knows neither Alice or Bob. # but knows neither Alice or Bob.
carol = carol =
{ ... }: { ... }:
{ {
imports = [ basicConfig ]; imports = [ basicConfig ];
environment.etc."cjdns.keys".text = '' environment.etc."cjdns.keys".text = ''
CJDNS_PRIVATE_KEY=${carolKey} CJDNS_PRIVATE_KEY=${carolKey}
CJDNS_ADMIN_PASSWORD=FOOBAR CJDNS_ADMIN_PASSWORD=FOOBAR
''; '';
networking.interfaces.eth1.ipv4.addresses = [ networking.interfaces.eth1.ipv4.addresses = [
{ {
address = "192.168.0.1"; address = "192.168.0.1";
prefixLength = 24; prefixLength = 24;
} }
]; ];
services.cjdns = { services.cjdns = {
authorizedPasswords = [ carolPassword ]; authorizedPasswords = [ carolPassword ];
ETHInterface.bind = "eth1"; ETHInterface.bind = "eth1";
UDPInterface.bind = "192.168.0.1:1024"; UDPInterface.bind = "192.168.0.1:1024";
};
networking.firewall.allowedUDPPorts = [ 1024 ];
}; };
networking.firewall.allowedUDPPorts = [ 1024 ];
};
}; };
testScript = '' testScript = ''
import re import re
start_all() start_all()
alice.wait_for_unit("cjdns.service") alice.wait_for_unit("cjdns.service")
bob.wait_for_unit("cjdns.service") bob.wait_for_unit("cjdns.service")
carol.wait_for_unit("cjdns.service") carol.wait_for_unit("cjdns.service")
def cjdns_ip(machine): def cjdns_ip(machine):
res = machine.succeed("ip -o -6 addr show dev tun0") res = machine.succeed("ip -o -6 addr show dev tun0")
ip = re.split("\s+|/", res)[3] ip = re.split("\s+|/", res)[3]
machine.log("has ip {}".format(ip)) machine.log("has ip {}".format(ip))
return ip return ip
alice_ip6 = cjdns_ip(alice) alice_ip6 = cjdns_ip(alice)
bob_ip6 = cjdns_ip(bob) bob_ip6 = cjdns_ip(bob)
carol_ip6 = cjdns_ip(carol) carol_ip6 = cjdns_ip(carol)
# ping a few times each to let the routing table establish itself # ping a few times each to let the routing table establish itself
alice.succeed("ping -c 4 {}".format(carol_ip6)) alice.succeed("ping -c 4 {}".format(carol_ip6))
bob.succeed("ping -c 4 {}".format(carol_ip6)) bob.succeed("ping -c 4 {}".format(carol_ip6))
carol.succeed("ping -c 4 {}".format(alice_ip6)) carol.succeed("ping -c 4 {}".format(alice_ip6))
carol.succeed("ping -c 4 {}".format(bob_ip6)) carol.succeed("ping -c 4 {}".format(bob_ip6))
alice.succeed("ping -c 4 {}".format(bob_ip6)) alice.succeed("ping -c 4 {}".format(bob_ip6))
bob.succeed("ping -c 4 {}".format(alice_ip6)) bob.succeed("ping -c 4 {}".format(alice_ip6))
alice.wait_for_unit("httpd.service") alice.wait_for_unit("httpd.service")
bob.succeed("curl --fail -g http://[{}]".format(alice_ip6)) bob.succeed("curl --fail -g http://[{}]".format(alice_ip6))
''; '';
} }
)

View File

@ -1,35 +1,33 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "clickhouse";
name = "clickhouse"; meta.maintainers = with pkgs.lib.maintainers; [ ];
meta.maintainers = with pkgs.lib.maintainers; [ ];
nodes.machine = { nodes.machine = {
services.clickhouse.enable = true; services.clickhouse.enable = true;
virtualisation.memorySize = 4096; virtualisation.memorySize = 4096;
}; };
testScript = testScript =
let let
# work around quote/substitution complexity by Nix, Perl, bash and SQL. # work around quote/substitution complexity by Nix, Perl, bash and SQL.
tableDDL = pkgs.writeText "ddl.sql" "CREATE TABLE `demo` (`value` FixedString(10)) engine = MergeTree PARTITION BY value ORDER BY tuple();"; tableDDL = pkgs.writeText "ddl.sql" "CREATE TABLE `demo` (`value` FixedString(10)) engine = MergeTree PARTITION BY value ORDER BY tuple();";
insertQuery = pkgs.writeText "insert.sql" "INSERT INTO `demo` (`value`) VALUES ('foo');"; insertQuery = pkgs.writeText "insert.sql" "INSERT INTO `demo` (`value`) VALUES ('foo');";
selectQuery = pkgs.writeText "select.sql" "SELECT * from `demo`"; selectQuery = pkgs.writeText "select.sql" "SELECT * from `demo`";
in in
'' ''
machine.start() machine.start()
machine.wait_for_unit("clickhouse.service") machine.wait_for_unit("clickhouse.service")
machine.wait_for_open_port(9000) machine.wait_for_open_port(9000)
machine.succeed( machine.succeed(
"cat ${tableDDL} | clickhouse-client" "cat ${tableDDL} | clickhouse-client"
) )
machine.succeed( machine.succeed(
"cat ${insertQuery} | clickhouse-client" "cat ${insertQuery} | clickhouse-client"
) )
machine.succeed( machine.succeed(
"cat ${selectQuery} | clickhouse-client | grep foo" "cat ${selectQuery} | clickhouse-client | grep foo"
) )
''; '';
} }
)

View File

@ -1,21 +1,19 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "cloudlog";
name = "cloudlog"; meta = {
meta = { maintainers = with pkgs.lib.maintainers; [ melling ];
maintainers = with pkgs.lib.maintainers; [ melling ]; };
nodes = {
machine = {
services.mysql.package = pkgs.mariadb;
services.cloudlog.enable = true;
}; };
nodes = { };
machine = { testScript = ''
services.mysql.package = pkgs.mariadb; start_all()
services.cloudlog.enable = true; machine.wait_for_unit("phpfpm-cloudlog")
}; machine.wait_for_open_port(80);
}; machine.wait_until_succeeds("curl -s -L --fail http://localhost | grep 'Login - Cloudlog'")
testScript = '' '';
start_all() }
machine.wait_for_unit("phpfpm-cloudlog")
machine.wait_for_open_port(80);
machine.wait_until_succeeds("curl -s -L --fail http://localhost | grep 'Login - Cloudlog'")
'';
}
)

View File

@ -1,156 +1,154 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }:
let let
user = "alice"; # from ./common/user-account.nix user = "alice"; # from ./common/user-account.nix
password = "foobar"; # from ./common/user-account.nix password = "foobar"; # from ./common/user-account.nix
in in
{ {
name = "cockpit"; name = "cockpit";
meta = { meta = {
maintainers = with lib.maintainers; [ lucasew ]; maintainers = with lib.maintainers; [ lucasew ];
}; };
nodes = { nodes = {
server = server =
{ config, ... }: { config, ... }:
{ {
imports = [ ./common/user-account.nix ]; imports = [ ./common/user-account.nix ];
security.polkit.enable = true; security.polkit.enable = true;
users.users.${user} = { users.users.${user} = {
extraGroups = [ "wheel" ]; extraGroups = [ "wheel" ];
};
services.cockpit = {
enable = true;
port = 7890;
openFirewall = true;
allowed-origins = [
"https://server:${toString config.services.cockpit.port}"
];
};
}; };
client = services.cockpit = {
{ config, ... }: enable = true;
{ port = 7890;
imports = [ ./common/user-account.nix ]; openFirewall = true;
environment.systemPackages = allowed-origins = [
let "https://server:${toString config.services.cockpit.port}"
seleniumScript = ];
pkgs.writers.writePython3Bin "selenium-script"
{
libraries = with pkgs.python3Packages; [ selenium ];
}
''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep
def log(msg):
from sys import stderr
print(f"[*] {msg}", file=stderr)
log("Initializing")
options = Options()
options.add_argument("--headless")
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501
driver = webdriver.Firefox(options=options, service=service)
driver.implicitly_wait(10)
log("Opening homepage")
driver.get("https://server:7890")
def wait_elem(by, query, timeout=10):
wait = WebDriverWait(driver, timeout)
wait.until(EC.presence_of_element_located((by, query)))
def wait_title_contains(title, timeout=10):
wait = WebDriverWait(driver, timeout)
wait.until(EC.title_contains(title))
def find_element(by, query):
return driver.find_element(by, query)
def set_value(elem, value):
script = 'arguments[0].value = arguments[1]'
return driver.execute_script(script, elem, value)
log("Waiting for the homepage to load")
# cockpit sets initial title as hostname
wait_title_contains("server")
wait_elem(By.CSS_SELECTOR, 'input#login-user-input')
log("Homepage loaded!")
log("Filling out username")
login_input = find_element(By.CSS_SELECTOR, 'input#login-user-input')
set_value(login_input, "${user}")
log("Filling out password")
password_input = find_element(By.CSS_SELECTOR, 'input#login-password-input')
set_value(password_input, "${password}")
log("Submitting credentials for login")
driver.find_element(By.CSS_SELECTOR, 'button#login-button').click()
# driver.implicitly_wait(1)
# driver.get("https://server:7890/system")
log("Waiting dashboard to load")
wait_title_contains("${user}@server")
log("Waiting for the frontend to initialize")
sleep(1)
log("Looking for that banner that tells about limited access")
container_iframe = find_element(By.CSS_SELECTOR, 'iframe.container-frame')
driver.switch_to.frame(container_iframe)
assert "Web console is running in limited access mode" in driver.page_source
log("Clicking the sudo button")
for button in driver.find_elements(By.TAG_NAME, "button"):
if 'admin' in button.text:
button.click()
driver.switch_to.default_content()
log("Checking that /nonexistent is not a thing")
assert '/nonexistent' not in driver.page_source
assert len(driver.find_elements(By.CSS_SELECTOR, '#machine-reconnect')) == 0
driver.close()
'';
in
with pkgs;
[
firefox-unwrapped
geckodriver
seleniumScript
];
}; };
}; };
client =
{ config, ... }:
{
imports = [ ./common/user-account.nix ];
environment.systemPackages =
let
seleniumScript =
pkgs.writers.writePython3Bin "selenium-script"
{
libraries = with pkgs.python3Packages; [ selenium ];
}
''
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep
testScript = ''
start_all()
server.wait_for_unit("sockets.target") def log(msg):
server.wait_for_open_port(7890) from sys import stderr
print(f"[*] {msg}", file=stderr)
client.succeed("curl -k https://server:7890 -o /dev/stderr")
print(client.succeed("whoami")) log("Initializing")
client.succeed('PYTHONUNBUFFERED=1 selenium-script')
''; options = Options()
} options.add_argument("--headless")
)
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501
driver = webdriver.Firefox(options=options, service=service)
driver.implicitly_wait(10)
log("Opening homepage")
driver.get("https://server:7890")
def wait_elem(by, query, timeout=10):
wait = WebDriverWait(driver, timeout)
wait.until(EC.presence_of_element_located((by, query)))
def wait_title_contains(title, timeout=10):
wait = WebDriverWait(driver, timeout)
wait.until(EC.title_contains(title))
def find_element(by, query):
return driver.find_element(by, query)
def set_value(elem, value):
script = 'arguments[0].value = arguments[1]'
return driver.execute_script(script, elem, value)
log("Waiting for the homepage to load")
# cockpit sets initial title as hostname
wait_title_contains("server")
wait_elem(By.CSS_SELECTOR, 'input#login-user-input')
log("Homepage loaded!")
log("Filling out username")
login_input = find_element(By.CSS_SELECTOR, 'input#login-user-input')
set_value(login_input, "${user}")
log("Filling out password")
password_input = find_element(By.CSS_SELECTOR, 'input#login-password-input')
set_value(password_input, "${password}")
log("Submitting credentials for login")
driver.find_element(By.CSS_SELECTOR, 'button#login-button').click()
# driver.implicitly_wait(1)
# driver.get("https://server:7890/system")
log("Waiting dashboard to load")
wait_title_contains("${user}@server")
log("Waiting for the frontend to initialize")
sleep(1)
log("Looking for that banner that tells about limited access")
container_iframe = find_element(By.CSS_SELECTOR, 'iframe.container-frame')
driver.switch_to.frame(container_iframe)
assert "Web console is running in limited access mode" in driver.page_source
log("Clicking the sudo button")
for button in driver.find_elements(By.TAG_NAME, "button"):
if 'admin' in button.text:
button.click()
driver.switch_to.default_content()
log("Checking that /nonexistent is not a thing")
assert '/nonexistent' not in driver.page_source
assert len(driver.find_elements(By.CSS_SELECTOR, '#machine-reconnect')) == 0
driver.close()
'';
in
with pkgs;
[
firefox-unwrapped
geckodriver
seleniumScript
];
};
};
testScript = ''
start_all()
server.wait_for_unit("sockets.target")
server.wait_for_open_port(7890)
client.succeed("curl -k https://server:7890 -o /dev/stderr")
print(client.succeed("whoami"))
client.succeed('PYTHONUNBUFFERED=1 selenium-script')
'';
}

View File

@ -1,26 +1,24 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "code-server";
name = "code-server";
nodes = { nodes = {
machine = machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
services.code-server = { services.code-server = {
enable = true; enable = true;
auth = "none"; auth = "none";
};
}; };
}; };
};
testScript = '' testScript = ''
start_all() start_all()
machine.wait_for_unit("code-server.service") machine.wait_for_unit("code-server.service")
machine.wait_for_open_port(4444) machine.wait_for_open_port(4444)
machine.succeed("curl -k --fail http://localhost:4444", timeout=10) machine.succeed("curl -k --fail http://localhost:4444", timeout=10)
''; '';
meta.maintainers = [ lib.maintainers.drupol ]; meta.maintainers = [ lib.maintainers.drupol ];
} }
)

View File

@ -1,25 +1,23 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "coder";
name = "coder"; meta.maintainers = pkgs.coder.meta.maintainers;
meta.maintainers = pkgs.coder.meta.maintainers;
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
services.coder = { services.coder = {
enable = true; enable = true;
accessUrl = "http://localhost:3000"; accessUrl = "http://localhost:3000";
};
}; };
};
testScript = '' testScript = ''
machine.start() machine.start()
machine.wait_for_unit("postgresql.service") machine.wait_for_unit("postgresql.service")
machine.wait_for_unit("coder.service") machine.wait_for_unit("coder.service")
machine.wait_for_open_port(3000) machine.wait_for_open_port(3000)
machine.succeed("curl --fail http://localhost:3000") machine.succeed("curl --fail http://localhost:3000")
''; '';
} }
)

View File

@ -1,41 +1,39 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "collectd";
name = "collectd"; meta = { };
meta = { };
nodes.machine = nodes.machine =
{ pkgs, lib, ... }: { pkgs, lib, ... }:
{ {
services.collectd = { services.collectd = {
enable = true; enable = true;
extraConfig = lib.mkBefore '' extraConfig = lib.mkBefore ''
Interval 30 Interval 30
'';
plugins = {
rrdtool = ''
DataDir "/var/lib/collectd/rrd"
''; '';
plugins = { load = "";
rrdtool = ''
DataDir "/var/lib/collectd/rrd"
'';
load = "";
};
}; };
environment.systemPackages = [ pkgs.rrdtool ];
}; };
environment.systemPackages = [ pkgs.rrdtool ];
};
testScript = '' testScript = ''
machine.wait_for_unit("collectd.service") machine.wait_for_unit("collectd.service")
hostname = machine.succeed("hostname").strip() hostname = machine.succeed("hostname").strip()
file = f"/var/lib/collectd/rrd/{hostname}/load/load.rrd" file = f"/var/lib/collectd/rrd/{hostname}/load/load.rrd"
machine.wait_for_file(file); machine.wait_for_file(file);
machine.succeed(f"rrdinfo {file} | logger") machine.succeed(f"rrdinfo {file} | logger")
# check that this file contains a shortterm metric # check that this file contains a shortterm metric
machine.succeed(f"rrdinfo {file} | grep -F 'ds[shortterm].min = '") machine.succeed(f"rrdinfo {file} | grep -F 'ds[shortterm].min = '")
# check that interval was set before the plugins # check that interval was set before the plugins
machine.succeed(f"rrdinfo {file} | grep -F 'step = 30'") machine.succeed(f"rrdinfo {file} | grep -F 'step = 30'")
# check that there are frequent updates # check that there are frequent updates
machine.succeed(f"cp {file} before") machine.succeed(f"cp {file} before")
machine.wait_until_fails(f"cmp before {file}") machine.wait_until_fails(f"cmp before {file}")
''; '';
} }
)

View File

@ -1,21 +1,19 @@
import ./make-test-python.nix ( { lib, ... }:
{ lib, ... }: {
{ name = "commafeed";
name = "commafeed";
nodes.server = { nodes.server = {
services.commafeed = { services.commafeed = {
enable = true; enable = true;
};
}; };
};
testScript = '' testScript = ''
server.start() server.start()
server.wait_for_unit("commafeed.service") server.wait_for_unit("commafeed.service")
server.wait_for_open_port(8082) server.wait_for_open_port(8082)
server.succeed("curl --fail --silent http://localhost:8082") server.succeed("curl --fail --silent http://localhost:8082")
''; '';
meta.maintainers = [ lib.maintainers.raroh73 ]; meta.maintainers = [ lib.maintainers.raroh73 ];
} }
)

View File

@ -1,85 +1,83 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "connman";
name = "connman"; meta = with lib.maintainers; {
meta = with lib.maintainers; { maintainers = [ rnhmjoj ];
maintainers = [ rnhmjoj ]; };
# Router running radvd on VLAN 1
nodes.router =
{ ... }:
{
imports = [ ../modules/profiles/minimal.nix ];
virtualisation.vlans = [ 1 ];
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
networking = {
useDHCP = false;
interfaces.eth1.ipv6.addresses = [
{
address = "fd12::1";
prefixLength = 64;
}
];
};
services.radvd = {
enable = true;
config = ''
interface eth1 {
AdvSendAdvert on;
AdvManagedFlag on;
AdvOtherConfigFlag on;
prefix fd12::/64 {
AdvAutonomous off;
};
};
'';
};
}; };
# Router running radvd on VLAN 1 # Client running connman, connected to VLAN 1
nodes.router = nodes.client =
{ ... }: { ... }:
{ {
imports = [ ../modules/profiles/minimal.nix ]; virtualisation.vlans = [ 1 ];
virtualisation.vlans = [ 1 ]; # add a virtual wlan interface
boot.kernelModules = [ "mac80211_hwsim" ];
boot.extraModprobeConfig = ''
options mac80211_hwsim radios=1
'';
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true; # Note: the overrides are needed because the wifi is
# disabled with mkVMOverride in qemu-vm.nix.
services.connman.enable = lib.mkOverride 0 true;
services.connman.networkInterfaceBlacklist = [ "eth0" ];
networking.wireless.enable = lib.mkOverride 0 true;
networking.wireless.interfaces = [ "wlan0" ];
};
networking = { testScript = ''
useDHCP = false; start_all()
interfaces.eth1.ipv6.addresses = [
{
address = "fd12::1";
prefixLength = 64;
}
];
};
services.radvd = { with subtest("Router is ready"):
enable = true; router.wait_for_unit("radvd.service")
config = ''
interface eth1 {
AdvSendAdvert on;
AdvManagedFlag on;
AdvOtherConfigFlag on;
prefix fd12::/64 {
AdvAutonomous off;
};
};
'';
};
};
# Client running connman, connected to VLAN 1 with subtest("Daemons are running"):
nodes.client = client.wait_for_unit("wpa_supplicant-wlan0.service")
{ ... }: client.wait_for_unit("connman.service")
{ client.wait_until_succeeds("connmanctl state | grep -q ready")
virtualisation.vlans = [ 1 ];
# add a virtual wlan interface with subtest("Wired interface is configured"):
boot.kernelModules = [ "mac80211_hwsim" ]; client.wait_until_succeeds("ip -6 route | grep -q fd12::/64")
boot.extraModprobeConfig = '' client.wait_until_succeeds("ping -c 1 fd12::1")
options mac80211_hwsim radios=1
'';
# Note: the overrides are needed because the wifi is with subtest("Can set up a wireless access point"):
# disabled with mkVMOverride in qemu-vm.nix. client.succeed("connmanctl enable wifi")
services.connman.enable = lib.mkOverride 0 true; client.wait_until_succeeds("connmanctl tether wifi on nixos-test reproducibility | grep -q 'Enabled'")
services.connman.networkInterfaceBlacklist = [ "eth0" ]; client.wait_until_succeeds("iw wlan0 info | grep -q nixos-test")
networking.wireless.enable = lib.mkOverride 0 true; '';
networking.wireless.interfaces = [ "wlan0" ]; }
};
testScript = ''
start_all()
with subtest("Router is ready"):
router.wait_for_unit("radvd.service")
with subtest("Daemons are running"):
client.wait_for_unit("wpa_supplicant-wlan0.service")
client.wait_for_unit("connman.service")
client.wait_until_succeeds("connmanctl state | grep -q ready")
with subtest("Wired interface is configured"):
client.wait_until_succeeds("ip -6 route | grep -q fd12::/64")
client.wait_until_succeeds("ping -c 1 fd12::1")
with subtest("Can set up a wireless access point"):
client.succeed("connmanctl enable wifi")
client.wait_until_succeeds("connmanctl tether wifi on nixos-test reproducibility | grep -q 'Enabled'")
client.wait_until_succeeds("iw wlan0 info | grep -q nixos-test")
'';
}
)

View File

@ -1,43 +1,41 @@
import ./make-test-python.nix ( { ... }:
{ ... }: {
{ name = "consul-template";
name = "consul-template";
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
services.consul-template.instances.example.settings = { services.consul-template.instances.example.settings = {
template = [ template = [
{ {
contents = '' contents = ''
{{ key "example" }} {{ key "example" }}
''; '';
perms = "0600"; perms = "0600";
destination = "/example"; destination = "/example";
} }
]; ];
};
services.consul = {
enable = true;
extraConfig = {
server = true;
bootstrap_expect = 1;
bind_addr = "127.0.0.1";
};
};
}; };
testScript = '' services.consul = {
machine.wait_for_unit("consul.service") enable = true;
machine.wait_for_open_port(8500) extraConfig = {
server = true;
bootstrap_expect = 1;
bind_addr = "127.0.0.1";
};
};
};
machine.wait_for_unit("consul-template-example.service") testScript = ''
machine.wait_for_unit("consul.service")
machine.wait_for_open_port(8500)
machine.wait_until_succeeds('consul kv put example example') machine.wait_for_unit("consul-template-example.service")
machine.wait_for_file("/example") machine.wait_until_succeeds('consul kv put example example')
machine.succeed('grep "example" /example')
''; machine.wait_for_file("/example")
} machine.succeed('grep "example" /example')
) '';
}

View File

@ -1,267 +1,265 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }:
let let
# Settings for both servers and agents # Settings for both servers and agents
webUi = true; webUi = true;
retry_interval = "1s"; retry_interval = "1s";
raft_multiplier = 1; raft_multiplier = 1;
defaultExtraConfig = { defaultExtraConfig = {
inherit retry_interval; inherit retry_interval;
performance = { performance = {
inherit raft_multiplier; inherit raft_multiplier;
};
};
allConsensusServerHosts = [
"192.168.1.1"
"192.168.1.2"
"192.168.1.3"
];
allConsensusClientHosts = [
"192.168.2.1"
"192.168.2.2"
];
firewallSettings = {
# See https://www.consul.io/docs/install/ports.html
allowedTCPPorts = [
8301
8302
8600
8500
8300
];
allowedUDPPorts = [
8301
8302
8600
];
};
client =
index:
{ pkgs, ... }:
let
ip = builtins.elemAt allConsensusClientHosts index;
in
{
environment.systemPackages = [ pkgs.consul ];
networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
{
address = ip;
prefixLength = 16;
}
];
networking.firewall = firewallSettings;
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "consul" ];
services.consul = {
enable = true;
inherit webUi;
extraConfig = defaultExtraConfig // {
server = false;
retry_join = allConsensusServerHosts;
bind_addr = ip;
};
}; };
}; };
allConsensusServerHosts = [ server =
"192.168.1.1" index:
"192.168.1.2" { pkgs, ... }:
"192.168.1.3" let
]; numConsensusServers = builtins.length allConsensusServerHosts;
thisConsensusServerHost = builtins.elemAt allConsensusServerHosts index;
allConsensusClientHosts = [ ip = thisConsensusServerHost; # since we already use IPs to identify servers
"192.168.2.1" in
"192.168.2.2" {
]; networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
{
firewallSettings = { address = ip;
# See https://www.consul.io/docs/install/ports.html prefixLength = 16;
allowedTCPPorts = [ }
8301
8302
8600
8500
8300
]; ];
allowedUDPPorts = [ networking.firewall = firewallSettings;
8301
8302
8600
];
};
client = nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "consul" ];
index:
{ pkgs, ... }:
let
ip = builtins.elemAt allConsensusClientHosts index;
in
{
environment.systemPackages = [ pkgs.consul ];
networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [ services.consul =
{ assert builtins.elem thisConsensusServerHost allConsensusServerHosts;
address = ip; {
prefixLength = 16;
}
];
networking.firewall = firewallSettings;
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "consul" ];
services.consul = {
enable = true; enable = true;
inherit webUi; inherit webUi;
extraConfig = defaultExtraConfig // { extraConfig = defaultExtraConfig // {
server = false; server = true;
retry_join = allConsensusServerHosts; bootstrap_expect = numConsensusServers;
# Tell Consul that we never intend to drop below this many servers.
# Ensures to not permanently lose consensus after temporary loss.
# See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
autopilot.min_quorum = numConsensusServers;
retry_join =
# If there's only 1 node in the network, we allow self-join;
# otherwise, the node must not try to join itself, and join only the other servers.
# See https://github.com/hashicorp/consul/issues/2868
if numConsensusServers == 1 then
allConsensusServerHosts
else
builtins.filter (h: h != thisConsensusServerHost) allConsensusServerHosts;
bind_addr = ip; bind_addr = ip;
}; };
}; };
};
server =
index:
{ pkgs, ... }:
let
numConsensusServers = builtins.length allConsensusServerHosts;
thisConsensusServerHost = builtins.elemAt allConsensusServerHosts index;
ip = thisConsensusServerHost; # since we already use IPs to identify servers
in
{
networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
{
address = ip;
prefixLength = 16;
}
];
networking.firewall = firewallSettings;
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "consul" ];
services.consul =
assert builtins.elem thisConsensusServerHost allConsensusServerHosts;
{
enable = true;
inherit webUi;
extraConfig = defaultExtraConfig // {
server = true;
bootstrap_expect = numConsensusServers;
# Tell Consul that we never intend to drop below this many servers.
# Ensures to not permanently lose consensus after temporary loss.
# See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
autopilot.min_quorum = numConsensusServers;
retry_join =
# If there's only 1 node in the network, we allow self-join;
# otherwise, the node must not try to join itself, and join only the other servers.
# See https://github.com/hashicorp/consul/issues/2868
if numConsensusServers == 1 then
allConsensusServerHosts
else
builtins.filter (h: h != thisConsensusServerHost) allConsensusServerHosts;
bind_addr = ip;
};
};
};
in
{
name = "consul";
nodes = {
server1 = server 0;
server2 = server 1;
server3 = server 2;
client1 = client 0;
client2 = client 1;
}; };
in
{
name = "consul";
testScript = '' nodes = {
servers = [server1, server2, server3] server1 = server 0;
machines = [server1, server2, server3, client1, client2] server2 = server 1;
server3 = server 2;
for m in machines: client1 = client 0;
m.wait_for_unit("consul.service") client2 = client 1;
};
testScript = ''
servers = [server1, server2, server3]
machines = [server1, server2, server3, client1, client2]
for m in machines:
m.wait_for_unit("consul.service")
def wait_for_healthy_servers(): def wait_for_healthy_servers():
# See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040 # See https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040
# for why the `Voter` column of `list-peers` has that info. # for why the `Voter` column of `list-peers` has that info.
# TODO: The `grep true` relies on the fact that currently in # TODO: The `grep true` relies on the fact that currently in
# the output like # the output like
# # consul operator raft list-peers # # consul operator raft list-peers
# Node ID Address State Voter RaftProtocol # Node ID Address State Voter RaftProtocol
# server3 ... 192.168.1.3:8300 leader true 3 # server3 ... 192.168.1.3:8300 leader true 3
# server2 ... 192.168.1.2:8300 follower true 3 # server2 ... 192.168.1.2:8300 follower true 3
# server1 ... 192.168.1.1:8300 follower false 3 # server1 ... 192.168.1.1:8300 follower false 3
# `Voter`is the only boolean column. # `Voter`is the only boolean column.
# Change this to the more reliable way to be defined by # Change this to the more reliable way to be defined by
# https://github.com/hashicorp/consul/issues/8118 # https://github.com/hashicorp/consul/issues/8118
# once that ticket is closed. # once that ticket is closed.
for m in machines: for m in machines:
m.wait_until_succeeds( m.wait_until_succeeds(
"[ $(consul operator raft list-peers | grep true | wc -l) == 3 ]" "[ $(consul operator raft list-peers | grep true | wc -l) == 3 ]"
) )
def wait_for_all_machines_alive(): def wait_for_all_machines_alive():
""" """
Note that Serf-"alive" does not mean "Raft"-healthy; Note that Serf-"alive" does not mean "Raft"-healthy;
see `wait_for_healthy_servers()` for that instead. see `wait_for_healthy_servers()` for that instead.
""" """
for m in machines: for m in machines:
m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]") m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
wait_for_healthy_servers() wait_for_healthy_servers()
# Also wait for clients to be alive. # Also wait for clients to be alive.
wait_for_all_machines_alive() wait_for_all_machines_alive()
client1.succeed("consul kv put testkey 42") client1.succeed("consul kv put testkey 42")
client2.succeed("[ $(consul kv get testkey) == 42 ]") client2.succeed("[ $(consul kv get testkey) == 42 ]")
def rolling_restart_test(proper_rolling_procedure=True): def rolling_restart_test(proper_rolling_procedure=True):
""" """
Tests that the cluster can tolearate failures of any single server, Tests that the cluster can tolearate failures of any single server,
following the recommended rolling upgrade procedure from following the recommended rolling upgrade procedure from
https://www.consul.io/docs/upgrading#standard-upgrades. https://www.consul.io/docs/upgrading#standard-upgrades.
Optionally, `proper_rolling_procedure=False` can be given Optionally, `proper_rolling_procedure=False` can be given
to wait only for each server to be back `Healthy`, not `Stable` to wait only for each server to be back `Healthy`, not `Stable`
in the Raft consensus, see Consul setting `ServerStabilizationTime` and in the Raft consensus, see Consul setting `ServerStabilizationTime` and
https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040. https://github.com/hashicorp/consul/issues/8118#issuecomment-645330040.
""" """
for server in servers: for server in servers:
server.block() server.block()
server.systemctl("stop consul") server.systemctl("stop consul")
# Make sure the stopped peer is recognized as being down # Make sure the stopped peer is recognized as being down
client1.wait_until_succeeds( client1.wait_until_succeeds(
f"[ $(consul members | grep {server.name} | grep -o -E 'failed|left' | wc -l) == 1 ]" f"[ $(consul members | grep {server.name} | grep -o -E 'failed|left' | wc -l) == 1 ]"
) )
# For each client, wait until they have connection again # For each client, wait until they have connection again
# using `kv get -recurse` before issuing commands. # using `kv get -recurse` before issuing commands.
client1.wait_until_succeeds("consul kv get -recurse") client1.wait_until_succeeds("consul kv get -recurse")
client2.wait_until_succeeds("consul kv get -recurse") client2.wait_until_succeeds("consul kv get -recurse")
# Do some consul actions while one server is down. # Do some consul actions while one server is down.
client1.succeed("consul kv put testkey 43") client1.succeed("consul kv put testkey 43")
client2.succeed("[ $(consul kv get testkey) == 43 ]") client2.succeed("[ $(consul kv get testkey) == 43 ]")
client2.succeed("consul kv delete testkey") client2.succeed("consul kv delete testkey")
server.unblock() server.unblock()
server.systemctl("start consul") server.systemctl("start consul")
if proper_rolling_procedure: if proper_rolling_procedure:
# Wait for recovery. # Wait for recovery.
wait_for_healthy_servers() wait_for_healthy_servers()
else: else:
# NOT proper rolling upgrade procedure, see above. # NOT proper rolling upgrade procedure, see above.
wait_for_all_machines_alive() wait_for_all_machines_alive()
# Wait for client connections. # Wait for client connections.
client1.wait_until_succeeds("consul kv get -recurse") client1.wait_until_succeeds("consul kv get -recurse")
client2.wait_until_succeeds("consul kv get -recurse") client2.wait_until_succeeds("consul kv get -recurse")
# Do some consul actions with server back up. # Do some consul actions with server back up.
client1.succeed("consul kv put testkey 44") client1.succeed("consul kv put testkey 44")
client2.succeed("[ $(consul kv get testkey) == 44 ]") client2.succeed("[ $(consul kv get testkey) == 44 ]")
client2.succeed("consul kv delete testkey") client2.succeed("consul kv delete testkey")
def all_servers_crash_simultaneously_test(): def all_servers_crash_simultaneously_test():
""" """
Tests that the cluster will eventually come back after all Tests that the cluster will eventually come back after all
servers crash simultaneously. servers crash simultaneously.
""" """
for server in servers: for server in servers:
server.block() server.block()
server.systemctl("stop --no-block consul") server.systemctl("stop --no-block consul")
for server in servers: for server in servers:
# --no-block is async, so ensure it has been stopped by now # --no-block is async, so ensure it has been stopped by now
server.wait_until_fails("systemctl is-active --quiet consul") server.wait_until_fails("systemctl is-active --quiet consul")
server.unblock() server.unblock()
server.systemctl("start consul") server.systemctl("start consul")
# Wait for recovery. # Wait for recovery.
wait_for_healthy_servers() wait_for_healthy_servers()
# Wait for client connections. # Wait for client connections.
client1.wait_until_succeeds("consul kv get -recurse") client1.wait_until_succeeds("consul kv get -recurse")
client2.wait_until_succeeds("consul kv get -recurse") client2.wait_until_succeeds("consul kv get -recurse")
# Do some consul actions with servers back up. # Do some consul actions with servers back up.
client1.succeed("consul kv put testkey 44") client1.succeed("consul kv put testkey 44")
client2.succeed("[ $(consul kv get testkey) == 44 ]") client2.succeed("[ $(consul kv get testkey) == 44 ]")
client2.succeed("consul kv delete testkey") client2.succeed("consul kv delete testkey")
# Run the tests. # Run the tests.
print("rolling_restart_test()") print("rolling_restart_test()")
rolling_restart_test() rolling_restart_test()
print("all_servers_crash_simultaneously_test()") print("all_servers_crash_simultaneously_test()")
all_servers_crash_simultaneously_test() all_servers_crash_simultaneously_test()
print("rolling_restart_test(proper_rolling_procedure=False)") print("rolling_restart_test(proper_rolling_procedure=False)")
rolling_restart_test(proper_rolling_procedure=False) rolling_restart_test(proper_rolling_procedure=False)
''; '';
} }
)

View File

@ -5,110 +5,108 @@ let
containerIp6 = "fc00::2/7"; containerIp6 = "fc00::2/7";
in in
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-bridge";
name = "containers-bridge"; meta = {
meta = { maintainers = with lib.maintainers; [
maintainers = with lib.maintainers; [ aristid
aristid aszlig
aszlig kampfschlaefer
kampfschlaefer ];
]; };
};
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
imports = [ ../modules/installer/cd-dvd/channel.nix ]; imports = [ ../modules/installer/cd-dvd/channel.nix ];
virtualisation.writableStore = true; virtualisation.writableStore = true;
networking.bridges = { networking.bridges = {
br0 = { br0 = {
interfaces = [ ]; interfaces = [ ];
};
}; };
networking.interfaces = { };
br0 = { networking.interfaces = {
ipv4.addresses = [ br0 = {
{ ipv4.addresses = [
address = hostIp; {
prefixLength = 24; address = hostIp;
} prefixLength = 24;
]; }
ipv6.addresses = [ ];
{ ipv6.addresses = [
address = hostIp6; {
prefixLength = 7; address = hostIp6;
} prefixLength = 7;
]; }
}; ];
}; };
containers.webserver = {
autoStart = true;
privateNetwork = true;
hostBridge = "br0";
localAddress = containerIp;
localAddress6 = containerIp6;
config = {
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ];
};
};
containers.web-noip = {
autoStart = true;
privateNetwork = true;
hostBridge = "br0";
config = {
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ];
};
};
virtualisation.additionalPaths = [ pkgs.stdenv ];
}; };
testScript = '' containers.webserver = {
machine.wait_for_unit("default.target") autoStart = true;
assert "webserver" in machine.succeed("nixos-container list") privateNetwork = true;
hostBridge = "br0";
localAddress = containerIp;
localAddress6 = containerIp6;
config = {
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ];
};
};
with subtest("Start the webserver container"): containers.web-noip = {
assert "up" in machine.succeed("nixos-container status webserver") autoStart = true;
privateNetwork = true;
hostBridge = "br0";
config = {
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ];
};
};
with subtest("Bridges exist inside containers"): virtualisation.additionalPaths = [ pkgs.stdenv ];
machine.succeed( };
"nixos-container run webserver -- ip link show eth0",
"nixos-container run web-noip -- ip link show eth0",
)
ip = "${containerIp}".split("/")[0] testScript = ''
machine.succeed(f"ping -n -c 1 {ip}") machine.wait_for_unit("default.target")
machine.succeed(f"curl --fail http://{ip}/ > /dev/null") assert "webserver" in machine.succeed("nixos-container list")
ip6 = "${containerIp6}".split("/")[0] with subtest("Start the webserver container"):
machine.succeed(f"ping -n -c 1 {ip6}") assert "up" in machine.succeed("nixos-container status webserver")
machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null")
with subtest( with subtest("Bridges exist inside containers"):
"nixos-container show-ip works in case of an ipv4 address " machine.succeed(
+ "with subnetmask in CIDR notation." "nixos-container run webserver -- ip link show eth0",
): "nixos-container run web-noip -- ip link show eth0",
result = machine.succeed("nixos-container show-ip webserver").rstrip() )
assert result == ip
with subtest("Stop the container"): ip = "${containerIp}".split("/")[0]
machine.succeed("nixos-container stop webserver") machine.succeed(f"ping -n -c 1 {ip}")
machine.fail( machine.succeed(f"curl --fail http://{ip}/ > /dev/null")
f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null",
f"curl --fail --connect-timeout 2 http://[{ip6}]/ > /dev/null",
)
# Destroying a declarative container should fail. ip6 = "${containerIp6}".split("/")[0]
machine.fail("nixos-container destroy webserver") machine.succeed(f"ping -n -c 1 {ip6}")
''; machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null")
}
) with subtest(
"nixos-container show-ip works in case of an ipv4 address "
+ "with subnetmask in CIDR notation."
):
result = machine.succeed("nixos-container show-ip webserver").rstrip()
assert result == ip
with subtest("Stop the container"):
machine.succeed("nixos-container stop webserver")
machine.fail(
f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null",
f"curl --fail --connect-timeout 2 http://[{ip6}]/ > /dev/null",
)
# Destroying a declarative container should fail.
machine.fail("nixos-container destroy webserver")
'';
}

View File

@ -1,48 +1,46 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let
customPkgs = pkgs.appendOverlays [ customPkgs = pkgs.appendOverlays [
(self: super: { (self: super: {
hello = super.hello.overrideAttrs (old: { hello = super.hello.overrideAttrs (old: {
name = "custom-hello"; name = "custom-hello";
}); });
}) })
]; ];
in in
{ {
name = "containers-custom-pkgs"; name = "containers-custom-pkgs";
meta = { meta = {
maintainers = with lib.maintainers; [ erikarvstedt ]; maintainers = with lib.maintainers; [ erikarvstedt ];
};
nodes.machine =
{ config, ... }:
{
assertions =
let
helloName = (builtins.head config.containers.test.config.system.extraDependencies).name;
in
[
{
assertion = helloName == "custom-hello";
message = "Unexpected value: ${helloName}";
}
];
containers.test = {
autoStart = true;
config =
{ pkgs, config, ... }:
{
nixpkgs.pkgs = customPkgs;
system.extraDependencies = [ pkgs.hello ];
};
};
}; };
nodes.machine = # This test only consists of evaluating the test machine
{ config, ... }: testScript = "pass";
{ }
assertions =
let
helloName = (builtins.head config.containers.test.config.system.extraDependencies).name;
in
[
{
assertion = helloName == "custom-hello";
message = "Unexpected value: ${helloName}";
}
];
containers.test = {
autoStart = true;
config =
{ pkgs, config, ... }:
{
nixpkgs.pkgs = customPkgs;
system.extraDependencies = [ pkgs.hello ];
};
};
};
# This test only consists of evaluating the test machine
testScript = "pass";
}
)

View File

@ -1,59 +1,57 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-ephemeral";
name = "containers-ephemeral"; meta = {
meta = { maintainers = with lib.maintainers; [ patryk27 ];
maintainers = with lib.maintainers; [ patryk27 ]; };
};
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
virtualisation.writableStore = true; virtualisation.writableStore = true;
containers.webserver = { containers.webserver = {
ephemeral = true; ephemeral = true;
privateNetwork = true; privateNetwork = true;
hostAddress = "10.231.136.1"; hostAddress = "10.231.136.1";
localAddress = "10.231.136.2"; localAddress = "10.231.136.2";
config = { config = {
services.nginx = { services.nginx = {
enable = true; enable = true;
virtualHosts.localhost = { virtualHosts.localhost = {
root = pkgs.runCommand "localhost" { } '' root = pkgs.runCommand "localhost" { } ''
mkdir "$out" mkdir "$out"
echo hello world > "$out/index.html" echo hello world > "$out/index.html"
''; '';
};
}; };
networking.firewall.allowedTCPPorts = [ 80 ];
}; };
networking.firewall.allowedTCPPorts = [ 80 ];
}; };
}; };
};
testScript = '' testScript = ''
assert "webserver" in machine.succeed("nixos-container list") assert "webserver" in machine.succeed("nixos-container list")
machine.succeed("nixos-container start webserver") machine.succeed("nixos-container start webserver")
with subtest("Container got its own root folder"): with subtest("Container got its own root folder"):
machine.succeed("ls /run/nixos-containers/webserver") machine.succeed("ls /run/nixos-containers/webserver")
with subtest("Container persistent directory is not created"): with subtest("Container persistent directory is not created"):
machine.fail("ls /var/lib/nixos-containers/webserver") machine.fail("ls /var/lib/nixos-containers/webserver")
# Since "start" returns after the container has reached # Since "start" returns after the container has reached
# multi-user.target, we should now be able to access it. # multi-user.target, we should now be able to access it.
ip = machine.succeed("nixos-container show-ip webserver").rstrip() ip = machine.succeed("nixos-container show-ip webserver").rstrip()
machine.succeed(f"ping -n -c1 {ip}") machine.succeed(f"ping -n -c1 {ip}")
machine.succeed(f"curl --fail http://{ip}/ > /dev/null") machine.succeed(f"curl --fail http://{ip}/ > /dev/null")
with subtest("Stop the container"): with subtest("Stop the container"):
machine.succeed("nixos-container stop webserver") machine.succeed("nixos-container stop webserver")
machine.fail(f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null") machine.fail(f"curl --fail --connect-timeout 2 http://{ip}/ > /dev/null")
with subtest("Container's root folder was removed"): with subtest("Container's root folder was removed"):
machine.fail("ls /run/nixos-containers/webserver") machine.fail("ls /run/nixos-containers/webserver")
''; '';
} }
)

View File

@ -1,115 +1,113 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-extra_veth";
name = "containers-extra_veth"; meta = {
meta = { maintainers = with lib.maintainers; [ kampfschlaefer ];
maintainers = with lib.maintainers; [ kampfschlaefer ]; };
};
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
imports = [ ../modules/installer/cd-dvd/channel.nix ]; imports = [ ../modules/installer/cd-dvd/channel.nix ];
virtualisation.writableStore = true; virtualisation.writableStore = true;
virtualisation.vlans = [ ]; virtualisation.vlans = [ ];
networking.useDHCP = false; networking.useDHCP = false;
networking.bridges = { networking.bridges = {
br0 = { br0 = {
interfaces = [ ]; interfaces = [ ];
};
br1 = {
interfaces = [ ];
};
}; };
networking.interfaces = { br1 = {
br0 = { interfaces = [ ];
ipv4.addresses = [
{
address = "192.168.0.1";
prefixLength = 24;
}
];
ipv6.addresses = [
{
address = "fc00::1";
prefixLength = 7;
}
];
};
br1 = {
ipv4.addresses = [
{
address = "192.168.1.1";
prefixLength = 24;
}
];
};
}; };
};
containers.webserver = { networking.interfaces = {
autoStart = true; br0 = {
privateNetwork = true; ipv4.addresses = [
hostBridge = "br0"; {
localAddress = "192.168.0.100/24"; address = "192.168.0.1";
localAddress6 = "fc00::2/7"; prefixLength = 24;
extraVeths = { }
veth1 = { ];
hostBridge = "br1"; ipv6.addresses = [
localAddress = "192.168.1.100/24"; {
}; address = "fc00::1";
veth2 = { prefixLength = 7;
hostAddress = "192.168.2.1"; }
localAddress = "192.168.2.100"; ];
}; };
}; br1 = {
config = { ipv4.addresses = [
networking.firewall.allowedTCPPorts = [ 80 ]; {
}; address = "192.168.1.1";
prefixLength = 24;
}
];
}; };
virtualisation.additionalPaths = [ pkgs.stdenv ];
}; };
testScript = '' containers.webserver = {
machine.wait_for_unit("default.target") autoStart = true;
assert "webserver" in machine.succeed("nixos-container list") privateNetwork = true;
hostBridge = "br0";
localAddress = "192.168.0.100/24";
localAddress6 = "fc00::2/7";
extraVeths = {
veth1 = {
hostBridge = "br1";
localAddress = "192.168.1.100/24";
};
veth2 = {
hostAddress = "192.168.2.1";
localAddress = "192.168.2.100";
};
};
config = {
networking.firewall.allowedTCPPorts = [ 80 ];
};
};
with subtest("Status of the webserver container is up"): virtualisation.additionalPaths = [ pkgs.stdenv ];
assert "up" in machine.succeed("nixos-container status webserver") };
with subtest("Ensure that the veths are inside the container"): testScript = ''
assert "state UP" in machine.succeed( machine.wait_for_unit("default.target")
"nixos-container run webserver -- ip link show veth1" assert "webserver" in machine.succeed("nixos-container list")
)
assert "state UP" in machine.succeed(
"nixos-container run webserver -- ip link show veth2"
)
with subtest("Ensure the presence of the extra veths"): with subtest("Status of the webserver container is up"):
assert "state UP" in machine.succeed("ip link show veth1") assert "up" in machine.succeed("nixos-container status webserver")
assert "state UP" in machine.succeed("ip link show veth2")
with subtest("Ensure the veth1 is part of br1 on the host"): with subtest("Ensure that the veths are inside the container"):
assert "master br1" in machine.succeed("ip link show veth1") assert "state UP" in machine.succeed(
"nixos-container run webserver -- ip link show veth1"
)
assert "state UP" in machine.succeed(
"nixos-container run webserver -- ip link show veth2"
)
with subtest("Ping on main veth"): with subtest("Ensure the presence of the extra veths"):
machine.succeed("ping -n -c 1 192.168.0.100") assert "state UP" in machine.succeed("ip link show veth1")
machine.succeed("ping -n -c 1 fc00::2") assert "state UP" in machine.succeed("ip link show veth2")
with subtest("Ping on the first extra veth"): with subtest("Ensure the veth1 is part of br1 on the host"):
machine.succeed("ping -n -c 1 192.168.1.100 >&2") assert "master br1" in machine.succeed("ip link show veth1")
with subtest("Ping on the second extra veth"): with subtest("Ping on main veth"):
machine.succeed("ping -n -c 1 192.168.2.100 >&2") machine.succeed("ping -n -c 1 192.168.0.100")
machine.succeed("ping -n -c 1 fc00::2")
with subtest("Container can be stopped"): with subtest("Ping on the first extra veth"):
machine.succeed("nixos-container stop webserver") machine.succeed("ping -n -c 1 192.168.1.100 >&2")
machine.fail("ping -n -c 1 192.168.1.100 >&2")
machine.fail("ping -n -c 1 192.168.2.100 >&2")
with subtest("Destroying a declarative container should fail"): with subtest("Ping on the second extra veth"):
machine.fail("nixos-container destroy webserver") machine.succeed("ping -n -c 1 192.168.2.100 >&2")
'';
} with subtest("Container can be stopped"):
) machine.succeed("nixos-container stop webserver")
machine.fail("ping -n -c 1 192.168.1.100 >&2")
machine.fail("ping -n -c 1 192.168.2.100 >&2")
with subtest("Destroying a declarative container should fail"):
machine.fail("nixos-container destroy webserver")
'';
}

View File

@ -1,55 +1,53 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-hosts";
name = "containers-hosts"; meta = {
meta = { maintainers = with lib.maintainers; [ montag451 ];
maintainers = with lib.maintainers; [ montag451 ]; };
};
nodes.machine = nodes.machine =
{ lib, ... }: { lib, ... }:
{ {
virtualisation.vlans = [ ]; virtualisation.vlans = [ ];
networking.bridges.br0.interfaces = [ ]; networking.bridges.br0.interfaces = [ ];
networking.interfaces.br0.ipv4.addresses = [ networking.interfaces.br0.ipv4.addresses = [
{ {
address = "10.11.0.254"; address = "10.11.0.254";
prefixLength = 24; prefixLength = 24;
} }
]; ];
# Force /etc/hosts to be the only source for host name resolution # Force /etc/hosts to be the only source for host name resolution
environment.etc."nsswitch.conf".text = lib.mkForce '' environment.etc."nsswitch.conf".text = lib.mkForce ''
hosts: files hosts: files
''; '';
containers.simple = { containers.simple = {
autoStart = true; autoStart = true;
privateNetwork = true; privateNetwork = true;
localAddress = "10.10.0.1"; localAddress = "10.10.0.1";
hostAddress = "10.10.0.254"; hostAddress = "10.10.0.254";
config = { }; config = { };
};
containers.netmask = {
autoStart = true;
privateNetwork = true;
hostBridge = "br0";
localAddress = "10.11.0.1/24";
config = { };
};
}; };
testScript = '' containers.netmask = {
start_all() autoStart = true;
machine.wait_for_unit("default.target") privateNetwork = true;
hostBridge = "br0";
localAddress = "10.11.0.1/24";
with subtest("Ping the containers using the entries added in /etc/hosts"): config = { };
for host in "simple.containers", "netmask.containers": };
machine.succeed(f"ping -n -c 1 {host}") };
'';
} testScript = ''
) start_all()
machine.wait_for_unit("default.target")
with subtest("Ping the containers using the entries added in /etc/hosts"):
for host in "simple.containers", "netmask.containers":
machine.succeed(f"ping -n -c 1 {host}")
'';
}

View File

@ -1,197 +1,195 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-imperative";
name = "containers-imperative"; meta = {
meta = { maintainers = with lib.maintainers; [
maintainers = with lib.maintainers; [ aristid
aristid aszlig
aszlig kampfschlaefer
kampfschlaefer ];
]; };
nodes.machine =
{
config,
pkgs,
lib,
...
}:
{
imports = [ ../modules/installer/cd-dvd/channel.nix ];
# XXX: Sandbox setup fails while trying to hardlink files from the host's
# store file system into the prepared chroot directory.
nix.settings.sandbox = false;
nix.settings.substituters = [ ]; # don't try to access cache.nixos.org
virtualisation.memorySize = 2048;
virtualisation.writableStore = true;
# Make sure we always have all the required dependencies for creating a
# container available within the VM, because we don't have network access.
virtualisation.additionalPaths =
let
emptyContainer = import ../lib/eval-config.nix {
modules = lib.singleton {
nixpkgs.hostPlatform = { inherit (pkgs.stdenv.hostPlatform) system; };
containers.foo.config = { };
};
# The system is inherited from the host above.
# Set it to null, to remove the "legacy" entrypoint's non-hermetic default.
system = null;
};
in
with pkgs;
[
stdenv
stdenvNoCC
emptyContainer.config.containers.foo.path
libxslt
desktop-file-utils
texinfo
docbook5
libxml2
docbook_xsl_ns
xorg.lndir
documentation-highlighter
perlPackages.ConfigIniFiles
];
}; };
nodes.machine = testScript =
{ let
config, tmpfilesContainerConfig = pkgs.writeText "container-config-tmpfiles" ''
pkgs, {
lib, systemd.tmpfiles.rules = [ "d /foo - - - - -" ];
... systemd.services.foo = {
}: serviceConfig.Type = "oneshot";
{ script = "ls -al /foo";
imports = [ ../modules/installer/cd-dvd/channel.nix ]; wantedBy = [ "multi-user.target" ];
};
# XXX: Sandbox setup fails while trying to hardlink files from the host's }
# store file system into the prepared chroot directory.
nix.settings.sandbox = false;
nix.settings.substituters = [ ]; # don't try to access cache.nixos.org
virtualisation.memorySize = 2048;
virtualisation.writableStore = true;
# Make sure we always have all the required dependencies for creating a
# container available within the VM, because we don't have network access.
virtualisation.additionalPaths =
let
emptyContainer = import ../lib/eval-config.nix {
modules = lib.singleton {
nixpkgs = { inherit (config.nixpkgs) localSystem; };
containers.foo.config = { };
};
# The system is inherited from the host above.
# Set it to null, to remove the "legacy" entrypoint's non-hermetic default.
system = null;
};
in
with pkgs;
[
stdenv
stdenvNoCC
emptyContainer.config.containers.foo.path
libxslt
desktop-file-utils
texinfo
docbook5
libxml2
docbook_xsl_ns
xorg.lndir
documentation-highlighter
perlPackages.ConfigIniFiles
];
};
testScript =
let
tmpfilesContainerConfig = pkgs.writeText "container-config-tmpfiles" ''
{
systemd.tmpfiles.rules = [ "d /foo - - - - -" ];
systemd.services.foo = {
serviceConfig.Type = "oneshot";
script = "ls -al /foo";
wantedBy = [ "multi-user.target" ];
};
}
'';
brokenCfg = pkgs.writeText "broken.nix" ''
{
assertions = [
{ assertion = false;
message = "I never evaluate";
}
];
}
'';
in
''
with subtest("Make sure we have a NixOS tree (required by nixos-container create)"):
machine.succeed("PAGER=cat nix-env -qa -A nixos.hello >&2")
id1, id2 = None, None
with subtest("Create some containers imperatively"):
id1 = machine.succeed("nixos-container create foo --ensure-unique-name").rstrip()
machine.log(f"created container {id1}")
id2 = machine.succeed("nixos-container create foo --ensure-unique-name").rstrip()
machine.log(f"created container {id2}")
assert id1 != id2
with subtest(f"Put the root of {id2} into a bind mount"):
machine.succeed(
f"mv /var/lib/nixos-containers/{id2} /id2-bindmount",
f"mount --bind /id2-bindmount /var/lib/nixos-containers/{id1}",
)
ip1 = machine.succeed(f"nixos-container show-ip {id1}").rstrip()
ip2 = machine.succeed(f"nixos-container show-ip {id2}").rstrip()
assert ip1 != ip2
with subtest(
"Create a directory and a file we can later check if it still exists "
+ "after destruction of the container"
):
machine.succeed("mkdir /nested-bindmount")
machine.succeed("echo important data > /nested-bindmount/dummy")
with subtest(
"Create a directory with a dummy file and bind-mount it into both containers."
):
for id in id1, id2:
important_path = f"/var/lib/nixos-containers/{id}/very/important/data"
machine.succeed(
f"mkdir -p {important_path}",
f"mount --bind /nested-bindmount {important_path}",
)
with subtest("Start one of them"):
machine.succeed(f"nixos-container start {id1}")
with subtest("Execute commands via the root shell"):
assert "Linux" in machine.succeed(f"nixos-container run {id1} -- uname")
with subtest("Execute a nix command via the root shell. (regression test for #40355)"):
machine.succeed(
f"nixos-container run {id1} -- nix-instantiate -E "
+ '\'derivation { name = "empty"; builder = "false"; system = "false"; }\' '
)
with subtest("Stop and start (regression test for #4989)"):
machine.succeed(f"nixos-container stop {id1}")
machine.succeed(f"nixos-container start {id1}")
# clear serial backlog for next tests
machine.succeed("logger eat console backlog 3ea46eb2-7f82-4f70-b810-3f00e3dd4c4d")
machine.wait_for_console_text(
"eat console backlog 3ea46eb2-7f82-4f70-b810-3f00e3dd4c4d"
)
with subtest("Stop a container early"):
machine.succeed(f"nixos-container stop {id1}")
machine.succeed(f"nixos-container start {id1} >&2 &")
machine.wait_for_console_text("Stage 2")
machine.succeed(f"nixos-container stop {id1}")
machine.wait_for_console_text(f"Container {id1} exited successfully")
machine.succeed(f"nixos-container start {id1}")
with subtest("Stop a container without machined (regression test for #109695)"):
machine.systemctl("stop systemd-machined")
machine.succeed(f"nixos-container stop {id1}")
machine.wait_for_console_text(f"Container {id1} has been shut down")
machine.succeed(f"nixos-container start {id1}")
with subtest("tmpfiles are present"):
machine.log("creating container tmpfiles")
machine.succeed(
"nixos-container create tmpfiles --config-file ${tmpfilesContainerConfig}"
)
machine.log("created, starting")
machine.succeed("nixos-container start tmpfiles")
machine.log("done starting, investigating")
machine.succeed(
"echo $(nixos-container run tmpfiles -- systemctl is-active foo.service) | grep -q active;"
)
machine.succeed("nixos-container destroy tmpfiles")
with subtest("Execute commands via the root shell"):
assert "Linux" in machine.succeed(f"nixos-container run {id1} -- uname")
with subtest("Destroy the containers"):
for id in id1, id2:
machine.succeed(f"nixos-container destroy {id}")
with subtest("Check whether destruction of any container has killed important data"):
machine.succeed("grep -qF 'important data' /nested-bindmount/dummy")
with subtest("Ensure that the container path is gone"):
print(machine.succeed("ls -lsa /var/lib/nixos-containers"))
machine.succeed(f"test ! -e /var/lib/nixos-containers/{id1}")
with subtest("Ensure that a failed container creation doesn'leave any state"):
machine.fail(
"nixos-container create b0rk --config-file ${brokenCfg}"
)
machine.succeed("test ! -e /var/lib/nixos-containers/b0rk")
''; '';
} brokenCfg = pkgs.writeText "broken.nix" ''
) {
assertions = [
{ assertion = false;
message = "I never evaluate";
}
];
}
'';
in
''
with subtest("Make sure we have a NixOS tree (required by nixos-container create)"):
machine.succeed("PAGER=cat nix-env -qa -A nixos.hello >&2")
id1, id2 = None, None
with subtest("Create some containers imperatively"):
id1 = machine.succeed("nixos-container create foo --ensure-unique-name").rstrip()
machine.log(f"created container {id1}")
id2 = machine.succeed("nixos-container create foo --ensure-unique-name").rstrip()
machine.log(f"created container {id2}")
assert id1 != id2
with subtest(f"Put the root of {id2} into a bind mount"):
machine.succeed(
f"mv /var/lib/nixos-containers/{id2} /id2-bindmount",
f"mount --bind /id2-bindmount /var/lib/nixos-containers/{id1}",
)
ip1 = machine.succeed(f"nixos-container show-ip {id1}").rstrip()
ip2 = machine.succeed(f"nixos-container show-ip {id2}").rstrip()
assert ip1 != ip2
with subtest(
"Create a directory and a file we can later check if it still exists "
+ "after destruction of the container"
):
machine.succeed("mkdir /nested-bindmount")
machine.succeed("echo important data > /nested-bindmount/dummy")
with subtest(
"Create a directory with a dummy file and bind-mount it into both containers."
):
for id in id1, id2:
important_path = f"/var/lib/nixos-containers/{id}/very/important/data"
machine.succeed(
f"mkdir -p {important_path}",
f"mount --bind /nested-bindmount {important_path}",
)
with subtest("Start one of them"):
machine.succeed(f"nixos-container start {id1}")
with subtest("Execute commands via the root shell"):
assert "Linux" in machine.succeed(f"nixos-container run {id1} -- uname")
with subtest("Execute a nix command via the root shell. (regression test for #40355)"):
machine.succeed(
f"nixos-container run {id1} -- nix-instantiate -E "
+ '\'derivation { name = "empty"; builder = "false"; system = "false"; }\' '
)
with subtest("Stop and start (regression test for #4989)"):
machine.succeed(f"nixos-container stop {id1}")
machine.succeed(f"nixos-container start {id1}")
# clear serial backlog for next tests
machine.succeed("logger eat console backlog 3ea46eb2-7f82-4f70-b810-3f00e3dd4c4d")
machine.wait_for_console_text(
"eat console backlog 3ea46eb2-7f82-4f70-b810-3f00e3dd4c4d"
)
with subtest("Stop a container early"):
machine.succeed(f"nixos-container stop {id1}")
machine.succeed(f"nixos-container start {id1} >&2 &")
machine.wait_for_console_text("Stage 2")
machine.succeed(f"nixos-container stop {id1}")
machine.wait_for_console_text(f"Container {id1} exited successfully")
machine.succeed(f"nixos-container start {id1}")
with subtest("Stop a container without machined (regression test for #109695)"):
machine.systemctl("stop systemd-machined")
machine.succeed(f"nixos-container stop {id1}")
machine.wait_for_console_text(f"Container {id1} has been shut down")
machine.succeed(f"nixos-container start {id1}")
with subtest("tmpfiles are present"):
machine.log("creating container tmpfiles")
machine.succeed(
"nixos-container create tmpfiles --config-file ${tmpfilesContainerConfig}"
)
machine.log("created, starting")
machine.succeed("nixos-container start tmpfiles")
machine.log("done starting, investigating")
machine.succeed(
"echo $(nixos-container run tmpfiles -- systemctl is-active foo.service) | grep -q active;"
)
machine.succeed("nixos-container destroy tmpfiles")
with subtest("Execute commands via the root shell"):
assert "Linux" in machine.succeed(f"nixos-container run {id1} -- uname")
with subtest("Destroy the containers"):
for id in id1, id2:
machine.succeed(f"nixos-container destroy {id}")
with subtest("Check whether destruction of any container has killed important data"):
machine.succeed("grep -qF 'important data' /nested-bindmount/dummy")
with subtest("Ensure that the container path is gone"):
print(machine.succeed("ls -lsa /var/lib/nixos-containers"))
machine.succeed(f"test ! -e /var/lib/nixos-containers/{id1}")
with subtest("Ensure that a failed container creation doesn'leave any state"):
machine.fail(
"nixos-container create b0rk --config-file ${brokenCfg}"
)
machine.succeed("test ! -e /var/lib/nixos-containers/b0rk")
'';
}

View File

@ -12,71 +12,69 @@ let
}; };
in in
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-ipv4-ipv6";
name = "containers-ipv4-ipv6"; meta = {
meta = { maintainers = with lib.maintainers; [
maintainers = with lib.maintainers; [ aristid
aristid aszlig
aszlig kampfschlaefer
kampfschlaefer ];
]; };
nodes.machine =
{ pkgs, ... }:
{
virtualisation.writableStore = true;
containers.webserver4 = webserverFor "10.231.136.1" "10.231.136.2";
containers.webserver6 = webserverFor "fc00::2" "fc00::1";
virtualisation.additionalPaths = [ pkgs.stdenv ];
}; };
nodes.machine = testScript =
{ pkgs, ... }: { nodes, ... }:
{ ''
virtualisation.writableStore = true; import time
containers.webserver4 = webserverFor "10.231.136.1" "10.231.136.2";
containers.webserver6 = webserverFor "fc00::2" "fc00::1";
virtualisation.additionalPaths = [ pkgs.stdenv ];
};
testScript =
{ nodes, ... }:
''
import time
def curl_host(ip): def curl_host(ip):
# put [] around ipv6 addresses for curl # put [] around ipv6 addresses for curl
host = ip if ":" not in ip else f"[{ip}]" host = ip if ":" not in ip else f"[{ip}]"
return f"curl --fail --connect-timeout 2 http://{host}/ > /dev/null" return f"curl --fail --connect-timeout 2 http://{host}/ > /dev/null"
def get_ip(container): def get_ip(container):
# need to distinguish because show-ip won't work for ipv6 # need to distinguish because show-ip won't work for ipv6
if container == "webserver4": if container == "webserver4":
ip = machine.succeed(f"nixos-container show-ip {container}").rstrip() ip = machine.succeed(f"nixos-container show-ip {container}").rstrip()
assert ip == "${nodes.machine.config.containers.webserver4.localAddress}" assert ip == "${nodes.machine.config.containers.webserver4.localAddress}"
return ip return ip
return "${nodes.machine.config.containers.webserver6.localAddress}" return "${nodes.machine.config.containers.webserver6.localAddress}"
for container in "webserver4", "webserver6": for container in "webserver4", "webserver6":
assert container in machine.succeed("nixos-container list") assert container in machine.succeed("nixos-container list")
with subtest(f"Start container {container}"): with subtest(f"Start container {container}"):
machine.succeed(f"nixos-container start {container}") machine.succeed(f"nixos-container start {container}")
# wait 2s for container to start and network to be up # wait 2s for container to start and network to be up
time.sleep(2) time.sleep(2)
# Since "start" returns after the container has reached # Since "start" returns after the container has reached
# multi-user.target, we should now be able to access it. # multi-user.target, we should now be able to access it.
ip = get_ip(container) ip = get_ip(container)
with subtest(f"{container} reacts to pings and HTTP requests"): with subtest(f"{container} reacts to pings and HTTP requests"):
machine.succeed(f"ping -n -c1 {ip}") machine.succeed(f"ping -n -c1 {ip}")
machine.succeed(curl_host(ip)) machine.succeed(curl_host(ip))
with subtest(f"Stop container {container}"): with subtest(f"Stop container {container}"):
machine.succeed(f"nixos-container stop {container}") machine.succeed(f"nixos-container stop {container}")
machine.fail(curl_host(ip)) machine.fail(curl_host(ip))
# Destroying a declarative container should fail. # Destroying a declarative container should fail.
machine.fail(f"nixos-container destroy {container}") machine.fail(f"nixos-container destroy {container}")
''; '';
} }
)

View File

@ -4,97 +4,95 @@ let
containerIp2 = "192.168.1.254"; containerIp2 = "192.168.1.254";
in in
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-macvlans";
name = "containers-macvlans"; meta = {
meta = { maintainers = with lib.maintainers; [ montag451 ];
maintainers = with lib.maintainers; [ montag451 ]; };
};
nodes = { nodes = {
machine1 = machine1 =
{ lib, ... }: { lib, ... }:
{ {
virtualisation.vlans = [ 1 ]; virtualisation.vlans = [ 1 ];
# To be able to ping containers from the host, it is necessary # To be able to ping containers from the host, it is necessary
# to create a macvlan on the host on the VLAN 1 network. # to create a macvlan on the host on the VLAN 1 network.
networking.macvlans.mv-eth1-host = { networking.macvlans.mv-eth1-host = {
interface = "eth1"; interface = "eth1";
mode = "bridge"; mode = "bridge";
}; };
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ]; networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
networking.interfaces.mv-eth1-host = { networking.interfaces.mv-eth1-host = {
ipv4.addresses = [ ipv4.addresses = [
{ {
address = "192.168.1.1"; address = "192.168.1.1";
prefixLength = 24; prefixLength = 24;
} }
]; ];
}; };
containers.test1 = { containers.test1 = {
autoStart = true; autoStart = true;
macvlans = [ "eth1" ]; macvlans = [ "eth1" ];
config = { config = {
networking.interfaces.mv-eth1 = { networking.interfaces.mv-eth1 = {
ipv4.addresses = [ ipv4.addresses = [
{ {
address = containerIp1; address = containerIp1;
prefixLength = 24; prefixLength = 24;
} }
]; ];
};
};
};
containers.test2 = {
autoStart = true;
macvlans = [ "eth1" ];
config = {
networking.interfaces.mv-eth1 = {
ipv4.addresses = [
{
address = containerIp2;
prefixLength = 24;
}
];
};
}; };
}; };
}; };
machine2 = containers.test2 = {
{ ... }: autoStart = true;
{ macvlans = [ "eth1" ];
virtualisation.vlans = [ 1 ];
config = {
networking.interfaces.mv-eth1 = {
ipv4.addresses = [
{
address = containerIp2;
prefixLength = 24;
}
];
};
};
}; };
};
}; machine2 =
{ ... }:
{
virtualisation.vlans = [ 1 ];
};
testScript = '' };
start_all()
machine1.wait_for_unit("default.target")
machine2.wait_for_unit("default.target")
with subtest( testScript = ''
"Ping between containers to check that macvlans are created in bridge mode" start_all()
): machine1.wait_for_unit("default.target")
machine1.succeed("nixos-container run test1 -- ping -n -c 1 ${containerIp2}") machine2.wait_for_unit("default.target")
with subtest("Ping containers from the host (machine1)"): with subtest(
machine1.succeed("ping -n -c 1 ${containerIp1}") "Ping between containers to check that macvlans are created in bridge mode"
machine1.succeed("ping -n -c 1 ${containerIp2}") ):
machine1.succeed("nixos-container run test1 -- ping -n -c 1 ${containerIp2}")
with subtest( with subtest("Ping containers from the host (machine1)"):
"Ping containers from the second machine to check that containers are reachable from the outside" machine1.succeed("ping -n -c 1 ${containerIp1}")
): machine1.succeed("ping -n -c 1 ${containerIp2}")
machine2.succeed("ping -n -c 1 ${containerIp1}")
machine2.succeed("ping -n -c 1 ${containerIp2}") with subtest(
''; "Ping containers from the second machine to check that containers are reachable from the outside"
} ):
) machine2.succeed("ping -n -c 1 ${containerIp1}")
machine2.succeed("ping -n -c 1 ${containerIp2}")
'';
}

View File

@ -1,44 +1,42 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-names";
name = "containers-names"; meta = {
meta = { maintainers = with lib.maintainers; [ patryk27 ];
maintainers = with lib.maintainers; [ patryk27 ]; };
nodes.machine =
{ ... }:
{
# We're using the newest kernel, so that we can test containers with long names.
# Please see https://github.com/NixOS/nixpkgs/issues/38509 for details.
boot.kernelPackages = pkgs.linuxPackages_latest;
containers =
let
container = subnet: {
autoStart = true;
privateNetwork = true;
hostAddress = "192.168.${subnet}.1";
localAddress = "192.168.${subnet}.2";
config = { };
};
in
{
first = container "1";
second = container "2";
really-long-name = container "3";
really-long-long-name-2 = container "4";
};
}; };
nodes.machine = testScript = ''
{ ... }: machine.wait_for_unit("default.target")
{
# We're using the newest kernel, so that we can test containers with long names.
# Please see https://github.com/NixOS/nixpkgs/issues/38509 for details.
boot.kernelPackages = pkgs.linuxPackages_latest;
containers = machine.succeed("ip link show | grep ve-first")
let machine.succeed("ip link show | grep ve-second")
container = subnet: { machine.succeed("ip link show | grep ve-really-lFYWO")
autoStart = true; machine.succeed("ip link show | grep ve-really-l3QgY")
privateNetwork = true; '';
hostAddress = "192.168.${subnet}.1"; }
localAddress = "192.168.${subnet}.2";
config = { };
};
in
{
first = container "1";
second = container "2";
really-long-name = container "3";
really-long-long-name-2 = container "4";
};
};
testScript = ''
machine.wait_for_unit("default.target")
machine.succeed("ip link show | grep ve-first")
machine.succeed("ip link show | grep ve-second")
machine.succeed("ip link show | grep ve-really-lFYWO")
machine.succeed("ip link show | grep ve-really-l3QgY")
'';
}
)

View File

@ -1,36 +1,34 @@
# Test for NixOS' container nesting. # Test for NixOS' container nesting.
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "nested";
name = "nested";
meta = with pkgs.lib.maintainers; { meta = with pkgs.lib.maintainers; {
maintainers = [ sorki ]; maintainers = [ sorki ];
}; };
nodes.machine = nodes.machine =
{ lib, ... }: { lib, ... }:
let let
makeNested = subConf: { makeNested = subConf: {
containers.nested = { containers.nested = {
autoStart = true; autoStart = true;
privateNetwork = true; privateNetwork = true;
config = subConf; config = subConf;
};
}; };
in };
makeNested (makeNested { }); in
makeNested (makeNested { });
testScript = '' testScript = ''
machine.start() machine.start()
machine.wait_for_unit("container@nested.service") machine.wait_for_unit("container@nested.service")
machine.succeed("systemd-run --pty --machine=nested -- machinectl list | grep nested") machine.succeed("systemd-run --pty --machine=nested -- machinectl list | grep nested")
print( print(
machine.succeed( machine.succeed(
"systemd-run --pty --machine=nested -- systemd-run --pty --machine=nested -- systemctl status" "systemd-run --pty --machine=nested -- systemd-run --pty --machine=nested -- systemctl status"
) )
) )
''; '';
} }
)

View File

@ -1,153 +1,151 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-physical_interfaces";
name = "containers-physical_interfaces"; meta = {
meta = { maintainers = with lib.maintainers; [ kampfschlaefer ];
maintainers = with lib.maintainers; [ kampfschlaefer ]; };
};
nodes = { nodes = {
server = server =
{ ... }: { ... }:
{ {
virtualisation.vlans = [ 1 ]; virtualisation.vlans = [ 1 ];
containers.server = { containers.server = {
privateNetwork = true; privateNetwork = true;
interfaces = [ "eth1" ]; interfaces = [ "eth1" ];
config = { config = {
networking.interfaces.eth1.ipv4.addresses = [ networking.interfaces.eth1.ipv4.addresses = [
{ {
address = "10.10.0.1"; address = "10.10.0.1";
prefixLength = 24; prefixLength = 24;
} }
]; ];
networking.firewall.enable = false; networking.firewall.enable = false;
};
}; };
}; };
bridged = };
{ ... }: bridged =
{ { ... }:
virtualisation.vlans = [ 1 ]; {
virtualisation.vlans = [ 1 ];
containers.bridged = { containers.bridged = {
privateNetwork = true; privateNetwork = true;
interfaces = [ "eth1" ]; interfaces = [ "eth1" ];
config = { config = {
networking.bridges.br0.interfaces = [ "eth1" ]; networking.bridges.br0.interfaces = [ "eth1" ];
networking.interfaces.br0.ipv4.addresses = [ networking.interfaces.br0.ipv4.addresses = [
{ {
address = "10.10.0.2"; address = "10.10.0.2";
prefixLength = 24; prefixLength = 24;
} }
]; ];
networking.firewall.enable = false; networking.firewall.enable = false;
};
}; };
}; };
};
bonded = bonded =
{ ... }: { ... }:
{ {
virtualisation.vlans = [ 1 ]; virtualisation.vlans = [ 1 ];
containers.bonded = { containers.bonded = {
privateNetwork = true; privateNetwork = true;
interfaces = [ "eth1" ]; interfaces = [ "eth1" ];
config = { config = {
networking.bonds.bond0 = { networking.bonds.bond0 = {
interfaces = [ "eth1" ]; interfaces = [ "eth1" ];
driverOptions.mode = "active-backup"; driverOptions.mode = "active-backup";
};
networking.interfaces.bond0.ipv4.addresses = [
{
address = "10.10.0.3";
prefixLength = 24;
}
];
networking.firewall.enable = false;
}; };
networking.interfaces.bond0.ipv4.addresses = [
{
address = "10.10.0.3";
prefixLength = 24;
}
];
networking.firewall.enable = false;
}; };
}; };
};
bridgedbond = bridgedbond =
{ ... }: { ... }:
{ {
virtualisation.vlans = [ 1 ]; virtualisation.vlans = [ 1 ];
containers.bridgedbond = { containers.bridgedbond = {
privateNetwork = true; privateNetwork = true;
interfaces = [ "eth1" ]; interfaces = [ "eth1" ];
config = { config = {
networking.bonds.bond0 = { networking.bonds.bond0 = {
interfaces = [ "eth1" ]; interfaces = [ "eth1" ];
driverOptions.mode = "active-backup"; driverOptions.mode = "active-backup";
};
networking.bridges.br0.interfaces = [ "bond0" ];
networking.interfaces.br0.ipv4.addresses = [
{
address = "10.10.0.4";
prefixLength = 24;
}
];
networking.firewall.enable = false;
}; };
networking.bridges.br0.interfaces = [ "bond0" ];
networking.interfaces.br0.ipv4.addresses = [
{
address = "10.10.0.4";
prefixLength = 24;
}
];
networking.firewall.enable = false;
}; };
}; };
}; };
};
testScript = '' testScript = ''
start_all() start_all()
with subtest("Prepare server"): with subtest("Prepare server"):
server.wait_for_unit("default.target") server.wait_for_unit("default.target")
server.succeed("ip link show dev eth1 >&2") server.succeed("ip link show dev eth1 >&2")
with subtest("Simple physical interface is up"): with subtest("Simple physical interface is up"):
server.succeed("nixos-container start server") server.succeed("nixos-container start server")
server.wait_for_unit("container@server") server.wait_for_unit("container@server")
server.succeed( server.succeed(
"systemctl -M server list-dependencies network-addresses-eth1.service >&2" "systemctl -M server list-dependencies network-addresses-eth1.service >&2"
) )
# The other tests will ping this container on its ip. Here we just check # The other tests will ping this container on its ip. Here we just check
# that the device is present in the container. # that the device is present in the container.
server.succeed("nixos-container run server -- ip a show dev eth1 >&2") server.succeed("nixos-container run server -- ip a show dev eth1 >&2")
with subtest("Physical device in bridge in container can ping server"): with subtest("Physical device in bridge in container can ping server"):
bridged.wait_for_unit("default.target") bridged.wait_for_unit("default.target")
bridged.succeed("nixos-container start bridged") bridged.succeed("nixos-container start bridged")
bridged.wait_for_unit("container@bridged") bridged.wait_for_unit("container@bridged")
bridged.succeed( bridged.succeed(
"systemctl -M bridged list-dependencies network-addresses-br0.service >&2", "systemctl -M bridged list-dependencies network-addresses-br0.service >&2",
"systemctl -M bridged status -n 30 -l network-addresses-br0.service", "systemctl -M bridged status -n 30 -l network-addresses-br0.service",
"nixos-container run bridged -- ping -w 10 -c 1 -n 10.10.0.1", "nixos-container run bridged -- ping -w 10 -c 1 -n 10.10.0.1",
) )
with subtest("Physical device in bond in container can ping server"): with subtest("Physical device in bond in container can ping server"):
bonded.wait_for_unit("default.target") bonded.wait_for_unit("default.target")
bonded.succeed("nixos-container start bonded") bonded.succeed("nixos-container start bonded")
bonded.wait_for_unit("container@bonded") bonded.wait_for_unit("container@bonded")
bonded.succeed( bonded.succeed(
"systemctl -M bonded list-dependencies network-addresses-bond0 >&2", "systemctl -M bonded list-dependencies network-addresses-bond0 >&2",
"systemctl -M bonded status -n 30 -l network-addresses-bond0 >&2", "systemctl -M bonded status -n 30 -l network-addresses-bond0 >&2",
"nixos-container run bonded -- ping -w 10 -c 1 -n 10.10.0.1", "nixos-container run bonded -- ping -w 10 -c 1 -n 10.10.0.1",
) )
with subtest("Physical device in bond in bridge in container can ping server"): with subtest("Physical device in bond in bridge in container can ping server"):
bridgedbond.wait_for_unit("default.target") bridgedbond.wait_for_unit("default.target")
bridgedbond.succeed("nixos-container start bridgedbond") bridgedbond.succeed("nixos-container start bridgedbond")
bridgedbond.wait_for_unit("container@bridgedbond") bridgedbond.wait_for_unit("container@bridgedbond")
bridgedbond.succeed( bridgedbond.succeed(
"systemctl -M bridgedbond list-dependencies network-addresses-br0.service >&2", "systemctl -M bridgedbond list-dependencies network-addresses-br0.service >&2",
"systemctl -M bridgedbond status -n 30 -l network-addresses-br0.service", "systemctl -M bridgedbond status -n 30 -l network-addresses-br0.service",
"nixos-container run bridgedbond -- ping -w 10 -c 1 -n 10.10.0.1", "nixos-container run bridgedbond -- ping -w 10 -c 1 -n 10.10.0.1",
) )
''; '';
} }
)

View File

@ -5,69 +5,67 @@ let
containerPort = 80; containerPort = 80;
in in
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-portforward";
name = "containers-portforward"; meta = {
meta = { maintainers = with lib.maintainers; [
maintainers = with lib.maintainers; [ aristid
aristid aszlig
aszlig kampfschlaefer
kampfschlaefer ianwookim
ianwookim ];
]; };
};
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
imports = [ ../modules/installer/cd-dvd/channel.nix ]; imports = [ ../modules/installer/cd-dvd/channel.nix ];
virtualisation.writableStore = true; virtualisation.writableStore = true;
containers.webserver = { containers.webserver = {
privateNetwork = true; privateNetwork = true;
hostAddress = hostIp; hostAddress = hostIp;
localAddress = containerIp; localAddress = containerIp;
forwardPorts = [ forwardPorts = [
{ {
protocol = "tcp"; protocol = "tcp";
hostPort = hostPort; hostPort = hostPort;
containerPort = containerPort; containerPort = containerPort;
} }
]; ];
config = { config = {
services.httpd.enable = true; services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org"; services.httpd.adminAddr = "foo@example.org";
networking.firewall.allowedTCPPorts = [ 80 ]; networking.firewall.allowedTCPPorts = [ 80 ];
};
}; };
virtualisation.additionalPaths = [ pkgs.stdenv ];
}; };
testScript = '' virtualisation.additionalPaths = [ pkgs.stdenv ];
container_list = machine.succeed("nixos-container list") };
assert "webserver" in container_list
# Start the webserver container. testScript = ''
machine.succeed("nixos-container start webserver") container_list = machine.succeed("nixos-container list")
assert "webserver" in container_list
# wait two seconds for the container to start and the network to be up # Start the webserver container.
machine.sleep(2) machine.succeed("nixos-container start webserver")
# Since "start" returns after the container has reached # wait two seconds for the container to start and the network to be up
# multi-user.target, we should now be able to access it. machine.sleep(2)
# ip = machine.succeed("nixos-container show-ip webserver").strip()
machine.succeed("ping -n -c1 ${hostIp}")
machine.succeed("curl --fail http://${hostIp}:${toString hostPort}/ > /dev/null")
# Stop the container. # Since "start" returns after the container has reached
machine.succeed("nixos-container stop webserver") # multi-user.target, we should now be able to access it.
machine.fail("curl --fail --connect-timeout 2 http://${hostIp}:${toString hostPort}/ > /dev/null") # ip = machine.succeed("nixos-container show-ip webserver").strip()
machine.succeed("ping -n -c1 ${hostIp}")
machine.succeed("curl --fail http://${hostIp}:${toString hostPort}/ > /dev/null")
# Destroying a declarative container should fail. # Stop the container.
machine.fail("nixos-container destroy webserver") machine.succeed("nixos-container stop webserver")
''; machine.fail("curl --fail --connect-timeout 2 http://${hostIp}:${toString hostPort}/ > /dev/null")
} # Destroying a declarative container should fail.
) machine.fail("nixos-container destroy webserver")
'';
}

View File

@ -1,61 +1,59 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-reloadable";
name = "containers-reloadable"; meta = {
meta = { maintainers = with lib.maintainers; [ danbst ];
maintainers = with lib.maintainers; [ danbst ]; };
};
nodes = { nodes = {
machine = machine =
{ lib, ... }: { lib, ... }:
{ {
containers.test1 = { containers.test1 = {
autoStart = true; autoStart = true;
config.environment.etc.check.text = "client_base"; config.environment.etc.check.text = "client_base";
}; };
# prevent make-test-python.nix to change IP # prevent make-test-python.nix to change IP
networking.interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [ ]; networking.interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [ ];
specialisation.c1.configuration = { specialisation.c1.configuration = {
containers.test1.config = { containers.test1.config = {
environment.etc.check.text = lib.mkForce "client_c1"; environment.etc.check.text = lib.mkForce "client_c1";
services.httpd.enable = true; services.httpd.enable = true;
services.httpd.adminAddr = "nixos@example.com"; services.httpd.adminAddr = "nixos@example.com";
};
};
specialisation.c2.configuration = {
containers.test1.config = {
environment.etc.check.text = lib.mkForce "client_c2";
services.nginx.enable = true;
};
}; };
}; };
};
testScript = '' specialisation.c2.configuration = {
machine.start() containers.test1.config = {
machine.wait_for_unit("default.target") environment.etc.check.text = lib.mkForce "client_c2";
services.nginx.enable = true;
};
};
};
};
assert "client_base" in machine.succeed("nixos-container run test1 cat /etc/check") testScript = ''
machine.start()
machine.wait_for_unit("default.target")
with subtest("httpd is available after activating config1"): assert "client_base" in machine.succeed("nixos-container run test1 cat /etc/check")
machine.succeed(
"/run/booted-system/specialisation/c1/bin/switch-to-configuration test >&2",
"[[ $(nixos-container run test1 cat /etc/check) == client_c1 ]] >&2",
"systemctl status httpd -M test1 >&2",
)
with subtest("httpd is not available any longer after switching to config2"): with subtest("httpd is available after activating config1"):
machine.succeed( machine.succeed(
"/run/booted-system/specialisation/c2/bin/switch-to-configuration test >&2", "/run/booted-system/specialisation/c1/bin/switch-to-configuration test >&2",
"[[ $(nixos-container run test1 cat /etc/check) == client_c2 ]] >&2", "[[ $(nixos-container run test1 cat /etc/check) == client_c1 ]] >&2",
"systemctl status nginx -M test1 >&2", "systemctl status httpd -M test1 >&2",
) )
machine.fail("systemctl status httpd -M test1 >&2")
'';
} with subtest("httpd is not available any longer after switching to config2"):
) machine.succeed(
"/run/booted-system/specialisation/c2/bin/switch-to-configuration test >&2",
"[[ $(nixos-container run test1 cat /etc/check) == client_c2 ]] >&2",
"systemctl status nginx -M test1 >&2",
)
machine.fail("systemctl status httpd -M test1 >&2")
'';
}

View File

@ -1,40 +1,38 @@
import ./make-test-python.nix ( { lib, ... }:
{ lib, ... }: {
{ name = "containers-require-bind-mounts";
name = "containers-require-bind-mounts"; meta.maintainers = with lib.maintainers; [ kira-bruneau ];
meta.maintainers = with lib.maintainers; [ kira-bruneau ];
nodes.machine = { nodes.machine = {
containers.require-bind-mounts = { containers.require-bind-mounts = {
bindMounts = { bindMounts = {
"/srv/data" = { }; "/srv/data" = { };
};
config = { };
};
virtualisation.fileSystems = {
"/srv/data" = {
fsType = "tmpfs";
options = [ "noauto" ];
};
}; };
config = { };
}; };
testScript = '' virtualisation.fileSystems = {
machine.wait_for_unit("default.target") "/srv/data" = {
fsType = "tmpfs";
options = [ "noauto" ];
};
};
};
assert "require-bind-mounts" in machine.succeed("nixos-container list") testScript = ''
machine.wait_for_unit("default.target")
assert "require-bind-mounts" in machine.succeed("nixos-container list")
assert "down" in machine.succeed("nixos-container status require-bind-mounts")
assert "inactive" in machine.fail("systemctl is-active srv-data.mount")
with subtest("bind mount host paths must be mounted to run container"):
machine.succeed("nixos-container start require-bind-mounts")
assert "up" in machine.succeed("nixos-container status require-bind-mounts")
assert "active" in machine.succeed("systemctl status srv-data.mount")
machine.succeed("systemctl stop srv-data.mount")
assert "down" in machine.succeed("nixos-container status require-bind-mounts") assert "down" in machine.succeed("nixos-container status require-bind-mounts")
assert "inactive" in machine.fail("systemctl is-active srv-data.mount") assert "inactive" in machine.fail("systemctl is-active srv-data.mount")
'';
with subtest("bind mount host paths must be mounted to run container"): }
machine.succeed("nixos-container start require-bind-mounts")
assert "up" in machine.succeed("nixos-container status require-bind-mounts")
assert "active" in machine.succeed("systemctl status srv-data.mount")
machine.succeed("systemctl stop srv-data.mount")
assert "down" in machine.succeed("nixos-container status require-bind-mounts")
assert "inactive" in machine.fail("systemctl is-active srv-data.mount")
'';
}
)

View File

@ -1,131 +1,129 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-restart_networking";
name = "containers-restart_networking"; meta = {
meta = { maintainers = with lib.maintainers; [ kampfschlaefer ];
maintainers = with lib.maintainers; [ kampfschlaefer ]; };
};
nodes = { nodes = {
client = { client = {
virtualisation.vlans = [ 1 ]; virtualisation.vlans = [ 1 ];
networking.firewall.enable = false; networking.firewall.enable = false;
containers.webserver = { containers.webserver = {
autoStart = true; autoStart = true;
privateNetwork = true; privateNetwork = true;
hostBridge = "br0"; hostBridge = "br0";
config = { config = {
networking.firewall.enable = false; networking.firewall.enable = false;
networking.interfaces.eth0.ipv4.addresses = [ networking.interfaces.eth0.ipv4.addresses = [
{ {
address = "192.168.1.122"; address = "192.168.1.122";
prefixLength = 24; prefixLength = 24;
} }
]; ];
};
}; };
};
networking.bridges.br0 = {
interfaces = [ ];
rstp = false;
};
networking.interfaces.br0.ipv4.addresses = [
{
address = "192.168.1.1";
prefixLength = 24;
}
];
networking.interfaces.eth1 = {
ipv4.addresses = lib.mkForce [ ];
ipv6.addresses = lib.mkForce [ ];
};
specialisation.eth1.configuration = {
networking.bridges.br0.interfaces = [ "eth1" ];
networking.interfaces = {
eth1.ipv4.addresses = lib.mkForce [ ];
eth1.ipv6.addresses = lib.mkForce [ ];
br0.ipv4.addresses = [
{
address = "192.168.1.2";
prefixLength = 24;
}
];
};
};
specialisation.eth1-rstp.configuration = {
networking.bridges.br0 = { networking.bridges.br0 = {
interfaces = [ ]; interfaces = [ "eth1" ];
rstp = false; rstp = lib.mkForce true;
}; };
networking.interfaces.br0.ipv4.addresses = [ networking.interfaces = {
{ eth1.ipv4.addresses = lib.mkForce [ ];
address = "192.168.1.1"; eth1.ipv6.addresses = lib.mkForce [ ];
prefixLength = 24; br0.ipv4.addresses = [
} {
]; address = "192.168.1.2";
prefixLength = 24;
networking.interfaces.eth1 = { }
ipv4.addresses = lib.mkForce [ ]; ];
ipv6.addresses = lib.mkForce [ ];
};
specialisation.eth1.configuration = {
networking.bridges.br0.interfaces = [ "eth1" ];
networking.interfaces = {
eth1.ipv4.addresses = lib.mkForce [ ];
eth1.ipv6.addresses = lib.mkForce [ ];
br0.ipv4.addresses = [
{
address = "192.168.1.2";
prefixLength = 24;
}
];
};
};
specialisation.eth1-rstp.configuration = {
networking.bridges.br0 = {
interfaces = [ "eth1" ];
rstp = lib.mkForce true;
};
networking.interfaces = {
eth1.ipv4.addresses = lib.mkForce [ ];
eth1.ipv6.addresses = lib.mkForce [ ];
br0.ipv4.addresses = [
{
address = "192.168.1.2";
prefixLength = 24;
}
];
};
}; };
}; };
}; };
};
testScript = '' testScript = ''
client.start() client.start()
client.wait_for_unit("default.target") client.wait_for_unit("default.target")
with subtest("Initial configuration connectivity check"): with subtest("Initial configuration connectivity check"):
client.succeed("ping 192.168.1.122 -c 1 -n >&2") client.succeed("ping 192.168.1.122 -c 1 -n >&2")
client.succeed("nixos-container run webserver -- ping -c 1 -n 192.168.1.1 >&2") client.succeed("nixos-container run webserver -- ping -c 1 -n 192.168.1.1 >&2")
client.fail("ip l show eth1 |grep 'master br0' >&2") client.fail("ip l show eth1 |grep 'master br0' >&2")
client.fail("grep eth1 /run/br0.interfaces >&2") client.fail("grep eth1 /run/br0.interfaces >&2")
with subtest("Bridged configuration without STP preserves connectivity"): with subtest("Bridged configuration without STP preserves connectivity"):
client.succeed( client.succeed(
"/run/booted-system/specialisation/eth1/bin/switch-to-configuration test >&2" "/run/booted-system/specialisation/eth1/bin/switch-to-configuration test >&2"
) )
client.succeed( client.succeed(
"ping 192.168.1.122 -c 1 -n >&2", "ping 192.168.1.122 -c 1 -n >&2",
"nixos-container run webserver -- ping -c 1 -n 192.168.1.2 >&2", "nixos-container run webserver -- ping -c 1 -n 192.168.1.2 >&2",
"ip l show eth1 |grep 'master br0' >&2", "ip l show eth1 |grep 'master br0' >&2",
"grep eth1 /run/br0.interfaces >&2", "grep eth1 /run/br0.interfaces >&2",
) )
# activating rstp needs another service, therefore the bridge will restart and the container will lose its connectivity # activating rstp needs another service, therefore the bridge will restart and the container will lose its connectivity
# with subtest("Bridged configuration with STP"): # with subtest("Bridged configuration with STP"):
# client.succeed("/run/booted-system/specialisation/eth1-rstp/bin/switch-to-configuration test >&2") # client.succeed("/run/booted-system/specialisation/eth1-rstp/bin/switch-to-configuration test >&2")
# client.execute("ip -4 a >&2") # client.execute("ip -4 a >&2")
# client.execute("ip l >&2") # client.execute("ip l >&2")
# #
# client.succeed( # client.succeed(
# "ping 192.168.1.122 -c 1 -n >&2", # "ping 192.168.1.122 -c 1 -n >&2",
# "nixos-container run webserver -- ping -c 1 -n 192.168.1.2 >&2", # "nixos-container run webserver -- ping -c 1 -n 192.168.1.2 >&2",
# "ip l show eth1 |grep 'master br0' >&2", # "ip l show eth1 |grep 'master br0' >&2",
# "grep eth1 /run/br0.interfaces >&2", # "grep eth1 /run/br0.interfaces >&2",
# ) # )
with subtest("Reverting to initial configuration preserves connectivity"): with subtest("Reverting to initial configuration preserves connectivity"):
client.succeed( client.succeed(
"/run/booted-system/bin/switch-to-configuration test >&2" "/run/booted-system/bin/switch-to-configuration test >&2"
) )
client.succeed("ping 192.168.1.122 -c 1 -n >&2") client.succeed("ping 192.168.1.122 -c 1 -n >&2")
client.succeed("nixos-container run webserver -- ping -c 1 -n 192.168.1.1 >&2") client.succeed("nixos-container run webserver -- ping -c 1 -n 192.168.1.1 >&2")
client.fail("ip l show eth1 |grep 'master br0' >&2") client.fail("ip l show eth1 |grep 'master br0' >&2")
client.fail("grep eth1 /run/br0.interfaces >&2") client.fail("grep eth1 /run/br0.interfaces >&2")
''; '';
} }
)

View File

@ -1,93 +1,91 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-tmpfs";
name = "containers-tmpfs"; meta = {
meta = { maintainers = with lib.maintainers; [ patryk27 ];
maintainers = with lib.maintainers; [ patryk27 ]; };
};
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
imports = [ ../modules/installer/cd-dvd/channel.nix ]; imports = [ ../modules/installer/cd-dvd/channel.nix ];
virtualisation.writableStore = true; virtualisation.writableStore = true;
containers.tmpfs = { containers.tmpfs = {
autoStart = true; autoStart = true;
tmpfs = [ tmpfs = [
# Mount var as a tmpfs # Mount var as a tmpfs
"/var" "/var"
# Add a nested mount inside a tmpfs # Add a nested mount inside a tmpfs
"/var/log" "/var/log"
# Add a tmpfs on a path that does not exist # Add a tmpfs on a path that does not exist
"/some/random/path" "/some/random/path"
]; ];
config = { }; config = { };
};
virtualisation.additionalPaths = [ pkgs.stdenv ];
}; };
testScript = '' virtualisation.additionalPaths = [ pkgs.stdenv ];
machine.wait_for_unit("default.target") };
assert "tmpfs" in machine.succeed("nixos-container list")
with subtest("tmpfs container is up"): testScript = ''
assert "up" in machine.succeed("nixos-container status tmpfs") machine.wait_for_unit("default.target")
assert "tmpfs" in machine.succeed("nixos-container list")
with subtest("tmpfs container is up"):
assert "up" in machine.succeed("nixos-container status tmpfs")
def tmpfs_cmd(command): def tmpfs_cmd(command):
return f"nixos-container run tmpfs -- {command} 2>/dev/null" return f"nixos-container run tmpfs -- {command} 2>/dev/null"
with subtest("/var is mounted as a tmpfs"): with subtest("/var is mounted as a tmpfs"):
machine.succeed(tmpfs_cmd("mountpoint -q /var")) machine.succeed(tmpfs_cmd("mountpoint -q /var"))
with subtest("/var/log is mounted as a tmpfs"): with subtest("/var/log is mounted as a tmpfs"):
assert "What: tmpfs" in machine.succeed( assert "What: tmpfs" in machine.succeed(
tmpfs_cmd("systemctl status var-log.mount --no-pager") tmpfs_cmd("systemctl status var-log.mount --no-pager")
) )
machine.succeed(tmpfs_cmd("mountpoint -q /var/log")) machine.succeed(tmpfs_cmd("mountpoint -q /var/log"))
with subtest("/some/random/path is mounted as a tmpfs"): with subtest("/some/random/path is mounted as a tmpfs"):
assert "What: tmpfs" in machine.succeed( assert "What: tmpfs" in machine.succeed(
tmpfs_cmd("systemctl status some-random-path.mount --no-pager") tmpfs_cmd("systemctl status some-random-path.mount --no-pager")
) )
machine.succeed(tmpfs_cmd("mountpoint -q /some/random/path")) machine.succeed(tmpfs_cmd("mountpoint -q /some/random/path"))
with subtest( with subtest(
"files created in the container in a non-tmpfs directory are visible on the host." "files created in the container in a non-tmpfs directory are visible on the host."
): ):
# This establishes legitimacy for the following tests # This establishes legitimacy for the following tests
machine.succeed( machine.succeed(
tmpfs_cmd("touch /root/test.file"), tmpfs_cmd("touch /root/test.file"),
tmpfs_cmd("ls -l /root | grep -q test.file"), tmpfs_cmd("ls -l /root | grep -q test.file"),
"test -e /var/lib/nixos-containers/tmpfs/root/test.file", "test -e /var/lib/nixos-containers/tmpfs/root/test.file",
) )
with subtest( with subtest(
"/some/random/path is writable and that files created there are not " "/some/random/path is writable and that files created there are not "
+ "in the hosts container dir but in the tmpfs" + "in the hosts container dir but in the tmpfs"
): ):
machine.succeed( machine.succeed(
tmpfs_cmd("touch /some/random/path/test.file"), tmpfs_cmd("touch /some/random/path/test.file"),
tmpfs_cmd("test -e /some/random/path/test.file"), tmpfs_cmd("test -e /some/random/path/test.file"),
) )
machine.fail("test -e /var/lib/nixos-containers/tmpfs/some/random/path/test.file") machine.fail("test -e /var/lib/nixos-containers/tmpfs/some/random/path/test.file")
with subtest( with subtest(
"files created in the hosts container dir in a path where a tmpfs " "files created in the hosts container dir in a path where a tmpfs "
+ "file system has been mounted are not visible to the container as " + "file system has been mounted are not visible to the container as "
+ "the do not exist in the tmpfs" + "the do not exist in the tmpfs"
): ):
machine.succeed( machine.succeed(
"touch /var/lib/nixos-containers/tmpfs/var/test.file", "touch /var/lib/nixos-containers/tmpfs/var/test.file",
"test -e /var/lib/nixos-containers/tmpfs/var/test.file", "test -e /var/lib/nixos-containers/tmpfs/var/test.file",
"ls -l /var/lib/nixos-containers/tmpfs/var/ | grep -q test.file 2>/dev/null", "ls -l /var/lib/nixos-containers/tmpfs/var/ | grep -q test.file 2>/dev/null",
) )
machine.fail(tmpfs_cmd("ls -l /var | grep -q test.file")) machine.fail(tmpfs_cmd("ls -l /var | grep -q test.file"))
''; '';
} }
)

View File

@ -1,26 +1,24 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "containers-unified-hierarchy";
name = "containers-unified-hierarchy"; meta = {
meta = { maintainers = with lib.maintainers; [ farnoy ];
maintainers = with lib.maintainers; [ farnoy ]; };
};
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
containers = { containers = {
test-container = { test-container = {
autoStart = true; autoStart = true;
config = { }; config = { };
};
}; };
}; };
};
testScript = '' testScript = ''
machine.wait_for_unit("default.target") machine.wait_for_unit("default.target")
machine.succeed("echo 'stat -fc %T /sys/fs/cgroup/ | grep cgroup2fs' | nixos-container root-login test-container") machine.succeed("echo 'stat -fc %T /sys/fs/cgroup/ | grep cgroup2fs' | nixos-container root-login test-container")
''; '';
} }
)

View File

@ -1,28 +1,26 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }:
let let
port = 3333; port = 3333;
in in
{ {
name = "convos"; name = "convos";
meta.maintainers = with lib.maintainers; [ sgo ]; meta.maintainers = with lib.maintainers; [ sgo ];
nodes = { nodes = {
machine = machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
services.convos = { services.convos = {
enable = true; enable = true;
listenPort = port; listenPort = port;
};
}; };
}; };
};
testScript = '' testScript = ''
machine.wait_for_unit("convos") machine.wait_for_unit("convos")
machine.wait_for_open_port(${toString port}) machine.wait_for_open_port(${toString port})
machine.succeed("curl -f http://localhost:${toString port}/") machine.succeed("curl -f http://localhost:${toString port}/")
''; '';
} }
)

View File

@ -1,38 +1,36 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "coturn";
name = "coturn"; nodes = {
nodes = { default = {
default = { services.coturn.enable = true;
services.coturn.enable = true; };
}; secretsfile = {
secretsfile = { boot.postBootCommands = ''
boot.postBootCommands = '' echo "some-very-secret-string" > /run/coturn-secret
echo "some-very-secret-string" > /run/coturn-secret '';
''; services.coturn = {
services.coturn = { enable = true;
enable = true; static-auth-secret-file = "/run/coturn-secret";
static-auth-secret-file = "/run/coturn-secret";
};
}; };
}; };
};
testScript = '' testScript = ''
start_all() start_all()
with subtest("by default works without configuration"): with subtest("by default works without configuration"):
default.wait_for_unit("coturn.service") default.wait_for_unit("coturn.service")
with subtest("works with static-auth-secret-file"): with subtest("works with static-auth-secret-file"):
secretsfile.wait_for_unit("coturn.service") secretsfile.wait_for_unit("coturn.service")
secretsfile.wait_for_open_port(3478) secretsfile.wait_for_open_port(3478)
secretsfile.succeed("grep 'some-very-secret-string' /run/coturn/turnserver.cfg") secretsfile.succeed("grep 'some-very-secret-string' /run/coturn/turnserver.cfg")
# Forbidden IP, fails: # Forbidden IP, fails:
secretsfile.fail("${pkgs.coturn}/bin/turnutils_uclient -W some-very-secret-string 127.0.0.1 -DgX -e 127.0.0.1 -n 1 -c -y") secretsfile.fail("${pkgs.coturn}/bin/turnutils_uclient -W some-very-secret-string 127.0.0.1 -DgX -e 127.0.0.1 -n 1 -c -y")
# allowed-peer-ip, should succeed: # allowed-peer-ip, should succeed:
secretsfile.succeed("${pkgs.coturn}/bin/turnutils_uclient -W some-very-secret-string 192.168.1.2 -DgX -e 192.168.1.2 -n 1 -c -y") secretsfile.succeed("${pkgs.coturn}/bin/turnutils_uclient -W some-very-secret-string 192.168.1.2 -DgX -e 192.168.1.2 -n 1 -c -y")
default.log(default.execute("systemd-analyze security coturn.service | grep -v ''")[1]) default.log(default.execute("systemd-analyze security coturn.service | grep -v ''")[1])
''; '';
} }
)

View File

@ -14,51 +14,49 @@ let
testpass = "cowabunga"; testpass = "cowabunga";
testlogin = "${testuser}:${testpass}@"; testlogin = "${testuser}:${testpass}@";
in in
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "couchdb";
name = "couchdb"; meta.maintainers = [ ];
meta.maintainers = [ ];
nodes = { nodes = {
couchdb3 = makeNode pkgs.couchdb3 testuser testpass; couchdb3 = makeNode pkgs.couchdb3 testuser testpass;
}; };
testScript = testScript =
let let
curlJqCheck = curlJqCheck =
login: action: path: jqexpr: result: login: action: path: jqexpr: result:
pkgs.writeScript "curl-jq-check-${action}-${path}.sh" '' pkgs.writeScript "curl-jq-check-${action}-${path}.sh" ''
RESULT=$(curl -X ${action} http://${login}127.0.0.1:5984/${path} | jq -r '${jqexpr}') RESULT=$(curl -X ${action} http://${login}127.0.0.1:5984/${path} | jq -r '${jqexpr}')
echo $RESULT >&2 echo $RESULT >&2
if [ "$RESULT" != "${result}" ]; then if [ "$RESULT" != "${result}" ]; then
exit 1 exit 1
fi fi
''; '';
in in
'' ''
start_all() start_all()
couchdb3.wait_for_unit("couchdb.service") couchdb3.wait_for_unit("couchdb.service")
couchdb3.wait_until_succeeds( couchdb3.wait_until_succeeds(
"${curlJqCheck testlogin "GET" "" ".couchdb" "Welcome"}" "${curlJqCheck testlogin "GET" "" ".couchdb" "Welcome"}"
) )
couchdb3.wait_until_succeeds( couchdb3.wait_until_succeeds(
"${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}" "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}"
) )
couchdb3.succeed("${curlJqCheck testlogin "PUT" "foo" ".ok" "true"}") couchdb3.succeed("${curlJqCheck testlogin "PUT" "foo" ".ok" "true"}")
couchdb3.succeed( couchdb3.succeed(
"${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "1"}" "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "1"}"
) )
couchdb3.succeed( couchdb3.succeed(
"${curlJqCheck testlogin "DELETE" "foo" ".ok" "true"}" "${curlJqCheck testlogin "DELETE" "foo" ".ok" "true"}"
) )
couchdb3.succeed( couchdb3.succeed(
"${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}" "${curlJqCheck testlogin "GET" "_all_dbs" ". | length" "0"}"
) )
couchdb3.succeed( couchdb3.succeed(
"${curlJqCheck testlogin "GET" "_node/couchdb@127.0.0.1" ".couchdb" "Welcome"}" "${curlJqCheck testlogin "GET" "_node/couchdb@127.0.0.1" ".couchdb" "Welcome"}"
) )
''; '';
} }
)

View File

@ -1,33 +1,31 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }:
{ {
name = "crabfit"; name = "crabfit";
meta.maintainers = [ ]; meta.maintainers = [ ];
nodes = { nodes = {
machine = machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
services.crabfit = { services.crabfit = {
enable = true; enable = true;
frontend.host = "http://127.0.0.1:3001"; frontend.host = "http://127.0.0.1:3001";
api.host = "127.0.0.1:3000"; api.host = "127.0.0.1:3000";
};
}; };
}; };
};
# TODO: Add a reverse proxy and a dns entry for testing # TODO: Add a reverse proxy and a dns entry for testing
testScript = '' testScript = ''
machine.wait_for_unit("crabfit-api") machine.wait_for_unit("crabfit-api")
machine.wait_for_unit("crabfit-frontend") machine.wait_for_unit("crabfit-frontend")
machine.wait_for_open_port(3000) machine.wait_for_open_port(3000)
machine.wait_for_open_port(3001) machine.wait_for_open_port(3001)
machine.succeed("curl -f http://localhost:3001/") machine.succeed("curl -f http://localhost:3001/")
''; '';
} }
)

View File

@ -1,59 +1,57 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: let
let client =
client = { pkgs, ... }:
{ pkgs, ... }: {
{ environment.systemPackages = [ pkgs.croc ];
environment.systemPackages = [ pkgs.croc ];
};
pass = "PassRelay";
in
{
name = "croc";
meta = with pkgs.lib.maintainers; {
maintainers = [
equirosa
SuperSandro2000
];
}; };
pass = "PassRelay";
in
{
name = "croc";
meta = with pkgs.lib.maintainers; {
maintainers = [
equirosa
SuperSandro2000
];
};
nodes = { nodes = {
relay = { relay = {
services.croc = { services.croc = {
enable = true; enable = true;
pass = pass; pass = pass;
openFirewall = true; openFirewall = true;
};
}; };
sender = client;
receiver = client;
}; };
sender = client;
receiver = client;
};
testScript = '' testScript = ''
start_all() start_all()
# wait until relay is up # wait until relay is up
relay.wait_for_unit("croc") relay.wait_for_unit("croc")
relay.wait_for_open_port(9009) relay.wait_for_open_port(9009)
relay.wait_for_open_port(9010) relay.wait_for_open_port(9010)
relay.wait_for_open_port(9011) relay.wait_for_open_port(9011)
relay.wait_for_open_port(9012) relay.wait_for_open_port(9012)
relay.wait_for_open_port(9013) relay.wait_for_open_port(9013)
# generate testfiles and send them # generate testfiles and send them
sender.wait_for_unit("multi-user.target") sender.wait_for_unit("multi-user.target")
sender.execute("echo Hello World > testfile01.txt") sender.execute("echo Hello World > testfile01.txt")
sender.execute("echo Hello Earth > testfile02.txt") sender.execute("echo Hello Earth > testfile02.txt")
sender.execute( sender.execute(
"env CROC_SECRET=topSecret croc --pass ${pass} --relay relay send testfile01.txt testfile02.txt >&2 &" "env CROC_SECRET=topSecret croc --pass ${pass} --relay relay send testfile01.txt testfile02.txt >&2 &"
) )
# receive the testfiles and check them # receive the testfiles and check them
receiver.succeed( receiver.succeed(
"env CROC_SECRET=topSecret croc --pass ${pass} --yes --relay relay" "env CROC_SECRET=topSecret croc --pass ${pass} --yes --relay relay"
) )
assert "Hello World" in receiver.succeed("cat testfile01.txt") assert "Hello World" in receiver.succeed("cat testfile01.txt")
assert "Hello Earth" in receiver.succeed("cat testfile02.txt") assert "Hello Earth" in receiver.succeed("cat testfile02.txt")
''; '';
} }
)

View File

@ -24,182 +24,180 @@
uses upstream for its tests. uses upstream for its tests.
*/ */
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let # Update with domains in TestImpersonate.TEST_URLS if needed from:
# Update with domains in TestImpersonate.TEST_URLS if needed from: # https://github.com/lwthiker/curl-impersonate/blob/main/tests/test_impersonate.py
# https://github.com/lwthiker/curl-impersonate/blob/main/tests/test_impersonate.py domains = [
domains = [ "www.wikimedia.org"
"www.wikimedia.org" "www.wikipedia.org"
"www.wikipedia.org" "www.mozilla.org"
"www.mozilla.org" "www.apache.org"
"www.apache.org" "www.kernel.org"
"www.kernel.org" "git-scm.com"
"git-scm.com" ];
];
tls-certs = tls-certs =
let let
# Configure CA with X.509 v3 extensions that would be trusted by curl # Configure CA with X.509 v3 extensions that would be trusted by curl
ca-cert-conf = pkgs.writeText "curl-impersonate-ca.cnf" '' ca-cert-conf = pkgs.writeText "curl-impersonate-ca.cnf" ''
basicConstraints = critical, CA:TRUE basicConstraints = critical, CA:TRUE
subjectKeyIdentifier = hash subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer:always authorityKeyIdentifier = keyid:always, issuer:always
keyUsage = critical, cRLSign, digitalSignature, keyCertSign keyUsage = critical, cRLSign, digitalSignature, keyCertSign
'';
# Configure leaf certificate with X.509 v3 extensions that would be trusted
# by curl and set subject-alternative names for test domains
tls-cert-conf = pkgs.writeText "curl-impersonate-tls.cnf" ''
basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer:always
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = critical, serverAuth
subjectAltName = @alt_names
[alt_names]
${lib.concatStringsSep "\n" (lib.imap0 (idx: domain: "DNS.${toString idx} = ${domain}") domains)}
'';
in
pkgs.runCommand "curl-impersonate-test-certs"
{
nativeBuildInputs = [ pkgs.openssl ];
}
''
# create CA certificate and key
openssl req -newkey rsa:4096 -keyout ca-key.pem -out ca-csr.pem -nodes -subj '/CN=curl-impersonate-ca.nixos.test'
openssl x509 -req -sha512 -in ca-csr.pem -key ca-key.pem -out ca.pem -extfile ${ca-cert-conf} -days 36500
openssl x509 -in ca.pem -text
# create server certificate and key
openssl req -newkey rsa:4096 -keyout key.pem -out csr.pem -nodes -subj '/CN=curl-impersonate.nixos.test'
openssl x509 -req -sha512 -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile ${tls-cert-conf} -days 36500
openssl x509 -in cert.pem -text
# output CA cert and server cert and key
mkdir -p $out
cp key.pem cert.pem ca.pem $out
'';
# Test script
curl-impersonate-test =
let
# Build miniature libcurl client used by test driver
minicurl =
pkgs.runCommandCC "minicurl"
{
buildInputs = [ pkgs.curl ];
}
''
mkdir -p $out/bin
$CC -Wall -Werror -o $out/bin/minicurl ${pkgs.curl-impersonate.src}/tests/minicurl.c `curl-config --libs`
'';
in
pkgs.writeShellScript "curl-impersonate-test" ''
set -euxo pipefail
# Test driver requirements
export PATH="${
with pkgs;
lib.makeBinPath [
bash
coreutils
python3Packages.pytest
nghttp2
tcpdump
]
}"
export PYTHONPATH="${
with pkgs.python3Packages;
makePythonPath [
pyyaml
pytest-asyncio
dpkt
ts1-signatures
]
}"
# Prepare test root prefix
mkdir -p usr/{bin,lib}
cp -rs ${pkgs.curl-impersonate}/* ${minicurl}/* usr/
cp -r ${pkgs.curl-impersonate.src}/tests ./
# Run tests
cd tests
pytest . --install-dir ../usr --capture-interface eth1 --exitfirst -k 'not test_http2_headers'
''; '';
in
{
name = "curl-impersonate";
meta = with lib.maintainers; { # Configure leaf certificate with X.509 v3 extensions that would be trusted
maintainers = [ ]; # by curl and set subject-alternative names for test domains
}; tls-cert-conf = pkgs.writeText "curl-impersonate-tls.cnf" ''
basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer:always
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = critical, serverAuth
subjectAltName = @alt_names
nodes = { [alt_names]
web = ${lib.concatStringsSep "\n" (lib.imap0 (idx: domain: "DNS.${toString idx} = ${domain}") domains)}
{ '';
nodes, in
pkgs, pkgs.runCommand "curl-impersonate-test-certs"
lib, {
config, nativeBuildInputs = [ pkgs.openssl ];
... }
}: ''
{ # create CA certificate and key
networking.firewall.allowedTCPPorts = [ openssl req -newkey rsa:4096 -keyout ca-key.pem -out ca-csr.pem -nodes -subj '/CN=curl-impersonate-ca.nixos.test'
80 openssl x509 -req -sha512 -in ca-csr.pem -key ca-key.pem -out ca.pem -extfile ${ca-cert-conf} -days 36500
443 openssl x509 -in ca.pem -text
];
services = { # create server certificate and key
nginx = { openssl req -newkey rsa:4096 -keyout key.pem -out csr.pem -nodes -subj '/CN=curl-impersonate.nixos.test'
enable = true; openssl x509 -req -sha512 -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile ${tls-cert-conf} -days 36500
virtualHosts."curl-impersonate.nixos.test" = { openssl x509 -in cert.pem -text
default = true;
addSSL = true; # output CA cert and server cert and key
sslCertificate = "${tls-certs}/cert.pem"; mkdir -p $out
sslCertificateKey = "${tls-certs}/key.pem"; cp key.pem cert.pem ca.pem $out
}; '';
# Test script
curl-impersonate-test =
let
# Build miniature libcurl client used by test driver
minicurl =
pkgs.runCommandCC "minicurl"
{
buildInputs = [ pkgs.curl ];
}
''
mkdir -p $out/bin
$CC -Wall -Werror -o $out/bin/minicurl ${pkgs.curl-impersonate.src}/tests/minicurl.c `curl-config --libs`
'';
in
pkgs.writeShellScript "curl-impersonate-test" ''
set -euxo pipefail
# Test driver requirements
export PATH="${
with pkgs;
lib.makeBinPath [
bash
coreutils
python3Packages.pytest
nghttp2
tcpdump
]
}"
export PYTHONPATH="${
with pkgs.python3Packages;
makePythonPath [
pyyaml
pytest-asyncio
dpkt
ts1-signatures
]
}"
# Prepare test root prefix
mkdir -p usr/{bin,lib}
cp -rs ${pkgs.curl-impersonate}/* ${minicurl}/* usr/
cp -r ${pkgs.curl-impersonate.src}/tests ./
# Run tests
cd tests
pytest . --install-dir ../usr --capture-interface eth1 --exitfirst -k 'not test_http2_headers'
'';
in
{
name = "curl-impersonate";
meta = with lib.maintainers; {
maintainers = [ ];
};
nodes = {
web =
{
nodes,
pkgs,
lib,
config,
...
}:
{
networking.firewall.allowedTCPPorts = [
80
443
];
services = {
nginx = {
enable = true;
virtualHosts."curl-impersonate.nixos.test" = {
default = true;
addSSL = true;
sslCertificate = "${tls-certs}/cert.pem";
sslCertificateKey = "${tls-certs}/key.pem";
}; };
}; };
}; };
};
curl = curl =
{ {
nodes, nodes,
pkgs, pkgs,
lib, lib,
config, config,
... ...
}: }:
{ {
networking.extraHosts = lib.concatStringsSep "\n" ( networking.extraHosts = lib.concatStringsSep "\n" (
map (domain: "${nodes.web.networking.primaryIPAddress} ${domain}") domains map (domain: "${nodes.web.networking.primaryIPAddress} ${domain}") domains
); );
security.pki.certificateFiles = [ "${tls-certs}/ca.pem" ]; security.pki.certificateFiles = [ "${tls-certs}/ca.pem" ];
}; };
}; };
testScript = testScript =
{ nodes, ... }: { nodes, ... }:
'' ''
start_all() start_all()
with subtest("Wait for network"): with subtest("Wait for network"):
web.systemctl("start network-online.target") web.systemctl("start network-online.target")
curl.systemctl("start network-online.target") curl.systemctl("start network-online.target")
web.wait_for_unit("network-online.target") web.wait_for_unit("network-online.target")
curl.wait_for_unit("network-online.target") curl.wait_for_unit("network-online.target")
with subtest("Wait for web server"): with subtest("Wait for web server"):
web.wait_for_unit("nginx.service") web.wait_for_unit("nginx.service")
web.wait_for_open_port(443) web.wait_for_open_port(443)
with subtest("Run curl-impersonate tests"): with subtest("Run curl-impersonate tests"):
curl.succeed("${curl-impersonate-test}") curl.succeed("${curl-impersonate-test}")
''; '';
} }
)

View File

@ -1,38 +1,36 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: {
{
name = "dae"; name = "dae";
meta = { meta = {
maintainers = with lib.maintainers; [ oluceps ]; maintainers = with lib.maintainers; [ oluceps ];
};
nodes.machine =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.curl ];
services.nginx = {
enable = true;
statusPage = true;
};
services.dae = {
enable = true;
config = ''
global { disable_waiting_network: true }
routing{}
'';
};
}; };
nodes.machine = testScript = ''
{ pkgs, ... }: machine.wait_for_unit("nginx.service")
{ machine.wait_for_unit("dae.service")
environment.systemPackages = [ pkgs.curl ];
services.nginx = {
enable = true;
statusPage = true;
};
services.dae = {
enable = true;
config = ''
global { disable_waiting_network: true }
routing{}
'';
};
};
testScript = '' machine.wait_for_open_port(80)
machine.wait_for_unit("nginx.service")
machine.wait_for_unit("dae.service")
machine.wait_for_open_port(80) machine.succeed("curl --fail --max-time 10 http://localhost")
'';
machine.succeed("curl --fail --max-time 10 http://localhost") }
'';
}
)

View File

@ -1,125 +1,123 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "db-rest";
name = "db-rest"; meta.maintainers = with pkgs.lib.maintainers; [ marie ];
meta.maintainers = with pkgs.lib.maintainers; [ marie ];
nodes = { nodes = {
database = { database = {
networking = { networking = {
interfaces.eth1 = { interfaces.eth1 = {
ipv4.addresses = [ ipv4.addresses = [
{ {
address = "192.168.2.10"; address = "192.168.2.10";
prefixLength = 24; prefixLength = 24;
} }
]; ];
};
firewall.allowedTCPPorts = [ 31638 ];
};
services.redis.servers.db-rest = {
enable = true;
bind = "0.0.0.0";
requirePass = "choochoo";
port = 31638;
}; };
firewall.allowedTCPPorts = [ 31638 ];
}; };
serverWithTcp = services.redis.servers.db-rest = {
{ pkgs, ... }: enable = true;
{ bind = "0.0.0.0";
environment = { requirePass = "choochoo";
etc = { port = 31638;
"db-rest/password-redis-db".text = ''
choochoo
'';
};
};
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.11";
prefixLength = 24;
}
];
};
firewall.allowedTCPPorts = [ 3000 ];
};
services.db-rest = {
enable = true;
host = "0.0.0.0";
redis = {
enable = true;
createLocally = false;
host = "192.168.2.10";
port = 31638;
passwordFile = "/etc/db-rest/password-redis-db";
useSSL = false;
};
};
};
serverWithUnixSocket =
{ pkgs, ... }:
{
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.12";
prefixLength = 24;
}
];
};
firewall.allowedTCPPorts = [ 3000 ];
};
services.db-rest = {
enable = true;
host = "0.0.0.0";
redis = {
enable = true;
createLocally = true;
};
};
};
client = {
environment.systemPackages = [ pkgs.jq ];
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.13";
prefixLength = 24;
}
];
};
};
}; };
}; };
testScript = '' serverWithTcp =
start_all() { pkgs, ... }:
{
environment = {
etc = {
"db-rest/password-redis-db".text = ''
choochoo
'';
};
};
with subtest("db-rest redis with TCP socket"): networking = {
database.wait_for_unit("redis-db-rest.service") interfaces.eth1 = {
database.wait_for_open_port(31638) ipv4.addresses = [
{
address = "192.168.2.11";
prefixLength = 24;
}
];
};
firewall.allowedTCPPorts = [ 3000 ];
};
serverWithTcp.wait_for_unit("db-rest.service") services.db-rest = {
serverWithTcp.wait_for_open_port(3000) enable = true;
host = "0.0.0.0";
redis = {
enable = true;
createLocally = false;
host = "192.168.2.10";
port = 31638;
passwordFile = "/etc/db-rest/password-redis-db";
useSSL = false;
};
};
};
client.succeed("curl --fail --get http://192.168.2.11:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'") serverWithUnixSocket =
{ pkgs, ... }:
{
networking = {
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.12";
prefixLength = 24;
}
];
};
firewall.allowedTCPPorts = [ 3000 ];
};
with subtest("db-rest redis with Unix socket"): services.db-rest = {
serverWithUnixSocket.wait_for_unit("db-rest.service") enable = true;
serverWithUnixSocket.wait_for_open_port(3000) host = "0.0.0.0";
redis = {
enable = true;
createLocally = true;
};
};
};
client.succeed("curl --fail --get http://192.168.2.12:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'") client = {
''; environment.systemPackages = [ pkgs.jq ];
} networking = {
) interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.2.13";
prefixLength = 24;
}
];
};
};
};
};
testScript = ''
start_all()
with subtest("db-rest redis with TCP socket"):
database.wait_for_unit("redis-db-rest.service")
database.wait_for_open_port(31638)
serverWithTcp.wait_for_unit("db-rest.service")
serverWithTcp.wait_for_open_port(3000)
client.succeed("curl --fail --get http://192.168.2.11:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'")
with subtest("db-rest redis with Unix socket"):
serverWithUnixSocket.wait_for_unit("db-rest.service")
serverWithUnixSocket.wait_for_open_port(3000)
client.succeed("curl --fail --get http://192.168.2.12:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'")
'';
}

View File

@ -1,44 +1,42 @@
import ./make-test-python.nix ( { lib, ... }:
{ lib, ... }: {
{ name = "dconf";
name = "dconf";
meta.maintainers = with lib.maintainers; [ meta.maintainers = with lib.maintainers; [
linsui linsui
]; ];
nodes.machine = nodes.machine =
{ {
config, config,
pkgs, pkgs,
lib, lib,
... ...
}: }:
{ {
users.extraUsers.alice = { users.extraUsers.alice = {
isNormalUser = true; isNormalUser = true;
};
programs.dconf = with lib.gvariant; {
enable = true;
profiles.user.databases = [
{
settings = {
"test/not".locked = mkInt32 1;
"test/is".locked = "locked";
};
locks = [
"/test/is/locked"
];
}
];
};
}; };
programs.dconf = with lib.gvariant; {
enable = true;
profiles.user.databases = [
{
settings = {
"test/not".locked = mkInt32 1;
"test/is".locked = "locked";
};
locks = [
"/test/is/locked"
];
}
];
};
};
testScript = '' testScript = ''
machine.succeed("test $(dconf read -d /test/not/locked) == 1") machine.succeed("test $(dconf read -d /test/not/locked) == 1")
machine.succeed("test $(dconf read -d /test/is/locked) == \"'locked'\"") machine.succeed("test $(dconf read -d /test/is/locked) == \"'locked'\"")
machine.fail("sudo -u alice dbus-run-session -- dconf write /test/is/locked \"@s 'unlocked'\"") machine.fail("sudo -u alice dbus-run-session -- dconf write /test/is/locked \"@s 'unlocked'\"")
machine.succeed("sudo -u alice dbus-run-session -- dconf write /test/not/locked \"@i 2\"") machine.succeed("sudo -u alice dbus-run-session -- dconf write /test/not/locked \"@i 2\"")
''; '';
} }
)

View File

@ -1,28 +1,26 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let port = 6000;
port = 6000; in
in {
{ name = "ddns-updater";
name = "ddns-updater";
meta.maintainers = with lib.maintainers; [ delliott ]; meta.maintainers = with lib.maintainers; [ delliott ];
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
services.ddns-updater = { services.ddns-updater = {
enable = true; enable = true;
environment = { environment = {
LISTENING_ADDRESS = ":" + (toString port); LISTENING_ADDRESS = ":" + (toString port);
};
}; };
}; };
};
testScript = '' testScript = ''
machine.wait_for_unit("ddns-updater.service") machine.wait_for_unit("ddns-updater.service")
machine.wait_for_open_port(${toString port}) machine.wait_for_open_port(${toString port})
machine.succeed("curl --fail http://localhost:${toString port}/") machine.succeed("curl --fail http://localhost:${toString port}/")
''; '';
} }
)

View File

@ -1,37 +1,35 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let httpPort = 800;
httpPort = 800; in
in {
{ name = "deconz";
name = "deconz";
meta.maintainers = with lib.maintainers; [ meta.maintainers = with lib.maintainers; [
bjornfor bjornfor
]; ];
nodes.machine = nodes.machine =
{ {
config, config,
pkgs, pkgs,
lib, lib,
... ...
}: }:
{ {
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
services.deconz = { services.deconz = {
enable = true; enable = true;
inherit httpPort; inherit httpPort;
extraArgs = [ extraArgs = [
"--dbg-err=2" "--dbg-err=2"
"--dbg-info=2" "--dbg-info=2"
]; ];
};
}; };
};
testScript = '' testScript = ''
machine.wait_for_unit("deconz.service") machine.wait_for_unit("deconz.service")
machine.succeed("curl -sfL http://localhost:${toString httpPort}") machine.succeed("curl -sfL http://localhost:${toString httpPort}")
''; '';
} }
)

View File

@ -1,57 +1,55 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "deepin";
name = "deepin";
meta.maintainers = lib.teams.deepin.members; meta.maintainers = lib.teams.deepin.members;
nodes.machine = nodes.machine =
{ ... }: { ... }:
{ {
imports = [ imports = [
./common/user-account.nix ./common/user-account.nix
]; ];
virtualisation.memorySize = 2048; virtualisation.memorySize = 2048;
services.xserver.enable = true; services.xserver.enable = true;
services.xserver.displayManager = { services.xserver.displayManager = {
lightdm.enable = true; lightdm.enable = true;
autoLogin = { autoLogin = {
enable = true; enable = true;
user = "alice"; user = "alice";
};
}; };
services.xserver.desktopManager.deepin.enable = true;
}; };
testScript = services.xserver.desktopManager.deepin.enable = true;
{ nodes, ... }: };
let
user = nodes.machine.users.users.alice;
in
''
with subtest("Wait for login"):
machine.wait_for_x()
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"): testScript =
machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}") { nodes, ... }:
let
user = nodes.machine.users.users.alice;
in
''
with subtest("Wait for login"):
machine.wait_for_x()
machine.wait_for_file("${user.home}/.Xauthority")
machine.succeed("xauth merge ${user.home}/.Xauthority")
with subtest("Check if Deepin session components actually start"): with subtest("Check that logging in has given the user ownership of devices"):
machine.wait_until_succeeds("pgrep -f dde-session-daemon") machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
machine.wait_for_window("dde-session-daemon")
machine.wait_until_succeeds("pgrep -f dde-desktop")
machine.wait_for_window("dde-desktop")
with subtest("Open deepin-terminal"): with subtest("Check if Deepin session components actually start"):
machine.succeed("su - ${user.name} -c 'DISPLAY=:0 deepin-terminal >&2 &'") machine.wait_until_succeeds("pgrep -f dde-session-daemon")
machine.wait_for_window("deepin-terminal") machine.wait_for_window("dde-session-daemon")
machine.sleep(20) machine.wait_until_succeeds("pgrep -f dde-desktop")
machine.screenshot("screen") machine.wait_for_window("dde-desktop")
'';
} with subtest("Open deepin-terminal"):
) machine.succeed("su - ${user.name} -c 'DISPLAY=:0 deepin-terminal >&2 &'")
machine.wait_for_window("deepin-terminal")
machine.sleep(20)
machine.screenshot("screen")
'';
}

View File

@ -1,69 +1,67 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "deluge";
name = "deluge"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ flokli ];
maintainers = [ flokli ]; };
};
nodes = { nodes = {
simple = { simple = {
services.deluge = { services.deluge = {
enable = true;
package = pkgs.deluge-2_x;
web = {
enable = true; enable = true;
package = pkgs.deluge-2_x;
web = {
enable = true;
openFirewall = true;
};
};
};
declarative = {
services.deluge = {
enable = true;
package = pkgs.deluge-2_x;
openFirewall = true; openFirewall = true;
declarative = true;
config = {
allow_remote = true;
download_location = "/var/lib/deluge/my-download";
daemon_port = 58846;
listen_ports = [
6881
6889
];
};
web = {
enable = true;
port = 3142;
};
authFile = pkgs.writeText "deluge-auth" ''
localclient:a7bef72a890:10
andrew:password:10
user3:anotherpass:5
'';
}; };
}; };
}; };
testScript = '' declarative = {
start_all() services.deluge = {
enable = true;
package = pkgs.deluge-2_x;
openFirewall = true;
declarative = true;
config = {
allow_remote = true;
download_location = "/var/lib/deluge/my-download";
daemon_port = 58846;
listen_ports = [
6881
6889
];
};
web = {
enable = true;
port = 3142;
};
authFile = pkgs.writeText "deluge-auth" ''
localclient:a7bef72a890:10
andrew:password:10
user3:anotherpass:5
'';
};
};
simple.wait_for_unit("deluged") };
simple.wait_for_unit("delugeweb")
simple.wait_for_open_port(8112)
declarative.wait_for_unit("network.target")
declarative.wait_until_succeeds("curl --fail http://simple:8112")
declarative.wait_for_unit("deluged") testScript = ''
declarative.wait_for_unit("delugeweb") start_all()
declarative.wait_until_succeeds("curl --fail http://declarative:3142")
# deluge-console always exits with 1. https://dev.deluge-torrent.org/ticket/3291 simple.wait_for_unit("deluged")
declarative.succeed( simple.wait_for_unit("delugeweb")
"(deluge-console 'connect 127.0.0.1:58846 andrew password; help' || true) | grep -q 'rm.*Remove a torrent'" simple.wait_for_open_port(8112)
) declarative.wait_for_unit("network.target")
''; declarative.wait_until_succeeds("curl --fail http://simple:8112")
}
) declarative.wait_for_unit("deluged")
declarative.wait_for_unit("delugeweb")
declarative.wait_until_succeeds("curl --fail http://declarative:3142")
# deluge-console always exits with 1. https://dev.deluge-torrent.org/ticket/3291
declarative.succeed(
"(deluge-console 'connect 127.0.0.1:58846 andrew password; help' || true) | grep -q 'rm.*Remove a torrent'"
)
'';
}

View File

@ -1,71 +1,69 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: let
let dependencyTrackPort = 8081;
dependencyTrackPort = 8081; in
in {
{ name = "dependency-track";
name = "dependency-track"; meta = {
meta = { maintainers = pkgs.lib.teams.cyberus.members;
maintainers = pkgs.lib.teams.cyberus.members; };
};
nodes = { nodes = {
server = server =
{ pkgs, ... }: { pkgs, ... }:
{ {
virtualisation = { virtualisation = {
cores = 2; cores = 2;
diskSize = 4096; diskSize = 4096;
memorySize = 1024 * 2; memorySize = 1024 * 2;
};
environment.systemPackages = with pkgs; [ curl ];
systemd.services.dependency-track = {
# source: https://github.com/DependencyTrack/dependency-track/blob/37e0ba59e8057c18a87a7a76e247a8f75677a56c/dev/scripts/data-nist-generate-dummy.sh
preStart = ''
set -euo pipefail
NIST_DIR="$HOME/.dependency-track/nist"
rm -rf "$NIST_DIR"
mkdir -p "$NIST_DIR"
for feed in $(seq "2024" "2002"); do
touch "$NIST_DIR/nvdcve-1.1-$feed.json.gz"
echo "9999999999999" > "$NIST_DIR/nvdcve-1.1-$feed.json.gz.ts"
done
'';
};
services.dependency-track = {
enable = true;
port = dependencyTrackPort;
nginx.domain = "localhost";
database.passwordFile = "${pkgs.writeText "dbPassword" ''hunter2'THE'''H''''E''}";
};
}; };
};
testScript = environment.systemPackages = with pkgs; [ curl ];
# python systemd.services.dependency-track = {
'' # source: https://github.com/DependencyTrack/dependency-track/blob/37e0ba59e8057c18a87a7a76e247a8f75677a56c/dev/scripts/data-nist-generate-dummy.sh
import json preStart = ''
set -euo pipefail
start_all() NIST_DIR="$HOME/.dependency-track/nist"
server.wait_for_unit("dependency-track.service") rm -rf "$NIST_DIR"
server.wait_until_succeeds( mkdir -p "$NIST_DIR"
"journalctl -o cat -u dependency-track.service | grep 'Dependency-Track is ready'"
for feed in $(seq "2024" "2002"); do
touch "$NIST_DIR/nvdcve-1.1-$feed.json.gz"
echo "9999999999999" > "$NIST_DIR/nvdcve-1.1-$feed.json.gz.ts"
done
'';
};
services.dependency-track = {
enable = true;
port = dependencyTrackPort;
nginx.domain = "localhost";
database.passwordFile = "${pkgs.writeText "dbPassword" ''hunter2'THE'''H''''E''}";
};
};
};
testScript =
# python
''
import json
start_all()
server.wait_for_unit("dependency-track.service")
server.wait_until_succeeds(
"journalctl -o cat -u dependency-track.service | grep 'Dependency-Track is ready'"
)
server.wait_for_open_port(${toString dependencyTrackPort})
with subtest("version api returns correct version"):
version = json.loads(
server.succeed("curl http://localhost/api/version")
) )
server.wait_for_open_port(${toString dependencyTrackPort}) assert version["version"] == "${pkgs.dependency-track.version}"
with subtest("version api returns correct version"): with subtest("nginx serves frontend"):
version = json.loads( server.succeed("curl http://localhost/ | grep \"<title>Dependency-Track</title>\"")
server.succeed("curl http://localhost/api/version") '';
) }
assert version["version"] == "${pkgs.dependency-track.version}"
with subtest("nginx serves frontend"):
server.succeed("curl http://localhost/ | grep \"<title>Dependency-Track</title>\"")
'';
}
)

View File

@ -1,43 +1,41 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: let
let server-port = 3141;
server-port = 3141; in
in {
{ name = "devpi-server";
name = "devpi-server"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ cafkafk ];
maintainers = [ cafkafk ]; };
};
nodes = { nodes = {
devpi = devpi =
{ ... }: { ... }:
{ {
services.devpi-server = { services.devpi-server = {
enable = true; enable = true;
host = "0.0.0.0"; host = "0.0.0.0";
port = server-port; port = server-port;
openFirewall = true; openFirewall = true;
secretFile = pkgs.writeText "devpi-secret" "v263P+V3YGDYUyfYL/RBURw+tCPMDw94R/iCuBNJrDhaYrZYjpA6XPFVDDH8ViN20j77y2PHoMM/U0opNkVQ2g=="; secretFile = pkgs.writeText "devpi-secret" "v263P+V3YGDYUyfYL/RBURw+tCPMDw94R/iCuBNJrDhaYrZYjpA6XPFVDDH8ViN20j77y2PHoMM/U0opNkVQ2g==";
};
}; };
};
client1 = client1 =
{ ... }: { ... }:
{ {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
devpi-client devpi-client
jq jq
]; ];
}; };
}; };
testScript = '' testScript = ''
start_all() start_all()
devpi.wait_for_unit("devpi-server.service") devpi.wait_for_unit("devpi-server.service")
devpi.wait_for_open_port(${builtins.toString server-port}) devpi.wait_for_open_port(${builtins.toString server-port})
client1.succeed("devpi getjson http://devpi:${builtins.toString server-port}") client1.succeed("devpi getjson http://devpi:${builtins.toString server-port}")
''; '';
} }
)

View File

@ -1,84 +1,82 @@
import ./make-test-python.nix ( { lib, ... }:
{ lib, ... }: {
{ name = "dex-oidc";
name = "dex-oidc"; meta.maintainers = with lib.maintainers; [ Flakebi ];
meta.maintainers = with lib.maintainers; [ Flakebi ];
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
environment.systemPackages = with pkgs; [ jq ]; environment.systemPackages = with pkgs; [ jq ];
services.dex = { services.dex = {
enable = true; enable = true;
settings = { settings = {
issuer = "http://127.0.0.1:8080/dex"; issuer = "http://127.0.0.1:8080/dex";
storage = { storage = {
type = "postgres"; type = "postgres";
config.host = "/var/run/postgresql"; config.host = "/var/run/postgresql";
};
web.http = "127.0.0.1:8080";
oauth2.skipApprovalScreen = true;
staticClients = [
{
id = "oidcclient";
name = "Client";
redirectURIs = [ "https://example.com/callback" ];
secretFile = "/etc/dex/oidcclient";
}
];
connectors = [
{
type = "mockPassword";
id = "mock";
name = "Example";
config = {
username = "admin";
password = "password";
};
}
];
}; };
}; web.http = "127.0.0.1:8080";
oauth2.skipApprovalScreen = true;
# This should not be set from nix but through other means to not leak the secret. staticClients = [
environment.etc."dex/oidcclient" = {
mode = "0400";
user = "dex";
text = "oidcclientsecret";
};
services.postgresql = {
enable = true;
ensureDatabases = [ "dex" ];
ensureUsers = [
{ {
name = "dex"; id = "oidcclient";
ensureDBOwnership = true; name = "Client";
redirectURIs = [ "https://example.com/callback" ];
secretFile = "/etc/dex/oidcclient";
}
];
connectors = [
{
type = "mockPassword";
id = "mock";
name = "Example";
config = {
username = "admin";
password = "password";
};
} }
]; ];
}; };
}; };
testScript = '' # This should not be set from nix but through other means to not leak the secret.
with subtest("Web server gets ready"): environment.etc."dex/oidcclient" = {
machine.wait_for_unit("dex.service", timeout=120) mode = "0400";
# Wait until server accepts connections user = "dex";
machine.wait_until_succeeds("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid'", timeout=120) text = "oidcclientsecret";
};
with subtest("Login"): services.postgresql = {
state = machine.succeed("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid' | sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip() enable = true;
print(f"Got state {state}") ensureDatabases = [ "dex" ];
# Login request returns 303 with redirect_url that has code as query parameter: ensureUsers = [
# https://example.com/callback?code=kibsamwdupuy2iwqnlbqei3u6&state= {
code = machine.succeed(f"curl -fs 'localhost:8080/dex/auth/mock/login?back=&state={state}' -d 'login=admin&password=password' -w '%{{redirect_url}}' | sed -n 's/.*code=\\(.*\\)&.*/\\1/p'") name = "dex";
print(f"Got approval code {code}") ensureDBOwnership = true;
bearer = machine.succeed(f"curl -fs localhost:8080/dex/token -u oidcclient:oidcclientsecret -d 'grant_type=authorization_code&redirect_uri=https://example.com/callback&code={code}' | jq .access_token -r").strip() }
print(f"Got access token {bearer}") ];
};
};
with subtest("Get userinfo"): testScript = ''
assert '"sub"' in machine.succeed( with subtest("Web server gets ready"):
f"curl -fs localhost:8080/dex/userinfo --oauth2-bearer {bearer}" machine.wait_for_unit("dex.service", timeout=120)
) # Wait until server accepts connections
''; machine.wait_until_succeeds("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid'", timeout=120)
}
) with subtest("Login"):
state = machine.succeed("curl -fs 'localhost:8080/dex/auth/mock?client_id=oidcclient&response_type=code&redirect_uri=https://example.com/callback&scope=openid' | sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip()
print(f"Got state {state}")
# Login request returns 303 with redirect_url that has code as query parameter:
# https://example.com/callback?code=kibsamwdupuy2iwqnlbqei3u6&state=
code = machine.succeed(f"curl -fs 'localhost:8080/dex/auth/mock/login?back=&state={state}' -d 'login=admin&password=password' -w '%{{redirect_url}}' | sed -n 's/.*code=\\(.*\\)&.*/\\1/p'")
print(f"Got approval code {code}")
bearer = machine.succeed(f"curl -fs localhost:8080/dex/token -u oidcclient:oidcclientsecret -d 'grant_type=authorization_code&redirect_uri=https://example.com/callback&code={code}' | jq .access_token -r").strip()
print(f"Got access token {bearer}")
with subtest("Get userinfo"):
assert '"sub"' in machine.succeed(
f"curl -fs localhost:8080/dex/userinfo --oauth2-bearer {bearer}"
)
'';
}

View File

@ -1,35 +1,33 @@
import ./make-test-python.nix ( {
{ pkgs,
pkgs, latestKernel ? false,
latestKernel ? false, ...
... }:
}:
{ {
name = "disable-installer-tools"; name = "disable-installer-tools";
nodes.machine = nodes.machine =
{ pkgs, lib, ... }: { pkgs, lib, ... }:
{ {
system.disableInstallerTools = true; system.disableInstallerTools = true;
boot.enableContainers = false; boot.enableContainers = false;
environment.defaultPackages = [ ]; environment.defaultPackages = [ ];
}; };
testScript = '' testScript = ''
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
with subtest("nixos installer tools should not be included"): with subtest("nixos installer tools should not be included"):
machine.fail("which nixos-rebuild") machine.fail("which nixos-rebuild")
machine.fail("which nixos-install") machine.fail("which nixos-install")
machine.fail("which nixos-generate-config") machine.fail("which nixos-generate-config")
machine.fail("which nixos-enter") machine.fail("which nixos-enter")
machine.fail("which nixos-version") machine.fail("which nixos-version")
machine.fail("which nixos-build-vms") machine.fail("which nixos-build-vms")
with subtest("perl should not be included"): with subtest("perl should not be included"):
machine.fail("which perl") machine.fail("which perl")
''; '';
} }
)

View File

@ -3,209 +3,207 @@
# 2. sending a private message to the admin user through the API # 2. sending a private message to the admin user through the API
# 3. replying to that message via email. # 3. replying to that message via email.
import ./make-test-python.nix ( {
{ pkgs,
pkgs, lib,
lib, package ? pkgs.discourse,
package ? pkgs.discourse, ...
... }:
}: let
let certs = import ./common/acme/server/snakeoil-certs.nix;
certs = import ./common/acme/server/snakeoil-certs.nix; clientDomain = "client.fake.domain";
clientDomain = "client.fake.domain"; discourseDomain = certs.domain;
discourseDomain = certs.domain; adminPassword = "eYAX85qmMJ5GZIHLaXGDAoszD7HSZp5d";
adminPassword = "eYAX85qmMJ5GZIHLaXGDAoszD7HSZp5d"; secretKeyBase = "381f4ac6d8f5e49d804dae72aa9c046431d2f34c656a705c41cd52fed9b4f6f76f51549f0b55db3b8b0dded7a00d6a381ebe9a4367d2d44f5e743af6628b4d42";
secretKeyBase = "381f4ac6d8f5e49d804dae72aa9c046431d2f34c656a705c41cd52fed9b4f6f76f51549f0b55db3b8b0dded7a00d6a381ebe9a4367d2d44f5e743af6628b4d42"; admin = {
admin = { email = "alice@${clientDomain}";
email = "alice@${clientDomain}"; username = "alice";
username = "alice"; fullName = "Alice Admin";
fullName = "Alice Admin"; passwordFile = "${pkgs.writeText "admin-pass" adminPassword}";
passwordFile = "${pkgs.writeText "admin-pass" adminPassword}"; };
}; in
in {
{ name = "discourse";
name = "discourse"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ talyz ];
maintainers = [ talyz ]; };
};
nodes.discourse = nodes.discourse =
{ nodes, ... }: { nodes, ... }:
{ {
virtualisation.memorySize = 2048; virtualisation.memorySize = 2048;
virtualisation.cores = 4; virtualisation.cores = 4;
virtualisation.useNixStoreImage = true; virtualisation.useNixStoreImage = true;
virtualisation.writableStore = false; virtualisation.writableStore = false;
imports = [ common/user-account.nix ]; imports = [ common/user-account.nix ];
security.pki.certificateFiles = [ security.pki.certificateFiles = [
certs.ca.cert certs.ca.cert
]; ];
networking.extraHosts = '' networking.extraHosts = ''
127.0.0.1 ${discourseDomain} 127.0.0.1 ${discourseDomain}
${nodes.client.networking.primaryIPAddress} ${clientDomain} ${nodes.client.networking.primaryIPAddress} ${clientDomain}
'';
services.postfix = {
enableSubmission = true;
enableSubmissions = true;
submissionsOptions = {
smtpd_sasl_auth_enable = "yes";
smtpd_client_restrictions = "permit";
};
};
environment.systemPackages = [ pkgs.jq ];
services.postgresql.package = pkgs.postgresql_15;
services.discourse = {
enable = true;
inherit admin package;
hostname = discourseDomain;
sslCertificate = "${certs.${discourseDomain}.cert}";
sslCertificateKey = "${certs.${discourseDomain}.key}";
secretKeyBaseFile = "${pkgs.writeText "secret-key-base" secretKeyBase}";
enableACME = false;
mail.outgoing.serverAddress = clientDomain;
mail.incoming.enable = true;
siteSettings = {
posting = {
min_post_length = 5;
min_first_post_length = 5;
min_personal_message_post_length = 5;
};
};
unicornTimeout = 900;
};
networking.firewall.allowedTCPPorts = [
25
465
];
};
nodes.client =
{ nodes, ... }:
{
imports = [ common/user-account.nix ];
security.pki.certificateFiles = [
certs.ca.cert
];
networking.extraHosts = ''
127.0.0.1 ${clientDomain}
${nodes.discourse.networking.primaryIPAddress} ${discourseDomain}
'';
services.dovecot2 = {
enable = true;
protocols = [ "imap" ];
};
services.postfix = {
enable = true;
origin = clientDomain;
relayDomains = [ clientDomain ];
config = {
compatibility_level = "2";
smtpd_banner = "ESMTP server";
myhostname = clientDomain;
mydestination = clientDomain;
};
};
environment.systemPackages =
let
replyToEmail = pkgs.writeScriptBin "reply-to-email" ''
#!${pkgs.python3.interpreter}
import imaplib
import smtplib
import ssl
import email.header
from email import message_from_bytes
from email.message import EmailMessage
with imaplib.IMAP4('localhost') as imap:
imap.login('alice', 'foobar')
imap.select()
status, data = imap.search(None, 'ALL')
assert status == 'OK'
nums = data[0].split()
assert len(nums) == 1
status, msg_data = imap.fetch(nums[0], '(RFC822)')
assert status == 'OK'
msg = email.message_from_bytes(msg_data[0][1])
subject = str(email.header.make_header(email.header.decode_header(msg['Subject'])))
reply_to = email.header.decode_header(msg['Reply-To'])[0][0]
message_id = email.header.decode_header(msg['Message-ID'])[0][0]
date = email.header.decode_header(msg['Date'])[0][0]
ctx = ssl.create_default_context()
with smtplib.SMTP_SSL(host='${discourseDomain}', context=ctx) as smtp:
reply = EmailMessage()
reply['Subject'] = 'Re: ' + subject
reply['To'] = reply_to
reply['From'] = 'alice@${clientDomain}'
reply['In-Reply-To'] = message_id
reply['References'] = message_id
reply['Date'] = date
reply.set_content("Test reply.")
smtp.send_message(reply)
smtp.quit()
'';
in
[ replyToEmail ];
networking.firewall.allowedTCPPorts = [ 25 ];
};
testScript =
{ nodes }:
let
request = builtins.toJSON {
title = "Private message";
raw = "This is a test message.";
target_recipients = admin.username;
archetype = "private_message";
};
in
''
discourse.start()
client.start()
discourse.wait_for_unit("discourse.service")
discourse.wait_for_file("/run/discourse/sockets/unicorn.sock")
discourse.wait_until_succeeds("curl -sS -f https://${discourseDomain}")
discourse.succeed(
"curl -sS -f https://${discourseDomain}/session/csrf -c cookie -b cookie -H 'Accept: application/json' | jq -r '\"X-CSRF-Token: \" + .csrf' > csrf_token",
"curl -sS -f https://${discourseDomain}/session -c cookie -b cookie -H @csrf_token -H 'Accept: application/json' -d 'login=${nodes.discourse.services.discourse.admin.username}' -d \"password=${adminPassword}\" | jq -e '.user.username == \"${nodes.discourse.services.discourse.admin.username}\"'",
"curl -sS -f https://${discourseDomain}/login -v -H 'Accept: application/json' -c cookie -b cookie 2>&1 | grep ${nodes.discourse.services.discourse.admin.username}",
)
client.wait_for_unit("postfix.service")
client.wait_for_unit("dovecot2.service")
discourse.succeed(
"sudo -u discourse discourse-rake api_key:create_master[master] >api_key",
'curl -sS -f https://${discourseDomain}/posts -X POST -H "Content-Type: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" -d \'${request}\' ',
)
client.wait_until_succeeds("reply-to-email")
discourse.wait_until_succeeds(
'curl -sS -f https://${discourseDomain}/topics/private-messages/system -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .topic_list.topics[0].id != null then .topic_list.topics[0].id else null end\' >topic_id'
)
discourse.succeed(
'curl -sS -f https://${discourseDomain}/t/$(<topic_id) -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .post_stream.posts[1].cooked == "<p>Test reply.</p>" then true else null end\' '
)
''; '';
}
) services.postfix = {
enableSubmission = true;
enableSubmissions = true;
submissionsOptions = {
smtpd_sasl_auth_enable = "yes";
smtpd_client_restrictions = "permit";
};
};
environment.systemPackages = [ pkgs.jq ];
services.postgresql.package = pkgs.postgresql_15;
services.discourse = {
enable = true;
inherit admin package;
hostname = discourseDomain;
sslCertificate = "${certs.${discourseDomain}.cert}";
sslCertificateKey = "${certs.${discourseDomain}.key}";
secretKeyBaseFile = "${pkgs.writeText "secret-key-base" secretKeyBase}";
enableACME = false;
mail.outgoing.serverAddress = clientDomain;
mail.incoming.enable = true;
siteSettings = {
posting = {
min_post_length = 5;
min_first_post_length = 5;
min_personal_message_post_length = 5;
};
};
unicornTimeout = 900;
};
networking.firewall.allowedTCPPorts = [
25
465
];
};
nodes.client =
{ nodes, ... }:
{
imports = [ common/user-account.nix ];
security.pki.certificateFiles = [
certs.ca.cert
];
networking.extraHosts = ''
127.0.0.1 ${clientDomain}
${nodes.discourse.networking.primaryIPAddress} ${discourseDomain}
'';
services.dovecot2 = {
enable = true;
protocols = [ "imap" ];
};
services.postfix = {
enable = true;
origin = clientDomain;
relayDomains = [ clientDomain ];
config = {
compatibility_level = "2";
smtpd_banner = "ESMTP server";
myhostname = clientDomain;
mydestination = clientDomain;
};
};
environment.systemPackages =
let
replyToEmail = pkgs.writeScriptBin "reply-to-email" ''
#!${pkgs.python3.interpreter}
import imaplib
import smtplib
import ssl
import email.header
from email import message_from_bytes
from email.message import EmailMessage
with imaplib.IMAP4('localhost') as imap:
imap.login('alice', 'foobar')
imap.select()
status, data = imap.search(None, 'ALL')
assert status == 'OK'
nums = data[0].split()
assert len(nums) == 1
status, msg_data = imap.fetch(nums[0], '(RFC822)')
assert status == 'OK'
msg = email.message_from_bytes(msg_data[0][1])
subject = str(email.header.make_header(email.header.decode_header(msg['Subject'])))
reply_to = email.header.decode_header(msg['Reply-To'])[0][0]
message_id = email.header.decode_header(msg['Message-ID'])[0][0]
date = email.header.decode_header(msg['Date'])[0][0]
ctx = ssl.create_default_context()
with smtplib.SMTP_SSL(host='${discourseDomain}', context=ctx) as smtp:
reply = EmailMessage()
reply['Subject'] = 'Re: ' + subject
reply['To'] = reply_to
reply['From'] = 'alice@${clientDomain}'
reply['In-Reply-To'] = message_id
reply['References'] = message_id
reply['Date'] = date
reply.set_content("Test reply.")
smtp.send_message(reply)
smtp.quit()
'';
in
[ replyToEmail ];
networking.firewall.allowedTCPPorts = [ 25 ];
};
testScript =
{ nodes }:
let
request = builtins.toJSON {
title = "Private message";
raw = "This is a test message.";
target_recipients = admin.username;
archetype = "private_message";
};
in
''
discourse.start()
client.start()
discourse.wait_for_unit("discourse.service")
discourse.wait_for_file("/run/discourse/sockets/unicorn.sock")
discourse.wait_until_succeeds("curl -sS -f https://${discourseDomain}")
discourse.succeed(
"curl -sS -f https://${discourseDomain}/session/csrf -c cookie -b cookie -H 'Accept: application/json' | jq -r '\"X-CSRF-Token: \" + .csrf' > csrf_token",
"curl -sS -f https://${discourseDomain}/session -c cookie -b cookie -H @csrf_token -H 'Accept: application/json' -d 'login=${nodes.discourse.services.discourse.admin.username}' -d \"password=${adminPassword}\" | jq -e '.user.username == \"${nodes.discourse.services.discourse.admin.username}\"'",
"curl -sS -f https://${discourseDomain}/login -v -H 'Accept: application/json' -c cookie -b cookie 2>&1 | grep ${nodes.discourse.services.discourse.admin.username}",
)
client.wait_for_unit("postfix.service")
client.wait_for_unit("dovecot2.service")
discourse.succeed(
"sudo -u discourse discourse-rake api_key:create_master[master] >api_key",
'curl -sS -f https://${discourseDomain}/posts -X POST -H "Content-Type: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" -d \'${request}\' ',
)
client.wait_until_succeeds("reply-to-email")
discourse.wait_until_succeeds(
'curl -sS -f https://${discourseDomain}/topics/private-messages/system -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .topic_list.topics[0].id != null then .topic_list.topics[0].id else null end\' >topic_id'
)
discourse.succeed(
'curl -sS -f https://${discourseDomain}/t/$(<topic_id) -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .post_stream.posts[1].cooked == "<p>Test reply.</p>" then true else null end\' '
)
'';
}

View File

@ -1,67 +1,65 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "documize";
name = "documize"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ ];
maintainers = [ ]; };
};
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
environment.systemPackages = [ pkgs.jq ]; environment.systemPackages = [ pkgs.jq ];
services.documize = { services.documize = {
enable = true; enable = true;
port = 3000; port = 3000;
dbtype = "postgresql"; dbtype = "postgresql";
db = "host=localhost port=5432 sslmode=disable user=documize password=documize dbname=documize"; db = "host=localhost port=5432 sslmode=disable user=documize password=documize dbname=documize";
};
systemd.services.documize-server = {
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
};
services.postgresql = {
enable = true;
initialScript = pkgs.writeText "psql-init" ''
CREATE ROLE documize WITH LOGIN PASSWORD 'documize';
CREATE DATABASE documize WITH OWNER documize;
'';
};
}; };
testScript = '' systemd.services.documize-server = {
start_all() after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
};
machine.wait_for_unit("documize-server.service") services.postgresql = {
machine.wait_for_open_port(3000) enable = true;
initialScript = pkgs.writeText "psql-init" ''
CREATE ROLE documize WITH LOGIN PASSWORD 'documize';
CREATE DATABASE documize WITH OWNER documize;
'';
};
};
dbhash = machine.succeed( testScript = ''
"curl -f localhost:3000 | grep 'property=\"dbhash' | grep -Po 'content=\"\\K[^\"]*'" start_all()
)
dbhash = dbhash.strip() machine.wait_for_unit("documize-server.service")
machine.wait_for_open_port(3000)
machine.succeed( dbhash = machine.succeed(
( "curl -f localhost:3000 | grep 'property=\"dbhash' | grep -Po 'content=\"\\K[^\"]*'"
"curl -X POST" )
" --data 'dbname=documize'"
" --data 'dbhash={}'"
" --data 'title=NixOS'"
" --data 'message=Docs'"
" --data 'firstname=Bob'"
" --data 'lastname=Foobar'"
" --data 'email=bob.foobar@nixos.org'"
" --data 'password=verysafe'"
" -f localhost:3000/api/setup"
).format(dbhash)
)
machine.succeed( dbhash = dbhash.strip()
'test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"'
) machine.succeed(
''; (
} "curl -X POST"
) " --data 'dbname=documize'"
" --data 'dbhash={}'"
" --data 'title=NixOS'"
" --data 'message=Docs'"
" --data 'firstname=Bob'"
" --data 'lastname=Foobar'"
" --data 'email=bob.foobar@nixos.org'"
" --data 'password=verysafe'"
" -f localhost:3000/api/setup"
).format(dbhash)
)
machine.succeed(
'test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"'
)
'';
}

View File

@ -1,48 +1,46 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: {
{ name = "doh-proxy-rust";
name = "doh-proxy-rust"; meta.maintainers = with lib.maintainers; [ stephank ];
meta.maintainers = with lib.maintainers; [ stephank ];
nodes = { nodes = {
machine = machine =
{ pkgs, lib, ... }: { pkgs, lib, ... }:
{ {
services.bind = { services.bind = {
enable = true; enable = true;
extraOptions = "empty-zones-enable no;"; extraOptions = "empty-zones-enable no;";
zones = lib.singleton { zones = lib.singleton {
name = "."; name = ".";
master = true; master = true;
file = pkgs.writeText "root.zone" '' file = pkgs.writeText "root.zone" ''
$TTL 3600 $TTL 3600
. IN SOA ns.example.org. admin.example.org. ( 1 3h 1h 1w 1d ) . IN SOA ns.example.org. admin.example.org. ( 1 3h 1h 1w 1d )
. IN NS ns.example.org. . IN NS ns.example.org.
ns.example.org. IN A 192.168.0.1 ns.example.org. IN A 192.168.0.1
''; '';
};
};
services.doh-proxy-rust = {
enable = true;
flags = [
"--server-address=127.0.0.1:53"
];
}; };
}; };
}; services.doh-proxy-rust = {
enable = true;
flags = [
"--server-address=127.0.0.1:53"
];
};
};
};
testScript = testScript =
{ nodes, ... }: { nodes, ... }:
'' ''
url = "http://localhost:3000/dns-query" url = "http://localhost:3000/dns-query"
query = "AAABAAABAAAAAAAAAm5zB2V4YW1wbGUDb3JnAAABAAE=" # IN A ns.example.org. query = "AAABAAABAAAAAAAAAm5zB2V4YW1wbGUDb3JnAAABAAE=" # IN A ns.example.org.
bin_ip = r"$'\xC0\xA8\x00\x01'" # 192.168.0.1, as shell binary string bin_ip = r"$'\xC0\xA8\x00\x01'" # 192.168.0.1, as shell binary string
machine.wait_for_unit("bind.service") machine.wait_for_unit("bind.service")
machine.wait_for_unit("doh-proxy-rust.service") machine.wait_for_unit("doh-proxy-rust.service")
machine.wait_for_open_port(53) machine.wait_for_open_port(53)
machine.wait_for_open_port(3000) machine.wait_for_open_port(3000)
machine.succeed(f"curl --fail -H 'Accept: application/dns-message' '{url}?dns={query}' | grep -F {bin_ip}") machine.succeed(f"curl --fail -H 'Accept: application/dns-message' '{url}?dns={query}' | grep -F {bin_ip}")
''; '';
} }
)

View File

@ -1,33 +1,31 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "domination";
name = "domination"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ fgaz ];
maintainers = [ fgaz ]; };
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
./common/x11.nix
];
services.xserver.enable = true;
environment.systemPackages = [ pkgs.domination ];
}; };
nodes.machine = enableOCR = true;
{ config, pkgs, ... }:
{
imports = [
./common/x11.nix
];
services.xserver.enable = true; testScript = ''
environment.systemPackages = [ pkgs.domination ]; machine.wait_for_x()
}; # Add a dummy sound card, or an error reporting popup will appear,
# covering the main window and preventing OCR
enableOCR = true; machine.execute("modprobe snd-dummy")
machine.execute("domination >&2 &")
testScript = '' machine.wait_for_window("Menu")
machine.wait_for_x() machine.wait_for_text(r"(New Game|Start Server|Load Game|Help Manual|Join Game|About|Play Online)")
# Add a dummy sound card, or an error reporting popup will appear, machine.screenshot("screen")
# covering the main window and preventing OCR '';
machine.execute("modprobe snd-dummy") }
machine.execute("domination >&2 &")
machine.wait_for_window("Menu")
machine.wait_for_text(r"(New Game|Start Server|Load Game|Help Manual|Join Game|About|Play Online)")
machine.screenshot("screen")
'';
}
)

View File

@ -1,24 +1,22 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: {
{ name = "drbd-driver";
name = "drbd-driver"; meta.maintainers = with pkgs.lib.maintainers; [ birkb ];
meta.maintainers = with pkgs.lib.maintainers; [ birkb ];
nodes = { nodes = {
machine = machine =
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
boot = { boot = {
kernelModules = [ "drbd" ]; kernelModules = [ "drbd" ];
extraModulePackages = with config.boot.kernelPackages; [ drbd ]; extraModulePackages = with config.boot.kernelPackages; [ drbd ];
kernelPackages = pkgs.linuxPackages; kernelPackages = pkgs.linuxPackages;
};
}; };
}; };
};
testScript = '' testScript = ''
machine.start(); machine.start();
machine.succeed("modinfo drbd | grep --extended-regexp '^version:\s+${pkgs.linuxPackages.drbd.version}$'") machine.succeed("modinfo drbd | grep --extended-regexp '^version:\s+${pkgs.linuxPackages.drbd.version}$'")
''; '';
} }
)

View File

@ -1,93 +1,91 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let drbdPort = 7789;
drbdPort = 7789;
drbdConfig = drbdConfig =
{ nodes, ... }: { nodes, ... }:
{ {
virtualisation.emptyDiskImages = [ 1 ]; virtualisation.emptyDiskImages = [ 1 ];
networking.firewall.allowedTCPPorts = [ drbdPort ]; networking.firewall.allowedTCPPorts = [ drbdPort ];
services.drbd = { services.drbd = {
enable = true; enable = true;
config = '' config = ''
global { global {
usage-count yes; usage-count yes;
}
common {
net {
protocol C;
ping-int 1;
}
}
resource r0 {
volume 0 {
device /dev/drbd0;
disk /dev/vdb;
meta-disk internal;
} }
common { on drbd1 {
net { address ${nodes.drbd1.networking.primaryIPAddress}:${toString drbdPort};
protocol C;
ping-int 1;
}
} }
resource r0 { on drbd2 {
volume 0 { address ${nodes.drbd2.networking.primaryIPAddress}:${toString drbdPort};
device /dev/drbd0;
disk /dev/vdb;
meta-disk internal;
}
on drbd1 {
address ${nodes.drbd1.networking.primaryIPAddress}:${toString drbdPort};
}
on drbd2 {
address ${nodes.drbd2.networking.primaryIPAddress}:${toString drbdPort};
}
} }
''; }
}; '';
}; };
in
{
name = "drbd";
meta = with pkgs.lib.maintainers; {
maintainers = [
ryantm
astro
birkb
];
}; };
in
{
name = "drbd";
meta = with pkgs.lib.maintainers; {
maintainers = [
ryantm
astro
birkb
];
};
nodes.drbd1 = drbdConfig; nodes.drbd1 = drbdConfig;
nodes.drbd2 = drbdConfig; nodes.drbd2 = drbdConfig;
testScript = testScript =
{ nodes }: { nodes }:
'' ''
drbd1.start() drbd1.start()
drbd2.start() drbd2.start()
drbd1.wait_for_unit("network.target") drbd1.wait_for_unit("network.target")
drbd2.wait_for_unit("network.target") drbd2.wait_for_unit("network.target")
drbd1.succeed( drbd1.succeed(
"drbdadm create-md r0", "drbdadm create-md r0",
"drbdadm up r0", "drbdadm up r0",
"drbdadm primary r0 --force", "drbdadm primary r0 --force",
) )
drbd2.succeed("drbdadm create-md r0", "drbdadm up r0") drbd2.succeed("drbdadm create-md r0", "drbdadm up r0")
drbd1.succeed( drbd1.succeed(
"mkfs.ext4 /dev/drbd0", "mkfs.ext4 /dev/drbd0",
"mkdir -p /mnt/drbd", "mkdir -p /mnt/drbd",
"mount /dev/drbd0 /mnt/drbd", "mount /dev/drbd0 /mnt/drbd",
"touch /mnt/drbd/hello", "touch /mnt/drbd/hello",
"umount /mnt/drbd", "umount /mnt/drbd",
"drbdadm secondary r0", "drbdadm secondary r0",
) )
drbd1.sleep(1) drbd1.sleep(1)
drbd2.succeed( drbd2.succeed(
"drbdadm primary r0", "drbdadm primary r0",
"mkdir -p /mnt/drbd", "mkdir -p /mnt/drbd",
"mount /dev/drbd0 /mnt/drbd", "mount /dev/drbd0 /mnt/drbd",
"ls /mnt/drbd/hello", "ls /mnt/drbd/hello",
) )
''; '';
} }
)

View File

@ -3,75 +3,73 @@
# client on the inside network, a server on the outside network, and a # client on the inside network, a server on the outside network, and a
# router connected to both that performs Network Address Translation # router connected to both that performs Network Address Translation
# for the client. # for the client.
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: let
let routerBase = lib.mkMerge [
routerBase = lib.mkMerge [ {
{ virtualisation.vlans = [
virtualisation.vlans = [ 2
2 1
1 ];
]; networking.nftables.enable = true;
networking.nftables.enable = true; networking.nat.internalIPs = [ "192.168.1.0/24" ];
networking.nat.internalIPs = [ "192.168.1.0/24" ]; networking.nat.externalInterface = "eth1";
networking.nat.externalInterface = "eth1"; }
} ];
]; in
in {
{ name = "dublin-traceroute";
name = "dublin-traceroute"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ baloo ];
maintainers = [ baloo ]; };
nodes.client =
{ nodes, ... }:
{
imports = [ ./common/user-account.nix ];
virtualisation.vlans = [ 1 ];
networking.defaultGateway =
(builtins.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address;
networking.nftables.enable = true;
programs.dublin-traceroute.enable = true;
}; };
nodes.client = nodes.router =
{ nodes, ... }: { ... }:
{ {
imports = [ ./common/user-account.nix ]; virtualisation.vlans = [
virtualisation.vlans = [ 1 ]; 2
1
];
networking.nftables.enable = true;
networking.nat.internalIPs = [ "192.168.1.0/24" ];
networking.nat.externalInterface = "eth1";
networking.nat.enable = true;
};
networking.defaultGateway = nodes.server =
(builtins.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address; { ... }:
networking.nftables.enable = true; {
virtualisation.vlans = [ 2 ];
networking.firewall.enable = false;
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
services.vsftpd.enable = true;
services.vsftpd.anonymousUser = true;
};
programs.dublin-traceroute.enable = true; testScript = ''
}; client.start()
router.start()
server.start()
nodes.router = server.wait_for_unit("network.target")
{ ... }: router.wait_for_unit("network.target")
{ client.wait_for_unit("network.target")
virtualisation.vlans = [
2
1
];
networking.nftables.enable = true;
networking.nat.internalIPs = [ "192.168.1.0/24" ];
networking.nat.externalInterface = "eth1";
networking.nat.enable = true;
};
nodes.server = # Make sure we can trace from an unprivileged user
{ ... }: client.succeed("sudo -u alice dublin-traceroute server")
{ '';
virtualisation.vlans = [ 2 ]; }
networking.firewall.enable = false;
services.httpd.enable = true;
services.httpd.adminAddr = "foo@example.org";
services.vsftpd.enable = true;
services.vsftpd.anonymousUser = true;
};
testScript = ''
client.start()
router.start()
server.start()
server.wait_for_unit("network.target")
router.wait_for_unit("network.target")
client.wait_for_unit("network.target")
# Make sure we can trace from an unprivileged user
client.succeed("sudo -u alice dublin-traceroute server")
'';
}
)

View File

@ -1,89 +1,87 @@
import ./make-test-python.nix ( { ... }:
{ ... }: {
{ name = "ecryptfs";
name = "ecryptfs";
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
imports = [ ./common/user-account.nix ]; imports = [ ./common/user-account.nix ];
boot.kernelModules = [ "ecryptfs" ]; boot.kernelModules = [ "ecryptfs" ];
security.pam.enableEcryptfs = true; security.pam.enableEcryptfs = true;
environment.systemPackages = with pkgs; [ keyutils ]; environment.systemPackages = with pkgs; [ keyutils ];
}; };
testScript = '' testScript = ''
def login_as_alice(): def login_as_alice():
machine.wait_until_tty_matches("1", "login: ") machine.wait_until_tty_matches("1", "login: ")
machine.send_chars("alice\n") machine.send_chars("alice\n")
machine.wait_until_tty_matches("1", "Password: ") machine.wait_until_tty_matches("1", "Password: ")
machine.send_chars("foobar\n") machine.send_chars("foobar\n")
machine.wait_until_tty_matches("1", "alice\@machine") machine.wait_until_tty_matches("1", "alice\@machine")
def logout(): def logout():
machine.send_chars("logout\n") machine.send_chars("logout\n")
machine.wait_until_tty_matches("1", "login: ") machine.wait_until_tty_matches("1", "login: ")
machine.wait_for_unit("default.target") machine.wait_for_unit("default.target")
with subtest("Set alice up with a password and a home"): with subtest("Set alice up with a password and a home"):
machine.succeed("(echo foobar; echo foobar) | passwd alice") machine.succeed("(echo foobar; echo foobar) | passwd alice")
machine.succeed("chown -R alice.users ~alice") machine.succeed("chown -R alice.users ~alice")
with subtest("Migrate alice's home"): with subtest("Migrate alice's home"):
out = machine.succeed("echo foobar | ecryptfs-migrate-home -u alice") out = machine.succeed("echo foobar | ecryptfs-migrate-home -u alice")
machine.log(f"ecryptfs-migrate-home said: {out}") machine.log(f"ecryptfs-migrate-home said: {out}")
with subtest("Log alice in (ecryptfs passwhrase is wrapped during first login)"): with subtest("Log alice in (ecryptfs passwhrase is wrapped during first login)"):
login_as_alice() login_as_alice()
machine.send_chars("logout\n") machine.send_chars("logout\n")
machine.wait_until_tty_matches("1", "login: ") machine.wait_until_tty_matches("1", "login: ")
# Why do I need to do this?? # Why do I need to do this??
machine.succeed("su alice -c ecryptfs-umount-private || true") machine.succeed("su alice -c ecryptfs-umount-private || true")
machine.sleep(1) machine.sleep(1)
with subtest("check that encrypted home is not mounted"): with subtest("check that encrypted home is not mounted"):
machine.fail("mount | grep ecryptfs") machine.fail("mount | grep ecryptfs")
with subtest("Show contents of the user keyring"): with subtest("Show contents of the user keyring"):
out = machine.succeed("su - alice -c 'keyctl list \@u'") out = machine.succeed("su - alice -c 'keyctl list \@u'")
machine.log(f"keyctl unlink said: {out}") machine.log(f"keyctl unlink said: {out}")
with subtest("Log alice again"): with subtest("Log alice again"):
login_as_alice() login_as_alice()
with subtest("Create some files in encrypted home"): with subtest("Create some files in encrypted home"):
machine.succeed("su alice -c 'touch ~alice/a'") machine.succeed("su alice -c 'touch ~alice/a'")
machine.succeed("su alice -c 'echo c > ~alice/b'") machine.succeed("su alice -c 'echo c > ~alice/b'")
with subtest("Logout"): with subtest("Logout"):
logout() logout()
# Why do I need to do this?? # Why do I need to do this??
machine.succeed("su alice -c ecryptfs-umount-private || true") machine.succeed("su alice -c ecryptfs-umount-private || true")
machine.sleep(1) machine.sleep(1)
with subtest("Check that the filesystem is not accessible"): with subtest("Check that the filesystem is not accessible"):
machine.fail("mount | grep ecryptfs") machine.fail("mount | grep ecryptfs")
machine.succeed("su alice -c 'test \! -f ~alice/a'") machine.succeed("su alice -c 'test \! -f ~alice/a'")
machine.succeed("su alice -c 'test \! -f ~alice/b'") machine.succeed("su alice -c 'test \! -f ~alice/b'")
with subtest("Log alice once more"): with subtest("Log alice once more"):
login_as_alice() login_as_alice()
with subtest("Check that the files are there"): with subtest("Check that the files are there"):
machine.sleep(1) machine.sleep(1)
machine.succeed("su alice -c 'test -f ~alice/a'") machine.succeed("su alice -c 'test -f ~alice/a'")
machine.succeed("su alice -c 'test -f ~alice/b'") machine.succeed("su alice -c 'test -f ~alice/b'")
machine.succeed('test "$(cat ~alice/b)" = "c"') machine.succeed('test "$(cat ~alice/b)" = "c"')
with subtest("Catch https://github.com/NixOS/nixpkgs/issues/16766"): with subtest("Catch https://github.com/NixOS/nixpkgs/issues/16766"):
machine.succeed("su alice -c 'ls -lh ~alice/'") machine.succeed("su alice -c 'ls -lh ~alice/'")
logout() logout()
''; '';
} }
)

View File

@ -1,71 +1,69 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: {
{ name = "endlessh-go";
name = "endlessh-go"; meta.maintainers = with lib.maintainers; [ azahi ];
meta.maintainers = with lib.maintainers; [ azahi ];
nodes = { nodes = {
server = server =
{ ... }: { ... }:
{ {
services.endlessh-go = { services.endlessh-go = {
enable = true; enable = true;
prometheus.enable = true; prometheus.enable = true;
openFirewall = true; openFirewall = true;
};
specialisation = {
unprivileged.configuration = {
services.endlessh-go = {
port = 2222;
prometheus.port = 9229;
};
}; };
specialisation = { privileged.configuration = {
unprivileged.configuration = { services.endlessh-go = {
services.endlessh-go = { port = 22;
port = 2222; prometheus.port = 92;
prometheus.port = 9229;
};
};
privileged.configuration = {
services.endlessh-go = {
port = 22;
prometheus.port = 92;
};
}; };
}; };
}; };
};
client = client =
{ pkgs, ... }: { pkgs, ... }:
{ {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
curl curl
netcat netcat
]; ];
}; };
}; };
testScript = '' testScript = ''
def activate_specialisation(name: str): def activate_specialisation(name: str):
server.succeed(f"/run/booted-system/specialisation/{name}/bin/switch-to-configuration test >&2") server.succeed(f"/run/booted-system/specialisation/{name}/bin/switch-to-configuration test >&2")
start_all() start_all()
with subtest("Unprivileged"): with subtest("Unprivileged"):
activate_specialisation("unprivileged") activate_specialisation("unprivileged")
server.wait_for_unit("endlessh-go.service") server.wait_for_unit("endlessh-go.service")
server.wait_for_open_port(2222) server.wait_for_open_port(2222)
server.wait_for_open_port(9229) server.wait_for_open_port(9229)
server.fail("curl -sSf server:9229/metrics | grep -q endlessh_client_closed_count_total") server.fail("curl -sSf server:9229/metrics | grep -q endlessh_client_closed_count_total")
client.succeed("nc -dvW5 server 2222") client.succeed("nc -dvW5 server 2222")
server.succeed("curl -sSf server:9229/metrics | grep -q endlessh_client_closed_count_total") server.succeed("curl -sSf server:9229/metrics | grep -q endlessh_client_closed_count_total")
client.fail("curl -sSfm 5 server:9229/metrics") client.fail("curl -sSfm 5 server:9229/metrics")
with subtest("Privileged"): with subtest("Privileged"):
activate_specialisation("privileged") activate_specialisation("privileged")
server.wait_for_unit("endlessh-go.service") server.wait_for_unit("endlessh-go.service")
server.wait_for_open_port(22) server.wait_for_open_port(22)
server.wait_for_open_port(92) server.wait_for_open_port(92)
server.fail("curl -sSf server:92/metrics | grep -q endlessh_client_closed_count_total") server.fail("curl -sSf server:92/metrics | grep -q endlessh_client_closed_count_total")
client.succeed("nc -dvW5 server 22") client.succeed("nc -dvW5 server 22")
server.succeed("curl -sSf server:92/metrics | grep -q endlessh_client_closed_count_total") server.succeed("curl -sSf server:92/metrics | grep -q endlessh_client_closed_count_total")
client.fail("curl -sSfm 5 server:92/metrics") client.fail("curl -sSfm 5 server:92/metrics")
''; '';
} }
)

View File

@ -1,52 +1,50 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: {
{ name = "endlessh";
name = "endlessh"; meta.maintainers = with lib.maintainers; [ azahi ];
meta.maintainers = with lib.maintainers; [ azahi ];
nodes = { nodes = {
server = server =
{ ... }: { ... }:
{ {
services.endlessh = { services.endlessh = {
enable = true; enable = true;
openFirewall = true; openFirewall = true;
};
specialisation = {
unprivileged.configuration.services.endlessh.port = 2222;
privileged.configuration.services.endlessh.port = 22;
};
}; };
client = specialisation = {
{ pkgs, ... }: unprivileged.configuration.services.endlessh.port = 2222;
{
environment.systemPackages = with pkgs; [ privileged.configuration.services.endlessh.port = 22;
curl
netcat
];
}; };
}; };
testScript = '' client =
def activate_specialisation(name: str): { pkgs, ... }:
server.succeed(f"/run/booted-system/specialisation/{name}/bin/switch-to-configuration test >&2") {
environment.systemPackages = with pkgs; [
curl
netcat
];
};
};
start_all() testScript = ''
def activate_specialisation(name: str):
server.succeed(f"/run/booted-system/specialisation/{name}/bin/switch-to-configuration test >&2")
with subtest("Unprivileged"): start_all()
activate_specialisation("unprivileged")
server.wait_for_unit("endlessh.service")
server.wait_for_open_port(2222)
client.succeed("nc -dvW5 server 2222")
with subtest("Privileged"): with subtest("Unprivileged"):
activate_specialisation("privileged") activate_specialisation("unprivileged")
server.wait_for_unit("endlessh.service") server.wait_for_unit("endlessh.service")
server.wait_for_open_port(22) server.wait_for_open_port(2222)
client.succeed("nc -dvW5 server 22") client.succeed("nc -dvW5 server 2222")
'';
} with subtest("Privileged"):
) activate_specialisation("privileged")
server.wait_for_unit("endlessh.service")
server.wait_for_open_port(22)
client.succeed("nc -dvW5 server 22")
'';
}

View File

@ -1,45 +1,43 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "engelsystem";
name = "engelsystem"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ talyz ];
maintainers = [ talyz ]; };
nodes.engelsystem =
{ ... }:
{
services.engelsystem = {
enable = true;
domain = "engelsystem";
createDatabase = true;
};
networking.firewall.allowedTCPPorts = [
80
443
];
environment.systemPackages = with pkgs; [
xmlstarlet
libxml2
];
}; };
nodes.engelsystem = testScript = ''
{ ... }: engelsystem.start()
{ engelsystem.wait_for_unit("phpfpm-engelsystem.service")
services.engelsystem = { engelsystem.wait_until_succeeds("curl engelsystem/login -sS -f")
enable = true; engelsystem.succeed(
domain = "engelsystem"; "curl engelsystem/login -sS -f -c cookie | xmllint -html -xmlout - >login"
createDatabase = true; )
}; engelsystem.succeed(
networking.firewall.allowedTCPPorts = [ "xml sel -T -t -m \"html/head/meta[@name='csrf-token']\" -v @content login >token"
80 )
443 engelsystem.succeed(
]; "curl engelsystem/login -sS -f -b cookie -F 'login=admin' -F 'password=asdfasdf' -F '_token=<token' -L | xmllint -html -xmlout - >news"
environment.systemPackages = with pkgs; [ )
xmlstarlet engelsystem.succeed(
libxml2 "test 'News - Engelsystem' = \"$(xml sel -T -t -c html/head/title news)\""
]; )
}; '';
}
testScript = ''
engelsystem.start()
engelsystem.wait_for_unit("phpfpm-engelsystem.service")
engelsystem.wait_until_succeeds("curl engelsystem/login -sS -f")
engelsystem.succeed(
"curl engelsystem/login -sS -f -c cookie | xmllint -html -xmlout - >login"
)
engelsystem.succeed(
"xml sel -T -t -m \"html/head/meta[@name='csrf-token']\" -v @content login >token"
)
engelsystem.succeed(
"curl engelsystem/login -sS -f -b cookie -F 'login=admin' -F 'password=asdfasdf' -F '_token=<token' -L | xmllint -html -xmlout - >news"
)
engelsystem.succeed(
"test 'News - Engelsystem' = \"$(xml sel -T -t -c html/head/title news)\""
)
'';
}
)

View File

@ -1,104 +1,102 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "enlightenment";
name = "enlightenment";
meta = with pkgs.lib.maintainers; { meta = with pkgs.lib.maintainers; {
maintainers = [ romildo ]; maintainers = [ romildo ];
timeout = 600; timeout = 600;
# OCR tests are flaky # OCR tests are flaky
broken = true; broken = true;
};
nodes.machine =
{ ... }:
{
imports = [ ./common/user-account.nix ];
services.xserver.enable = true;
services.xserver.desktopManager.enlightenment.enable = true;
services.xserver.displayManager = {
lightdm.enable = true;
autoLogin = {
enable = true;
user = "alice";
};
};
environment.systemPackages = [ pkgs.xdotool ];
services.acpid.enable = true;
services.connman.enable = true;
services.connman.package = pkgs.connmanMinimal;
}; };
nodes.machine = enableOCR = true;
{ ... }:
{
imports = [ ./common/user-account.nix ];
services.xserver.enable = true;
services.xserver.desktopManager.enlightenment.enable = true;
services.xserver.displayManager = {
lightdm.enable = true;
autoLogin = {
enable = true;
user = "alice";
};
};
environment.systemPackages = [ pkgs.xdotool ];
services.acpid.enable = true;
services.connman.enable = true;
services.connman.package = pkgs.connmanMinimal;
};
enableOCR = true; testScript =
{ nodes, ... }:
let
user = nodes.machine.config.users.users.alice;
in
''
with subtest("Ensure x starts"):
machine.wait_for_x()
machine.wait_for_file("${user.home}/.Xauthority")
machine.succeed("xauth merge ${user.home}/.Xauthority")
testScript = with subtest("Check that logging in has given the user ownership of devices"):
{ nodes, ... }: machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
let
user = nodes.machine.config.users.users.alice;
in
''
with subtest("Ensure x starts"):
machine.wait_for_x()
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"): with subtest("First time wizard"):
machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}") machine.wait_for_text("Default") # Language
machine.screenshot("wizard1")
machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.screenshot("wizard2")
with subtest("First time wizard"): machine.wait_for_text("English") # Keyboard (default)
machine.wait_for_text("Default") # Language machine.screenshot("wizard3")
machine.screenshot("wizard1") machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.screenshot("wizard2")
machine.wait_for_text("English") # Keyboard (default) machine.wait_for_text("Standard") # Profile (default)
machine.screenshot("wizard3") machine.screenshot("wizard4")
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("Standard") # Profile (default) machine.wait_for_text("Title") # Sizing (default)
machine.screenshot("wizard4") machine.screenshot("wizard5")
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("Title") # Sizing (default) machine.wait_for_text("clicked") # Windows Focus
machine.screenshot("wizard5") machine.succeed("xdotool mousemove 512 370 click 1") # Click
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.screenshot("wizard6")
machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("clicked") # Windows Focus machine.wait_for_text("Connman") # Network Management (default)
machine.succeed("xdotool mousemove 512 370 click 1") # Click machine.screenshot("wizard7")
machine.screenshot("wizard6") machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("Connman") # Network Management (default) machine.wait_for_text("BlusZ") # Bluetooth Management (default)
machine.screenshot("wizard7") machine.screenshot("wizard8")
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("BlusZ") # Bluetooth Management (default) machine.wait_for_text("OpenGL") # Compositing (default)
machine.screenshot("wizard8") machine.screenshot("wizard9")
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("OpenGL") # Compositing (default) machine.wait_for_text("update") # Updates
machine.screenshot("wizard9") machine.succeed("xdotool mousemove 512 495 click 1") # Disable
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.screenshot("wizard10")
machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("update") # Updates machine.wait_for_text("taskbar") # Taskbar
machine.succeed("xdotool mousemove 512 495 click 1") # Disable machine.succeed("xdotool mousemove 480 410 click 1") # Enable
machine.screenshot("wizard10") machine.screenshot("wizard11")
machine.succeed("xdotool mousemove 512 740 click 1") # Next machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("taskbar") # Taskbar machine.wait_for_text("Home") # The desktop
machine.succeed("xdotool mousemove 480 410 click 1") # Enable machine.screenshot("wizard12")
machine.screenshot("wizard11")
machine.succeed("xdotool mousemove 512 740 click 1") # Next
machine.wait_for_text("Home") # The desktop with subtest("Run Terminology"):
machine.screenshot("wizard12") machine.succeed("terminology >&2 &")
machine.sleep(5)
with subtest("Run Terminology"): machine.send_chars("ls --color -alF\n")
machine.succeed("terminology >&2 &") machine.sleep(2)
machine.sleep(5) machine.screenshot("terminology")
machine.send_chars("ls --color -alF\n") '';
machine.sleep(2) }
machine.screenshot("terminology")
'';
}
)

View File

@ -1,49 +1,47 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "environment";
name = "environment"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ];
maintainers = [ nequissimus ]; };
};
nodes.machine = nodes.machine =
{ pkgs, lib, ... }: { pkgs, lib, ... }:
lib.mkMerge [ lib.mkMerge [
{ {
boot.kernelPackages = pkgs.linuxPackages; boot.kernelPackages = pkgs.linuxPackages;
environment.etc.plainFile.text = '' environment.etc.plainFile.text = ''
Hello World Hello World
''; '';
environment.etc."folder/with/file".text = '' environment.etc."folder/with/file".text = ''
Foo Bar! Foo Bar!
''; '';
environment.sessionVariables = { environment.sessionVariables = {
TERMINFO_DIRS = "/run/current-system/sw/share/terminfo"; TERMINFO_DIRS = "/run/current-system/sw/share/terminfo";
NIXCON = "awesome"; NIXCON = "awesome";
SHOULD_NOT_BE_SET = "oops"; SHOULD_NOT_BE_SET = "oops";
}; };
} }
{ {
environment.sessionVariables = { environment.sessionVariables = {
SHOULD_NOT_BE_SET = lib.mkForce null; SHOULD_NOT_BE_SET = lib.mkForce null;
}; };
} }
]; ];
testScript = '' testScript = ''
machine.succeed('[ -L "/etc/plainFile" ]') machine.succeed('[ -L "/etc/plainFile" ]')
assert "Hello World" in machine.succeed('cat "/etc/plainFile"') assert "Hello World" in machine.succeed('cat "/etc/plainFile"')
machine.succeed('[ -d "/etc/folder" ]') machine.succeed('[ -d "/etc/folder" ]')
machine.succeed('[ -d "/etc/folder/with" ]') machine.succeed('[ -d "/etc/folder/with" ]')
machine.succeed('[ -L "/etc/folder/with/file" ]') machine.succeed('[ -L "/etc/folder/with/file" ]')
assert "Hello World" in machine.succeed('cat "/etc/plainFile"') assert "Hello World" in machine.succeed('cat "/etc/plainFile"')
assert "/run/current-system/sw/share/terminfo" in machine.succeed( assert "/run/current-system/sw/share/terminfo" in machine.succeed(
"echo ''${TERMINFO_DIRS}" "echo ''${TERMINFO_DIRS}"
) )
assert "awesome" in machine.succeed("echo ''${NIXCON}") assert "awesome" in machine.succeed("echo ''${NIXCON}")
machine.fail("printenv SHOULD_NOT_BE_SET") machine.fail("printenv SHOULD_NOT_BE_SET")
''; '';
} }
)

View File

@ -1,42 +1,40 @@
import ./make-test-python.nix ( { lib, pkgs, ... }:
{ lib, pkgs, ... }: let
let pythonShebang = pkgs.writeScript "python-shebang" ''
pythonShebang = pkgs.writeScript "python-shebang" '' #!/usr/bin/python
#!/usr/bin/python print("OK")
print("OK") '';
'';
bashShebang = pkgs.writeScript "bash-shebang" '' bashShebang = pkgs.writeScript "bash-shebang" ''
#!/usr/bin/bash #!/usr/bin/bash
echo "OK" echo "OK"
''; '';
in in
{ {
name = "envfs"; name = "envfs";
nodes.machine.services.envfs.enable = true; nodes.machine.services.envfs.enable = true;
testScript = '' testScript = ''
start_all() start_all()
machine.wait_until_succeeds("mountpoint -q /usr/bin/") machine.wait_until_succeeds("mountpoint -q /usr/bin/")
machine.succeed( machine.succeed(
"PATH=${pkgs.coreutils}/bin /usr/bin/cp --version", "PATH=${pkgs.coreutils}/bin /usr/bin/cp --version",
# check fallback paths # check fallback paths
"PATH= /usr/bin/sh --version", "PATH= /usr/bin/sh --version",
"PATH= /usr/bin/env --version", "PATH= /usr/bin/env --version",
"PATH= test -e /usr/bin/sh", "PATH= test -e /usr/bin/sh",
"PATH= test -e /usr/bin/env", "PATH= test -e /usr/bin/env",
# also picks up PATH that was set after execve # also picks up PATH that was set after execve
"! /usr/bin/hello", "! /usr/bin/hello",
"PATH=${pkgs.hello}/bin /usr/bin/hello", "PATH=${pkgs.hello}/bin /usr/bin/hello",
) )
out = machine.succeed("PATH=${pkgs.python3}/bin ${pythonShebang}") out = machine.succeed("PATH=${pkgs.python3}/bin ${pythonShebang}")
print(out) print(out)
assert out == "OK\n" assert out == "OK\n"
out = machine.succeed("PATH=${pkgs.bash}/bin ${bashShebang}") out = machine.succeed("PATH=${pkgs.bash}/bin ${bashShebang}")
print(out) print(out)
assert out == "OK\n" assert out == "OK\n"
''; '';
} }
)

View File

@ -1,23 +1,21 @@
import ./make-test-python.nix ( { pkgs, ... }:
{ pkgs, ... }: {
{ name = "ergo";
name = "ergo"; meta = with pkgs.lib.maintainers; {
meta = with pkgs.lib.maintainers; { maintainers = [ mmahut ];
maintainers = [ mmahut ]; };
};
nodes = { nodes = {
machine = machine =
{ ... }: { ... }:
{ {
services.ergo.enable = true; services.ergo.enable = true;
services.ergo.api.keyHash = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf"; services.ergo.api.keyHash = "324dcf027dd4a30a932c441f365a25e86b173defa4b8e58948253471b81b72cf";
}; };
}; };
testScript = '' testScript = ''
start_all() start_all()
machine.wait_for_unit("ergo.service") machine.wait_for_unit("ergo.service")
''; '';
} }
)

View File

@ -9,100 +9,98 @@ let
iiDir = "/tmp/irc"; iiDir = "/tmp/irc";
in in
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "ergochat";
name = "ergochat"; nodes =
nodes = {
{ "${server}" = {
"${server}" = { networking.firewall.allowedTCPPorts = [ ircPort ];
networking.firewall.allowedTCPPorts = [ ircPort ]; services.ergochat = {
services.ergochat = { enable = true;
enable = true; settings.server.motd = pkgs.writeText "ergo.motd" ''
settings.server.motd = pkgs.writeText "ergo.motd" '' The default MOTD doesn't contain the word "nixos" in it.
The default MOTD doesn't contain the word "nixos" in it. This one does.
This one does. '';
'';
};
}; };
} };
// lib.listToAttrs ( }
builtins.map ( // lib.listToAttrs (
client: builtins.map (
lib.nameValuePair client { client:
imports = [ lib.nameValuePair client {
./common/user-account.nix imports = [
]; ./common/user-account.nix
];
systemd.services.ii = { systemd.services.ii = {
requires = [ "network.target" ]; requires = [ "network.target" ];
wantedBy = [ "default.target" ]; wantedBy = [ "default.target" ];
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
ExecPreStartPre = "mkdir -p ${iiDir}"; ExecPreStartPre = "mkdir -p ${iiDir}";
ExecStart = '' ExecStart = ''
${lib.getBin pkgs.ii}/bin/ii -n ${client} -s ${server} -i ${iiDir} ${lib.getBin pkgs.ii}/bin/ii -n ${client} -s ${server} -i ${iiDir}
''; '';
User = "alice"; User = "alice";
};
}; };
} };
) clients }
); ) clients
);
testScript = testScript =
let let
msg = client: "Hello, my name is ${client}"; msg = client: "Hello, my name is ${client}";
clientScript = clientScript =
client: client:
[ [
'' ''
${client}.wait_for_unit("network.target") ${client}.wait_for_unit("network.target")
${client}.systemctl("start ii") ${client}.systemctl("start ii")
${client}.wait_for_unit("ii") ${client}.wait_for_unit("ii")
${client}.wait_for_file("${iiDir}/${server}/out") ${client}.wait_for_file("${iiDir}/${server}/out")
'' ''
# look for the custom text in the MOTD. # look for the custom text in the MOTD.
'' ''
${client}.wait_until_succeeds("grep 'nixos' ${iiDir}/${server}/out") ${client}.wait_until_succeeds("grep 'nixos' ${iiDir}/${server}/out")
'' ''
# wait until first PING from server arrives before joining, # wait until first PING from server arrives before joining,
# so we don't try it too early # so we don't try it too early
'' ''
${client}.wait_until_succeeds("grep 'PING' ${iiDir}/${server}/out") ${client}.wait_until_succeeds("grep 'PING' ${iiDir}/${server}/out")
'' ''
# join ${channel} # join ${channel}
'' ''
${client}.succeed("echo '/j #${channel}' > ${iiDir}/${server}/in") ${client}.succeed("echo '/j #${channel}' > ${iiDir}/${server}/in")
${client}.wait_for_file("${iiDir}/${server}/#${channel}/in") ${client}.wait_for_file("${iiDir}/${server}/#${channel}/in")
'' ''
# send a greeting # send a greeting
'' ''
${client}.succeed(
"echo '${msg client}' > ${iiDir}/${server}/#${channel}/in"
)
''
# check that all greetings arrived on all clients
]
++ builtins.map (other: ''
${client}.succeed( ${client}.succeed(
"grep '${msg other}$' ${iiDir}/${server}/#${channel}/out" "echo '${msg client}' > ${iiDir}/${server}/#${channel}/in"
) )
'') clients; ''
# check that all greetings arrived on all clients
]
++ builtins.map (other: ''
${client}.succeed(
"grep '${msg other}$' ${iiDir}/${server}/#${channel}/out"
)
'') clients;
# foldl', but requires a non-empty list instead of a start value # foldl', but requires a non-empty list instead of a start value
reduce = f: list: builtins.foldl' f (builtins.head list) (builtins.tail list); reduce = f: list: builtins.foldl' f (builtins.head list) (builtins.tail list);
in in
'' ''
start_all() start_all()
${server}.systemctl("status ergochat") ${server}.systemctl("status ergochat")
${server}.wait_for_open_port(${toString ircPort}) ${server}.wait_for_open_port(${toString ircPort})
# run clientScript for all clients so that every list # run clientScript for all clients so that every list
# entry is executed by every client before advancing # entry is executed by every client before advancing
# to the next one. # to the next one.
'' ''
+ lib.concatStrings (reduce (lib.zipListsWith (cs: c: cs + c)) (builtins.map clientScript clients)); + lib.concatStrings (reduce (lib.zipListsWith (cs: c: cs + c)) (builtins.map clientScript clients));
} }
)

View File

@ -1,29 +1,27 @@
import ./make-test-python.nix ( { pkgs, lib, ... }:
{ pkgs, lib, ... }: {
{ name = "eris-server";
name = "eris-server"; meta.maintainers = with lib.maintainers; [ ehmry ];
meta.maintainers = with lib.maintainers; [ ehmry ];
nodes.server = { nodes.server = {
environment.systemPackages = [ environment.systemPackages = [
pkgs.eris-go pkgs.eris-go
pkgs.eriscmd pkgs.eriscmd
]; ];
services.eris-server = { services.eris-server = {
enable = true; enable = true;
decode = true; decode = true;
listenHttp = "[::1]:80"; listenHttp = "[::1]:80";
backends = [ "badger+file:///var/cache/eris.badger?get&put" ]; backends = [ "badger+file:///var/cache/eris.badger?get&put" ];
mountpoint = "/eris"; mountpoint = "/eris";
};
}; };
};
testScript = '' testScript = ''
start_all() start_all()
server.wait_for_unit("eris-server.service") server.wait_for_unit("eris-server.service")
server.wait_for_open_port(5683) server.wait_for_open_port(5683)
server.wait_for_open_port(80) server.wait_for_open_port(80)
server.succeed("eriscmd get http://[::1] $(echo 'Hail ERIS!' | eriscmd put coap+tcp://[::1]:5683)") server.succeed("eriscmd get http://[::1] $(echo 'Hail ERIS!' | eriscmd put coap+tcp://[::1]:5683)")
''; '';
} }
)

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