diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b9bf52b0068a..53d0773f0502 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1310,6 +1310,7 @@ ./services/networking/netbird/server.nix ./services/networking/netclient.nix ./services/networking/netfoil.nix + ./services/networking/netplan.nix ./services/networking/networkd-dispatcher.nix ./services/networking/networkmanager.nix ./services/networking/newt.nix diff --git a/nixos/modules/services/networking/netplan.nix b/nixos/modules/services/networking/netplan.nix new file mode 100644 index 000000000000..a4ab0f901f55 --- /dev/null +++ b/nixos/modules/services/networking/netplan.nix @@ -0,0 +1,61 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.networking.netplan; + networkdEnabled = (config.networking.useNetworkd || config.systemd.network.enable); + networkmanagerEnabled = config.networking.networkmanager.enable; +in +{ + options = { + networking.netplan = { + enable = lib.mkEnableOption "Whether to enable the netplan-configure service at boot"; + package = lib.mkPackageOption pkgs "netplan" { }; + configFiles = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = '' + { + "10-example.yaml" = \'\' + --- + network: + version: 2 + dummy-devices: + example: + addresses: ["fd00::feed:cafe/32"] + \'\' + } + ''; + description = "Netplan YAML configuration files to write under /etc/netplan/"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + warnings = lib.optionals (!(networkdEnabled || networkmanagerEnabled)) [ + "You enabled the netplan-configure service, but you haven't enabled a backend for it. It's likely you want to enable either networkd or NetworkManager." + ]; + environment = { + systemPackages = [ cfg.package ]; + etc = builtins.listToAttrs ( + builtins.map (key: { + name = "netplan/" + key; + value = { + text = cfg.configFiles."${key}"; + user = "root"; + group = "root"; + mode = "0600"; + }; + }) (pkgs.lib.attrNames cfg.configFiles) + ); + }; + systemd = { + packages = [ cfg.package ]; + services.netplan-configure.wantedBy = [ "sysinit.target" ]; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a04a79351ea0..bd4cea838057 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1078,6 +1078,7 @@ in netbox_4_5 = handleTest ./web-apps/netbox/default.nix { netbox = pkgs.netbox_4_5; }; netdata = runTest ./netdata.nix; netfoil = runTest ./netfoil.nix; + netplan = runTest ./netplan.nix; networking.networkd = handleTest ./networking/networkd-and-scripted.nix { networkd = true; }; networking.networkmanager = handleTest ./networking/networkmanager.nix { }; networking.scripted = handleTest ./networking/networkd-and-scripted.nix { networkd = false; }; diff --git a/nixos/tests/netplan.nix b/nixos/tests/netplan.nix new file mode 100644 index 000000000000..588bacce4b47 --- /dev/null +++ b/nixos/tests/netplan.nix @@ -0,0 +1,144 @@ +{ pkgs, lib, ... }: + +let + # Test by creating a "group" of interfaces: a VRF, plus a dummy interface under it, to which we'll assign an IPv6 ULA address + # This must be done for both configs under /etc ("static") and configs under /run ("dynamic"). So each test group gets an address + test-groups = { + "static" = "fd00::feed:cafe/32"; + "dynamic" = "fd00::dead:beef/32"; + }; + + # Generate link information, interface getter commands, and configuration files, to be used on all test VMs + # The variable respects the globs: + # - fixtures.{static,dynamic}.links.{vrf,dummy}.{name,getter-cmd,address} (NB: address is unavailable in 'vrf') + # - fixtures.{static,dynamic}.cfgs.{networkd,NetworkManager}.{filename,src} + fixtures = lib.mapAttrs ( + group: address: + let + vrf-name = "np${group}"; + in + { + links.vrf.name = "${vrf-name}"; + links.vrf.getter-cmd = "ip -o link show dev ${vrf-name} type vrf"; + links.dummy.name = "${vrf-name}-lo"; + links.dummy.getter-cmd = "ip -o link show dev ${vrf-name}-lo type dummy master ${vrf-name}"; + links.dummy.address = address; + cfgs = lib.genAttrs [ "networkd" "NetworkManager" ] (renderer: get-config-file group renderer); + } + ) test-groups; + + # Utility to generate netplan configuration objects + get-config-file = ( + group: renderer: + let + links = fixtures.${group}.links; + content = '' + --- + network: + version: 2 + renderer: ${renderer} + dummy-devices: + ${links.dummy.name}: + addresses: ["${links.dummy.address}"] + vrfs: + ${links.vrf.name}: + table: 130 + interfaces: [${links.dummy.name}] + ''; + in + { + filename = "10-test-${group}-${renderer}.yaml"; + src = pkgs.writeText "netplan-test-${group}-${renderer}" content; + } + ); + +in +{ + name = "netplan"; + meta.maintainers = with lib.maintainers; [ + mkg20001 + ]; + + nodes = { + networkd = { + networking.useNetworkd = true; + systemd.network.enable = true; + networking.useDHCP = false; + networking.netplan.enable = true; + networking.netplan.configFiles = { + ${fixtures.static.cfgs.networkd.filename} = builtins.readFile fixtures.static.cfgs.networkd.src; + }; + systemd.tmpfiles.settings."netplan"."/run/netplan".d = { + user = "root"; + group = "root"; + mode = "0700"; + }; + }; + networkmanager = { + networking.networkmanager.enable = true; + networking.netplan.enable = true; + networking.netplan.configFiles = { + ${fixtures.static.cfgs.NetworkManager.filename} = + builtins.readFile fixtures.static.cfgs.NetworkManager.src; + }; + systemd.tmpfiles.settings."netplan"."/run/netplan".d = { + user = "root"; + group = "root"; + mode = "0700"; + }; + }; + }; + + testScript = '' + import json + + fixtures = json.loads('${builtins.toJSON fixtures}') + start_all() + + # Test both networkd and networkmanager backends + for machine in [networkd, networkmanager]: + + # Prepare variables for testing + renderer = "networkd" if machine is networkd else "NetworkManager" + configfiles = dict() + links = dict() + for group in ['static', 'dynamic']: + links[group] = list(fixtures[group]['links'].items()) + configfiles[group] = fixtures[group]['cfgs'][renderer] + + # Wait for tests to be runnable + machine.wait_for_unit("network.target") + + # Basic tests + with subtest("Check that netplan package has been installed and is able to run"): + machine.succeed("which netplan") + machine.succeed("netplan --help") + + # Module tests + with subtest("Check that the nixos module wrote the configuration file"): + machine.succeed(f"ls /etc/netplan/{configfiles['static']['filename']}") + + # Run the interface checks twice: once before applying the dynamic config, and once after + for dynamic_config in [False, True]: + + # If testing the synamic, write out the fixture YAML file, and apply it + if dynamic_config: + with subtest("Run netplan apply to configure dynamic interfaces"): + destination_filename = f"/run/netplan/{configfiles['dynamic']['filename']}" + machine.copy_from_host_via_shell(configfiles['dynamic']['src'], destination_filename) + machine.succeed(f"chown root:root {destination_filename}") + machine.succeed(f"chmod 0700 {destination_filename}") + machine.succeed("netplan apply") + + # Check that the interfaces exist and are of the right type + # Commands below rely on '-o pipefail' being set in the shell + links_to_test = links['static'] + (links['dynamic'] if dynamic_config else []) + with subtest(f"Check that netplan correctly configured the network interfaces ({'/run populated' if dynamic_config else 'right after boot'})"): + for link_type, link in links_to_test: + link_name = link['name'] + machine.wait_until_succeeds(f"{link['getter-cmd']} | grep {link_name}") + # For the dummy interface, check the IP address as well + if link_type == 'dummy': + machine.wait_until_succeeds(f"ip -o addr show {link_name} to {link['address']} | grep {link_name}") + ''; +} diff --git a/pkgs/by-name/ne/netplan/package.nix b/pkgs/by-name/ne/netplan/package.nix index bfddf1c00643..a4f43bf2bb3e 100644 --- a/pkgs/by-name/ne/netplan/package.nix +++ b/pkgs/by-name/ne/netplan/package.nix @@ -9,68 +9,87 @@ python3, libuuid, bash-completion, + meson, + ninja, + cmake, + iproute2, + makeWrapper, lib, + nixosTests, }: +let + pythonenv = python3.withPackages ( + p: with p; [ + pyyaml + cffi + setuptools + ] + ); +in stdenv.mkDerivation (finalAttrs: { pname = "netplan"; - version = "0.106.1"; + version = "1.2.2"; src = fetchFromGitHub { owner = "canonical"; repo = "netplan"; rev = finalAttrs.version; - hash = "sha256-wQ4gd9+9YU92WGRMjSiF/zLCGxhaSl8s22pH1jr+Mm0="; + hash = "sha256-3gvTQGQxQoQWfrcsVEF0ekdCjldRL6gh3k23NIXXZCQ="; }; + strictDeps = true; nativeBuildInputs = [ + pythonenv pkg-config - glib pandoc + meson + ninja + cmake + makeWrapper ]; buildInputs = [ + pythonenv systemd glib libyaml - (python3.withPackages ( - p: with p; [ - pyyaml - netifaces - dbus-python - rich - ] - )) libuuid bash-completion + iproute2 ]; + env.PKG_CONFIG_BASH_COMPLETION_COMPLETIONSDIR = "${placeholder "out"}/share/bash-completion/completions"; + env.PKG_CONFIG_SYSTEMD_SYSTEMDSYSTEMGENERATORDIR = "${placeholder "out"}/lib/systemd/system-generators"; + env.PKG_CONFIG_SYSTEMD_SYSTEMDSYSTEMUNITDIR = "${placeholder "out"}/lib/systemd/system"; + postPatch = '' - substituteInPlace netplan/libnetplan.py \ - --replace "/lib/netplan/generate" "$out/lib/netplan/generate" \ - --replace "ctypes.util.find_library('netplan')" "\"$out/lib/libnetplan.so\"" - - substituteInPlace Makefile \ - --replace 'SYSTEMD_GENERATOR_DIR=' 'SYSTEMD_GENERATOR_DIR ?= ' \ - --replace 'SYSTEMD_UNIT_DIR=' 'SYSTEMD_UNIT_DIR ?= ' \ - --replace 'BASH_COMPLETIONS_DIR=' 'BASH_COMPLETIONS_DIR ?= ' \ - --replace 'pkg-config' '$(PKG_CONFIG)' - - # from upstream https://github.com/canonical/netplan/blob/ee0d5df7b1dfbc3197865f02c724204b955e0e58/rpm/netplan.spec#L81 - sed -e "s/-Werror//g" -i Makefile - - substituteInPlace netplan/cli/utils.py \ - --replace-fail "/usr/libexec/netplan/generate" "${placeholder "out"}/lib/netplan/generate" + substituteInPlace netplan-configure.service \ + --replace-fail "/usr/libexec/netplan/" "${placeholder "out"}/libexec/netplan/" + substituteInPlace netplan_cli/cli/utils.py \ + --replace-fail "/usr/libexec/netplan/" "${placeholder "out"}/libexec/netplan/" ''; - makeFlags = [ - "PREFIX=" - "DESTDIR=$(out)" - "SYSTEMD_GENERATOR_DIR=lib/systemd/system-generators/" - "SYSTEMD_UNIT_DIR=lib/systemd/units/" - "BASH_COMPLETIONS_DIR=share/bash-completion/completions" + # Wrap the systemd generator to force its argv0 value, ensuring it detects itself being invoked as such + # As netplan installs a systemd generator to function, it requires `systemd.packages = [ pkgs.netplan ];` to make systemd use it + postFixup = '' + wrapProgram $out/bin/netplan \ + --prefix PYTHONPATH : "$out/${pythonenv.sitePackages}:${pythonenv}/${pythonenv.sitePackages}" \ + --prefix LD_LIBRARY_PATH : "$out/lib" \ + --prefix PATH : "${lib.makeBinPath [ iproute2 ]}" \ + --inherit-argv0 + wrapProgram $out/lib/systemd/system-generators/netplan \ + --argv0 /etc/systemd/system-generators/netplan + ''; + + mesonFlags = [ + (lib.mesonBool "testing" false) ]; + passthru.tests = { + inherit (nixosTests) netplan; + }; + meta = { description = "Backend-agnostic network configuration in YAML"; homepage = "https://netplan.io";