ovs db plugin: add support of external_ids

Example:

```
interfaces:
- name: ovs0
  type: ovs-interface
  state: up
  ovs-db:
    external_ids:
      gris_test: foo
```

The external_ids is full editing, user have to provide full information.
If not mentioned, ovs DB will use old state without changing coming from this
plugin. If defined a `external_ids: {}`, will remove existing external
IDS except the NetworkManager internal one.

There is not checkpoint support for ovsdb plugin yet.

For schema, add class `OvsDB` to hold the common `OvsDB.OVS_DB_SUBTREE`
which is shared by `OVSBridge` and `OVSInterface`.

Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Gris Ge 2020-06-15 20:59:21 +08:00
parent 99d2974182
commit cc39341fff
10 changed files with 333 additions and 5 deletions

View File

@ -22,10 +22,11 @@ from operator import itemgetter
import subprocess
from libnmstate.error import NmstateValueError
from libnmstate.schema import OVSBridge
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceType
from libnmstate.schema import InterfaceState
from libnmstate.schema import OVSBridge
from libnmstate.schema import OvsDB
from .bridge import BridgeIface
from .base_iface import BaseIface
@ -140,6 +141,11 @@ class OvsBridgeIface(BridgeIface):
] = new_port_configs
self.sort_slaves()
def state_for_verify(self):
state = super().state_for_verify()
_convert_external_ids_values_to_string(state)
return state
def _lookup_ovs_port_by_interface(ports, slave_name):
for port in ports:
@ -184,6 +190,11 @@ class OvsInternalIface(BaseIface):
def need_parent(self):
return True
def state_for_verify(self):
state = super().state_for_verify()
_convert_external_ids_values_to_string(state)
return state
def is_ovs_running():
try:
@ -197,3 +208,11 @@ def is_ovs_running():
return True
except Exception:
return False
def _convert_external_ids_values_to_string(iface_info):
external_ids = iface_info.get(OvsDB.OVS_DB_SUBTREE, {}).get(
OvsDB.EXTERNAL_IDS, {}
)
for key, value in external_ids.items():
external_ids[key] = str(value)

View File

@ -17,10 +17,121 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
import os
import time
import ovs
from ovs.db.idl import Transaction, Idl, SchemaHelper
from libnmstate.plugin import NmstatePlugin
from libnmstate.schema import Interface
from libnmstate.schema import OVSInterface
from libnmstate.schema import OVSBridge
from libnmstate.schema import OvsDB
from libnmstate.error import NmstateTimeoutError
from libnmstate.error import NmstatePermissionError
from libnmstate.error import NmstateValueError
from libnmstate.error import NmstatePluginError
TIMEOUT = 5
DEFAULT_OVS_DB_SOCKET_PATH = "/run/openvswitch/db.sock"
DEFAULT_OVS_SCHEMA_PATH = "/usr/share/openvswitch/vswitch.ovsschema"
NM_EXTERNAL_ID = "NM.connection.uuid"
class _Changes:
def __init__(self, table_name, column_name, row_name, column_value):
self.table_name = table_name
self.column_name = column_name
self.row_name = row_name
self.column_value = column_value
def __str__(self):
return f"{self.__dict__}"
class NmstateOvsdbPlugin(NmstatePlugin):
def __init__(self):
self._schema = None
self._idl = None
self._transaction = None
self._seq_no = 0
self._load_schema()
self._connect_to_ovs_db()
def unload(self):
if self._transaction:
self._transaction.abort()
self._transaction = None
if self._idl:
self._idl.close()
self._idl = None
def _load_schema(self):
schema_path = os.environ.get(
"OVS_SCHEMA_PATH", DEFAULT_OVS_SCHEMA_PATH
)
if not os.path.exists(schema_path):
raise NmstateValueError(
f"OVS schema file {schema_path} does not exist, "
"please define the correct one via "
"environment variable 'OVS_SCHEMA_PATH'"
)
if not os.access(schema_path, os.R_OK):
raise NmstatePermissionError(
f"Has no read permission to OVS schema file {schema_path}"
)
self._schema = SchemaHelper(schema_path)
self._schema.register_columns(
"Interface", [OvsDB.EXTERNAL_IDS, "name"]
)
self._schema.register_columns("Bridge", [OvsDB.EXTERNAL_IDS, "name"])
def _connect_to_ovs_db(self):
socket_path = os.environ.get(
"OVS_DB_UNIX_SOCKET_PATH", DEFAULT_OVS_DB_SOCKET_PATH
)
if not os.path.exists(socket_path):
raise NmstateValueError(
f"OVS database socket file {socket_path} does not exist, "
"please start the OVS daemon or define the socket path via "
"environment variable 'OVS_DB_UNIX_SOCKET_PATH'"
)
if not os.access(socket_path, os.R_OK):
raise NmstatePermissionError(
f"Has no read permission to OVS db socket file {socket_path}"
)
self._idl = Idl(f"unix:{socket_path}", self._schema)
self.refresh_content()
if not self._idl.has_ever_connected():
self._idl = None
raise NmstatePluginError("Failed to connect to OVS DB")
def refresh_content(self):
if self._idl:
timeout_end = time.time() + TIMEOUT
self._idl.run()
if self._idl.change_seqno == self._seq_no and self._seq_no:
return
while True:
changed = self._idl.run()
cur_seq_no = self._idl.change_seqno
if cur_seq_no != self._seq_no or changed:
self._seq_no = cur_seq_no
return
poller = ovs.poller.Poller()
self._idl.wait(poller)
poller.timer_wait(TIMEOUT * 1000)
poller.block()
if time.time() > timeout_end:
raise NmstateTimeoutError(
f"Plugin {self.name} timeout({TIMEOUT} "
"seconds) when refresh OVS database connection"
)
@property
def name(self):
return "nmstate-plugin-ovsdb"
@ -34,7 +145,129 @@ class NmstateOvsdbPlugin(NmstatePlugin):
return NmstatePlugin.PLUGIN_CAPABILITY_IFACE
def get_interfaces(self):
return []
ifaces = []
for row in list(self._idl.tables["Interface"].rows.values()) + list(
self._idl.tables["Bridge"].rows.values()
):
ifaces.append(
{
Interface.NAME: row.name,
OvsDB.OVS_DB_SUBTREE: {
OvsDB.EXTERNAL_IDS: row.external_ids
},
}
)
return ifaces
def apply_changes(self, net_state):
self.refresh_content()
pending_changes = []
for iface in net_state.ifaces.values():
if not iface.is_changed and not iface.is_desired:
continue
if not iface.is_up:
continue
if iface.iface_type == OVSBridge.TYPE:
table_name = "Bridge"
elif iface.iface_type == OVSInterface.TYPE:
table_name = "Interface"
else:
continue
pending_changes.extend(_generate_db_change(table_name, iface))
if pending_changes and self._idl:
self._start_transaction()
self._db_write(pending_changes)
self._commit_transaction()
def _db_write(self, changes):
changes_index = {change.row_name: change for change in changes}
changed_tables = set(change.table_name for change in changes)
updated_names = []
for changed_table in changed_tables:
for row in self._idl.tables[changed_table].rows.values():
if row.name in changes_index:
change = changes_index[row.name]
setattr(row, change.column_name, change.column_value)
updated_names.append(change.row_name)
new_rows = set(changes_index.keys()) - set(updated_names)
if new_rows:
raise NmstatePluginError(
f"BUG: row {new_rows} does not exists in OVS DB "
"and currently we don't create new row"
)
def _start_transaction(self):
self._transaction = Transaction(self._idl)
def _commit_transaction(self):
if self._transaction:
status = self._transaction.commit()
timeout_end = time.time() + TIMEOUT
while status == Transaction.INCOMPLETE:
self._idl.run()
poller = ovs.poller.Poller()
self._idl.wait(poller)
self._transaction.wait(poller)
poller.timer_wait(TIMEOUT * 1000)
poller.block()
if time.time() > timeout_end:
raise NmstateTimeoutError(
f"Plugin {self.name} timeout({TIMEOUT} "
"seconds) when commit OVS database transaction"
)
status = self._transaction.commit()
if status == Transaction.SUCCESS:
self.refresh_content()
transaction_error = self._transaction.get_error()
self._transaction = None
if status not in (Transaction.SUCCESS, Transaction.UNCHANGED):
raise NmstatePluginError(
f"Plugin {self.name} failure on commiting OVS database "
f"transaction: status: {status} "
f"error: {transaction_error}"
)
else:
raise NmstatePluginError(
"BUG: _commit_transaction() invoked with "
"self._transaction is None"
)
def _generate_db_change(table_name, iface_state):
return _generate_db_change_external_ids(table_name, iface_state)
def _generate_db_change_external_ids(table_name, iface_state):
pending_changes = []
desire_ids = iface_state.original_dict.get(OvsDB.OVS_DB_SUBTREE, {}).get(
OvsDB.EXTERNAL_IDS
)
if desire_ids and not isinstance(desire_ids, dict):
raise NmstateValueError("Invalid external_ids, should be dictionary")
if desire_ids or desire_ids == {}:
# should include external_id required by NetworkManager.
merged_ids = (
iface_state.to_dict()
.get(OvsDB.OVS_DB_SUBTREE, {})
.get(OvsDB.EXTERNAL_IDS, {})
)
if NM_EXTERNAL_ID in merged_ids:
desire_ids[NM_EXTERNAL_ID] = merged_ids[NM_EXTERNAL_ID]
# Convert all value to string
for key, value in desire_ids.items():
desire_ids[key] = str(value)
pending_changes.append(
_Changes(
table_name, OvsDB.EXTERNAL_IDS, iface_state.name, desire_ids
)
)
return pending_changes
NMSTATE_PLUGIN = NmstateOvsdbPlugin

View File

@ -255,7 +255,17 @@ class VXLAN:
DESTINATION_PORT = "destination-port"
class OVSBridge(Bridge):
class OvsDB:
OVS_DB_SUBTREE = "ovs-db"
# Don't use hypen as this is OVS data base entry
EXTERNAL_IDS = "external_ids"
class OVSInterface(OvsDB):
TYPE = InterfaceType.OVS_INTERFACE
class OVSBridge(Bridge, OvsDB):
TYPE = "ovs-bridge"
class Options:

View File

@ -332,6 +332,8 @@ definitions:
type: string
enum:
- ovs-bridge
ovs-db:
type: object
bridge:
type: object
properties:
@ -402,6 +404,8 @@ definitions:
type: string
enum:
- ovs-interface
ovs-db:
type: object
interface-dummy:
rw:
properties:

View File

@ -10,6 +10,7 @@ RUN dnf -y install dnf-plugins-core epel-release && \
NetworkManager-team \
NetworkManager-config-server \
openvswitch2.11 \
python3-openvswitch2.11 \
systemd-udev \
python3-devel \
python3-gobject-base \

View File

@ -6,6 +6,7 @@ RUN dnf -y install --setopt=install_weak_deps=False \
NetworkManager-team \
NetworkManager-config-server \
openvswitch \
python3-openvswitch \
systemd-udev \
\
python3-gobject-base \

View File

@ -34,10 +34,19 @@ Recommends: NetworkManager-config-server
Suggests: NetworkManager-ovs
Suggests: NetworkManager-team
%package -n nmstate-plugin-ovsdb
Summary: nmstate plugin for OVS database manipulation
Requires: python3-%{libname} = %{?epoch:%{epoch}:}%{version}-%{release}
# The python-openvswitch rpm pacakge is not in the same repo with nmstate,
# hence state it as Recommends, no requires.
Recommends: python3dist(ovs)
%description -n python3-%{libname}
This package contains the Python 3 library for Nmstate.
%description -n nmstate-plugin-ovsdb
This package contains the nmstate plugin for OVS database manipulation.
%prep
%setup -q
@ -56,8 +65,14 @@ This package contains the Python 3 library for Nmstate.
%files -n python3-%{libname}
%license LICENSE
%{python3_sitelib}/%{libname}
%{python3_sitelib}/%{srcname}-*.egg-info/
%{python3_sitelib}/%{libname}
%exclude %{python3_sitelib}/%{libname}/plugins/nmstate_plugin_*
%exclude %{python3_sitelib}/%{libname}/plugins/__pycache__/nmstate_plugin_*
%files -n nmstate-plugin-ovsdb
%{python3_sitelib}/%{libname}/plugins/nmstate_plugin_ovsdb*
%{python3_sitelib}/%{libname}/plugins/__pycache__/nmstate_plugin_ovsdb*
%changelog
@CHANGELOG@

View File

@ -28,6 +28,7 @@ from libnmstate.schema import InterfaceIPv4
from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import OVSBridge
from libnmstate.schema import OvsDB
from libnmstate.error import NmstateDependencyError
from libnmstate.error import NmstateValueError
@ -45,6 +46,7 @@ from .testlib.vlan import vlan_interface
BOND1 = "bond1"
BRIDGE1 = "br1"
PORT1 = "ovs1"
PORT2 = "ovs2"
VLAN_IFNAME = "eth101"
MAC1 = "02:FF:FF:FF:FF:01"
@ -306,3 +308,27 @@ def test_add_invalid_slave_ip_config(eth1_up):
with bridge.create() as state:
desired_state[Interface.KEY].append(state[Interface.KEY][0])
libnmstate.apply(desired_state)
def test_ovsdb_new_bridge_with_external_id():
bridge = Bridge(BRIDGE1)
bridge.set_ovs_db({OvsDB.EXTERNAL_IDS: {"foo": "abc", "bak": 1}})
bridge.add_internal_port(
PORT1,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: {"foo": "abcd", "bak": 2}},
)
with bridge.create() as state:
assertlib.assert_state_match(state)
def test_ovsdb_set_external_ids_for_existing_bridge(bridge_with_ports):
bridge = bridge_with_ports
bridge.set_ovs_db({OvsDB.EXTERNAL_IDS: {"foo": "abc", "bak": 1}})
bridge.add_internal_port(
PORT2,
ipv4_state={InterfaceIPv4.ENABLED: False},
ovs_db={OvsDB.EXTERNAL_IDS: {"foo": "abcd", "bak": 2}},
)
bridge.apply()
assertlib.assert_state_match(bridge.state)

View File

@ -24,6 +24,7 @@ from libnmstate.schema import DNS
from libnmstate.schema import Route
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceType
from libnmstate.schema import OvsDB
from . import statelib
@ -88,6 +89,7 @@ def _prepare_state_for_verify(desired_state_data):
full_desired_state.remove_absent_entries()
full_desired_state.normalize()
_fix_bond_state(current_state)
_fix_ovsdb_external_ids(full_desired_state)
return full_desired_state, current_state
@ -120,3 +122,12 @@ def _fix_bond_state(current_state):
and "arp_ip_target" not in bond_options
):
bond_options["arp_ip_target"] = ""
def _fix_ovsdb_external_ids(state):
for iface_state in state.state[Interface.KEY]:
external_ids = iface_state.get(OvsDB.OVS_DB_SUBTREE, {}).get(
OvsDB.EXTERNAL_IDS, {}
)
for key, value in external_ids.items():
external_ids[key] = str(value)

View File

@ -26,6 +26,7 @@ from libnmstate.schema import Interface
from libnmstate.schema import InterfaceState
from libnmstate.schema import InterfaceType
from libnmstate.schema import OVSBridge
from libnmstate.schema import OVSInterface
from . import cmdlib
@ -47,6 +48,9 @@ class Bridge:
OVSBridge.OPTIONS_SUBTREE
] = options
def set_ovs_db(self, ovs_db_config):
self._bridge_iface[OVSBridge.OVS_DB_SUBTREE] = ovs_db_config
def add_system_port(self, name):
self._add_port(name)
@ -64,7 +68,9 @@ class Bridge:
OVSBridge.Port.LinkAggregation.MODE
] = mode
def add_internal_port(self, name, *, mac=None, ipv4_state=None):
def add_internal_port(
self, name, *, mac=None, ipv4_state=None, ovs_db=None
):
ifstate = {
Interface.NAME: name,
Interface.TYPE: InterfaceType.OVS_INTERFACE,
@ -73,6 +79,8 @@ class Bridge:
ifstate[Interface.MAC] = mac
if ipv4_state:
ifstate[Interface.IPV4] = ipv4_state
if ovs_db:
ifstate[OVSInterface.OVS_DB_SUBTREE] = ovs_db
self._add_port(name)
self._ifaces.append(ifstate)