nixos/frr: refactor

- use upstream service and scripts
- switch to integrated-vtysh-config, abandon per-daemon config
- use always daemon names in options (e.g. ospf -> ospfd)
- zebra, mgmtd and staticd are always enabled
- abandon vtyListenAddress, vtyListenPort options; use
  just "extraOptions" or "options" instead, respectively
- extend test to test staticd
- update release-notes
- pkgs.servers.frr: fix sbindir and remove FHS PATH
- introduce services.frr.openFilesLimit option
This commit is contained in:
Frank Doepper 2024-07-10 11:42:34 +02:00
parent 35865a4d34
commit ecdfb14ef9
5 changed files with 228 additions and 185 deletions

View File

@ -85,7 +85,7 @@ In addition to numerous new and upgraded packages, this release has the followin
- [filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html), a lightweight shipper for forwarding and centralizing log data. Available as [services.filebeat](#opt-services.filebeat.enable). - [filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html), a lightweight shipper for forwarding and centralizing log data. Available as [services.filebeat](#opt-services.filebeat.enable).
- [FRRouting](https://frrouting.org/), a popular suite of Internet routing protocol daemons (BGP, BFD, OSPF, IS-IS, VRRP and others). Available as [services.frr](#opt-services.frr.babel.enable). - [FRRouting](https://frrouting.org/), a popular suite of Internet routing protocol daemons (BGP, BFD, OSPF, IS-IS, VRRP and others). Available as [services.frr](#opt-services.frr.babeld.enable).
- [Grafana Mimir](https://grafana.com/oss/mimir/), an open source, horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus. Available as [services.mimir](#opt-services.mimir.enable). - [Grafana Mimir](https://grafana.com/oss/mimir/), an open source, horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus. Available as [services.mimir](#opt-services.mimir.enable).

View File

@ -519,6 +519,12 @@
- `ceph` has been upgraded to v19. See the [Ceph "squid" release notes](https://docs.ceph.com/en/latest/releases/squid/#v19-2-0-squid) for details and recommended upgrade procedure. - `ceph` has been upgraded to v19. See the [Ceph "squid" release notes](https://docs.ceph.com/en/latest/releases/squid/#v19-2-0-squid) for details and recommended upgrade procedure.
- `services.frr` has been refactored to use upstream service scripts. The per-daemon configurations
have been removed in favour of an `integrated-vtysh-config` style config. The daemon submodules
now use the daemon name (e.g. `ospfd`) instead of the protocol name (`ospf`). The daemons `zebra`,
`mgmtd` and `staticd` are always enabled if a config is present. The `vtyListenAddress` and
`vtyListenPort` options have been removed; use `options` or `extraOptions` instead, respectively.
- `opencv2` and `opencv3` have been removed, as they are obsolete and - `opencv2` and `opencv3` have been removed, as they are obsolete and
were not used by any other package. External users are encouraged to were not used by any other package. External users are encouraged to
migrate to OpenCV 4. migrate to OpenCV 4.

View File

@ -1,10 +1,55 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.services.frr; cfg = config.services.frr;
services = [ daemons = [
"static" "bgpd"
"ospfd"
"ospf6d"
"ripd"
"ripngd"
"isisd"
"pimd"
"pim6d"
"ldpd"
"nhrpd"
"eigrpd"
"babeld"
"sharpd"
"pbrd"
"bfdd"
"fabricd"
"vrrpd"
"pathd"
];
daemonDefaultOptions = {
zebra = "-A 127.0.0.1 -s 90000000";
mgmtd = "-A 127.0.0.1";
bgpd = "-A 127.0.0.1";
ospfd = "-A 127.0.0.1";
ospf6d = "-A ::1";
ripd = "-A 127.0.0.1";
ripngd = "-A ::1";
isisd = "-A 127.0.0.1";
pimd = "-A 127.0.0.1";
pim6d = "-A ::1";
ldpd = "-A 127.0.0.1";
nhrpd = "-A 127.0.0.1";
eigrpd = "-A 127.0.0.1";
babeld = "-A 127.0.0.1";
sharpd = "-A 127.0.0.1";
pbrd = "-A 127.0.0.1";
staticd = "-A 127.0.0.1";
bfdd = "-A 127.0.0.1";
fabricd = "-A 127.0.0.1";
vrrpd = "-A 127.0.0.1";
pathd = "-A 127.0.0.1";
};
renamedServices = [
"bgp" "bgp"
"ospf" "ospf"
"ospf6" "ospf6"
@ -22,210 +67,194 @@ let
"fabric" "fabric"
]; ];
allServices = services ++ [ "zebra" "mgmt" ]; obsoleteServices = renamedServices ++ [ "static" "mgmt" "zebra" ];
allDaemons = builtins.attrNames daemonDefaultOptions;
isEnabled = service: cfg.${service}.enable; isEnabled = service: cfg.${service}.enable;
daemonName = service: if service == "zebra" then service else "${service}d"; daemonLine = d: "${d}=${if isEnabled d then "yes" else "no"}";
configFile = service: configFile =
let if cfg.configFile != null then
scfg = cfg.${service}; cfg.configFile
in else
if scfg.configFile != null then scfg.configFile pkgs.writeText "frr.conf" ''
else pkgs.writeText "${daemonName service}.conf" ! FRR configuration
'' !
! FRR ${daemonName service} configuration hostname ${config.networking.hostName}
! log syslog
hostname ${config.networking.hostName} service password-encryption
log syslog service integrated-vtysh-config
service password-encryption !
! ${cfg.config}
${scfg.config} !
! end
end '';
'';
serviceOptions = service: serviceOptions =
service:
{ {
enable = lib.mkEnableOption "the FRR ${lib.toUpper service} routing protocol"; options = lib.mkOption {
type = lib.types.listOf lib.types.str;
configFile = lib.mkOption { default = [ daemonDefaultOptions.${service} ];
type = lib.types.nullOr lib.types.path;
default = null;
example = "/etc/frr/${daemonName service}.conf";
description = '' description = ''
Configuration file to use for FRR ${daemonName service}. Options for the FRR ${service} daemon.
By default the NixOS generated files are used.
''; '';
}; };
config = lib.mkOption {
type = lib.types.lines;
default = "";
example =
let
examples = {
rip = ''
router rip
network 10.0.0.0/8
'';
ospf = ''
router ospf
network 10.0.0.0/8 area 0
'';
bgp = ''
router bgp 65001
neighbor 10.0.0.1 remote-as 65001
'';
};
in
examples.${service} or "";
description = ''
${daemonName service} configuration statements.
'';
};
vtyListenAddress = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
Address to bind to for the VTY interface.
'';
};
vtyListenPort = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
TCP Port to bind to for the VTY interface.
'';
};
extraOptions = lib.mkOption { extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = []; default = [ ];
description = '' description = ''
Extra options for the daemon. Extra options to be appended to the FRR ${service} daemon options.
''; '';
}; };
}; }
// (if (builtins.elem service daemons) then { enable = lib.mkEnableOption "FRR ${service}"; } else { });
in in
{ {
###### interface ###### interface
imports = [ imports =
{ [
options.services.frr = { {
zebra = (serviceOptions "zebra") // { options.services.frr = {
enable = lib.mkOption { configFile = lib.mkOption {
type = lib.types.bool; type = lib.types.nullOr lib.types.path;
default = lib.any isEnabled services; default = null;
example = "/etc/frr/frr.conf";
description = '' description = ''
Whether to enable the Zebra routing manager. Configuration file to use for FRR.
By default the NixOS generated files are used.
The Zebra routing manager is automatically enabled '';
if any routing protocols are configured. };
config = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
router rip
network 10.0.0.0/8
router ospf
network 10.0.0.0/8 area 0
router bgp 65001
neighbor 10.0.0.1 remote-as 65001
'';
description = ''
FRR configuration statements.
'';
};
openFilesLimit = lib.mkOption {
type = lib.types.ints.unsigned;
default = 1024;
description = ''
This is the maximum number of FD's that will be available. Use a
reasonable value for your setup if you are expecting a large number
of peers in say BGP.
''; '';
}; };
}; };
mgmt = (serviceOptions "mgmt") // { }
enable = lib.mkOption { { options.services.frr = (lib.genAttrs allDaemons serviceOptions); }
type = lib.types.bool; (lib.mkRemovedOptionModule [ "services" "frr" "zebra" "enable" ] "FRR zebra is always enabled")
default = isEnabled "static"; ]
defaultText = lib.literalExpression "config.services.frr.static.enable"; ++ (map (d: lib.mkRenamedOptionModule [ "services" "frr" d "enable" ] [ "services" "frr" "${d}d" "enable" ]) renamedServices)
description = '' ++ (map (d: lib.mkRenamedOptionModule [ "services" "frr" d "extraOptions" ] [ "services" "frr" "${d}d" "extraOptions" ]) (renamedServices ++ [ "static" "mgmt" ]))
Whether to enable the Configuration management daemon. ++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "enable" ] "FRR ${d}d is always enabled") [ "static" "mgmt" ])
++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "config" ] "FRR switched to integrated-vtysh-config, please use services.frr.config") obsoleteServices)
The Configuration management daemon is automatically ++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "configFile" ] "FRR switched to integrated-vtysh-config, please use services.frr.config or services.frr.configFile") obsoleteServices)
enabled if needed, at the moment this is when staticd ++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "vtyListenAddress" ] "Please change -A option in services.frr.${d}.options instead") obsoleteServices)
is enabled. ++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "vtyListenPort" ] "Please use `-P «vtyListenPort»` option with services.frr.${d}.extraOptions instead, or change services.frr.${d}.options accordingly") obsoleteServices)
''; ;
};
};
};
}
{ options.services.frr = (lib.genAttrs services serviceOptions); }
];
###### implementation ###### implementation
config = lib.mkIf (lib.any isEnabled allServices) { config =
let
environment.systemPackages = [ daemonList = lib.concatStringsSep "\n" (map daemonLine daemons);
pkgs.frr # for the vtysh tool daemonOptionLine = d: "${d}_options=\"${lib.concatStringsSep " " (cfg.${d}.options ++ cfg.${d}.extraOptions)}\"";
]; daemonOptions = lib.concatStringsSep "\n" (map daemonOptionLine allDaemons);
users.users.frr = {
description = "FRR daemon user";
isSystemUser = true;
group = "frr";
};
users.groups = {
frr = {};
# Members of the frrvty group can use vtysh to inspect the FRR daemons
frrvty = { members = [ "frr" ]; };
};
environment.etc = let
mkEtcLink = service: {
name = "frr/${daemonName service}.conf";
value.source = configFile service;
};
in in
(builtins.listToAttrs lib.mkIf (lib.any isEnabled daemons || cfg.configFile != null || cfg.config != "") {
(map mkEtcLink (lib.filter isEnabled allServices))) // {
"frr/vtysh.conf".text = ""; environment.systemPackages = [
pkgs.frr # for the vtysh tool
];
users.users.frr = {
description = "FRR daemon user";
isSystemUser = true;
group = "frr";
}; };
systemd.tmpfiles.rules = [ users.groups = {
"d /run/frr 0750 frr frr -" frr = { };
]; # Members of the frrvty group can use vtysh to inspect the FRR daemons
frrvty = {
members = [ "frr" ];
};
};
systemd.services = environment.etc = {
let "frr/frr.conf".source = configFile;
frrService = service: "frr/vtysh.conf".text = ''
let service integrated-vtysh-config
scfg = cfg.${service}; '';
daemon = daemonName service; "frr/daemons".text = ''
in # This file tells the frr package which daemons to start.
lib.nameValuePair daemon ({ #
wantedBy = [ "multi-user.target" ]; # The watchfrr, zebra and staticd daemons are always started.
after = [ "network-pre.target" "systemd-sysctl.service" ] ++ lib.optionals (service != "zebra") [ "zebra.service" ]; #
bindsTo = lib.optionals (service != "zebra") [ "zebra.service" ]; # This part is auto-generated from services.frr.<daemon>.enable config
wants = [ "network.target" ]; ${daemonList}
description = if service == "zebra" then "FRR Zebra routing manager" # If this option is set the /etc/init.d/frr script automatically loads
else "FRR ${lib.toUpper service} routing daemon"; # the config via "vtysh -b" when the servers are started.
#
vtysh_enable=yes
unitConfig.Documentation = if service == "zebra" then "man:zebra(8)" # This part is auto-generated from services.frr.<daemon>.options or
else "man:${daemon}(8) man:zebra(8)"; # services.frr.<daemon>.extraOptions
${daemonOptions}
'';
};
restartTriggers = lib.mkIf (service != "mgmt") [ systemd.tmpfiles.rules = [ "d /run/frr 0750 frr frr -" ];
(configFile service)
];
reloadIfChanged = (service != "mgmt");
serviceConfig = { systemd.services.frr = {
PIDFile = "frr/${daemon}.pid"; description = "FRRouting";
ExecStart = "${pkgs.frr}/libexec/frr/${daemon}" documentation = [ "https://frrouting.readthedocs.io/en/latest/setup.html" ];
+ lib.optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}" wants = [ "network.target" ];
+ lib.optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}" after = [
+ " " + (lib.concatStringsSep " " scfg.extraOptions); "network-pre.target"
ExecReload = lib.mkIf (service != "mgmt") "${pkgs.python3.interpreter} ${pkgs.frr}/libexec/frr/frr-reload.py --reload --daemon ${daemon} --bindir ${pkgs.frr}/bin --rundir /run/frr /etc/frr/${daemon}.conf"; "systemd-sysctl.service"
Restart = "on-abnormal"; ];
}; before = [ "network.target" ];
}); wantedBy = [ "multi-user.target" ];
in startLimitIntervalSec = 180;
lib.listToAttrs (map frrService (lib.filter isEnabled allServices)); reloadIfChanged = true;
restartTriggers = [
}; configFile
daemonList
];
serviceConfig = {
Nice = -5;
Type = "forking";
NotifyAccess = "all";
StartLimitBurst = "3";
TimeoutSec = 120;
WatchdogSec = 60;
RestartSec = 5;
Restart = "always";
LimitNOFILE = cfg.openFilesLimit;
PIDFile = "/run/frr/watchfrr.pid";
ExecStart = "${pkgs.frr}/libexec/frr/frrinit.sh start";
ExecStop = "${pkgs.frr}/libexec/frr/frrinit.sh stop";
ExecReload = "${pkgs.frr}/libexec/frr/frrinit.sh reload";
};
};
};
meta.maintainers = with lib.maintainers; [ woffs ]; meta.maintainers = with lib.maintainers; [ woffs ];
} }

View File

@ -38,7 +38,11 @@ import ./make-test-python.nix ({ pkgs, ... }:
{ nodes, ... }: { nodes, ... }:
{ {
virtualisation.vlans = [ 1 ]; virtualisation.vlans = [ 1 ];
networking.defaultGateway = ifAddr nodes.router1 "eth1"; services.frr = {
config = ''
ip route 192.168.0.0/16 ${ifAddr nodes.router1 "eth1"}
'';
};
}; };
router1 = router1 =
@ -47,13 +51,13 @@ import ./make-test-python.nix ({ pkgs, ... }:
virtualisation.vlans = [ 1 2 ]; virtualisation.vlans = [ 1 2 ];
boot.kernel.sysctl."net.ipv4.ip_forward" = "1"; boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT"; networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT";
services.frr.ospf = { services.frr = {
enable = true; ospfd.enable = true;
config = ospfConf1; config = ospfConf1;
}; };
specialisation.ospf.configuration = { specialisation.ospf.configuration = {
services.frr.ospf.config = ospfConf2; services.frr.config = ospfConf2;
}; };
}; };
@ -63,8 +67,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
virtualisation.vlans = [ 3 2 ]; virtualisation.vlans = [ 3 2 ];
boot.kernel.sysctl."net.ipv4.ip_forward" = "1"; boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT"; networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT";
services.frr.ospf = { services.frr = {
enable = true; ospfd.enable = true;
config = ospfConf2; config = ospfConf2;
}; };
}; };
@ -73,7 +77,11 @@ import ./make-test-python.nix ({ pkgs, ... }:
{ nodes, ... }: { nodes, ... }:
{ {
virtualisation.vlans = [ 3 ]; virtualisation.vlans = [ 3 ];
networking.defaultGateway = ifAddr nodes.router2 "eth1"; services.frr = {
config = ''
ip route 192.168.0.0/16 ${ifAddr nodes.router2 "eth1"}
'';
};
}; };
}; };
@ -86,10 +94,9 @@ import ./make-test-python.nix ({ pkgs, ... }:
for machine in client, router1, router2, server: for machine in client, router1, router2, server:
machine.wait_for_unit("network.target") machine.wait_for_unit("network.target")
with subtest("Wait for Zebra and OSPFD"): with subtest("Wait for FRR"):
for gw in router1, router2: for gw in client, router1, router2, server:
gw.wait_for_unit("zebra") gw.wait_for_unit("frr")
gw.wait_for_unit("ospfd")
router1.succeed("${nodes.router1.config.system.build.toplevel}/specialisation/ospf/bin/switch-to-configuration test >&2") router1.succeed("${nodes.router1.config.system.build.toplevel}/specialisation/ospf/bin/switch-to-configuration test >&2")

View File

@ -154,7 +154,7 @@ stdenv.mkDerivation (finalAttrs: {
"--enable-user=frr" "--enable-user=frr"
"--enable-vty-group=frrvty" "--enable-vty-group=frrvty"
"--localstatedir=/run/frr" "--localstatedir=/run/frr"
"--sbindir=$(out)/libexec/frr" "--sbindir=${placeholder "out"}/libexec/frr"
"--sysconfdir=/etc/frr" "--sysconfdir=/etc/frr"
"--with-clippy=${finalAttrs.clippy-helper}/bin/clippy" "--with-clippy=${finalAttrs.clippy-helper}/bin/clippy"
# general options # general options
@ -198,7 +198,8 @@ stdenv.mkDerivation (finalAttrs: {
postPatch = '' postPatch = ''
substituteInPlace tools/frr-reload \ substituteInPlace tools/frr-reload \
--replace /usr/lib/frr/ $out/libexec/frr/ --replace-quiet /usr/lib/frr/ $out/libexec/frr/
sed -i '/^PATH=/ d' tools/frr.in tools/frrcommon.sh.in
''; '';
doCheck = true; doCheck = true;