staging-nixos merge for 2026-04-02 (#506019)

This commit is contained in:
zowoq
2026-04-02 13:41:02 +00:00
committed by GitHub
35 changed files with 1113 additions and 251 deletions
@@ -1,14 +1,24 @@
# Ad-Hoc Configuration {#ad-hoc-network-config}
You can use [](#opt-networking.localCommands) to
specify shell commands to be run at the end of `network-setup.service`. This
is useful for doing network configuration not covered by the existing NixOS
modules. For instance, to statically configure an IPv6 address:
You can use [](#opt-networking.localCommands) to specify shell commands to be
run after the network interfaces have been created, but not necessarily fully
configured.
This is useful for doing network configuration not covered by the existing
NixOS modules. For example, you can create a network namespace and a pair
of virtual ethernet devices like this:
```nix
{
networking.localCommands = ''
ip -6 addr add 2001:610:685:1::1/64 dev eth0
ip netns add mynet
ip link add name veth-in type veth peer name veth-out
ip link set dev veth-out netns mynet
'';
}
```
::: {.note}
The commands should ideally be idempotent, so it's recommended to perform
cleanups of the state you create (e.g. virtual interfaces), or at least make
sure possible failures are handled.
:::
@@ -26,9 +26,16 @@ servers:
```
::: {.note}
Statically configured interfaces are set up by the systemd service
`interface-name-cfg.service`. The default gateway and name server
configuration is performed by `network-setup.service`.
Addresses and routes for statically configured interfaces and the default
gateway are set up by systemd services named
`network-addresses-<interface>.service`. The name servers configuration,
instead, is performed by `network-local-commands.service` using resolvconf.
:::
::: {.note}
If needed, for example if addresses/routes were added/removed,
you can reset the network configuration by running
`systemctl restart networking-scripted.target`
:::
The host name is set using [](#opt-networking.hostName):
@@ -223,6 +223,13 @@ See <https://github.com/NixOS/nixpkgs/issues/481673>.
Note for NetworkManager users: before these changes NetworkManager used to spawn its own wpa_supplicant daemon, but now it relies on `networking.wireless`. So, if you had `networking.wireless.enable = false` in your configuration, you should remove that line.
- Some implementation details of the NixOS network-interfaces module have been changed:
- In the "scripted" backend, `network-setup.service` has been removed and the network configuration services are now part of `network.target`, which is now directly pulled into `multi-user.target`.
- Interface addresses, routes and default gateways are now configured asynchronously as soon as the underlying network devices become available (fixes issue [#154737](https://github.com/NixOS/nixpkgs/issues/154737)).
- In both "networkd" and "scripted" backends, the configuration of name servers is now part of `network-local-commands.service` (fixes issue [#445496](https://github.com/NixOS/nixpkgs/issues/445496)).
- The issue that resulted in a completely unconfigured network if both `resolvconf` was disabled and no default gateway configured, has also been fixed.
- `kratos` has been updated from 1.3.1 to [25.4.0](https://github.com/ory/kratos/releases/tag/v25.4.0). Upstream switched to a new versioning scheme (year.major.minor). Notable breaking changes:
- The `migrate sql` CLI command is now `migrate sql up`
@@ -228,6 +228,7 @@ class Driver:
general_symbols = dict(
start_all=self.start_all,
test_script=self.test_script,
machines=self.machines,
machines_qemu=self.machines_qemu,
machines_nspawn=self.machines_nspawn,
vlans=self.vlans,
@@ -243,6 +244,8 @@ class Driver:
serial_stdout_on=self.serial_stdout_on,
polling_condition=self.polling_condition,
BaseMachine=BaseMachine, # for typing
QemuMachine=QemuMachine, # for typing
NspawnMachine=NspawnMachine, # for typing
t=AssertionTester(),
debug=self.debug,
)
@@ -369,7 +372,7 @@ class Driver:
*,
name: str | None = None,
keep_machine_state: bool = False,
) -> BaseMachine:
) -> QemuMachine:
"""
Create a `QemuMachine`. This currently only supports qemu "nodes", not containers.
"""
+3 -1
View File
@@ -36,7 +36,7 @@ class CreateMachineProtocol(Protocol):
name: Optional[str] = None,
keep_machine_state: bool = False,
**kwargs: Any, # to allow usage of deprecated keep_vm_state
) -> BaseMachine:
) -> QemuMachine:
raise Exception("This is just type information for the Nix test driver")
@@ -45,6 +45,8 @@ subtest: Callable[[str], ContextManager[None]]
retry: RetryProtocol
test_script: Callable[[], None]
machines: List[BaseMachine]
machines_qemu: List[QemuMachine]
machines_nspawn: List[NspawnMachine]
vlans: List[VLan]
driver: Driver
log: AbstractLogger
+146
View File
@@ -666,6 +666,137 @@ let
};
};
slurm = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
If set, ONLY prevents users from logging into nodes if they have no
jobs in the node. This module is a legacy implementation with
functionality limited to login restrictions.
'';
};
adopt = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
If set, it prevents users from logging into nodes if they have no jobs
in the node. It also tracks any other spawned processes for accounting
and ensures complete job cleanup when a job is completed for any
successful connection. Spawned processes get "adopted" as external
steps into the current job. As such, those steps get integrated with
Slurm accounting and control group facilities.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = moduleSettingsType;
options = {
action_no_jobs = lib.mkOption {
type = lib.types.enum [
"ignore"
"deny"
];
default = "deny";
description = ''
What to do if no jobs from the user are found, deny or ignore
(pass along to next PAM module).
'';
};
action_unknown = lib.mkOption {
type = lib.types.enum [
"newest"
"allow"
"deny"
];
default = "newest";
description = ''
If the user has jobs, attach them to the newest job. Allow
the connection through without adoption.
'';
};
action_adopt_failure = lib.mkOption {
type = lib.types.enum [
"allow"
"deny"
];
default = "deny";
description = ''
What to do if the process is unable to be adopted into a job.
`allow` matches the upstream default which is only really
suitable for testing; production systems will want `deny`
as a default.
'';
};
action_generic_failure = lib.mkOption {
type = lib.types.enum [
"ignore"
"allow"
"deny"
];
default = "ignore";
description = ''
Catch all for failures related to kernel issues or slurmd
access. Ignore falls through to the next PAM module, allowing
the connection to go through without adoption.
'';
};
disable_x11 = lib.mkOption {
type = lib.types.enum [
"0"
"1"
];
default = "0";
description = ''
Disable or enable x11 sessions. '0' means the adopted connection
has Slurm X11 forwarding with DISPLAY overwritten using X11
tunnel endpoint details.
'';
};
nodename = lib.mkOption {
type = with lib.types; nullOr nonEmptyStr;
default = null;
example = "compute-a-01";
description = ''
Set this only when the Slurm `NodeName` for this machine
differs from `hostname -s`. If unset, `pam_slurm_adopt`
uses the host short name.
'';
};
join_container = lib.mkOption {
type = lib.types.enum [
"true"
"false"
];
default = "true";
description = ''
Attach to a container created by job_container/tmpfs
'';
};
};
};
default = {
service = name;
};
description = ''
Slurm Adopt Settings. More information is available at:
- https://slurm.schedmd.com/pam_slurm_adopt.html
'';
};
};
};
zfs = lib.mkOption {
default = config.security.pam.zfs.enable;
defaultText = lib.literalExpression "config.security.pam.zfs.enable";
@@ -810,6 +941,12 @@ let
control = "sufficient";
modulePath = "${config.systemd.package}/lib/security/pam_systemd_home.so";
}
{
name = "slurm";
enable = cfg.slurm.enable;
control = "required";
modulePath = "${pkgs.slurm}/lib/security/pam_slurm.so";
}
# The required pam_unix.so module has to come after all the sufficient modules
# because otherwise, the account lookup will fail if the user does not exist
# locally, for example with MySQL- or LDAP-auth.
@@ -818,6 +955,15 @@ let
control = "required";
modulePath = "${package}/lib/security/pam_unix.so";
}
# pam_slurm_adopt must be the last module in the account stack.
{
name = "slurm_adopt";
enable = cfg.slurm.adopt.enable;
control = "required";
modulePath = "${pkgs.slurm}/lib/security/pam_slurm_adopt.so";
settings = cfg.slurm.adopt.settings;
}
];
auth = autoOrderRules (
@@ -685,12 +685,14 @@ struct(GrubState => {
efi => '$',
devices => '$',
efiMountPoint => '$',
grub => '$',
grubEfi => '$',
extraGrubInstallArgs => '@',
});
# If you add something to the state file, only add it to the end
# because it is read line-by-line.
sub readGrubState {
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", extraGrubInstallArgs => () );
my $defaultGrubState = GrubState->new(name => "", version => "", efi => "", devices => "", efiMountPoint => "", grub => "", grubEfi => "", extraGrubInstallArgs => () );
open my $fh, "<", "$bootPath/grub/state" or return $defaultGrubState;
local $/ = "\n";
my $name = <$fh>;
@@ -721,8 +723,10 @@ sub readGrubState {
}
my %jsonState = %{decode_json($jsonStateLine)};
my @extraGrubInstallArgs = exists($jsonState{'extraGrubInstallArgs'}) ? @{$jsonState{'extraGrubInstallArgs'}} : ();
my $grubValue = exists($jsonState{'grub'}) ? $jsonState{'grub'} : "";
my $grubEfiValue = exists($jsonState{'grubEfi'}) ? $jsonState{'grubEfi'} : "";
close $fh;
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs );
my $grubState = GrubState->new(name => $name, version => $version, efi => $efi, devices => $devices, efiMountPoint => $efiMountPoint, extraGrubInstallArgs => \@extraGrubInstallArgs, grub => $grubValue, grubEfi => $grubEfiValue );
return $grubState
}
@@ -734,15 +738,16 @@ my @prevExtraGrubInstallArgs = @{$prevGrubState->extraGrubInstallArgs};
my $devicesDiffer = scalar (List::Compare->new( '-u', '-a', \@deviceTargets, \@prevDeviceTargets)->get_symmetric_difference());
my $extraGrubInstallArgsDiffer = scalar (List::Compare->new( '-u', '-a', \@extraGrubInstallArgs, \@prevExtraGrubInstallArgs)->get_symmetric_difference());
my $nameDiffer = get("fullName") ne $prevGrubState->name;
my $versionDiffer = get("fullVersion") ne $prevGrubState->version;
my $efiDiffer = $efiTarget ne $prevGrubState->efi;
my $efiMountPointDiffer = $efiSysMountPoint ne $prevGrubState->efiMountPoint;
# re-installing grub once the package store path changes is necessary, because
# introducing patches or adjusting builds does not always bump the version number
my $grubStorePathsDiffer = ($grub ne $prevGrubState->grub) || ($grubEfi ne $prevGrubState->grubEfi);
if (($ENV{'NIXOS_INSTALL_GRUB'} // "") eq "1") {
warn "NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER";
$ENV{'NIXOS_INSTALL_BOOTLOADER'} = "1";
}
my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $nameDiffer || $versionDiffer || $efiDiffer || $efiMountPointDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
my $requireNewInstall = $devicesDiffer || $extraGrubInstallArgsDiffer || $efiDiffer || $efiMountPointDiffer || $grubStorePathsDiffer || (($ENV{'NIXOS_INSTALL_BOOTLOADER'} // "") eq "1");
# install a symlink so that grub can detect the boot drive
my $tmpDir = File::Temp::tempdir(CLEANUP => 1) or die "Failed to create temporary space: $!";
@@ -795,7 +800,9 @@ if ($requireNewInstall != 0) {
print $fh join( ",", @deviceTargets ), "\n" or die;
print $fh $efiSysMountPoint, "\n" or die;
my %jsonState = (
extraGrubInstallArgs => \@extraGrubInstallArgs
extraGrubInstallArgs => \@extraGrubInstallArgs,
grub => $grub,
grubEfi => $grubEfi
);
my $jsonStateLine = encode_json(\%jsonState);
print $fh $jsonStateLine, "\n" or die;
@@ -51,6 +51,71 @@ let
(lib.concatStringsSep " ")
];
# Converts an IPv4 address literal to a list of bits
parseAddr.ipv4 =
addr:
let
pad = b: lib.replicate (8 - builtins.length b) 0 ++ b;
toBin = n: pad (lib.toBaseDigits 2 (lib.toInt n));
in
lib.concatMap toBin (builtins.splitVersion addr);
# Converts an IPv6 address literal to a list of bits
parseAddr.ipv6 =
addr:
let
pad = b: lib.replicate (16 - builtins.length b) 0 ++ b;
fromHex = n: (builtins.fromTOML "n = 0x${n}").n;
toBin = n: pad (lib.toBaseDigits 2 (fromHex n));
normal = (lib.network.ipv6.fromString addr).address;
in
lib.concatMap toBin (lib.splitString ":" normal);
# Checks if `addr` is part of the `net` subnet
inSubnet =
v: net: addr:
let
prefix = lib.take net.prefixLength (parseAddr.${v} net.address);
match = lib.zipListsWith (a: b: a == b) prefix (parseAddr.${v} addr);
in
lib.all lib.id match;
# Checks if the netmask of all addresses on interface `iface` includes
# the IP address of `gateway`
#
# Note: this is used to check whether networking.defaultGateway relies on
# the given interface, either explicitly, via the `interface` (optional),
# or explicitly, by using an address in a subnet of this interface.
#
# Configuration of the default gateway is then performed as part of that
# interface setup in `configureAddrs`, below.
isGateway =
v: gateway: iface:
lib.any lib.id (
[ (iface.name == gateway.interface) ]
++ map (net: inSubnet v net gateway.address) iface.${v}.addresses
);
# Checks if `gateway` uses an address from `iface` as default source
#
# Note: this is needed to delay the configuration of the gateway and default
# source until the right interfaces and address have been set up, otherwise
# the commands will fail.
hasSource =
v: gateway: iface:
builtins.elem gateway.source (map (i: i.address) iface.${v}.addresses);
# Interfaces corresponding to the default gateways
gateway4Iface = builtins.filter (isGateway "ipv4" cfg.defaultGateway) interfaces;
gateway6Iface = builtins.filter (isGateway "ipv6" cfg.defaultGateway6) interfaces;
# Interfaces corresponding to the default source addresses
#
# Note: the use of `head` here is safe because these expressions
# are evaluated only when `needsSourceIface`, see `configureAddrs` below.
source4Iface = builtins.head (builtins.filter (hasSource "ipv4" cfg.defaultGateway) interfaces);
source6Iface = builtins.head (builtins.filter (hasSource "ipv6" cfg.defaultGateway6) interfaces);
# warn that these attributes are deprecated (2017-2-2)
# Should be removed in the release after next
bondDeprecation = rec {
@@ -118,121 +183,71 @@ let
else
optional (!config.boot.isContainer) (subsystemDevice dev);
hasDefaultGatewaySet =
(cfg.defaultGateway != null && cfg.defaultGateway.address != "")
|| (cfg.enableIPv6 && cfg.defaultGateway6 != null && cfg.defaultGateway6.address != "");
needNetworkSetup =
cfg.resolvconf.enable || cfg.defaultGateway != null || cfg.defaultGateway6 != null;
networkLocalCommands = lib.mkIf needNetworkSetup {
after = [ "network-setup.service" ];
bindsTo = [ "network-setup.service" ];
};
networkSetup = lib.mkIf needNetworkSetup {
description = "Networking Setup";
after = [ "network-pre.target" ];
before = [
"network.target"
"shutdown.target"
];
wants = [ "network.target" ];
# exclude bridges from the partOf relationship to fix container networking bug #47210
partOf = map (i: "network-addresses-${i.name}.service") (
filter (i: !(hasAttr i.name cfg.bridges)) interfaces
);
conflicts = [ "shutdown.target" ];
wantedBy = [ "multi-user.target" ] ++ optional hasDefaultGatewaySet "network-online.target";
unitConfig.ConditionCapability = "CAP_NET_ADMIN";
path = [ pkgs.iproute2 ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig.DefaultDependencies = false;
script = ''
${optionalString config.networking.resolvconf.enable ''
# Set the static DNS configuration, if given.
${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF
${optionalString (cfg.nameservers != [ ] && cfg.domain != null) ''
domain ${cfg.domain}
''}
${optionalString (cfg.search != [ ]) ("search " + concatStringsSep " " cfg.search)}
${flip concatMapStrings cfg.nameservers (ns: ''
nameserver ${ns}
'')}
EOF
''}
# Set the default gateway
${flip concatMapStrings
[
{
version = "-4";
gateway = cfg.defaultGateway;
}
{
version = "-6";
gateway = cfg.defaultGateway6;
}
]
(
{ version, gateway }:
optionalString (gateway != null && gateway.address != "") ''
${optionalString (gateway.interface != null) ''
ip ${version} route replace ${gateway.address} proto static ${
formatIpArgs {
metric = gateway.metric;
dev = gateway.interface;
}
}
''}
ip ${version} route replace default proto static ${
formatIpArgs {
metric = gateway.metric;
via = gateway.address;
window = cfg.defaultGatewayWindowSize;
dev = gateway.interface;
src = gateway.source;
}
}
''
)
}
'';
};
# For each interface <foo>, create a job network-addresses-<foo>.service"
# that performs static address configuration. It has a "wants"
# dependency on <foo>.service, which is supposed to create
# the interface and need not exist (i.e. for hardware
# interfaces). It has a binds-to dependency on the actual
# network device, so it only gets started after the interface
# has appeared, and it's stopped when the interface
# disappears.
# For each interface <foo>, creates a network-addresses-<foo>.service
# job that performs static address configuration.
#
# It has a Wants dependency on <foo>-netdev.service, which creates
# create the interface, or on a device unit (for hardware interfaces).
# It also has a BindsTo dependency on the device unit: so, it only gets
# started after the interface has appeared and it's stopped when the
# interface disappears.
#
# Unless in a container, the job is not made part of network.target, so
# if an interface is not found (e.g. a USB interface not plugged in) it
# will not hang the boot sequence.
#
# If the interface is the default gateway, the job will also set the
# default gateway and delay network-online.target.
configureAddrs =
i:
let
ips = interfaceIps i;
isDefaultGateway4 = cfg.defaultGateway != null && builtins.elem i gateway4Iface;
isDefaultGateway6 = cfg.defaultGateway6 != null && builtins.elem i gateway6Iface;
needsSourceIface4 =
isDefaultGateway4 && cfg.defaultGateway.source != null && i.name != source4Iface.name;
needsSourceIface6 =
isDefaultGateway6 && cfg.defaultGateway6.source != null && i.name != source6Iface.name;
configureGateway =
version: gateway:
optionalString (gateway.address != "") ''
echo -n "setting ${i.name} as default IPv${version} gateway... "
${optionalString (gateway.interface != null) ''
ip -${version} route replace ${gateway.address} proto static ${
formatIpArgs {
metric = gateway.metric;
dev = gateway.interface;
}
}
''}
ip -${version} route replace default proto static ${
formatIpArgs {
metric = gateway.metric;
via = gateway.address;
window = cfg.defaultGatewayWindowSize;
dev = gateway.interface;
src = gateway.source;
}
}
echo "done"
'';
in
nameValuePair "network-addresses-${i.name}" {
description = "Address configuration of ${i.name}";
wantedBy = [
"network-setup.service"
"network.target"
];
# order before network-setup because the routes that are configured
# there may need ip addresses configured
before = [ "network-setup.service" ];
wantedBy =
deviceDependency i.name
++ optional config.boot.isContainer "network.target"
++ optional (isDefaultGateway4 || isDefaultGateway6) "network-online.target";
bindsTo = deviceDependency i.name;
after = [ "network-pre.target" ] ++ (deviceDependency i.name);
partOf = [ "networking-scripted.target" ];
after = [
"network-pre.target"
]
++ optional needsSourceIface4 "network-addresses-${source4Iface.name}.service"
++ optional needsSourceIface6 "network-addresses-${source6Iface.name}.service"
++ deviceDependency i.name;
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
# Restart rather than stop+start this unit to prevent the
@@ -284,6 +299,10 @@ let
fi
''
)}
# Set the default gateway
${optionalString isDefaultGateway4 (configureGateway "4" cfg.defaultGateway)}
${optionalString isDefaultGateway6 (configureGateway "6" cfg.defaultGateway6)}
'';
preStop = ''
state="/run/nixos/network/routes/${i.name}"
@@ -311,13 +330,13 @@ let
nameValuePair "${i.name}-netdev" {
description = "Virtual Network Interface ${i.name}";
bindsTo = optional (!config.boot.isContainer) "dev-net-tun.device";
partOf = [ "networking-scripted.target" ];
after = optional (!config.boot.isContainer) "dev-net-tun.device" ++ [ "network-pre.target" ];
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice i.name)
];
before = [ "network-setup.service" ];
before = [ "network.target" ];
path = [ pkgs.iproute2 ];
serviceConfig = {
Type = "oneshot";
@@ -343,18 +362,21 @@ let
description = "Bridge Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps ++ optional v.rstp "mstpd.service";
partOf = [ "network-setup.service" ] ++ optional v.rstp "mstpd.service";
partOf = [
"network.target"
"networking-scripted.target"
]
++ optional v.rstp "mstpd.service";
after = [
"network-pre.target"
]
++ deps
++ optional v.rstp "mstpd.service"
++ map (i: "network-addresses-${i}.service") v.interfaces;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -448,15 +470,14 @@ let
description = "Open vSwitch Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
]
++ internalConfigs;
# before = [ "network-setup.service" ];
# should work without internalConfigs dependencies because address/link configuration depends
# on the device, which is created by ovs-vswitchd with type=internal, but it does not...
before = [ "network-setup.service" ] ++ internalConfigs;
partOf = [ "network-setup.service" ]; # shutdown the bridge when network is shutdown
before = [ "network.target" ] ++ internalConfigs;
partOf = [
"network.target"
"networking-scripted.target"
]; # shutdown the bridge when network is shutdown
bindsTo = [ "ovs-vswitchd.service" ]; # requires ovs-vswitchd to be alive at all times
after = [
"network-pre.target"
@@ -521,12 +542,12 @@ let
description = "Bond Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "networking-scripted.target" ];
after = [ "network-pre.target" ] ++ deps ++ map (i: "network-addresses-${i}.service") v.interfaces;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [
@@ -570,12 +591,12 @@ let
description = "MACVLAN Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "networking-scripted.target" ];
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -602,12 +623,12 @@ let
description = "IPVLAN Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "networking-scripted.target" ];
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -647,12 +668,12 @@ let
description = "FOU endpoint ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "networking-scripted.target" ];
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -677,12 +698,11 @@ let
description = "IPv6 in IPv4 Tunnel Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -720,12 +740,12 @@ let
description = "IP in IP Tunnel Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "networking-scripted.target" ];
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -768,12 +788,12 @@ let
description = "GRE Tunnel Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "networking-scripted.target" ];
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -803,13 +823,15 @@ let
description = "VLAN Interface ${n}";
wantedBy = [
"network.target"
"network-setup.service"
(subsystemDevice n)
];
bindsTo = deps;
partOf = [ "network-setup.service" ];
partOf = [
"network.target"
"networking-scripted.target"
];
after = [ "network-pre.target" ] ++ deps;
before = [ "network-setup.service" ];
before = [ "network.target" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute2 ];
@@ -845,10 +867,27 @@ let
// mapAttrs' createGreDevice cfg.greTunnels
// mapAttrs' createVlanDevice cfg.vlans
// {
network-setup = networkSetup;
network-local-commands = networkLocalCommands;
network-local-commands = {
after = [ "network-pre.target" ];
wantedBy = [ "network.target" ];
};
};
# Note: the scripted networking backend consistent of many
# independent services that are linked to the network.target.
# Since there is no daemon (e.g systemd-networkd) that is
# started as part of the system and pulls in network.target.
# Thus, to start these services we link network.target directly
# to multi-user.target, this has the same result.
systemd.targets.network.wantedBy = [ "multi-user.target" ];
# This target serves no purpose during the boot, but can be
# used to quickly reset the network configuration by running
# systemctl restart networking-scripted.target
systemd.targets.networking-scripted = {
description = "NixOS scripted networking setup";
};
services.udev.extraRules = ''
KERNEL=="tun", TAG+="systemd"
'';
+17 -4
View File
@@ -747,10 +747,9 @@ in
default = "";
example = "text=anything; echo You can put $text here.";
description = ''
Shell commands to be executed at the end of the
`network-setup` systemd service. Note that if
you are using DHCP to obtain the network configuration,
interfaces may not be fully configured yet.
Shell commands to be executed after all the network
interfaces have been created, but not necessarily
fully configured.
'';
};
@@ -1851,6 +1850,20 @@ in
'';
};
};
networking.localCommands = lib.mkIf config.networking.resolvconf.enable ''
# Set the static DNS configuration, if given.
${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF
${optionalString (cfg.nameservers != [ ] && cfg.domain != null) ''
domain ${cfg.domain}
''}
${optionalString (cfg.search != [ ]) ("search " + concatStringsSep " " cfg.search)}
${flip concatMapStrings cfg.nameservers (ns: ''
nameserver ${ns}
'')}
EOF
'';
services.mstpd = mkIf needsMstpd { enable = true; };
virtualisation.vswitch = mkIf (cfg.vswitches != { }) { enable = true; };
+1
View File
@@ -1472,6 +1472,7 @@ in
slimserver = runTest ./slimserver.nix;
slipshow = runTest ./slipshow.nix;
slurm = runTest ./slurm.nix;
slurm-pam = runTest ./slurm-pam.nix;
smokeping = runTest ./smokeping.nix;
snapcast = runTest ./snapcast.nix;
snapper = runTest ./snapper.nix;
+6 -6
View File
@@ -21,14 +21,14 @@
node_id: str
host: str
def get_node_fqn(machine: Machine) -> GarageNode:
def get_node_fqn(machine: BaseMachine) -> GarageNode:
node_id, host = machine.succeed("garage node id").split('@')
return GarageNode(node_id=node_id, host=host)
def get_node_id(machine: Machine) -> str:
def get_node_id(machine: BaseMachine) -> str:
return get_node_fqn(machine).node_id
def get_layout_version(machine: Machine) -> int:
def get_layout_version(machine: BaseMachine) -> int:
version_data = machine.succeed("garage layout show")
m = cur_version_regex.search(version_data)
if m and m.group('ver') is not None:
@@ -36,17 +36,17 @@
else:
raise ValueError('Cannot find current layout version')
def apply_garage_layout(machine: Machine, layouts: List[str]):
def apply_garage_layout(machine: BaseMachine, layouts: List[str]):
for layout in layouts:
machine.succeed(f"garage layout assign {layout}")
version = get_layout_version(machine)
machine.succeed(f"garage layout apply --version {version}")
def create_api_key(machine: Machine, key_name: str) -> S3Key:
def create_api_key(machine: BaseMachine, key_name: str) -> S3Key:
output = machine.succeed(f"garage key create {key_name}")
return parse_api_key_data(output)
def get_api_key(machine: Machine, key_pattern: str) -> S3Key:
def get_api_key(machine: BaseMachine, key_pattern: str) -> S3Key:
output = machine.succeed(f"garage key info {key_pattern}")
return parse_api_key_data(output)
+1 -1
View File
@@ -1,7 +1,7 @@
import json
class IncusHost(Machine):
class IncusHost(QemuMachine):
def __init__(self, base):
with subtest("Wait for startup"):
base.wait_for_unit("incus.service")
+1 -1
View File
@@ -72,7 +72,7 @@ in
from typing import Dict, Optional
def get_machine_env(machine: Machine, user: Optional[str] = None) -> Dict[str, str]:
def get_machine_env(machine: BaseMachine, user: Optional[str] = None) -> Dict[str, str]:
"""
Gets the environment from a given machine, and returns it as a
dictionary in the form:
@@ -44,17 +44,13 @@ let
defaultGateway6 = {
address = "fd00:1234:5678:1::1";
interface = "enp1s0";
source = "fd00:1234:5678:1::3";
source = "fd00:1234:5678:1::3"; # implicit dependency on enp2s0
};
interfaces.enp1s0.ipv6.addresses = [
{
address = "fd00:1234:5678:1::2";
prefixLength = 64;
}
{
address = "fd00:1234:5678:1::3";
prefixLength = 128;
}
];
interfaces.enp1s0.ipv4.addresses = [
{
@@ -76,6 +72,12 @@ let
prefixLength = 24;
}
];
interfaces.enp2s0.ipv6.addresses = [
{
address = "fd00:1234:5678:1::3";
prefixLength = 128;
}
];
};
};
testScript = ''
@@ -108,6 +110,41 @@ let
client.succeed("ip -6 route show default | grep -q 'src fd00:1234:5678:1::3'")
'';
};
dynamicInterface = {
name = "dynamicInterface";
nodes.machine = clientConfig {
networking.interfaces.usb0 = {
ipv6.addresses = lib.singleton {
address = "fd::1";
prefixLength = 127;
};
};
networking.defaultGateway6 = {
address = "fd::";
interface = "usb0";
source = "fd::1";
};
};
testScript = ''
with subtest("Network comes up without usb0"):
machine.wait_for_unit("network.target")
with subtest("multi-user.target does not hang"):
machine.require_unit_state("multi-user.target", "active")
with subtest("usb0 is configured when plugged in"):
machine.succeed("ip link add usb0 type sit local 1.2.3.4")
machine.wait_until_succeeds("ip addr show dev usb0 | grep -q fd::1")
with subtest("Network is now online"):
machine.systemctl("start network-online.target")
machine.require_unit_state("network-online.target", "active")
with subtest("Default gateway is now set"):
machine.succeed("ip -6 route show default | grep -q 'via fd::'")
machine.succeed("ip -6 route show default | grep -q 'src fd::1'")
'';
};
routeType = {
name = "RouteType";
nodes.client = clientConfig {
+3 -3
View File
@@ -86,7 +86,7 @@ rec {
output = node1.succeed("crm_resource -r cat --locate")
match = re.search("is running on: (.+)", output)
if match:
for machine in machines:
for machine in machines_qemu:
if machine.name == match.group(1):
current_node = machine
break
@@ -96,7 +96,7 @@ rec {
current_node.crash()
# pick another node that's still up
for machine in machines:
for machine in machines_qemu:
if machine.booted:
check_node = machine
# find where the service has been started next
@@ -105,7 +105,7 @@ rec {
match = re.search("is running on: (.+)", output)
# output will remain the old current_node until the crash is detected by pacemaker
if match and match.group(1) != current_node.name:
for machine in machines:
for machine in machines_qemu:
if machine.name == match.group(1):
next_node = machine
break
@@ -112,22 +112,22 @@ in
assert stored_hash == pass_hash, f"{username} user password does not match"
with subtest("alice user has correct password"):
for machine in machines:
for machine in machines_qemu:
assert_password_sha512crypt_match(machine, "alice", "${password1}")
assert "${hashed_sha512crypt}" not in machine.succeed("getent shadow alice"), f"{machine}: alice user password is not correct"
with subtest("bob user has correct password"):
for machine in machines:
for machine in machines_qemu:
print(machine.succeed("getent shadow bob"))
assert "${hashed_bcrypt}" in machine.succeed("getent shadow bob"), f"{machine}: bob user password is not correct"
with subtest("cat user has correct password"):
for machine in machines:
for machine in machines_qemu:
print(machine.succeed("getent shadow cat"))
assert "${hashed_bcrypt}" in machine.succeed("getent shadow cat"), f"{machine}: cat user password is not correct"
with subtest("dan user has correct password"):
for machine in machines:
for machine in machines_qemu:
print(machine.succeed("getent shadow dan"))
assert "${hashed_bcrypt}" in machine.succeed("getent shadow dan"), f"{machine}: dan user password is not correct"
@@ -138,11 +138,11 @@ in
assert_password_sha512crypt_match(immutable, "greg", "${password1}")
assert "${hashed_sha512crypt}" not in immutable.succeed("getent shadow greg"), "greg user password is not correct"
for machine in machines:
for machine in machines_qemu:
machine.wait_for_unit("multi-user.target")
machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
def check_login(machine: Machine, tty_number: str, username: str, password: str):
def check_login(machine: QemuMachine, tty_number: str, username: str, password: str):
machine.send_key(f"alt-f{tty_number}")
machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
machine.wait_for_unit(f"getty@tty{tty_number}.service")
@@ -158,11 +158,11 @@ in
assert username in machine.succeed(f"cat /tmp/{tty_number}"), f"{machine}: {username} password is not correct"
with subtest("Test initialPassword override"):
for machine in machines:
for machine in machines_qemu:
check_login(machine, "2", "egon", "${password1}")
with subtest("Test initialHashedPassword override"):
for machine in machines:
for machine in machines_qemu:
check_login(machine, "3", "fran", "meow")
'';
}
+4 -4
View File
@@ -103,7 +103,7 @@
Result = namedtuple("Result", ["command", "machine", "status", "out", "value"])
Value = namedtuple("Value", ["type", "data"])
def busctl(node: Machine, *args: Any, user: Optional[str] = None) -> Result:
def busctl(node: BaseMachine, *args: Any, user: Optional[str] = None) -> Result:
command = f"busctl --json=short {shlex.join(map(str, args))}"
if user is not None:
command = f"su - {user} -c {shlex.quote(command)}"
@@ -121,7 +121,7 @@
if result.status == 0:
raise Exception(f"command `{result.command}` unexpectedly succeeded")
def rtkit_make_process_realtime(node: Machine, pid: int, priority: int, user: Optional[str] = None) -> Result:
def rtkit_make_process_realtime(node: BaseMachine, pid: int, priority: int, user: Optional[str] = None) -> Result:
return busctl(node, "call", "org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.RealtimeKit1", "MakeThreadRealtimeWithPID", "ttu", pid, 0, priority, user=user)
def get_max_realtime_priority() -> int:
@@ -133,7 +133,7 @@
def parse_chrt(out: str, field: str) -> str:
return next(map(lambda l: l.split(": ")[1], filter(lambda l: field in l, out.splitlines())))
def get_pid(node: Machine, unit: str, user: Optional[str] = None) -> int:
def get_pid(node: BaseMachine, unit: str, user: Optional[str] = None) -> int:
node.wait_for_unit(unit, user=user)
(status, out) = node.systemctl(f"show -P MainPID {unit}", user=user)
if status == 0:
@@ -142,7 +142,7 @@
node.log(out)
raise Exception(f"unable to determine MainPID of {unit} (systemctl exit code {status})")
def assert_sched(node: Machine, pid: int, policy: Optional[str] = None, priority: Optional[int] = None):
def assert_sched(node: BaseMachine, pid: int, policy: Optional[str] = None, priority: Optional[int] = None):
out = node.succeed(f"chrt -p {pid}")
node.log(out)
if policy is not None:
+411
View File
@@ -0,0 +1,411 @@
{ lib, pkgs, ... }:
let
slurmConf = "/etc/slurm.conf";
inherit (import ./ssh-keys.nix pkgs)
snakeOilPrivateKey
snakeOilPublicKey
;
sshConf = ''
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
'';
sshConfigPubkey = pkgs.writeText "ssh_config_pubkey" (
sshConf
+ ''
BatchMode yes
PreferredAuthentications publickey
KbdInteractiveAuthentication no
PasswordAuthentication no
IdentityFile /root/privkey.snakeoil
''
);
sshConfigPassword = pkgs.writeText "ssh_config_password" (
sshConf
+ ''
BatchMode no
PreferredAuthentications password
PubkeyAuthentication no
KbdInteractiveAuthentication no
PasswordAuthentication yes
''
);
sshOpts = "-F " + sshConfigPubkey;
sshPassOpts = "-F " + sshConfigPassword;
adoptRemoteScript = pkgs.writeShellScript "slurm-pam-adopt-remote" ''
echo $$ > /home/submitter/ssh.pid
trap : TERM INT
while true; do
sleep 1
done
'';
mkWaitJob =
node: name:
pkgs.writeText "${name}.sbatch" ''
#!${pkgs.runtimeShell}
#SBATCH --job-name=${name}
#SBATCH --nodes=1
#SBATCH --nodelist=${node}
while true; do sleep 60; done
'';
slurmconfig =
{ config, ... }:
{
services.slurm = {
controlMachine = "control";
nodeName = map (n: n + " CPUs=1 State=UNKNOWN") [
"regular"
"pamslurm"
"pamslurmadopt"
];
partitionName = [ "debug Nodes=ALL Default=YES MaxTime=INFINITE State=UP" ];
extraConfig = ''
PrologFlags=contain
ProctrackType=proctrack/cgroup
TaskPlugin=task/cgroup,task/affinity
SlurmdDebug=debug
'';
};
services.openssh = {
enable = true;
settings = {
AllowUsers = [ "submitter" ];
PubkeyAuthentication = true;
# leave password auth available on the regular node for
# regression testing plain pam_unix behavior
KbdInteractiveAuthentication = false;
PasswordAuthentication = true;
};
};
environment.systemPackages = [
pkgs.pamtester
pkgs.sshpass
];
networking.firewall.enable = false;
systemd.tmpfiles.rules = [
"f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest"
];
environment.etc."slurm.conf".source = "${config.services.slurm.etcSlurm}/slurm.conf";
systemd.services.sshd.environment.SLURM_CONF = slurmConf;
users.groups.submitter = { };
users.users.submitter = {
isNormalUser = true;
createHome = true;
initialPassword = "submitter";
group = "submitter";
openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
};
};
in
{
name = "slurm-pam";
meta.maintainers = [ lib.maintainers.edwtjo ];
nodes =
let
computeNode =
{ ... }:
{
imports = [ slurmconfig ];
services.slurm = {
client.enable = true;
};
};
computePAMNode =
{ ... }:
{
imports = [ computeNode ];
security.pam.services.sshd.slurm.enable = true;
services.openssh.settings = {
KbdInteractiveAuthentication = false;
PasswordAuthentication = lib.mkForce false;
PubkeyAuthentication = true;
};
};
computePAMAdoptNode =
{ ... }:
{
imports = [ computeNode ];
# NOTE: Prolog, Epilog needed for more advanced tests.
# This is an upstream recommended workaround for removing pam_systemd
services.slurm.extraConfig = ''
LaunchParameters=ulimit_pam_adopt
SrunProlog=${pkgs.writers.writeBash "slurm-prolog" ''
loginctl enable-linger $SLURM_JOB_USER
exit 0
''}
TaskProlog=${pkgs.writers.writeBash "slurm-taskprolog" ''
echo "export XDG_RUNTIME_DIR=/run/user/$SLURM_JOB_UID"
echo "export XDG_SESSION_ID=$(</proc/self/sessionid)"
echo "export XDG_SESSION_TYPE=tty"
echo "export XDG_SESSION_CLASS=user"
''}
SrunEpilog=${pkgs.writers.writeBash "slurm-epilog" ''
#Only disable linger if this is the last job running for this user.
O_P=0
for pid in $(scontrol listpids | awk -v jid=$SLURM_JOB_ID 'NR!=1 { if ($2 != jid && $1 != "-1"){print $1} }'); do
ps --noheader -o euser p $pid | grep -q $SLURM_JOB_USER && O_P=1
done
if [ $O_P -eq 0 ]; then
loginctl disable-linger $SLURM_JOB_USER
fi
exit 0
''}
'';
security.pam.services.sshd.slurm.adopt.enable = true;
# disable `pam_systemd` on adopt node
security.pam.services.sshd.startSession = lib.mkForce false;
security.pam.services.sshd.slurm.adopt.settings.action_adopt_failure = "allow";
};
in
{
control =
{ ... }:
{
imports = [ slurmconfig ];
services.slurm = {
server.enable = true;
};
};
submit =
{ ... }:
{
imports = [ slurmconfig ];
services.slurm = {
enableStools = true;
};
};
regular = computeNode;
pamslurm = computePAMNode;
pamslurmadopt = computePAMAdoptNode;
};
testScript = ''
start_all()
# Make sure cluster state is ready before testing PAM behavior.
#
# This shows that the controller is up, all compute nodes have registered,
# and Slurm sees every test node as schedulable (`idle`). Without this,
# later SSH failures would be ambiguous: they could come from PAM policy,
# missing node registration, or startup races.
with subtest("cluster_is_ready"):
control.wait_for_unit("slurmctld.service")
for node in [regular, pamslurm, pamslurmadopt]:
node.wait_for_unit("slurmd")
submit.wait_for_unit("multi-user.target")
submit.wait_until_succeeds(
"sinfo -Nh -o '%N %T' | grep -Fx 'regular idle' && "
"sinfo -Nh -o '%N %T' | grep -Fx 'pamslurm idle' && "
"sinfo -Nh -o '%N %T' | grep -Fx 'pamslurmadopt idle'"
)
# Sanity-check the Slurm cluster itself before layering PAM expectations on
# top of it. This ensures the scheduler can place a job across all three
# nodes, so later PAM-specific failures are not caused by a broken cluster.
with subtest("run_distributed_command"):
submit.succeed("srun -N 3 hostname | sort | uniq | wc -l | xargs test 3 -eq")
# Verify that the generated PAM stacks match the intended integration:
#
# - `pamslurm` should include the legacy `pam_slurm.so`.
# - `pamslurmadopt` should include `pam_slurm_adopt.so`.
# - `pam_slurm_adopt.so` must be the last account module.
# - `pam_systemd.so` must be absent because it conflicts with
# `pam_slurm_adopt`.
#
# This hopefully catches regressions before trying any live login.
with subtest("pam_files_are_as_expected"):
# `pam_systemd` conflicts with `pam_slurm_adopt`
pamslurm.succeed("grep -q '${pkgs.slurm}/lib/security/pam_slurm.so' /etc/pam.d/sshd")
pamslurmadopt.succeed("grep -q '${pkgs.slurm}/lib/security/pam_slurm_adopt.so' /etc/pam.d/sshd")
pamslurmadopt.succeed(
"awk '$1 == \"account\" { print $3 }' /etc/pam.d/sshd | tail -n1 | "
"grep -Fx '${pkgs.slurm}/lib/security/pam_slurm_adopt.so'"
)
pamslurmadopt.fail("grep -q pam_systemd.so /etc/pam.d/sshd")
# Install the test SSH private key on the submit host once so all later SSH
# checks use the same credential and client configuration.
with subtest("prepare_ssh_key"):
submit.succeed(
"install -m 0600 ${snakeOilPrivateKey} /root/privkey.snakeoil"
)
# Regression check: a normal node with no Slurm PAM restrictions
# must still allow ordinary public-key SSH login for the test user.
with subtest("regular_node_allows_pubkey_ssh"):
submit.succeed(
"ssh ${sshOpts} submitter@regular true",
timeout=30
)
# Regression check: ordinary password SSH must also still work on
# the regular node. This guards against accidentally breaking plain
# `pam_unix` behavior while adding Slurm-specific PAM hooks elsewhere.
with subtest("regular_node_allows_password_ssh"):
submit.succeed(
"sshpass -p submitter ssh ${sshPassOpts} submitter@regular true",
timeout=30
)
# Negative companion to the previous test: wrong passwords must still be
# rejected on the regular node, proving that the password path is actually
# authenticating and not silently bypassed.
with subtest("regular_node_rejects_wrong_password_ssh"):
submit.fail(
"sshpass -p wrong-password ssh ${sshPassOpts} submitter@regular true",
timeout=30
)
# Direct PAM regression check on the regular node. This bypasses SSH and
# exercises the local PAM service stack itself, confirming that correct
# `pam_unix` authentication still succeeds.
with subtest("regular_node_pam_unix_accepts_correct_password"):
regular.succeed(
"printf 'submitter\\n' | pamtester login submitter authenticate acct_mgmt"
)
# Negative companion to the previous test: wrong passwords must still be
# rejected on the regular node, proving that the password path is actually
# authenticating and not silently bypassed.
with subtest("regular_node_pam_unix_rejects_wrong_password"):
regular.fail(
"printf 'wrong-password\\n' | pamtester login submitter authenticate"
)
# Core policy check for both Slurm-protected nodes: without any active job
# owned by the user on the target node, SSH must be denied.
#
# This is the main negative regression test for the Slurm PAM integration.
with subtest("deny_ssh_without_job"):
submit.fail(
"ssh ${sshOpts} submitter@pamslurm true",
timeout=30
)
submit.fail(
"ssh ${sshOpts} submitter@pamslurmadopt true",
timeout=30
)
# Verify node-local visibility for the legacy `pam_slurm` integration.
#
# We start a long-running allocation specifically on `pamslurm` as the
# `submitter` user, then confirm Slurm reports that exact node/user/job
# combination. The important negative assertion is that this job must NOT
# grant access to a different node (`pamslurmadopt`).
#
# We intentionally keep this as a visibility/isolation test rather than a
# positive SSH-success test, since `pam_slurm` module is less
# deterministicly to assert positive here than `pam_slurm_adopt`.
with subtest("pam_slurm_job_visibility_is_node_local"):
submit.succeed(
"runuser -u submitter -- sh -lc "
"\"srun --job-name=wait-pamslurm --nodelist=pamslurm sleep 300 "
">/tmp/wait-pamslurm.out 2>/tmp/wait-pamslurm.err &\""
)
submit.wait_until_succeeds(
"squeue -h -u submitter -o '%N %u %T %j' | "
"grep -Fx 'pamslurm submitter RUNNING wait-pamslurm'"
)
submit.fail(
"ssh ${sshOpts} submitter@pamslurmadopt true",
timeout=30
)
submit.succeed("runuser -u submitter -- scancel -n wait-pamslurm")
submit.wait_until_succeeds(
"! squeue -h -u submitter -o '%j' | grep -Fx wait-pamslurm"
)
# Positive integration tests for `pam_slurm_adopt`.
#
# This subtest checks the following:
#
# 1. a job can be created specifically on `pamslurmadopt`;
# 2. Slurm reports that the job is running on that node;
# 3. the node has local Slurm state for that job (`scontrol listpids`);
# 4. SSH login is allowed while the job is active;
# 5. a long-lived SSH session is adopted into Slurm's job tracking;
# 6. cancelling the job tears down the adopted process;
# 7. SSH access is denied again once the job is gone.
#
# Validates both policy enforcement and process adoption/cleanup.
with subtest("pam_slurm_adopt_adopts_connection"):
pamslurmadopt_job = submit.succeed(
"runuser -u submitter -- sbatch --parsable ${mkWaitJob "pamslurmadopt" "wait-pamslurmadopt"}"
).strip()
submit.wait_until_succeeds(
f"test \"$(squeue -h -j {pamslurmadopt_job} -o %T)\" = RUNNING"
)
submit.wait_until_succeeds(
f"squeue -h -j {pamslurmadopt_job} -o %N | grep -Fx pamslurmadopt"
)
pamslurmadopt.wait_until_succeeds(
f"scontrol listpids | awk -v jid='{pamslurmadopt_job}' 'NR > 1 && $2 == jid {{ found = 1 }} END {{ exit !found }}'"
)
# Short SSH probe: verifies that login is allowed at all while the job
# is active, before we move on to the stronger adoption assertions.
submit.succeed(
"ssh ${sshOpts} submitter@pamslurmadopt true",
timeout=30
)
# Start a persistent SSH session whose remote process records its PID.
# We later use that PID to prove the session was adopted into Slurm's
# accounting/control path and then cleaned up when the job is cancelled.
submit.succeed(
"sh -lc '"
"ssh ${sshOpts} submitter@pamslurmadopt ${adoptRemoteScript}"
">/tmp/adopt-ssh.log 2>&1 & "
"echo -n \\$! > /tmp/adopt-ssh.clientpid'"
)
# Wait until the remote helper has started and published its PID.
pamslurmadopt.wait_until_succeeds("test -s /home/submitter/ssh.pid")
# Prove that the SSH-spawned remote process is visible through Slurm's
# local process listing, i.e. that the session was adopted into the job.
pamslurmadopt.wait_until_succeeds(
"pid=$(cat /home/submitter/ssh.pid); "
"scontrol listpids | awk 'NR > 1 { print $1 }' | grep -Fx \"$pid\""
)
remote_pid = pamslurmadopt.succeed("cat /home/submitter/ssh.pid").strip()
# Cancel the allocation and verify the entire chain is torn down:
# the Slurm job disappears, the adopted remote process exits, and the
# local SSH client exits as well.
submit.succeed(f"runuser -u submitter -- scancel {pamslurmadopt_job}")
submit.wait_until_succeeds(
f"test -z \"$(squeue -h -j {pamslurmadopt_job})\"",
timeout=60,
)
pamslurmadopt.wait_until_succeeds(
f"! test -e /proc/{remote_pid}",
timeout=60,
)
submit.wait_until_succeeds(
"! kill -0 $(cat /tmp/adopt-ssh.clientpid)",
timeout=60,
)
# Once the job is gone, SSH must be denied
# again on the adopt-protected node.
submit.fail(
"ssh ${sshOpts} submitter@pamslurmadopt true",
timeout=30
)
'';
}
+64 -16
View File
@@ -148,6 +148,8 @@ in
};
testScript = ''
start_all()
with subtest("can_start_slurmdbd"):
dbd.wait_for_unit("slurmdbd.service")
dbd.wait_for_open_port(6819)
@@ -155,36 +157,82 @@ in
with subtest("cluster_is_initialized"):
control.wait_for_unit("multi-user.target")
control.wait_for_unit("slurmctld.service")
control.wait_until_succeeds("sacctmgr list cluster | awk '{ print $1 }' | grep default")
control.wait_for_open_port(6817)
start_all()
with subtest("can_start_slurmd"):
for node in [node1, node2, node3]:
node.wait_for_unit("slurmd")
node.wait_for_unit("slurmd.service")
node.wait_for_open_port(6818)
# Test that the cluster works and can distribute jobs;
submit.wait_for_unit("multi-user.target")
submit.wait_for_unit("multi-user.target")
control.wait_until_succeeds(
"sacctmgr -nP list cluster format=cluster | grep -qx default"
)
# Test that the cluster works and can distribute jobs;
control.wait_until_succeeds(
"sinfo -Nh -o '%N %T' | grep -Fx 'node1 idle' && "
"sinfo -Nh -o '%N %T' | grep -Fx 'node2 idle' && "
"sinfo -Nh -o '%N %T' | grep -Fx 'node3 idle'"
)
with subtest("run_distributed_command"):
# Run `hostname` on 3 nodes of the partition (so on all the 3 nodes).
# The output must contain the 3 different names
submit.succeed("srun -N 3 hostname | sort | uniq | wc -l | xargs test 3 -eq")
submit.succeed(
"test \"$(srun -J distributed-hostname-check -N 3 hostname | sort -u | tr '\n' ' ')\" = 'node1 node2 node3 '"
)
with subtest("check_slurm_dbd_job"):
# find the srun job from above in the database
control.wait_until_succeeds("sacct | grep hostname")
with subtest("check_slurm_dbd_job_for_srun"):
# find the srun job from above in the database
submit.wait_until_succeeds(
"sacct -X -P -n --name=distributed-hostname-check -o JobName,State | "
"grep -Eq '^distributed-hostname-check\\|COMPLETED(\\+.*)?$'"
)
with subtest("run_PMIx_mpitest"):
submit.succeed("srun -N 3 --mpi=pmix mpitest | grep size=3")
submit.succeed(
"out=$(srun -N 3 --mpi=pmix mpitest); "
"echo \"$out\"; "
"echo \"$out\" | grep -Fx 'size=3'; "
"test \"$(echo \"$out\" | grep -c 'hello world from process')\" -eq 3"
)
with subtest("run_sbatch"):
submit.succeed("sbatch --wait ${sbatchScript}")
submit.succeed("grep 'sbatch success' ${sbatchOutput}")
submit.succeed(
"jobid=$(sbatch --parsable --wait ${sbatchScript}); "
"echo \"$jobid\" > /tmp/sbatch.jobid"
)
submit.succeed("grep -Fx 'sbatch success' ${sbatchOutput}")
submit.wait_until_succeeds(
"sacct -X -j $(cat /tmp/sbatch.jobid) -n -o State | grep -Eq 'COMPLETED|COMPLETED\\+'"
)
submit.succeed("test -z \"$(squeue -h)\"")
with subtest("cluster_returns_to_idle"):
control.wait_until_succeeds(
"sinfo -Nh -o '%N %T' | grep -Fx 'node1 idle' && "
"sinfo -Nh -o '%N %T' | grep -Fx 'node2 idle' && "
"sinfo -Nh -o '%N %T' | grep -Fx 'node3 idle'"
)
with subtest("rest"):
rest.wait_for_unit("slurmrestd.service")
token = control.succeed("scontrol token").split('=')[1].rstrip()
rest.succeed("${pkgs.curl}/bin/curl -sk -H X-SLURM-USER-TOKEN:%s -X GET 'http://localhost:6820/slurm/v0.0.43/diag'" % token)
rest.wait_for_open_port(6820)
token = control.succeed("scontrol token").split('=', 1)[1].strip()
rest.succeed(
"${pkgs.curl}/bin/curl -fsS "
"-H X-SLURM-USER-TOKEN:%s "
"http://localhost:6820/slurm/v0.0.43/diag | grep -q 'meta'" % token
)
with subtest("rest_rejects_invalid_token"):
rest.fail(
"${pkgs.curl}/bin/curl -fsS "
"-H X-SLURM-USER-TOKEN:not-a-real-token "
"http://localhost:6820/slurm/v0.0.43/diag"
)
'';
}
+1 -1
View File
@@ -72,7 +72,7 @@
server.wait_for_unit("multi-user.target")
client.wait_for_unit("multi-user.target")
def copy_pems(machine: Machine, domain: str):
def copy_pems(machine: BaseMachine, domain: str):
machine.succeed("mkdir /run/secrets")
machine.copy_from_host(
source=f"{tmpdir}/{domain}/cert.pem",
+2 -2
View File
@@ -29,11 +29,11 @@
stdenv.mkDerivation (finalAttrs: {
pname = "bind";
version = "9.20.18";
version = "9.20.21";
src = fetchurl {
url = "https://downloads.isc.org/isc/bind9/${finalAttrs.version}/bind-${finalAttrs.version}.tar.xz";
hash = "sha256-38VGyZCsRRVSnNRcTdmVhisYrootDLKSCOiJal0yUzE=";
hash = "sha256-FeG1oifSiQ98ToI6bqAY3nDuLzoOhZy/89gqrYWQ3gM=";
};
outputs = [
+10 -4
View File
@@ -7,13 +7,13 @@
stdenv.mkDerivation (finalAttrs: {
pname = "libiscsi";
version = "1.20.0";
version = "1.20.3";
src = fetchFromGitHub {
owner = "sahlberg";
repo = "libiscsi";
rev = finalAttrs.version;
sha256 = "sha256-idiK9JowKhGAk5F5qJ57X14Q2Y0TbIKRI02onzLPkas=";
sha256 = "sha256-ARajWZ5/LIfFNCdp3HvQiyhR455+sJNzUPbBrz/pZ7E=";
};
postPatch = ''
@@ -24,8 +24,14 @@ stdenv.mkDerivation (finalAttrs: {
nativeBuildInputs = [ autoreconfHook ];
env = lib.optionalAttrs (stdenv.hostPlatform.is32bit || stdenv.hostPlatform.isDarwin) {
# iscsi-discard.c:223:57: error: format specifies type 'unsigned long' but the argument has type 'uint64_t' (aka 'unsigned long long') [-Werror,-Wformat]
NIX_CFLAGS_COMPILE = "-Wno-error=format";
NIX_CFLAGS_COMPILE = toString [
# iscsi-discard.c:223:57: error: format specifies type 'unsigned long' but the argument has type 'uint64_t' (aka 'unsigned long long') [-Werror,-Wformat]
"-Wno-error=format"
# multithreading.c:257:16: error: 'sem_init' is deprecated [-Werror,-Wdeprecated-declarations]
"-Wno-error=deprecated-declarations"
# scsi-lowlevel.c:1244:11: error: cast from 'uint8_t *' (aka 'unsigned char *') to 'uint16_t *' (aka 'unsigned short *') increases required alignment from 1 to 2 [-Werror,-Wcast-align]
"-Wno-error=cast-align"
];
};
meta = {
+5 -10
View File
@@ -11,6 +11,7 @@
enableDarwinSandbox ? true,
# for passthru.tests
nix,
lix,
lowdown-unsandboxed,
}:
@@ -18,7 +19,7 @@ stdenv.mkDerivation rec {
pname = "lowdown${
lib.optionalString (stdenv.hostPlatform.isDarwin && !enableDarwinSandbox) "-unsandboxed"
}";
version = "2.0.4";
version = "3.0.1";
outputs = [
"out"
@@ -29,7 +30,7 @@ stdenv.mkDerivation rec {
src = fetchurl {
url = "https://kristaps.bsd.lv/lowdown/snapshots/lowdown-${version}.tar.gz";
sha512 = "649a508b7727df6e7e1203abb3853e05f167b64832fd5e1271f142ccf782e600b1de73c72dc02673d7b175effdc54f2c0f60318208a968af9f9763d09cf4f9ef";
sha512 = "fe68e1b7ff23f3992398356d7aa9a330dfd7b72e22bea9a91eeef74182b209ecea0c9f3e2b2216e1a07b2358da2b746238ec9cbbdeebdd3551cef14dd2d79f46";
};
# https://github.com/kristapsdz/lowdown/pull/171
@@ -42,12 +43,6 @@ stdenv.mkDerivation rec {
]
++ lib.optionals stdenv.hostPlatform.isDarwin [ fixDarwinDylibNames ];
postPatch = ''
# fails test, some column width mismatch
rm regress/table-footnotes.md
rm regress/table-styles.md
'';
# The Darwin sandbox calls fail inside Nix builds, presumably due to
# being nested inside another sandbox.
preConfigure = lib.optionalString (stdenv.hostPlatform.isDarwin && !enableDarwinSandbox) ''
@@ -103,7 +98,7 @@ stdenv.mkDerivation rec {
runHook preInstallCheck
echo '# TEST' > test.md
$out/bin/lowdown test.md
$out/bin/lowdown test.md | grep '[hH]1'
runHook postInstallCheck
'';
@@ -113,7 +108,7 @@ stdenv.mkDerivation rec {
passthru.tests = {
# most important consumers in nixpkgs
inherit nix lowdown-unsandboxed;
inherit nix lix lowdown-unsandboxed;
};
meta = {
@@ -717,6 +717,12 @@ def switch_to_configuration(
},
remote=target_host,
sudo=sudo,
# switch-to-configuration is not expected to produce meaningful
# stdout, but if it (or any of its children) does, it would leak
# into our stdout and break the "only the store path on stdout"
# contract documented in services.py (see print_result). Redirect
# its stdout to our stderr defensively.
stdout=sys.stderr,
)
@@ -249,6 +249,7 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
"boot",
],
check=True,
stdout=ANY,
**(
DEFAULT_RUN_KWARGS
| {
@@ -547,6 +548,7 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
"switch",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -795,6 +797,7 @@ def test_execute_nix_switch_build_target_host(
"switch",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -920,6 +923,7 @@ def test_execute_nix_switch_flake_target_host(
"switch",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -1047,6 +1051,7 @@ def test_execute_nix_switch_flake_build_host(
"switch",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -1132,6 +1137,7 @@ def test_execute_switch_rollback(mock_run: Mock, tmp_path: Path) -> None:
"switch",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -1307,6 +1313,7 @@ def test_execute_test_flake(mock_run: Mock, tmp_path: Path) -> None:
call(
[config_path / "bin/switch-to-configuration", "test"],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -1372,6 +1379,7 @@ def test_execute_test_rollback(
"test",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -1433,6 +1441,7 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None:
"switch",
],
check=True,
stdout=ANY,
**(
DEFAULT_RUN_KWARGS
| {
@@ -1551,6 +1560,7 @@ def test_execute_switch_store_path_target_host(
"switch",
],
check=True,
stdout=ANY,
**DEFAULT_RUN_KWARGS,
),
]
@@ -770,6 +770,7 @@ def test_switch_to_configuration_without_systemd_run(
},
sudo=False,
remote=None,
stdout=sys.stderr,
)
with pytest.raises(m.NixOSRebuildError) as e:
@@ -811,6 +812,7 @@ def test_switch_to_configuration_without_systemd_run(
},
sudo=True,
remote=target_host,
stdout=sys.stderr,
)
@@ -846,6 +848,7 @@ def test_switch_to_configuration_with_systemd_run(
},
sudo=False,
remote=None,
stdout=sys.stderr,
)
target_host = m.Remote("user@localhost", [], None, "ssh")
@@ -875,6 +878,7 @@ def test_switch_to_configuration_with_systemd_run(
},
sudo=True,
remote=target_host,
stdout=sys.stderr,
)
+3 -3
View File
@@ -16,18 +16,18 @@
rustPlatform.buildRustPackage (finalAttrs: {
pname = "ruff";
version = "0.15.7";
version = "0.15.8";
src = fetchFromGitHub {
owner = "astral-sh";
repo = "ruff";
tag = finalAttrs.version;
hash = "sha256-aDRFNJKvxuHPYaZtoM+93DxJGsTPMLKGBH5QhIiTh0Y=";
hash = "sha256-hjllQ7lw2hecrwhPQlcq97LLxFYR28vCY14Ty6eBox4=";
};
cargoBuildFlags = [ "--package=ruff" ];
cargoHash = "sha256-m3VkHXhjemXVOFFVSUOVz0xD2Rc2pMsP+dnMYQD29uI=";
cargoHash = "sha256-iQKX+TqfP9r5P+UJKLqNysHYJm8N6qCCDfs8KYSb1vY=";
nativeBuildInputs = [ installShellFiles ];
+24 -3
View File
@@ -34,6 +34,7 @@
http-parser,
# enable internal X11 support via libssh2
enableX11 ? true,
enablePAM ? true,
enableNVML ? config.cudaSupport,
cudaPackages,
symlinkJoin,
@@ -144,17 +145,36 @@ stdenv.mkDerivation (finalAttrs: {
"--without-rpath" # Required for configure to pick up the right dlopen path
]
++ (lib.optional (!enableX11) "--disable-x11")
++ (lib.optional enableNVML "--with-nvml");
++ (lib.optional enableNVML "--with-nvml")
++ (lib.optional enablePAM "--enable-pam --with-pam_dir=${placeholder "out"}/lib/security");
preConfigure = ''
patchShebangs ./doc/html/shtml2html.py
patchShebangs ./doc/man/man2html.py
''
+ (lib.optionalString enablePAM ''
mkdir -p $out/lib/security
'');
postConfigure = lib.optionalString enablePAM ''
rm -rf $out
'';
postInstall = ''
rm -f $out/lib/*.la $out/lib/slurm/*.la
postBuild = lib.optionalString enablePAM ''
make -C contribs/pam
make -C contribs/pam_slurm_adopt
'';
postInstall =
(lib.optionalString enablePAM ''
export LIBRARY_PATH="$PWD/src/api/.libs:''${LIBRARY_PATH:+:$LIBRARY_PATH}"
mkdir -p $out/lib/security
make -C contribs/pam install
make -C contribs/pam_slurm_adopt install
'')
+ ''
rm -f $out/lib/*.la $out/lib/slurm/*.la $out/lib/security/*.la
'';
enableParallelBuilding = true;
passthru.tests.slurm = nixosTests.slurm;
@@ -166,6 +186,7 @@ stdenv.mkDerivation (finalAttrs: {
license = lib.licenses.gpl2Only;
maintainers = with lib.maintainers; [
markuskowa
edwtjo
];
};
})
+2 -2
View File
@@ -17,13 +17,13 @@
}:
stdenv.mkDerivation (finalAttrs: {
pname = "wolfssl-${variant}";
version = "5.8.4";
version = "5.9.0";
src = fetchFromGitHub {
owner = "wolfSSL";
repo = "wolfssl";
tag = "v${finalAttrs.version}-stable";
hash = "sha256-vfJKmDdM0r591t5GnuSS7NyiUYXCQOTKbWLVydB3N9s=";
hash = "sha256-Ov59Zt0UskADQThdzr9wni2junSpy3jiABWpiGr8xtg=";
};
postPatch = ''
@@ -0,0 +1,27 @@
From 3cf1ca7491bd5b6680e80355d76442ae14db681e Mon Sep 17 00:00:00 2001
From: Alyssa Ross <hi@alyssa.is>
Date: Sun, 29 Mar 2026 13:18:09 +0200
Subject: [PATCH] build system: fix potential buffer overflow
This could potentially write one byte past the end of line.
Identified by fortify-headers.
---
scripts/basic/split-include.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/basic/split-include.c b/scripts/basic/split-include.c
index 6ef29195e..93011d511 100644
--- a/scripts/basic/split-include.c
+++ b/scripts/basic/split-include.c
@@ -195,7 +195,7 @@ int main(int argc, const char * argv [])
ERROR_EXIT( "find" );
line[0] = '\n';
- while (fgets(line+1, buffer_size, fp_find))
+ while (fgets(line+1, buffer_size-1, fp_find))
{
if (strstr(list_target, line) == NULL)
{
--
2.53.0
@@ -90,6 +90,8 @@ stdenv.mkDerivation rec {
excludes = [ "networking/httpd_ratelimit_cgi.c" ]; # New since release.
hash = "sha256-Msm9sDZrVx7ofunnvnTS73SPKUUpR3Tv5xZ/wBd+rts=";
})
# https://lists.busybox.net/pipermail/busybox/2026-March/092010.html
./build-system-buffer-overflow.patch
]
++ lib.optional (stdenv.hostPlatform != stdenv.buildPlatform) ./clang-cross.patch;
+10 -10
View File
@@ -1,7 +1,7 @@
{
"testing": {
"version": "7.0-rc5",
"hash": "sha256:00q4scz3kyrbd8v23pjdzgmaz9scmxg10cqlfwrrd7xj0hxp3pah",
"version": "7.0-rc6",
"hash": "sha256:085bc06mbav64rmsm6a34j2y75nrqpqp4qd8ld8mb9acf1i4iw45",
"lts": false
},
"6.1": {
@@ -20,23 +20,23 @@
"lts": true
},
"6.6": {
"version": "6.6.130",
"hash": "sha256:139480lyi3if8pd2j3yld5a01lk7113kbcn2kxpzyk29p5kslq14",
"version": "6.6.132",
"hash": "sha256:1d1fdd5wpphlm68yb16csaaijv0lf38ynl3gpvvdspsg22xjpdn5",
"lts": true
},
"6.12": {
"version": "6.12.78",
"hash": "sha256:0gdgykr4nqk1dzb5ms2m07saxx58zvacpfv8ynhfrv7snjs835vi",
"version": "6.12.80",
"hash": "sha256:0lrylj87bb8ky29pbplpncrfhmgqqbq3d49iqgdwv7p7jvc929f9",
"lts": true
},
"6.18": {
"version": "6.18.20",
"hash": "sha256:0lrm76rdlr92kjq3g410qdff9v49mpdf400lmsh7hq74k2ymlyl3",
"version": "6.18.21",
"hash": "sha256:0ks735y6jq4yy3jaicjsj4dn4n3kk2skf9dqh9dyifipn57j2f0w",
"lts": true
},
"6.19": {
"version": "6.19.10",
"hash": "sha256:072s76238rnf87yhdy15nbxfyq7x3ch7p2v14dq4pq551qd48va6",
"version": "6.19.11",
"hash": "sha256:16ymkc5r3hw05z7l7ih3qw406qlszz1l7b4g5yz0hv15ddxrs0r0",
"lts": false
}
}
+16 -4
View File
@@ -41,6 +41,13 @@ let
hash = "sha256-uu/SIG8fgVVWhsGxmszTPHwe4SQtLgbxdShOMKbeg2w=";
};
lixLowdown30Patch = fetchpatch {
name = "lix-lowdown-3.0-support.patch";
url = "https://git.lix.systems/lix-project/lix/commit/af0390c27bdc401ece8f8192cb3024f0ff08e977.patch";
excludes = [ "flake.nix" ];
hash = "sha256-ZBkbgeZ/D7H2teX8bPy5NEG1aXbQVksTDV3aVBZdRPM=";
};
makeLixScope =
{
attrName,
@@ -229,6 +236,8 @@ lib.makeExtensible (
})
lixMdbookPatch
lixLowdown30Patch
];
};
};
@@ -253,7 +262,10 @@ lib.makeExtensible (
hash = "sha256-APm8m6SVEAO17BBCka13u85/87Bj+LePP7Y3zHA3Mpg=";
};
patches = [ lixMdbookPatch ];
patches = [
lixMdbookPatch
lixLowdown30Patch
];
};
};
@@ -283,14 +295,14 @@ lib.makeExtensible (
attrName = "git";
lix-args = rec {
version = "2.96.0-pre-20260317_${builtins.substring 0 12 src.rev}";
version = "2.96.0-pre-20260318_${builtins.substring 0 12 src.rev}";
src = fetchFromGitea {
domain = "git.lix.systems";
owner = "lix-project";
repo = "lix";
rev = "96db7c79cf2a9a06725360b0d12e5de583bef07d";
hash = "sha256-Ixwk38HArs7MZXxdWRkSZFzUhUdlCro+8+M/sO+fE/Y=";
rev = "8294cd534b2f01ee967b28aa73fcab1535d62b3d";
hash = "sha256-BFijbNDCrfzpDdW+gNauP25QsTvEZ39dygWEI/RYeyY=";
};
cargoDeps = rustPlatform.fetchCargoVendor {
+32 -11
View File
@@ -134,6 +134,21 @@ let
patches_common = lib.optional (
stdenv.system == "aarch64-darwin"
) ./patches/skip-flaky-darwin-tests.patch;
# Lowdown 3.0 compatibility patch for nix 2.312.33; fetched from the
# upstream backport (same diff on every maintenance branch after
# fetchpatch strips metadata). Nix 2.34.4+ and the git snapshot
# already include the fix in their tagged source.
lowdown30Patch = pkgs.fetchpatch {
name = "nix-lowdown-3.0-support.patch";
url = "https://github.com/NixOS/nix/commit/472c35c561bd9e8db1465e0677f1efe2cb88c568.patch";
hash = "sha256-ZCQgI/euBN8t9rgdCsGRgrcEWG3T5MUc+bQc4tIcHuI=";
};
# Lowdown 3.0 compatibility patch for nix 2.28 and 2.30, which have a
# different markdown.cc layout (no LOWDOWN_TERM_NORELLINK branch) and
# never received an upstream backport.
lowdown30PatchOld = ./patches/lowdown-3.0-compat-2.28-2.30.patch;
in
lib.makeExtensible (
self:
@@ -149,6 +164,7 @@ lib.makeExtensible (
url = "https://github.com/NixOS/nix/commit/5a64138e862fe364e751c5c286e8db8c466aaee7.patch?full_index=1";
hash = "sha256-vFv/D08x9urtoIE9wiC7Lln4Eq3sgNBwU7TBE1iyrfI=";
})
lowdown30PatchOld
];
};
@@ -172,22 +188,27 @@ lib.makeExtensible (
url = "https://github.com/NixOS/nix/commit/5cbd7856de0a9c13351f98e32a1e26d0854d87fd.patch?full_index=1";
hash = "sha256-r2ZF1zBZDKMvyX6X4VsaTMrg0zdjn59Jf6Hqg56r29E=";
})
lowdown30PatchOld
]
);
nix_2_30 = addTests "nix_2_30" self.nixComponents_2_30.nix-everything;
nixComponents_2_31 = nixDependencies.callPackage ./modular/packages.nix rec {
version = "2.31.3";
inherit teams;
otherSplices = generateSplicesForNixComponents "nixComponents_2_31";
src = fetchFromGitHub {
owner = "NixOS";
repo = "nix";
tag = version;
hash = "sha256-oe0YWe8f+pwQH4aYD2XXLW5iEHyXNUddurqJ5CUVCIk=";
};
};
nixComponents_2_31 =
(nixDependencies.callPackage ./modular/packages.nix rec {
version = "2.31.3";
inherit teams;
otherSplices = generateSplicesForNixComponents "nixComponents_2_31";
src = fetchFromGitHub {
owner = "NixOS";
repo = "nix";
tag = version;
hash = "sha256-oe0YWe8f+pwQH4aYD2XXLW5iEHyXNUddurqJ5CUVCIk=";
};
}).appendPatches
[
lowdown30Patch
];
nix_2_31 = addTests "nix_2_31" self.nixComponents_2_31.nix-everything;
@@ -0,0 +1,27 @@
--- a/src/libcmd/markdown.cc
+++ b/src/libcmd/markdown.cc
@@ -37,7 +37,13 @@
.vmargin = 0,
# endif
.feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES,
- .oflags = LOWDOWN_TERM_NOLINK,
+ .oflags =
+# if HAVE_LOWDOWN_3
+ LOWDOWN_NOLINK
+# else
+ LOWDOWN_TERM_NOLINK
+# endif
+ ,
};
auto doc = lowdown_doc_new(&opts);
--- a/src/libcmd/meson.build
+++ b/src/libcmd/meson.build
@@ -36,6 +36,7 @@
configdata.set('HAVE_LOWDOWN', lowdown.found().to_int())
# The API changed slightly around terminal initialization.
configdata.set('HAVE_LOWDOWN_1_4', lowdown.version().version_compare('>= 1.4.0').to_int())
+configdata.set('HAVE_LOWDOWN_3', lowdown.version().version_compare('>= 3.0.0').to_int())
readline_flavor = get_option('readline-flavor')
if readline_flavor == 'editline'