openvswitch: better integration with systemd

Systemd dependencies for scripted mode
were refactored according to analysis in #34586.

networking.vswitches can now be used with systemd-networkd,
although they are not supported by the daemon, a nixos receipe
creates the switch and attached required interfaces (just like
the scripted version).

Vlans and internal interfaces are implemented following the
  template format i.e. each interface is
described using an attributeSet (vlan and type at the moment).
If vlan is present, then interface is added to the vswitch with
given tag (access mode). Type internal enabled vswitch to create
interfaces (see openvswitch docs).

Added configuration for configuring supported openFlow version on
the vswitch

This commit is a split from the original PR #35127.
This commit is contained in:
Netix (Espinet François) 2018-12-29 11:16:51 +01:00
parent aa4a1b01d5
commit cd3597b486
No known key found for this signature in database
GPG key ID: 0176F318CB25629B
6 changed files with 151 additions and 34 deletions

View file

@ -19,7 +19,7 @@ let
map (i: i.name) (filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces) map (i: i.name) (filter (i: if i.useDHCP != null then !i.useDHCP else i.ipv4.addresses != [ ]) interfaces)
++ mapAttrsToList (i: _: i) config.networking.sits ++ mapAttrsToList (i: _: i) config.networking.sits
++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bridges)) ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bridges))
++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.vswitches)) ++ flatten (concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues config.networking.vswitches))
++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bonds)) ++ concatLists (attrValues (mapAttrs (n: v: v.interfaces) config.networking.bonds))
++ config.networking.dhcpcd.denyInterfaces; ++ config.networking.dhcpcd.denyInterfaces;

View file

@ -10,7 +10,7 @@ let
slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds) slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds)
++ concatMap (i: i.interfaces) (attrValues cfg.bridges) ++ concatMap (i: i.interfaces) (attrValues cfg.bridges)
++ concatMap (i: i.interfaces) (attrValues cfg.vswitches) ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches)
++ concatMap (i: [i.interface]) (attrValues cfg.macvlans) ++ concatMap (i: [i.interface]) (attrValues cfg.macvlans)
++ concatMap (i: [i.interface]) (attrValues cfg.vlans); ++ concatMap (i: [i.interface]) (attrValues cfg.vlans);
@ -336,34 +336,47 @@ let
createVswitchDevice = n: v: nameValuePair "${n}-netdev" createVswitchDevice = n: v: nameValuePair "${n}-netdev"
(let (let
deps = concatLists (map deviceDependency v.interfaces); deps = concatLists (map deviceDependency (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces)));
internalConfigs = concatMap (i: ["network-link-${i}.service" "network-addresses-${i}.service"]) (attrNames (filterAttrs (_: config: config.type == "internal") v.interfaces));
ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules; ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
in in
{ description = "Open vSwitch Interface ${n}"; { description = "Open vSwitch Interface ${n}";
wantedBy = [ "network-setup.service" "vswitchd.service" ] ++ deps; wantedBy = [ "network-setup.service" (subsystemDevice n) ] ++ internalConfigs;
bindsTo = [ "vswitchd.service" (subsystemDevice n) ] ++ deps; # before = [ "network-setup.service" ];
partOf = [ "network-setup.service" "vswitchd.service" ]; # should work without internalConfigs dependencies because address/link configuration depends
after = [ "network-pre.target" "vswitchd.service" ] ++ deps; # on the device, which is created by ovs-vswitchd with type=internal, but it does not...
before = [ "network-setup.service" ]; before = [ "network-setup.service" ] ++ internalConfigs;
partOf = [ "network-setup.service" ]; # shutdown the bridge when network is shutdown
bindsTo = [ "ovs-vswitchd.service" ]; # requires ovs-vswitchd to be alive at all times
after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps; # start switch after physical interfaces and vswitch daemon
wants = deps; # if one or more interface fails, the switch should continue to run
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true; serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute config.virtualisation.vswitch.package ]; path = [ pkgs.iproute config.virtualisation.vswitch.package ];
preStart = ''
echo "Resetting Open vSwitch ${n}..."
ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
-- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
'';
script = '' script = ''
echo "Removing old Open vSwitch ${n}..." echo "Configuring Open vSwitch ${n}..."
ovs-vsctl --if-exists del-br ${n} ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
echo "Adding Open vSwitch ${n}..."
ovs-vsctl -- add-br ${n} ${concatMapStrings (i: " -- add-port ${n} ${i}") v.interfaces} \
${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \ ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \
${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)} ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
echo "Adding OpenFlow rules for Open vSwitch ${n}..." echo "Adding OpenFlow rules for Open vSwitch ${n}..."
ovs-ofctl add-flows ${n} ${ofRules} ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
''; '';
postStop = '' postStop = ''
echo "Cleaning Open vSwitch ${n}"
echo "Shuting down internal ${n} interface"
ip link set ${n} down || true ip link set ${n} down || true
ovs-ofctl del-flows ${n} || true echo "Deleting flows for ${n}"
ovs-vsctl --if-exists del-br ${n} ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
echo "Deleting Open vSwitch ${n}"
ovs-vsctl --if-exists del-br ${n} || true
''; '';
}); });
@ -476,9 +489,9 @@ let
# Remove Dead Interfaces # Remove Dead Interfaces
ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}"
ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}" ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}"
# We try to bring up the logical VLAN interface. If the master # We try to bring up the logical VLAN interface. If the master
# interface the logical interface is dependent upon is not up yet we will # interface the logical interface is dependent upon is not up yet we will
# fail to immediately bring up the logical interface. The resulting logical # fail to immediately bring up the logical interface. The resulting logical
# interface will brought up later when the master interface is up. # interface will brought up later when the master interface is up.
ip link set "${n}" up || true ip link set "${n}" up || true

View file

@ -1,4 +1,4 @@
{ config, lib, utils, ... }: { config, lib, utils, pkgs, ... }:
with utils; with utils;
with lib; with lib;
@ -18,7 +18,10 @@ let
concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds)) concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds))
++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges)) ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges))
++ map (sit: sit.dev) (attrValues cfg.sits) ++ map (sit: sit.dev) (attrValues cfg.sits)
++ map (vlan: vlan.interface) (attrValues cfg.vlans); ++ map (vlan: vlan.interface) (attrValues cfg.vlans)
# add dependency to physical or independently created vswitch member interface
# TODO: warn the user that any address configured on those interfaces will be useless
++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);
in in
@ -51,11 +54,6 @@ in
networking.dhcpcd.enable = mkDefault false; networking.dhcpcd.enable = mkDefault false;
systemd.services.network-local-commands = {
after = [ "systemd-networkd.service" ];
bindsTo = [ "systemd-networkd.service" ];
};
systemd.network = systemd.network =
let let
domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain); domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
@ -233,6 +231,63 @@ in
# This forces the network interface creator to initialize slaves. # This forces the network interface creator to initialize slaves.
networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves); networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves);
systemd.services = let
# We must escape interfaces due to the systemd interpretation
subsystemDevice = interface:
"sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
# support for creating openvswitch switches
createVswitchDevice = n: v: nameValuePair "${n}-netdev"
(let
deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces));
ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
in
{ description = "Open vSwitch Interface ${n}";
wantedBy = [ "network.target" (subsystemDevice n) ];
# and create bridge before systemd-networkd starts because it might create internal interfaces
before = [ "systemd-networkd.service" ];
# shutdown the bridge when network is shutdown
partOf = [ "network.target" ];
# requires ovs-vswitchd to be alive at all times
bindsTo = [ "ovs-vswitchd.service" ];
# start switch after physical interfaces and vswitch daemon
after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps;
wants = deps; # if one or more interface fails, the switch should continue to run
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
path = [ pkgs.iproute config.virtualisation.vswitch.package ];
preStart = ''
echo "Resetting Open vSwitch ${n}..."
ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
-- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
'';
script = ''
echo "Configuring Open vSwitch ${n}..."
ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \
${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}
echo "Adding OpenFlow rules for Open vSwitch ${n}..."
ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
'';
postStop = ''
echo "Cleaning Open vSwitch ${n}"
echo "Shuting down internal ${n} interface"
ip link set ${n} down || true
echo "Deleting flows for ${n}"
ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
echo "Deleting Open vSwitch ${n}"
ovs-vsctl --if-exists del-br ${n} || true
'';
});
in mapAttrs' createVswitchDevice cfg.vswitches
// {
"network-local-commands" = {
after = [ "systemd-networkd.service" ];
bindsTo = [ "systemd-networkd.service" ];
};
};
}; };
} }

View file

@ -13,7 +13,7 @@ let
slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds) slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds)
++ concatMap (i: i.interfaces) (attrValues cfg.bridges) ++ concatMap (i: i.interfaces) (attrValues cfg.bridges)
++ concatMap (i: i.interfaces) (attrValues cfg.vswitches); ++ concatMap (i: attrNames (filterAttrs (name: config: ! (config.type == "internal" || hasAttr name cfg.interfaces)) i.interfaces)) (attrValues cfg.vswitches);
slaveIfs = map (i: cfg.interfaces.${i}) (filter (i: cfg.interfaces ? ${i}) slaves); slaveIfs = map (i: cfg.interfaces.${i}) (filter (i: cfg.interfaces ? ${i}) slaves);
@ -310,6 +310,32 @@ let
}; };
vswitchInterfaceOpts = {name, ...}: {
options = {
name = mkOption {
description = "Name of the interface";
example = "eth0";
type = types.str;
};
vlan = mkOption {
description = "Vlan tag to apply to interface";
example = 10;
type = types.nullOr types.int;
default = null;
};
type = mkOption {
description = "Openvswitch type to assign to interface";
example = "internal";
type = types.nullOr types.str;
default = null;
};
};
};
hexChars = stringToCharacters "0123456789abcdef"; hexChars = stringToCharacters "0123456789abcdef";
isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s)); isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s));
@ -460,8 +486,8 @@ in
networking.vswitches = mkOption { networking.vswitches = mkOption {
default = { }; default = { };
example = example =
{ vs0.interfaces = [ "eth0" "eth1" ]; { vs0.interfaces = { eth0 = { }; lo1 = { type="internal"; }; };
vs1.interfaces = [ "eth2" "wlan0" ]; vs1.interfaces = [ { name = "eth2"; } { name = "lo2"; type="internal"; } ];
}; };
description = description =
'' ''
@ -478,9 +504,8 @@ in
interfaces = mkOption { interfaces = mkOption {
example = [ "eth0" "eth1" ]; example = [ "eth0" "eth1" ];
type = types.listOf types.str; description = "The physical network interfaces connected by the vSwitch.";
description = type = with types; loaOf (submodule vswitchInterfaceOpts);
"The physical network interfaces connected by the vSwitch.";
}; };
controllers = mkOption { controllers = mkOption {
@ -504,6 +529,25 @@ in
''; '';
}; };
# TODO: custom "openflow version" type, with list from existing openflow protocols
supportedOpenFlowVersions = mkOption {
type = types.listOf types.str;
example = [ "OpenFlow10" "OpenFlow13" "OpenFlow14" ];
default = [ "OpenFlow13" ];
description = ''
Supported versions to enable on this switch.
'';
};
# TODO: use same type as elements from supportedOpenFlowVersions
openFlowVersion = mkOption {
type = types.str;
default = "OpenFlow13";
description = ''
Version of OpenFlow protocol to use when communicating with the switch internally (e.g. with <literal>openFlowRules</literal>).
'';
};
extraOvsctlCmds = mkOption { extraOvsctlCmds = mkOption {
type = types.lines; type = types.lines;
default = ""; default = "";

View file

@ -219,7 +219,7 @@ in {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
requires = [ "libvirtd-config.service" ]; requires = [ "libvirtd-config.service" ];
after = [ "systemd-udev-settle.service" "libvirtd-config.service" ] after = [ "systemd-udev-settle.service" "libvirtd-config.service" ]
++ optional vswitch.enable "vswitchd.service"; ++ optional vswitch.enable "ovs-vswitchd.service";
environment.LIBVIRTD_ARGS = ''--config "${configFile}" ${concatStringsSep " " cfg.extraOptions}''; environment.LIBVIRTD_ARGS = ''--config "${configFile}" ${concatStringsSep " " cfg.extraOptions}'';

View file

@ -124,7 +124,7 @@ in {
''; '';
}; };
systemd.services.vswitchd = { systemd.services.ovs-vswitchd = {
description = "Open_vSwitch Daemon"; description = "Open_vSwitch Daemon";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
bindsTo = [ "ovsdb.service" ]; bindsTo = [ "ovsdb.service" ];
@ -139,6 +139,8 @@ in {
PIDFile = "/run/openvswitch/ovs-vswitchd.pid"; PIDFile = "/run/openvswitch/ovs-vswitchd.pid";
# Use service type 'forking' to correctly determine when vswitchd is ready. # Use service type 'forking' to correctly determine when vswitchd is ready.
Type = "forking"; Type = "forking";
Restart = "always";
RestartSec = 3;
}; };
}; };
@ -182,4 +184,7 @@ in {
''; '';
}; };
})])); })]));
meta.maintainers = with maintainers; [ netixx ];
} }