nm.team: Team interface basic implementation

This is a team interface state example:

interfaces:
- name: team0
  type: team
  state: up
  ipv4:
    enabled: false
  ipv6:
    enabled: false
  mtu: 1500
  team:
    ports:
    - name: eth1
    - name: eth2
    runner:
      mode: loadbalance

Signed-off-by: Fernando Fernandez Mancera <ffmancera@riseup.net>
This commit is contained in:
Fernando Fernandez Mancera 2019-12-23 09:51:37 +01:00 committed by Edward Haas
parent 224b99150c
commit 8585d91ebf
12 changed files with 224 additions and 29 deletions

View File

@ -283,7 +283,7 @@ if [[ "$CI" == "true" ]];then
fi
mkdir -p $EXPORT_DIR
CONTAINER_ID="$(${CONTAINER_CMD} run --privileged -d -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v $PROJECT_PATH:$CONTAINER_WORKSPACE -v $EXPORT_DIR:$CONT_EXPORT_DIR $CONTAINER_IMAGE)"
CONTAINER_ID="$(${CONTAINER_CMD} run --privileged -d -e CI=$CI -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v $PROJECT_PATH:$CONTAINER_WORKSPACE -v $EXPORT_DIR:$CONT_EXPORT_DIR $CONTAINER_IMAGE)"
[ -n "$debug_exit_shell" ] && trap open_shell EXIT || trap run_exit EXIT
if [[ -v copr_repo ]];then

View File

@ -103,6 +103,7 @@ def _interfaces():
iface_info.update(nm.vlan.get_info(dev))
iface_info.update(nm.vxlan.get_info(dev))
iface_info.update(nm.bridge.get_info(dev))
iface_info.update(nm.team.get_info(dev))
if nm.bond.is_bond_type_id(type_id):
bondinfo = nm.bond.get_bond_info(dev)

View File

@ -400,7 +400,7 @@ def _build_connection_profile(iface_desired_state, base_con_profile=None):
if sriov_setting:
settings.append(sriov_setting)
team_setting = team.create_setting(iface_desired_state)
team_setting = team.create_setting(iface_desired_state, base_con_profile)
if team_setting:
settings.append(team_setting)

View File

@ -196,6 +196,11 @@ class ConnectionProfile:
"Missing 'NetworkManager-ovs' plugin"
f" to handle device={self.devname}"
)
elif self._is_team_plugin_missing(e):
self._mainloop.quit(
"Missing 'NetworkManager-team' plugin"
f" to handle device={self.devname}"
)
else:
self._mainloop.quit(
"Connection activation failed on {} {}: error={}".format(
@ -257,6 +262,15 @@ class ConnectionProfile:
)
)
def _is_team_plugin_missing(self, err):
return (
isinstance(err, nmclient.GLib.GError)
and err.domain == nmclient.NM_MANAGER_ERROR_DOMAIN
and self._con_profile.is_type(
nmclient.NM.SETTING_TEAM_SETTING_NAME
)
)
def _get_activation_metadata(self):
if self._nmdevice:
activation_type = "device"

View File

@ -17,14 +17,99 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from libnmstate.error import NmstateNotImplementedError
import json
from libnmstate.nm import connection as nm_connection
from libnmstate.nm import nmclient
from libnmstate.schema import Interface
from libnmstate.schema import Team
def create_setting(iface_state):
setting = None
team_config = iface_state.get(Team.CONFIG_SUBTREE)
if team_config:
raise NmstateNotImplementedError
TEAMD_JSON_DEVICE = "device"
return setting
def create_setting(iface_state, base_con_profile):
team_setting = None
team_config = iface_state.get(Team.CONFIG_SUBTREE)
if not team_config:
return None
if base_con_profile:
team_setting = base_con_profile.get_setting_duplicate(
nmclient.NM.SETTING_TEAM_SETTING_NAME
)
if not team_setting:
team_setting = nmclient.NM.SettingTeam.new()
teamd_config = _convert_team_config_to_teamd_format(
team_config, iface_state[Interface.NAME]
)
team_setting.props.config = json.dumps(teamd_config)
return team_setting
def _convert_team_config_to_teamd_format(team_config, ifname):
team_config[TEAMD_JSON_DEVICE] = ifname
team_ports = team_config.get(Team.PORT_SUBTREE, ())
team_ports_formatted = {
port[Team.Port.NAME]: _dict_key_filter(port, Team.Port.NAME)
for port in team_ports
}
team_config[Team.PORT_SUBTREE] = team_ports_formatted
return team_config
def _dict_key_filter(dict_to_filter, key):
return dict(filter(lambda elem: elem[0] == key, dict_to_filter.items()))
def get_info(device):
"""
Provide the current active teamd values for an interface.
Ref: https://bugzilla.redhat.com/1792232
"""
info = {}
connection = nm_connection.ConnectionProfile()
connection.import_by_device(device)
if not connection.profile:
return info
team_setting = connection.profile.get_setting_by_name(
nmclient.NM.SETTING_TEAM_SETTING_NAME
)
if team_setting:
teamd_json = team_setting.get_config()
if teamd_json:
teamd_config = json.loads(teamd_json)
team_config = _convert_teamd_config_to_nmstate_config(teamd_config)
info[Team.CONFIG_SUBTREE] = team_config
return info
def _convert_teamd_config_to_nmstate_config(team_config):
team_config.pop("device")
port_config = team_config.get(Team.PORT_SUBTREE)
if port_config:
team_port = _teamd_port_to_nmstate_port(port_config)
team_config[Team.PORT_SUBTREE] = team_port
return team_config
def _teamd_port_to_nmstate_port(port_config):
port_list = []
for name, port in port_config.items():
port.update({Team.Port.NAME: name})
port_list.append(port)
return port_list

View File

@ -44,6 +44,7 @@ class Api2Nm:
"ethernet": nmclient.NM.SETTING_WIRED_SETTING_NAME,
"bond": nmclient.NM.SETTING_BOND_SETTING_NAME,
"dummy": nmclient.NM.SETTING_DUMMY_SETTING_NAME,
"team": nmclient.NM.SETTING_TEAM_SETTING_NAME,
"vlan": nmclient.NM.SETTING_VLAN_SETTING_NAME,
"vxlan": nmclient.NM.SETTING_VXLAN_SETTING_NAME,
"linux-bridge": nmclient.NM.SETTING_BRIDGE_SETTING_NAME,

View File

@ -17,6 +17,7 @@ RUN dnf copr enable nmstate/ovs-el8 -y && \
RUN dnf -y install --setopt=install_weak_deps=False \
NetworkManager \
NetworkManager-ovs \
NetworkManager-team \
NetworkManager-config-server \
openvswitch2.11 \
systemd-udev \

View File

@ -11,6 +11,7 @@ RUN bash ./docker_enable_systemd.sh && rm ./docker_enable_systemd.sh
RUN dnf -y install --setopt=install_weak_deps=False \
NetworkManager \
NetworkManager-ovs \
NetworkManager-team \
NetworkManager-config-server \
dhcp-client \
openvswitch \

View File

@ -29,8 +29,10 @@ Requires: NetworkManager-libnm >= 1:1.20
Recommends: NetworkManager
# Avoid automatically generated profiles
Recommends: NetworkManager-config-server
# Use Suggests for NetworkManager-ovs since it is only required for OVS support
# Use Suggests for NetworkManager-ovs and NetworkManager-team since it is only
# required for OVS and team support
Suggests: NetworkManager-ovs
Suggests: NetworkManager-team
%description -n python3-%{libname}

View File

@ -25,6 +25,7 @@ import pytest
import libnmstate
from .testlib import cmdlib
from .testlib import ifacelib
@ -98,3 +99,10 @@ def _get_osname():
if line.startswith("PRETTY_NAME="):
return line.split("=", maxsplit=1)[1].strip().strip('"')
return ""
@pytest.fixture(scope="session", autouse=True)
def download_nm_team_plugin():
cmdlib.exec_cmd(
("dnf", "install", "-y", "--downloadonly", "NetworkManager-team",)
)

View File

@ -17,34 +17,115 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from contextlib import contextmanager
import os
import pytest
import libnmstate
from libnmstate.error import NmstateNotImplementedError
from libnmstate.error import NmstateLibnmError
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import Team
from .testlib import assertlib
from .testlib import cmdlib
DNF_INSTALL_NM_TEAM_PLUGIN_CMD = (
"dnf",
"install",
"-y",
"--cacheonly",
"NetworkManager-team",
)
DNF_REMOVE_NM_TEAM_PLUGIN_CMD = (
"dnf",
"remove",
"-y",
"-q",
"NetworkManager-team",
)
SYSTEMCTL_RESTART_NM_CMD = ("systemctl", "restart", "NetworkManager")
TEAM0 = "team0"
TEAM0_STATE = {
Interface.KEY: [
{
Interface.NAME: TEAM0,
Interface.TYPE: InterfaceType.TEAM,
Team.CONFIG_SUBTREE: {
Team.PORT_SUBTREE: [{Team.Port.NAME: "eth1"}]
},
}
]
}
@pytest.mark.xfail(
raises=NmstateNotImplementedError,
reason="Team interface is not supported yet",
strict=True,
pytestmark = pytest.mark.skipif(
os.environ.get("CI") == "true",
reason="Team kmod not available in Travis CI",
)
def test_sriov_not_implemented():
libnmstate.apply(TEAM0_STATE)
def test_create_team_iface():
with team_interface(TEAM0) as team_state:
assertlib.assert_state(team_state)
def test_edit_team_iface():
with team_interface(TEAM0) as team_state:
team_state[Interface.KEY][0][Team.CONFIG_SUBTREE] = {
Team.RUNNER_SUBTREE: {
Team.Runner.NAME: Team.Runner.RunnerMode.LOAD_BALANCE
},
Team.PORT_SUBTREE: [{Team.Port.NAME: "eth1"}],
}
libnmstate.apply(team_state)
assertlib.assert_state(team_state)
def test_nm_team_plugin_missing():
with disable_nm_team_plugin():
with pytest.raises(NmstateLibnmError):
libnmstate.apply(
{
Interface.KEY: [
{
Interface.NAME: TEAM0,
Interface.TYPE: InterfaceType.TEAM,
Interface.STATE: InterfaceState.UP,
}
]
}
)
@contextmanager
def team_interface(ifname):
desired_state = {
Interface.KEY: [
{
Interface.NAME: ifname,
Interface.TYPE: InterfaceType.TEAM,
Interface.STATE: InterfaceState.UP,
}
]
}
libnmstate.apply(desired_state)
try:
yield desired_state
finally:
libnmstate.apply(
{
Interface.KEY: [
{
Interface.NAME: ifname,
Interface.STATE: InterfaceState.ABSENT,
}
]
}
)
@contextmanager
def disable_nm_team_plugin():
cmdlib.exec_cmd(DNF_REMOVE_NM_TEAM_PLUGIN_CMD)
cmdlib.exec_cmd(SYSTEMCTL_RESTART_NM_CMD)
try:
yield
finally:
cmdlib.exec_cmd(DNF_INSTALL_NM_TEAM_PLUGIN_CMD)
cmdlib.exec_cmd(SYSTEMCTL_RESTART_NM_CMD)

View File

@ -48,6 +48,7 @@ def test_api2nm_iface_type_map(NM_mock):
"ovs-bridge": NM_mock.SETTING_OVS_BRIDGE_SETTING_NAME,
"ovs-port": NM_mock.SETTING_OVS_PORT_SETTING_NAME,
"ovs-interface": NM_mock.SETTING_OVS_INTERFACE_SETTING_NAME,
"team": NM_mock.SETTING_TEAM_SETTING_NAME,
"vlan": NM_mock.SETTING_VLAN_SETTING_NAME,
"linux-bridge": NM_mock.SETTING_BRIDGE_SETTING_NAME,
"vxlan": NM_mock.SETTING_VXLAN_SETTING_NAME,