nm ovs: Support modification of OVS external_ids

NetworkManager 1.30+ has introduced the support of changing OVS `external_ids`.

Integration test cases included.

Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Gris Ge 2021-01-24 22:10:55 +08:00
parent 57e2547572
commit c99e708cd8
6 changed files with 220 additions and 26 deletions

View File

@ -27,6 +27,7 @@ from libnmstate.schema import InterfaceType
from libnmstate.schema import LinuxBridge as LB
from libnmstate.schema import OVSBridge as OvsB
from libnmstate.schema import OVSInterface
from libnmstate.schema import OvsDB
from libnmstate.ifaces.bridge import BridgeIface
@ -42,6 +43,7 @@ from .lldp import apply_lldp_setting
from .macvlan import create_setting as create_macvlan_setting
from .ovs import create_bridge_setting as create_ovs_bridge_setting
from .ovs import create_interface_setting as create_ovs_interface_setting
from .ovs import create_ovsdb_external_ids_setting
from .ovs import create_port_setting as create_ovs_port_setting
from .sriov import create_setting as create_sriov_setting
from .team import create_setting as create_team_setting
@ -151,7 +153,7 @@ def create_new_nm_simple_conn(iface, nm_profile):
settings.append(linux_bridge_setting)
elif iface.type == InterfaceType.OVS_BRIDGE:
ovs_bridge_state = iface_info.get(OvsB.CONFIG_SUBTREE, {})
ovs_bridge_options = ovs_bridge_state.get(OvsB.OPTIONS_SUBTREE)
ovs_bridge_options = ovs_bridge_state.get(OvsB.OPTIONS_SUBTREE, {})
if ovs_bridge_options:
settings.append(create_ovs_bridge_setting(ovs_bridge_options))
elif iface.type == InterfaceType.OVS_PORT:
@ -208,6 +210,20 @@ def create_new_nm_simple_conn(iface, nm_profile):
if veth_setting:
settings.append(veth_setting)
if (
iface.controller_type
in (
InterfaceType.OVS_BRIDGE,
InterfaceType.OVS_PORT,
)
or iface.type == InterfaceType.OVS_BRIDGE
):
nm_setting = create_ovsdb_external_ids_setting(
iface_info.get(OvsDB.OVS_DB_SUBTREE, {})
)
if nm_setting:
settings.append(nm_setting)
nm_simple_conn = NM.SimpleConnection.new()
for setting in settings:
nm_simple_conn.add_setting(setting)

View File

@ -20,13 +20,14 @@
import logging
from operator import itemgetter
from libnmstate.ifaces import ovs
from libnmstate.ifaces.bridge import BridgeIface
from libnmstate.ifaces.ovs import OvsPortIface
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceType
from libnmstate.schema import OVSBridge as OB
from libnmstate.schema import OVSInterface
from libnmstate.ifaces import ovs
from libnmstate.ifaces.bridge import BridgeIface
from libnmstate.ifaces.ovs import OvsPortIface
from libnmstate.schema import OvsDB
from .common import NM
@ -35,6 +36,8 @@ PORT_PROFILE_PREFIX = "ovs-port-"
CONTROLLER_TYPE_METADATA = "_controller_type"
CONTROLLER_METADATA = "_controller"
SETTING_OVS_EXTERNALIDS = "SettingOvsExternalIDs"
SETTING_OVS_EXTERNAL_IDS_SETTING_NAME = "ovs-external-ids"
NM_OVS_VLAN_MODE_MAP = {
"trunk": OB.Port.Vlan.Mode.TRUNK,
@ -70,6 +73,21 @@ def create_bridge_setting(options_state):
return bridge_setting
def create_ovsdb_external_ids_setting(ovsdb_conf):
if _is_nm_support_ovs_external_ids():
nm_setting = getattr(NM, SETTING_OVS_EXTERNALIDS).new()
for key, value in ovsdb_conf.get(OvsDB.EXTERNAL_IDS, {}).items():
if not key.startswith("NM."):
nm_setting.set_data(key, str(value))
return nm_setting
else:
logging.warn(
"Please upgrade NetworkManger to 1.30+ "
"for the support OVS external ID modification"
)
return None
def create_port_setting(port_state):
port_setting = NM.SettingOvsPort.new()
@ -138,6 +156,22 @@ def get_ovs_bridge_info(nm_dev_ovs_br):
return iface_info
def get_ovsdb_external_ids(nm_profile):
iface_info = {}
if _is_nm_support_ovs_external_ids():
nm_setting = nm_profile.get_setting_by_name(
SETTING_OVS_EXTERNAL_IDS_SETTING_NAME
)
if nm_setting:
external_ids = {}
for key in nm_setting.get_data_keys():
external_ids[key] = nm_setting.get_data(key)
iface_info[OvsDB.OVS_DB_SUBTREE] = {
OvsDB.EXTERNAL_IDS: external_ids
}
return iface_info
def _get_bridge_nmstate_ports_info(nm_dev_ovs_br):
ports_info = []
for nm_dev_ovs_port in nm_dev_ovs_br.get_slaves():
@ -288,3 +322,7 @@ def create_iface_for_nm_ovs_port(iface):
CONTROLLER_TYPE_METADATA: iface_info[CONTROLLER_TYPE_METADATA],
}
)
def _is_nm_support_ovs_external_ids():
return hasattr(NM, SETTING_OVS_EXTERNALIDS)

View File

@ -46,6 +46,7 @@ from .lldp import get_info as get_lldp_info
from .macvlan import get_current_macvlan_type
from .ovs import get_interface_info as get_ovs_interface_info
from .ovs import get_ovs_bridge_info
from .ovs import get_ovsdb_external_ids
from .ovs import has_ovs_capability
from .profiles import NmProfiles
from .profiles import get_all_applied_configs
@ -151,6 +152,9 @@ class NetworkManagerPlugin(NmstatePlugin):
elif iface_info[Interface.TYPE] == InterfaceType.OVS_PORT:
continue
if applied_config:
iface_info.update(get_ovsdb_external_ids(applied_config))
info.append(iface_info)
info.sort(key=itemgetter("name"))

View File

@ -24,16 +24,23 @@ from libnmstate.schema import Interface
from libnmstate.schema import InterfaceIPv4
from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import OvsDB
from libnmstate.schema import VLAN
from ..testlib import assertlib
from ..testlib import statelib
from ..testlib import cmdlib
from ..testlib import statelib
from ..testlib.env import nm_major_minor_version
from ..testlib.ovslib import Bridge
from ..testlib.plugin import tmp_plugin_dir
BRIDGE0 = "brtest0"
IFACE0 = "ovstest0"
OVSDB_EXT_IDS_CONF1 = {"foo": "abc", "bak": 1}
OVSDB_EXT_IDS_CONF1_STR = {"foo": "abc", "bak": "1"}
OVSDB_EXT_IDS_CONF2 = {"bak": 2}
OVSDB_EXT_IDS_CONF2_STR = {"bak": "2"}
@pytest.fixture
@ -157,3 +164,99 @@ def _get_ovs_port_profile_uuid_of_ovs_interface(iface_name):
check=True,
)
return ovs_port_uuid
@pytest.fixture
def disable_ovsdb_plugin():
with tmp_plugin_dir():
yield
@pytest.mark.skipif(
nm_major_minor_version() <= 1.28,
reason="OVS external ID is not supported by NetworkManager 1.28-.",
)
class TestNmOvsExternalIds:
def test_create_ovs_bridge_with_external_ids(self, disable_ovsdb_plugin):
bridge = Bridge(BRIDGE0)
bridge.set_ovs_db({OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF1})
bridge.add_internal_port(
IFACE0,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF2},
)
with bridge.create() as state:
assertlib.assert_state_match(state)
new_state = statelib.show_only((BRIDGE0, IFACE0))
assert (
new_state[Interface.KEY][0][OvsDB.OVS_DB_SUBTREE][
OvsDB.EXTERNAL_IDS
]
== OVSDB_EXT_IDS_CONF1_STR
)
assert (
new_state[Interface.KEY][1][OvsDB.OVS_DB_SUBTREE][
OvsDB.EXTERNAL_IDS
]
== OVSDB_EXT_IDS_CONF2_STR
)
def test_modify_ovs_bridge_external_ids(self, disable_ovsdb_plugin):
bridge = Bridge(BRIDGE0)
bridge.set_ovs_db({OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF1})
bridge.add_internal_port(
IFACE0,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF2},
)
with bridge.create():
changed_bridge = Bridge(BRIDGE0)
changed_bridge.set_ovs_db(
{OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF2}
)
changed_bridge.add_internal_port(
IFACE0,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF1},
)
changed_bridge.apply()
assertlib.assert_state_match(changed_bridge.state)
new_state = statelib.show_only((BRIDGE0, IFACE0))
assert (
new_state[Interface.KEY][0][OvsDB.OVS_DB_SUBTREE][
OvsDB.EXTERNAL_IDS
]
== OVSDB_EXT_IDS_CONF2_STR
)
assert (
new_state[Interface.KEY][1][OvsDB.OVS_DB_SUBTREE][
OvsDB.EXTERNAL_IDS
]
== OVSDB_EXT_IDS_CONF1_STR
)
def test_ovs_bridge_remoev_external_ids(self, disable_ovsdb_plugin):
bridge = Bridge(BRIDGE0)
bridge.set_ovs_db({OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF1})
bridge.add_internal_port(
IFACE0,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: OVSDB_EXT_IDS_CONF2},
)
with bridge.create():
changed_bridge = Bridge(BRIDGE0)
changed_bridge.set_ovs_db({OvsDB.EXTERNAL_IDS: {}})
changed_bridge.add_internal_port(
IFACE0,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: {}},
)
changed_bridge.apply()
assertlib.assert_state_match(changed_bridge.state)
new_state = statelib.show_only((BRIDGE0, IFACE0))
assert not new_state[Interface.KEY][0][OvsDB.OVS_DB_SUBTREE][
OvsDB.EXTERNAL_IDS
]
assert not new_state[Interface.KEY][1][OvsDB.OVS_DB_SUBTREE][
OvsDB.EXTERNAL_IDS
]

View File

@ -18,8 +18,6 @@
#
import copy
import os
import tempfile
import pytest
@ -37,6 +35,7 @@ from libnmstate.plugin import NmstatePlugin
from .testlib import statelib
from .testlib.servicelib import disable_service
from .testlib.plugin import tmp_plugin_dir
FOO_IFACE_NAME = "foo1"
@ -151,37 +150,39 @@ LO_IFACE_INFO = {
@pytest.fixture
def tmp_plugin_dir():
with tempfile.TemporaryDirectory() as plugin_dir:
os.environ["NMSTATE_PLUGIN_DIR"] = plugin_dir
yield plugin_dir
os.environ.pop("NMSTATE_PLUGIN_DIR")
def with_foo_plugin():
with tmp_plugin_dir() as plugin_dir:
_gen_plugin_foo(plugin_dir)
yield
@pytest.fixture
def with_foo_plugin(tmp_plugin_dir):
_gen_plugin_foo(tmp_plugin_dir)
def with_multiple_plugins():
with tmp_plugin_dir() as plugin_dir:
_gen_plugin_foo(plugin_dir)
_gen_plugin_bar(plugin_dir)
yield
@pytest.fixture
def with_multiple_plugins(tmp_plugin_dir):
_gen_plugin_foo(tmp_plugin_dir)
_gen_plugin_bar(tmp_plugin_dir)
def with_route_plugin():
with tmp_plugin_dir() as plugin_dir:
_gen_plugin_route_foo(plugin_dir)
yield
@pytest.fixture
def with_route_plugin(tmp_plugin_dir):
_gen_plugin_route_foo(tmp_plugin_dir)
def with_route_rule_plugin():
with tmp_plugin_dir() as plugin_dir:
_gen_plugin_route_rule_foo(plugin_dir)
yield
@pytest.fixture
def with_route_rule_plugin(tmp_plugin_dir):
_gen_plugin_route_rule_foo(tmp_plugin_dir)
@pytest.fixture
def with_dns_plugin(tmp_plugin_dir):
_gen_plugin_dns_foo(tmp_plugin_dir)
def with_dns_plugin():
with tmp_plugin_dir() as plugin_dir:
_gen_plugin_dns_foo(plugin_dir)
yield
def _gen_plugin(

View File

@ -0,0 +1,32 @@
#
# Copyright (c) 2021 Red Hat, Inc.
#
# This file is part of nmstate
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from contextlib import contextmanager
import os
import tempfile
@contextmanager
def tmp_plugin_dir():
with tempfile.TemporaryDirectory() as plugin_dir:
os.environ["NMSTATE_PLUGIN_DIR"] = plugin_dir
try:
yield plugin_dir
finally:
os.environ.pop("NMSTATE_PLUGIN_DIR")