nixos/tests: don't include switch-to-configuration in DUT by default (#340445)
This commit is contained in:
commit
a9c0a2e2a1
@ -3,7 +3,7 @@
|
||||
# even in `inheritParentConfig = false` specialisations.
|
||||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) mkForce;
|
||||
inherit (lib) mkDefault mkForce;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@ -22,6 +22,11 @@ in
|
||||
label = mkForce "test";
|
||||
};
|
||||
}
|
||||
|
||||
({ config, ... }: {
|
||||
# Don't pull in switch-to-configuration by default, except when specialisations are involved.
|
||||
# This is mostly a Hydra optimization, so we don't rebuild all the tests every time switch-to-configuration-ng changes.
|
||||
key = "no-switch-to-configuration";
|
||||
system.switch.enable = mkDefault (config.isSpecialisation || config.specialisation != {});
|
||||
})
|
||||
];
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ with lib;
|
||||
{
|
||||
boot.loader.grub.device = mkOverride 0 "nodev";
|
||||
specialisation = mkOverride 0 {};
|
||||
isSpecialisation = mkOverride 0 true;
|
||||
}
|
||||
|
@ -23,6 +23,12 @@ let
|
||||
in
|
||||
{
|
||||
options = {
|
||||
isSpecialisation = mkOption {
|
||||
type = lib.types.bool;
|
||||
internal = true;
|
||||
default = false;
|
||||
description = "Whether this system is a specialisation of another.";
|
||||
};
|
||||
|
||||
specialisation = mkOption {
|
||||
default = { };
|
||||
|
@ -7,25 +7,24 @@ import ./make-test-python.nix ({ lib, ... }:
|
||||
};
|
||||
|
||||
nodes = {
|
||||
default = {
|
||||
machine = {
|
||||
services.chrony.enable = true;
|
||||
};
|
||||
graphene-hardened = {
|
||||
services.chrony.enable = true;
|
||||
services.chrony.enableMemoryLocking = true;
|
||||
environment.memoryAllocator.provider = "graphene-hardened";
|
||||
# dhcpcd privsep is incompatible with graphene-hardened
|
||||
networking.useNetworkd = true;
|
||||
|
||||
specialisation.hardened.configuration = {
|
||||
services.chrony.enableMemoryLocking = true;
|
||||
environment.memoryAllocator.provider = "graphene-hardened";
|
||||
# dhcpcd privsep is incompatible with graphene-hardened
|
||||
networking.useNetworkd = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = {nodes, ...} : let
|
||||
graphene-hardened = nodes.graphene-hardened.system.build.toplevel;
|
||||
in ''
|
||||
default.start()
|
||||
default.wait_for_unit('multi-user.target')
|
||||
default.succeed('systemctl is-active chronyd.service')
|
||||
default.succeed('${graphene-hardened}/bin/switch-to-configuration test')
|
||||
default.succeed('systemctl is-active chronyd.service')
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit('multi-user.target')
|
||||
machine.succeed('systemctl is-active chronyd.service')
|
||||
machine.succeed('/run/booted-system/specialisation/hardened/bin/switch-to-configuration test')
|
||||
machine.succeed('systemctl restart chronyd.service')
|
||||
machine.wait_for_unit('chronyd.service')
|
||||
'';
|
||||
})
|
||||
|
@ -1,71 +1,57 @@
|
||||
import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
let
|
||||
client_base = {
|
||||
containers.test1 = {
|
||||
autoStart = true;
|
||||
config = {
|
||||
environment.etc.check.text = "client_base";
|
||||
};
|
||||
};
|
||||
|
||||
# prevent make-test-python.nix to change IP
|
||||
networking.interfaces = {
|
||||
eth1.ipv4.addresses = lib.mkOverride 0 [ ];
|
||||
};
|
||||
};
|
||||
in {
|
||||
{
|
||||
name = "containers-reloadable";
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ danbst ];
|
||||
};
|
||||
|
||||
nodes = {
|
||||
client = { ... }: {
|
||||
imports = [ client_base ];
|
||||
};
|
||||
|
||||
client_c1 = { lib, ... }: {
|
||||
imports = [ client_base ];
|
||||
|
||||
containers.test1.config = {
|
||||
environment.etc.check.text = lib.mkForce "client_c1";
|
||||
services.httpd.enable = true;
|
||||
services.httpd.adminAddr = "nixos@example.com";
|
||||
machine = { lib, ... }: {
|
||||
containers.test1 = {
|
||||
autoStart = true;
|
||||
config.environment.etc.check.text = "client_base";
|
||||
};
|
||||
};
|
||||
client_c2 = { lib, ... }: {
|
||||
imports = [ client_base ];
|
||||
|
||||
containers.test1.config = {
|
||||
environment.etc.check.text = lib.mkForce "client_c2";
|
||||
services.nginx.enable = true;
|
||||
# prevent make-test-python.nix to change IP
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkOverride 0 [ ];
|
||||
|
||||
specialisation.c1.configuration = {
|
||||
containers.test1.config = {
|
||||
environment.etc.check.text = lib.mkForce "client_c1";
|
||||
services.httpd.enable = true;
|
||||
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 = {nodes, ...}: let
|
||||
c1System = nodes.client_c1.config.system.build.toplevel;
|
||||
c2System = nodes.client_c2.config.system.build.toplevel;
|
||||
in ''
|
||||
client.start()
|
||||
client.wait_for_unit("default.target")
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("default.target")
|
||||
|
||||
assert "client_base" in client.succeed("nixos-container run test1 cat /etc/check")
|
||||
assert "client_base" in machine.succeed("nixos-container run test1 cat /etc/check")
|
||||
|
||||
with subtest("httpd is available after activating config1"):
|
||||
client.succeed(
|
||||
"${c1System}/bin/switch-to-configuration test >&2",
|
||||
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"):
|
||||
client.succeed(
|
||||
"${c2System}/bin/switch-to-configuration test >&2",
|
||||
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",
|
||||
)
|
||||
client.fail("systemctl status httpd -M test1 >&2")
|
||||
machine.fail("systemctl status httpd -M test1 >&2")
|
||||
'';
|
||||
|
||||
})
|
||||
|
@ -1,20 +1,4 @@
|
||||
let
|
||||
client_base = {
|
||||
networking.firewall.enable = false;
|
||||
|
||||
containers.webserver = {
|
||||
autoStart = true;
|
||||
privateNetwork = true;
|
||||
hostBridge = "br0";
|
||||
config = {
|
||||
networking.firewall.enable = false;
|
||||
networking.interfaces.eth0.ipv4.addresses = [
|
||||
{ address = "192.168.1.122"; prefixLength = 24; }
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
in import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
{
|
||||
name = "containers-restart_networking";
|
||||
meta = {
|
||||
@ -22,46 +6,55 @@ in import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
};
|
||||
|
||||
nodes = {
|
||||
client = { lib, ... }: client_base // {
|
||||
client = {
|
||||
virtualisation.vlans = [ 1 ];
|
||||
|
||||
networking.firewall.enable = false;
|
||||
|
||||
containers.webserver = {
|
||||
autoStart = true;
|
||||
privateNetwork = true;
|
||||
hostBridge = "br0";
|
||||
config = {
|
||||
networking.firewall.enable = false;
|
||||
networking.interfaces.eth0.ipv4.addresses = [
|
||||
{ address = "192.168.1.122"; prefixLength = 24; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
networking.bridges.br0 = {
|
||||
interfaces = [];
|
||||
rstp = false;
|
||||
};
|
||||
networking.interfaces = {
|
||||
eth1.ipv4.addresses = lib.mkOverride 0 [ ];
|
||||
br0.ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ];
|
||||
|
||||
networking.interfaces.br0.ipv4.addresses = [ { address = "192.168.1.1"; prefixLength = 24; } ];
|
||||
|
||||
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; } ];
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
client_eth1 = { lib, ... }: client_base // {
|
||||
networking.bridges.br0 = {
|
||||
interfaces = [ "eth1" ];
|
||||
rstp = false;
|
||||
};
|
||||
networking.interfaces = {
|
||||
eth1.ipv4.addresses = lib.mkOverride 0 [ ];
|
||||
br0.ipv4.addresses = [ { address = "192.168.1.2"; prefixLength = 24; } ];
|
||||
};
|
||||
};
|
||||
client_eth1_rstp = { lib, ... }: client_base // {
|
||||
networking.bridges.br0 = {
|
||||
interfaces = [ "eth1" ];
|
||||
rstp = true;
|
||||
};
|
||||
networking.interfaces = {
|
||||
eth1.ipv4.addresses = lib.mkOverride 0 [ ];
|
||||
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 = {nodes, ...}: let
|
||||
originalSystem = nodes.client.config.system.build.toplevel;
|
||||
eth1_bridged = nodes.client_eth1.config.system.build.toplevel;
|
||||
eth1_rstp = nodes.client_eth1_rstp.config.system.build.toplevel;
|
||||
in ''
|
||||
testScript = ''
|
||||
client.start()
|
||||
|
||||
client.wait_for_unit("default.target")
|
||||
@ -75,7 +68,7 @@ in import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
|
||||
with subtest("Bridged configuration without STP preserves connectivity"):
|
||||
client.succeed(
|
||||
"${eth1_bridged}/bin/switch-to-configuration test >&2"
|
||||
"/run/booted-system/specialisation/eth1/bin/switch-to-configuration test >&2"
|
||||
)
|
||||
|
||||
client.succeed(
|
||||
@ -87,7 +80,7 @@ in import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
|
||||
# activating rstp needs another service, therefore the bridge will restart and the container will lose its connectivity
|
||||
# with subtest("Bridged configuration with STP"):
|
||||
# client.succeed("${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 l >&2")
|
||||
#
|
||||
@ -100,7 +93,7 @@ in import ./make-test-python.nix ({ pkgs, lib, ... }:
|
||||
|
||||
with subtest("Reverting to initial configuration preserves connectivity"):
|
||||
client.succeed(
|
||||
"${originalSystem}/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")
|
||||
|
@ -14,17 +14,10 @@ import ./make-test-python.nix ( { pkgs, nftables, ... } : {
|
||||
networking.nftables.enable = nftables;
|
||||
services.httpd.enable = true;
|
||||
services.httpd.adminAddr = "foo@example.org";
|
||||
};
|
||||
|
||||
# Dummy configuration to check whether firewall.service will be honored
|
||||
# during system activation. This only needs to be different to the
|
||||
# original walled configuration so that there is a change in the service
|
||||
# file.
|
||||
walled2 =
|
||||
{ ... }:
|
||||
{ networking.firewall.enable = true;
|
||||
networking.firewall.rejectPackets = true;
|
||||
networking.nftables.enable = nftables;
|
||||
specialisation.different-config.configuration = {
|
||||
networking.firewall.rejectPackets = true;
|
||||
};
|
||||
};
|
||||
|
||||
attacker =
|
||||
@ -36,7 +29,6 @@ import ./make-test-python.nix ( { pkgs, nftables, ... } : {
|
||||
};
|
||||
|
||||
testScript = { nodes, ... }: let
|
||||
newSystem = nodes.walled2.system.build.toplevel;
|
||||
unit = if nftables then "nftables" else "firewall";
|
||||
in ''
|
||||
start_all()
|
||||
@ -62,7 +54,7 @@ import ./make-test-python.nix ( { pkgs, nftables, ... } : {
|
||||
|
||||
# Check whether activation of a new configuration reloads the firewall.
|
||||
walled.succeed(
|
||||
"${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF ${unit}.service"
|
||||
"/run/booted-system/specialisation/different-config/bin/switch-to-configuration test 2>&1 | grep -qF ${unit}.service"
|
||||
)
|
||||
'';
|
||||
})
|
||||
|
@ -635,6 +635,7 @@ let
|
||||
(python3.withPackages (p: [ p.mistune ]))
|
||||
shared-mime-info
|
||||
sudo
|
||||
switch-to-configuration-ng
|
||||
texinfo
|
||||
unionfs-fuse
|
||||
xorg.lndir
|
||||
@ -648,6 +649,10 @@ let
|
||||
in [
|
||||
(pkgs.grub2.override { inherit zfsSupport; })
|
||||
(pkgs.grub2_efi.override { inherit zfsSupport; })
|
||||
pkgs.nixos-artwork.wallpapers.simple-dark-gray-bootloader
|
||||
pkgs.perlPackages.FileCopyRecursive
|
||||
pkgs.perlPackages.XMLSAX
|
||||
pkgs.perlPackages.XMLSAXBase
|
||||
])
|
||||
++ optionals (bootLoader == "systemd-boot") [
|
||||
pkgs.zstd.bin
|
||||
|
@ -7,19 +7,19 @@ import ./make-test-python.nix ({ pkgs, ...} : {
|
||||
};
|
||||
|
||||
nodes = {
|
||||
machine = { ... }: {
|
||||
users.mutableUsers = false;
|
||||
};
|
||||
mutable = { ... }: {
|
||||
users.mutableUsers = true;
|
||||
users.users.dry-test.isNormalUser = true;
|
||||
machine = {
|
||||
specialisation.immutable.configuration = {
|
||||
users.mutableUsers = false;
|
||||
};
|
||||
|
||||
specialisation.mutable.configuration = {
|
||||
users.mutableUsers = true;
|
||||
users.users.dry-test.isNormalUser = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = {nodes, ...}: let
|
||||
immutableSystem = nodes.machine.config.system.build.toplevel;
|
||||
mutableSystem = nodes.mutable.config.system.build.toplevel;
|
||||
in ''
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("default.target")
|
||||
|
||||
@ -30,7 +30,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
|
||||
machine.succeed("sudo useradd foobar")
|
||||
assert "foobar" in machine.succeed("cat /etc/passwd")
|
||||
machine.succeed(
|
||||
"${immutableSystem}/bin/switch-to-configuration test"
|
||||
"/run/booted-system/specialisation/immutable/bin/switch-to-configuration test"
|
||||
)
|
||||
assert "foobar" not in machine.succeed("cat /etc/passwd")
|
||||
|
||||
@ -39,7 +39,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
|
||||
with subtest("Password is wrapped in mutable mode"):
|
||||
assert "/run/current-system/" in machine.succeed("which passwd")
|
||||
machine.succeed(
|
||||
"${mutableSystem}/bin/switch-to-configuration test"
|
||||
"/run/booted-system/specialisation/mutable/bin/switch-to-configuration test"
|
||||
)
|
||||
assert "/run/wrappers/" in machine.succeed("which passwd")
|
||||
|
||||
@ -63,7 +63,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
|
||||
expected_hashes[file] = machine.succeed(f"sha256sum {file}")
|
||||
expected_stats[file] = machine.succeed(f"stat {file}")
|
||||
|
||||
machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
|
||||
machine.succeed("/run/booted-system/specialisation/mutable/bin/switch-to-configuration dry-activate")
|
||||
|
||||
machine.fail('test -e /home/dry-test') # home was not recreated
|
||||
for file in files_to_check:
|
||||
|
@ -6,17 +6,6 @@
|
||||
import ./make-test-python.nix ({ pkgs, lib, withFirewall, nftables ? false, ... }:
|
||||
let
|
||||
unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
|
||||
|
||||
routerBase =
|
||||
lib.mkMerge [
|
||||
{ virtualisation.vlans = [ 2 1 ];
|
||||
networking.firewall.enable = withFirewall;
|
||||
networking.firewall.filterForward = nftables;
|
||||
networking.nftables.enable = nftables;
|
||||
networking.nat.internalIPs = [ "192.168.1.0/24" ];
|
||||
networking.nat.externalInterface = "eth1";
|
||||
}
|
||||
];
|
||||
in
|
||||
{
|
||||
name = "nat" + (lib.optionalString nftables "Nftables")
|
||||
@ -26,27 +15,27 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, nftables ? false, ...
|
||||
};
|
||||
|
||||
nodes =
|
||||
{ client =
|
||||
{ pkgs, nodes, ... }:
|
||||
lib.mkMerge [
|
||||
{ virtualisation.vlans = [ 1 ];
|
||||
networking.defaultGateway =
|
||||
(pkgs.lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address;
|
||||
networking.nftables.enable = nftables;
|
||||
}
|
||||
];
|
||||
{
|
||||
client = { lib, nodes, ... }: {
|
||||
virtualisation.vlans = [ 1 ];
|
||||
networking.defaultGateway =
|
||||
(lib.head nodes.router.networking.interfaces.eth2.ipv4.addresses).address;
|
||||
networking.nftables.enable = nftables;
|
||||
};
|
||||
|
||||
router =
|
||||
{ ... }: lib.mkMerge [
|
||||
routerBase
|
||||
{ networking.nat.enable = true; }
|
||||
];
|
||||
router = { lib, ... }: {
|
||||
virtualisation.vlans = [ 2 1 ];
|
||||
networking.firewall.enable = withFirewall;
|
||||
networking.firewall.filterForward = nftables;
|
||||
networking.nftables.enable = nftables;
|
||||
networking.nat.enable = true;
|
||||
networking.nat.internalIPs = [ "192.168.1.0/24" ];
|
||||
networking.nat.externalInterface = "eth1";
|
||||
|
||||
routerDummyNoNat =
|
||||
{ ... }: lib.mkMerge [
|
||||
routerBase
|
||||
{ networking.nat.enable = false; }
|
||||
];
|
||||
specialisation.no-nat.configuration = {
|
||||
networking.nat.enable = lib.mkForce false;
|
||||
};
|
||||
};
|
||||
|
||||
server =
|
||||
{ ... }:
|
||||
@ -59,11 +48,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, nftables ? false, ...
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }: let
|
||||
routerDummyNoNatClosure = nodes.routerDummyNoNat.system.build.toplevel;
|
||||
routerClosure = nodes.router.system.build.toplevel;
|
||||
in ''
|
||||
testScript = ''
|
||||
client.start()
|
||||
router.start()
|
||||
server.start()
|
||||
@ -94,14 +79,14 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, nftables ? false, ...
|
||||
|
||||
# If we turn off NAT, the client shouldn't be able to reach the server.
|
||||
router.succeed(
|
||||
"${routerDummyNoNatClosure}/bin/switch-to-configuration test 2>&1"
|
||||
"/run/booted-system/specialisation/no-nat/bin/switch-to-configuration test 2>&1"
|
||||
)
|
||||
client.fail("curl -4 --fail --connect-timeout 5 http://server/ >&2")
|
||||
client.fail("ping -4 -c 1 server >&2")
|
||||
|
||||
# And make sure that reloading the NAT job works.
|
||||
router.succeed(
|
||||
"${routerClosure}/bin/switch-to-configuration test 2>&1"
|
||||
"/run/booted-system/bin/switch-to-configuration test 2>&1"
|
||||
)
|
||||
# FIXME: this should not be necessary, but nat.service is not started because
|
||||
# network.target is not triggered
|
||||
|
@ -7,6 +7,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
|
||||
nodes.machine = { pkgs, ... }: {
|
||||
imports = [ ../modules/profiles/minimal.nix ];
|
||||
|
||||
system.switch.enable = true;
|
||||
|
||||
systemd.services.restart-me = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
|
@ -591,6 +591,7 @@ in {
|
||||
};
|
||||
|
||||
other = {
|
||||
system.switch.enable = true;
|
||||
users.mutableUsers = true;
|
||||
};
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ let
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
environment.systemPackages = [ pkgs.efibootmgr ];
|
||||
system.switch.enable = true;
|
||||
};
|
||||
|
||||
commonXbootldr = { config, lib, pkgs, ... }:
|
||||
|
@ -3,6 +3,7 @@ import ./make-test-python.nix ({ lib, ... }: {
|
||||
meta = with lib.maintainers; { maintainers = [ chkno ]; };
|
||||
|
||||
nodes.machine = {
|
||||
system.switch.enable = true;
|
||||
system.userActivationScripts.foo = "mktemp ~/user-activation-ran.XXXXXX";
|
||||
users.users.alice = {
|
||||
initialPassword = "pass1";
|
||||
|
Loading…
Reference in New Issue
Block a user