From ecdfb14ef92a1221686c3a81c7b60b061838618d Mon Sep 17 00:00:00 2001 From: Frank Doepper Date: Wed, 10 Jul 2024 11:42:34 +0200 Subject: [PATCH] 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 --- .../manual/release-notes/rl-2205.section.md | 2 +- .../manual/release-notes/rl-2411.section.md | 6 + nixos/modules/services/networking/frr.nix | 371 ++++++++++-------- nixos/tests/frr.nix | 29 +- pkgs/servers/frr/default.nix | 5 +- 5 files changed, 228 insertions(+), 185 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md index e729fdcbb139..2f869b07e072 100644 --- a/nixos/doc/manual/release-notes/rl-2205.section.md +++ b/nixos/doc/manual/release-notes/rl-2205.section.md @@ -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). -- [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). diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 43ab8278329a..f9831f550f45 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -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. +- `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 were not used by any other package. External users are encouraged to migrate to OpenCV 4. diff --git a/nixos/modules/services/networking/frr.nix b/nixos/modules/services/networking/frr.nix index fd5673651f36..a70d1dd6554f 100644 --- a/nixos/modules/services/networking/frr.nix +++ b/nixos/modules/services/networking/frr.nix @@ -1,10 +1,55 @@ { config, lib, pkgs, ... }: + let cfg = config.services.frr; - services = [ - "static" + daemons = [ + "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" "ospf" "ospf6" @@ -22,210 +67,194 @@ let "fabric" ]; - allServices = services ++ [ "zebra" "mgmt" ]; + obsoleteServices = renamedServices ++ [ "static" "mgmt" "zebra" ]; + + allDaemons = builtins.attrNames daemonDefaultOptions; 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: - let - scfg = cfg.${service}; - in - if scfg.configFile != null then scfg.configFile - else pkgs.writeText "${daemonName service}.conf" - '' - ! FRR ${daemonName service} configuration - ! - hostname ${config.networking.hostName} - log syslog - service password-encryption - ! - ${scfg.config} - ! - end - ''; + configFile = + if cfg.configFile != null then + cfg.configFile + else + pkgs.writeText "frr.conf" '' + ! FRR configuration + ! + hostname ${config.networking.hostName} + log syslog + service password-encryption + service integrated-vtysh-config + ! + ${cfg.config} + ! + end + ''; - serviceOptions = service: + serviceOptions = + service: { - enable = lib.mkEnableOption "the FRR ${lib.toUpper service} routing protocol"; - - configFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - example = "/etc/frr/${daemonName service}.conf"; + options = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ daemonDefaultOptions.${service} ]; description = '' - Configuration file to use for FRR ${daemonName service}. - By default the NixOS generated files are used. + Options for the FRR ${service} daemon. ''; }; - - 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 { type = lib.types.listOf lib.types.str; - default = []; + default = [ ]; 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 { ###### interface - imports = [ - { - options.services.frr = { - zebra = (serviceOptions "zebra") // { - enable = lib.mkOption { - type = lib.types.bool; - default = lib.any isEnabled services; + imports = + [ + { + options.services.frr = { + configFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/etc/frr/frr.conf"; description = '' - Whether to enable the Zebra routing manager. - - The Zebra routing manager is automatically enabled - if any routing protocols are configured. + Configuration file to use for FRR. + By default the NixOS generated files are used. + ''; + }; + 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 { - type = lib.types.bool; - default = isEnabled "static"; - defaultText = lib.literalExpression "config.services.frr.static.enable"; - description = '' - Whether to enable the Configuration management daemon. - - The Configuration management daemon is automatically - enabled if needed, at the moment this is when staticd - is enabled. - ''; - }; - }; - }; - } - { options.services.frr = (lib.genAttrs services serviceOptions); } - ]; + } + { options.services.frr = (lib.genAttrs allDaemons serviceOptions); } + (lib.mkRemovedOptionModule [ "services" "frr" "zebra" "enable" ] "FRR zebra is always enabled") + ] + ++ (map (d: lib.mkRenamedOptionModule [ "services" "frr" d "enable" ] [ "services" "frr" "${d}d" "enable" ]) renamedServices) + ++ (map (d: lib.mkRenamedOptionModule [ "services" "frr" d "extraOptions" ] [ "services" "frr" "${d}d" "extraOptions" ]) (renamedServices ++ [ "static" "mgmt" ])) + ++ (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) + ++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "configFile" ] "FRR switched to integrated-vtysh-config, please use services.frr.config or services.frr.configFile") obsoleteServices) + ++ (map (d: lib.mkRemovedOptionModule [ "services" "frr" d "vtyListenAddress" ] "Please change -A option in services.frr.${d}.options instead") obsoleteServices) + ++ (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) + ; ###### implementation - config = lib.mkIf (lib.any isEnabled allServices) { - - environment.systemPackages = [ - pkgs.frr # for the vtysh tool - ]; - - 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; - }; + config = + let + daemonList = lib.concatStringsSep "\n" (map daemonLine daemons); + daemonOptionLine = d: "${d}_options=\"${lib.concatStringsSep " " (cfg.${d}.options ++ cfg.${d}.extraOptions)}\""; + daemonOptions = lib.concatStringsSep "\n" (map daemonOptionLine allDaemons); in - (builtins.listToAttrs - (map mkEtcLink (lib.filter isEnabled allServices))) // { - "frr/vtysh.conf".text = ""; + lib.mkIf (lib.any isEnabled daemons || cfg.configFile != null || cfg.config != "") { + + environment.systemPackages = [ + pkgs.frr # for the vtysh tool + ]; + + users.users.frr = { + description = "FRR daemon user"; + isSystemUser = true; + group = "frr"; }; - systemd.tmpfiles.rules = [ - "d /run/frr 0750 frr frr -" - ]; + users.groups = { + frr = { }; + # Members of the frrvty group can use vtysh to inspect the FRR daemons + frrvty = { + members = [ "frr" ]; + }; + }; - systemd.services = - let - frrService = service: - let - scfg = cfg.${service}; - daemon = daemonName service; - in - lib.nameValuePair daemon ({ - wantedBy = [ "multi-user.target" ]; - after = [ "network-pre.target" "systemd-sysctl.service" ] ++ lib.optionals (service != "zebra") [ "zebra.service" ]; - bindsTo = lib.optionals (service != "zebra") [ "zebra.service" ]; - wants = [ "network.target" ]; + environment.etc = { + "frr/frr.conf".source = configFile; + "frr/vtysh.conf".text = '' + service integrated-vtysh-config + ''; + "frr/daemons".text = '' + # This file tells the frr package which daemons to start. + # + # The watchfrr, zebra and staticd daemons are always started. + # + # This part is auto-generated from services.frr..enable config + ${daemonList} - description = if service == "zebra" then "FRR Zebra routing manager" - else "FRR ${lib.toUpper service} routing daemon"; + # If this option is set the /etc/init.d/frr script automatically loads + # the config via "vtysh -b" when the servers are started. + # + vtysh_enable=yes - unitConfig.Documentation = if service == "zebra" then "man:zebra(8)" - else "man:${daemon}(8) man:zebra(8)"; + # This part is auto-generated from services.frr..options or + # services.frr..extraOptions + ${daemonOptions} + ''; + }; - restartTriggers = lib.mkIf (service != "mgmt") [ - (configFile service) - ]; - reloadIfChanged = (service != "mgmt"); + systemd.tmpfiles.rules = [ "d /run/frr 0750 frr frr -" ]; - serviceConfig = { - PIDFile = "frr/${daemon}.pid"; - ExecStart = "${pkgs.frr}/libexec/frr/${daemon}" - + lib.optionalString (scfg.vtyListenAddress != "") " -A ${scfg.vtyListenAddress}" - + lib.optionalString (scfg.vtyListenPort != null) " -P ${toString scfg.vtyListenPort}" - + " " + (lib.concatStringsSep " " scfg.extraOptions); - 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"; - Restart = "on-abnormal"; - }; - }); - in - lib.listToAttrs (map frrService (lib.filter isEnabled allServices)); - - }; + systemd.services.frr = { + description = "FRRouting"; + documentation = [ "https://frrouting.readthedocs.io/en/latest/setup.html" ]; + wants = [ "network.target" ]; + after = [ + "network-pre.target" + "systemd-sysctl.service" + ]; + before = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + startLimitIntervalSec = 180; + 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 ]; - } diff --git a/nixos/tests/frr.nix b/nixos/tests/frr.nix index edd702dc60e6..a975d4b402a1 100644 --- a/nixos/tests/frr.nix +++ b/nixos/tests/frr.nix @@ -38,7 +38,11 @@ import ./make-test-python.nix ({ pkgs, ... }: { nodes, ... }: { virtualisation.vlans = [ 1 ]; - networking.defaultGateway = ifAddr nodes.router1 "eth1"; + services.frr = { + config = '' + ip route 192.168.0.0/16 ${ifAddr nodes.router1 "eth1"} + ''; + }; }; router1 = @@ -47,13 +51,13 @@ import ./make-test-python.nix ({ pkgs, ... }: virtualisation.vlans = [ 1 2 ]; boot.kernel.sysctl."net.ipv4.ip_forward" = "1"; networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT"; - services.frr.ospf = { - enable = true; + services.frr = { + ospfd.enable = true; config = ospfConf1; }; 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 ]; boot.kernel.sysctl."net.ipv4.ip_forward" = "1"; networking.firewall.extraCommands = "iptables -A nixos-fw -i eth2 -p ospfigp -j ACCEPT"; - services.frr.ospf = { - enable = true; + services.frr = { + ospfd.enable = true; config = ospfConf2; }; }; @@ -73,7 +77,11 @@ import ./make-test-python.nix ({ pkgs, ... }: { nodes, ... }: { 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: machine.wait_for_unit("network.target") - with subtest("Wait for Zebra and OSPFD"): - for gw in router1, router2: - gw.wait_for_unit("zebra") - gw.wait_for_unit("ospfd") + with subtest("Wait for FRR"): + for gw in client, router1, router2, server: + gw.wait_for_unit("frr") router1.succeed("${nodes.router1.config.system.build.toplevel}/specialisation/ospf/bin/switch-to-configuration test >&2") diff --git a/pkgs/servers/frr/default.nix b/pkgs/servers/frr/default.nix index bcb829d716bf..29f2d42d28f8 100644 --- a/pkgs/servers/frr/default.nix +++ b/pkgs/servers/frr/default.nix @@ -154,7 +154,7 @@ stdenv.mkDerivation (finalAttrs: { "--enable-user=frr" "--enable-vty-group=frrvty" "--localstatedir=/run/frr" - "--sbindir=$(out)/libexec/frr" + "--sbindir=${placeholder "out"}/libexec/frr" "--sysconfdir=/etc/frr" "--with-clippy=${finalAttrs.clippy-helper}/bin/clippy" # general options @@ -198,7 +198,8 @@ stdenv.mkDerivation (finalAttrs: { postPatch = '' 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;