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:
parent
99d2974182
commit
cc39341fff
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 \
|
||||
|
@ -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 \
|
||||
|
@ -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@
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user