schema, ovs: introduce OVS Patch support

This patch introduces the schema for OVS Patch support. In order to add
a patch interface to a OVS bridge, the user needs to define an OVS
interface with the patch configuration in the proper subtree.

In addition, it introduces the implementation needed for the OVS patch
port support.

Example:

```
interfaces:
- name: patch0
  type: ovs-interface
  state: up
  patch:
    peer: patch1
- name: ovs-br0
  type: ovs-bridge
  state: up
  bridge:
    options:
      stp: true
    port:
    - name: patch0
- name: patch1
  type: ovs-interface
  state: up
  patch:
    peer: patch0
- name: ovs-br1
  type: ovs-bridge
  state: up
  bridge:
    options:
      stp: true
    port:
    - name: patch1
```
Signed-off-by: Fernando Fernandez Mancera <ffmancera@riseup.net>
This commit is contained in:
Fernando Fernandez Mancera 2020-03-26 11:28:06 +01:00 committed by Gris Ge
parent cfdc6cfbc5
commit 25df7dabda
16 changed files with 342 additions and 10 deletions

View File

@ -0,0 +1,28 @@
---
interfaces:
- name: patch0
type: ovs-interface
state: up
patch:
peer: patch1
- name: ovs-br0
type: ovs-bridge
state: up
bridge:
options:
stp: true
port:
- name: patch0
- name: patch1
type: ovs-interface
state: up
patch:
peer: patch0
- name: ovs-br1
type: ovs-bridge
state: up
bridge:
options:
stp: true
port:
- name: patch1

View File

@ -0,0 +1,6 @@
---
interfaces:
- name: ovs-br0
state: absent
- name: ovs-br1
state: absent

View File

@ -296,6 +296,10 @@ class BaseIface:
def mac(self):
return self._info.get(Interface.MAC)
@property
def mtu(self):
return self._info.get(Interface.MTU)
def _capitalize_mac(self):
if self.mac:
self._info[Interface.MAC] = self.mac.upper()

View File

@ -126,6 +126,7 @@ class Ifaces:
self._match_child_iface_state_with_parent()
self._mark_orphen_as_absent()
self._bring_slave_up_if_not_in_desire()
self._validate_ovs_patch_peers()
def _bring_slave_up_if_not_in_desire(self):
"""
@ -140,6 +141,29 @@ class Ifaces:
slave_iface.mark_as_up()
slave_iface.mark_as_changed()
def _validate_ovs_patch_peers(self):
"""
When OVS patch peer does not exist or is down, raise an error.
"""
for iface in self._ifaces.values():
if iface.iface_type == InterfaceType.OVS_INTERFACE and iface.is_up:
if iface.peer:
peer_iface = self._ifaces.get(iface.peer)
if not peer_iface or not peer_iface.is_up:
raise NmstateValueError(
f"OVS patch port peer {iface.peer} must exist and "
"be up"
)
elif (
not peer_iface.iface_type
== InterfaceType.OVS_INTERFACE
or not peer_iface.is_patch_port
):
raise NmstateValueError(
f"OVS patch port peer {iface.peer} must be an OVS"
" patch port"
)
def _handle_master_slave_list_change(self):
"""
* Mark slave interface as changed if master removed.

View File

@ -23,9 +23,11 @@ import subprocess
from libnmstate.error import NmstateValueError
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceIP
from libnmstate.schema import InterfaceType
from libnmstate.schema import InterfaceState
from libnmstate.schema import OVSBridge
from libnmstate.schema import OVSInterface
from libnmstate.schema import OvsDB
from .bridge import BridgeIface
@ -190,11 +192,53 @@ class OvsInternalIface(BaseIface):
def need_parent(self):
return True
@property
def patch_config(self):
return self._info.get(OVSInterface.PATCH_CONFIG_SUBTREE)
def state_for_verify(self):
state = super().state_for_verify()
_convert_external_ids_values_to_string(state)
return state
@property
def is_patch_port(self):
return self.patch_config and self.patch_config.get(
OVSInterface.Patch.PEER
)
@property
def peer(self):
return (
self.patch_config.get(OVSInterface.Patch.PEER)
if self.patch_config
else None
)
def pre_edit_validation_and_cleanup(self):
super().pre_edit_validation_and_cleanup()
self._validate_ovs_mtu_mac_confliction()
def _validate_ovs_mtu_mac_confliction(self):
if self.is_patch_port:
if (
self.original_dict.get(Interface.IPV4, {}).get(
InterfaceIP.ENABLED
)
or self.original_dict.get(Interface.IPV6, {}).get(
InterfaceIP.ENABLED
)
or self.original_dict.get(Interface.MTU)
or self.original_dict.get(Interface.MAC)
):
raise NmstateValueError(
"OVS Patch interface cannot contain MAC address, MTU"
" or IP configuration."
)
else:
self._info.pop(Interface.MTU, None)
self._info.pop(Interface.MAC, None)
def is_ovs_running():
try:

View File

@ -26,6 +26,7 @@ from libnmstate.schema import InterfaceState
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 Team
from libnmstate.ifaces.bond import BondIface
from libnmstate.ifaces.bridge import BridgeIface
@ -507,7 +508,10 @@ def _build_connection_profile(
ovs_port_options = iface_desired_state.get(OvsB.OPTIONS_SUBTREE)
settings.append(ovs.create_port_setting(ovs_port_options))
elif iface_type == InterfaceType.OVS_INTERFACE:
settings.append(ovs.create_interface_setting())
patch_state = iface_desired_state.get(
OVSInterface.PATCH_CONFIG_SUBTREE
)
settings.extend(ovs.create_interface_setting(patch_state))
bridge_port_options = iface_desired_state.get(
BridgeIface.BRPORT_OPTIONS_METADATA

View File

@ -19,6 +19,7 @@
import logging
from libnmstate.schema import OVSBridge as OB
from libnmstate.schema import OVSInterface
from . import connection
from .common import NM
@ -95,10 +96,24 @@ def create_port_setting(port_state):
return port_setting
def create_interface_setting():
def create_interface_setting(patch_state):
interface_setting = NM.SettingOvsInterface.new()
interface_setting.props.type = "internal"
return interface_setting
settings = [interface_setting]
if patch_state and patch_state.get(OVSInterface.Patch.PEER):
interface_setting.props.type = "patch"
settings.append(create_patch_setting(patch_state))
else:
interface_setting.props.type = "internal"
return settings
def create_patch_setting(patch_state):
patch_setting = NM.SettingOvsPatch.new()
patch_setting.props.peer = patch_state[OVSInterface.Patch.PEER]
return patch_setting
def is_ovs_bridge_type_id(type_id):
@ -133,6 +148,33 @@ def get_ovs_info(context, bridge_device, devices_info):
return {}
def get_interface_info(act_con):
"""
Get OVS interface information from the NM profile.
"""
info = {}
if act_con:
patch_setting = _get_patch_setting(act_con)
if patch_setting:
info[OVSInterface.PATCH_CONFIG_SUBTREE] = {
OVSInterface.Patch.PEER: patch_setting.props.peer,
}
return info
def _get_patch_setting(act_con):
"""
Get NM.SettingOvsPatch from NM.ActiveConnection.
For any error, return None.
"""
remote_con = act_con.get_connection()
if remote_con:
return remote_con.get_setting_ovs_patch()
return None
def get_slaves(nm_device):
return nm_device.get_slaves()

View File

@ -130,6 +130,8 @@ class NetworkManagerPlugin(NmstatePlugin):
iface_info = _remove_ovs_bridge_unsupported_entries(
iface_info
)
elif nm_ovs.is_ovs_interface_type_id(type_id):
iface_info.update(nm_ovs.get_interface_info(act_con))
elif nm_ovs.is_ovs_port_type_id(type_id):
continue

View File

@ -284,6 +284,10 @@ class OvsDB:
class OVSInterface(OvsDB):
TYPE = InterfaceType.OVS_INTERFACE
PATCH_CONFIG_SUBTREE = "patch"
class Patch:
PEER = "peer"
class OVSBridge(Bridge, OvsDB):

View File

@ -450,6 +450,11 @@ definitions:
- ovs-interface
ovs-db:
type: object
patch:
type: object
properties:
peer:
type: string
interface-dummy:
rw:
properties:

View File

@ -22,9 +22,11 @@ import os
import pytest
from .testlib import assertlib
from .testlib.env import nm_is_not_supporting_ovs_patch_port
from .testlib.examplelib import example_state
from libnmstate import netinfo
from libnmstate.error import NmstateLibnmError
from libnmstate.error import NmstateNotSupportedError
from libnmstate.schema import DNS
@ -163,3 +165,19 @@ def test_port_vlan(eth1_up):
assertlib.assert_state(desired_state)
assertlib.assert_absent("linux-br0")
@pytest.mark.xfail(
nm_is_not_supporting_ovs_patch_port(),
reason="Using the interface name is supported only with NM 1.22.16/1.24.2"
" or greater",
raises=NmstateLibnmError,
strict=True,
)
def test_add_ovs_patch_and_remove():
with example_state(
"ovsbridge_patch_create.yml", cleanup="ovsbridge_patch_delete.yml"
) as desired_state:
assertlib.assert_state(desired_state)
assertlib.assert_absent("patch0")

View File

@ -281,15 +281,17 @@ def _create_internal_iface_setting(iface_name, master_name):
iface_type=InterfaceType.OVS_INTERFACE,
)
iface_con_setting.set_master(master_name, InterfaceType.OVS_PORT)
bridge_internal_iface_setting = nm.ovs.create_interface_setting()
bridge_internal_iface_setting = nm.ovs.create_interface_setting(None)
ipv4_setting = nm.ipv4.create_setting({}, None)
ipv6_setting = nm.ipv6.create_setting({}, None)
return (
settings = [
iface_con_setting.setting,
bridge_internal_iface_setting,
ipv4_setting,
ipv6_setting,
)
]
settings.extend(bridge_internal_iface_setting)
return settings
def _delete_iface(ctx, devname):

View File

@ -29,12 +29,15 @@ from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import OVSBridge
from libnmstate.schema import OvsDB
from libnmstate.schema import OVSInterface
from libnmstate.error import NmstateDependencyError
from libnmstate.error import NmstateLibnmError
from libnmstate.error import NmstateValueError
from .testlib import assertlib
from .testlib import cmdlib
from .testlib import statelib
from .testlib.env import nm_is_not_supporting_ovs_patch_port
from .testlib.nmplugin import disable_nm_plugin
from .testlib.ovslib import Bridge
from .testlib.servicelib import disable_service
@ -44,9 +47,12 @@ from .testlib.vlan import vlan_interface
BOND1 = "bond1"
BRIDGE0 = "br0"
BRIDGE1 = "br1"
PORT1 = "ovs1"
PORT2 = "ovs2"
PATCH0 = "patch0"
PATCH1 = "patch1"
VLAN_IFNAME = "eth101"
MAC1 = "02:FF:FF:FF:FF:01"
@ -332,3 +338,101 @@ def test_ovsdb_set_external_ids_for_existing_bridge(bridge_with_ports):
)
bridge.apply()
assertlib.assert_state_match(bridge.state)
class TestOvsPatch:
@pytest.mark.xfail(
nm_is_not_supporting_ovs_patch_port(),
reason="Using the interface name is supported only with NM "
"1.22.16/1.24.2 or greater.",
raises=NmstateLibnmError,
strict=True,
)
def test_create_and_remove_patch_port(self):
patch0_state = {OVSInterface.Patch.PEER: "patch1"}
patch1_state = {OVSInterface.Patch.PEER: "patch0"}
bridge = Bridge(BRIDGE0)
bridge.add_internal_port(PATCH0, patch_state=patch0_state)
desired_state = bridge.state
bridge = Bridge(BRIDGE1)
bridge.add_internal_port(PATCH1, patch_state=patch1_state)
desired_state[Interface.KEY].extend(bridge.state[Interface.KEY])
try:
libnmstate.apply(desired_state)
assertlib.assert_state_match(desired_state)
finally:
for iface in desired_state[Interface.KEY]:
iface[Interface.STATE] = InterfaceState.ABSENT
libnmstate.apply(desired_state)
assertlib.assert_absent(BRIDGE1)
assertlib.assert_absent(BRIDGE0)
assertlib.assert_absent(PATCH0)
assertlib.assert_absent(PATCH1)
def test_add_patch_to_existing_interface_invalid(self):
patch0_state = {OVSInterface.Patch.PEER: "falsepatch"}
bridge = Bridge(BRIDGE0)
bridge.add_internal_port(PATCH0)
desired_state = bridge.state
bridge = Bridge(BRIDGE1)
bridge.add_internal_port(PATCH1)
desired_state[Interface.KEY].extend(bridge.state[Interface.KEY])
try:
desired_state[Interface.KEY][1][
OVSInterface.PATCH_CONFIG_SUBTREE
] = patch0_state
desired_state[Interface.KEY][1][Interface.MTU] = 1500
with pytest.raises(NmstateValueError):
libnmstate.apply(desired_state)
finally:
for iface in desired_state[Interface.KEY]:
iface[Interface.STATE] = InterfaceState.ABSENT
iface[OVSInterface.PATCH_CONFIG_SUBTREE] = {}
libnmstate.apply(desired_state)
assertlib.assert_absent(BRIDGE1)
assertlib.assert_absent(BRIDGE0)
assertlib.assert_absent(PATCH0)
assertlib.assert_absent(PATCH1)
@pytest.mark.xfail(
nm_is_not_supporting_ovs_patch_port(),
reason="Using the interface name is supported only with NM "
"1.22.16/1.24.2 or greater",
raises=NmstateLibnmError,
strict=True,
)
def test_add_patch_to_existing_interface_valid(self):
patch0_state = {OVSInterface.Patch.PEER: "patch1"}
patch1_state = {OVSInterface.Patch.PEER: "patch0"}
bridge = Bridge(BRIDGE0)
bridge.add_internal_port(PATCH0)
desired_state = bridge.state
bridge = Bridge(BRIDGE1)
bridge.add_internal_port(PATCH1)
desired_state[Interface.KEY].extend(bridge.state[Interface.KEY])
try:
desired_state[Interface.KEY][1].pop(Interface.MTU, None)
desired_state[Interface.KEY][1].pop(Interface.MAC, None)
desired_state[Interface.KEY][1][
OVSInterface.PATCH_CONFIG_SUBTREE
] = patch0_state
desired_state[Interface.KEY][3].pop(Interface.MTU, None)
desired_state[Interface.KEY][3].pop(Interface.MAC, None)
desired_state[Interface.KEY][3][
OVSInterface.PATCH_CONFIG_SUBTREE
] = patch1_state
libnmstate.apply(desired_state)
assertlib.assert_state_match(desired_state)
finally:
for iface in desired_state[Interface.KEY]:
iface[Interface.STATE] = InterfaceState.ABSENT
libnmstate.apply(desired_state)
assertlib.assert_absent(BRIDGE1)
assertlib.assert_absent(BRIDGE0)
assertlib.assert_absent(PATCH0)
assertlib.assert_absent(PATCH1)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2019 Red Hat, Inc.
# Copyright (c) 2019-2020 Red Hat, Inc.
#
# This file is part of nmstate
#
@ -34,3 +34,13 @@ def is_nm_older_than_1_25_2():
assert match
return StrictVersion(match.group(1)) < StrictVersion("1.25.2")
def nm_is_not_supporting_ovs_patch_port():
_, output, _ = exec_cmd(["nmcli", "-v"], check=True)
match = re.compile("version ([0-9.]+)").search(output)
return StrictVersion(match.group(1)) <= StrictVersion("1.22.14") or (
StrictVersion(match.group(1)) >= StrictVersion("1.24.0")
and StrictVersion(match.group(1)) <= StrictVersion("1.24.2")
)

View File

@ -69,7 +69,7 @@ class Bridge:
] = mode
def add_internal_port(
self, name, *, mac=None, ipv4_state=None, ovs_db=None
self, name, *, mac=None, ipv4_state=None, ovs_db=None, patch_state=None
):
ifstate = {
Interface.NAME: name,
@ -81,6 +81,8 @@ class Bridge:
ifstate[Interface.IPV4] = ipv4_state
if ovs_db:
ifstate[OVSInterface.OVS_DB_SUBTREE] = ovs_db
if patch_state:
ifstate[OVSInterface.PATCH_CONFIG_SUBTREE] = patch_state
self._add_port(name)
self._ifaces.append(ifstate)

View File

@ -31,6 +31,7 @@ from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import LinuxBridge as LB
from libnmstate.schema import OVSBridge
from libnmstate.schema import OVSInterface
from libnmstate.schema import Route
from libnmstate.schema import RouteRule
from libnmstate.schema import Team
@ -747,3 +748,35 @@ def generate_vlan_id_range_config(min_vlan_id, max_vlan_id):
LB.Port.Vlan.TrunkTags.MAX_RANGE: max_vlan_id,
}
}
class TestOVSInterface:
def test_valid_ovs_interface_with_peer(self, default_data):
ovs_patch0 = {OVSInterface.Patch.PEER: "ovs1"}
ovs_patch1 = {OVSInterface.Patch.PEER: "ovs0"}
default_data[Interface.KEY].append(
{
Interface.NAME: "ovs0",
Interface.TYPE: InterfaceType.OVS_INTERFACE,
OVSInterface.PATCH_CONFIG_SUBTREE: ovs_patch0,
}
)
default_data[Interface.KEY].append(
{
Interface.NAME: "ovs1",
Interface.TYPE: InterfaceType.OVS_INTERFACE,
OVSInterface.PATCH_CONFIG_SUBTREE: ovs_patch1,
}
)
libnmstate.validator.schema_validate(default_data)
def test_valid_ovs_interface_without_peer(self, default_data):
default_data[Interface.KEY].append(
{
Interface.NAME: "ovs0",
Interface.TYPE: InterfaceType.OVS_INTERFACE,
}
)
libnmstate.validator.schema_validate(default_data)