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:
parent
cfdc6cfbc5
commit
25df7dabda
28
examples/ovsbridge_patch_create.yml
Normal file
28
examples/ovsbridge_patch_create.yml
Normal 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
|
6
examples/ovsbridge_patch_delete.yml
Normal file
6
examples/ovsbridge_patch_delete.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
interfaces:
|
||||
- name: ovs-br0
|
||||
state: absent
|
||||
- name: ovs-br1
|
||||
state: absent
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -450,6 +450,11 @@ definitions:
|
||||
- ovs-interface
|
||||
ovs-db:
|
||||
type: object
|
||||
patch:
|
||||
type: object
|
||||
properties:
|
||||
peer:
|
||||
type: string
|
||||
interface-dummy:
|
||||
rw:
|
||||
properties:
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user