Blame libnmstate/plugins/nmstate_plugin_ovsdb.py

Packit Service 0535c1
#
Packit Service 0535c1
# Copyright (c) 2020 Red Hat, Inc.
Packit Service 0535c1
#
Packit Service 0535c1
# This file is part of nmstate
Packit Service 0535c1
#
Packit Service 0535c1
# This program is free software: you can redistribute it and/or modify
Packit Service 0535c1
# it under the terms of the GNU Lesser General Public License as published by
Packit Service 0535c1
# the Free Software Foundation, either version 2.1 of the License, or
Packit Service 0535c1
# (at your option) any later version.
Packit Service 0535c1
#
Packit Service 0535c1
# This program is distributed in the hope that it will be useful,
Packit Service 0535c1
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit Service 0535c1
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit Service 0535c1
# GNU Lesser General Public License for more details.
Packit Service 0535c1
#
Packit Service 0535c1
# You should have received a copy of the GNU Lesser General Public License
Packit Service 0535c1
# along with this program. If not, see <https://www.gnu.org/licenses/>.
Packit Service 0535c1
#
Packit Service 0535c1
Packit Service 0535c1
import os
Packit Service 0535c1
import time
Packit Service 0535c1
Packit Service 0535c1
import ovs
Packit Service 0535c1
from ovs.db.idl import Transaction, Idl, SchemaHelper
Packit Service 0535c1
Packit Service 0535c1
from libnmstate.plugin import NmstatePlugin
Packit Service 0535c1
from libnmstate.schema import Interface
Packit Service 0535c1
from libnmstate.schema import OVSInterface
Packit Service 0535c1
from libnmstate.schema import OVSBridge
Packit Service 0535c1
from libnmstate.schema import OvsDB
Packit Service 0535c1
from libnmstate.error import NmstateNotImplementedError
Packit Service 0535c1
from libnmstate.error import NmstateTimeoutError
Packit Service 0535c1
from libnmstate.error import NmstatePermissionError
Packit Service 0535c1
from libnmstate.error import NmstateValueError
Packit Service 0535c1
from libnmstate.error import NmstatePluginError
Packit Service 0535c1
Packit Service 0535c1
TIMEOUT = 5
Packit Service 0535c1
Packit Service 0535c1
DEFAULT_OVS_DB_SOCKET_PATH = "/run/openvswitch/db.sock"
Packit Service 0535c1
DEFAULT_OVS_SCHEMA_PATH = "/usr/share/openvswitch/vswitch.ovsschema"
Packit Service 0535c1
Packit Service 0535c1
NM_EXTERNAL_ID = "NM.connection.uuid"
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
class _Changes:
Packit Service 0535c1
    def __init__(self, table_name, column_name, row_name, column_value):
Packit Service 0535c1
        self.table_name = table_name
Packit Service 0535c1
        self.column_name = column_name
Packit Service 0535c1
        self.row_name = row_name
Packit Service 0535c1
        self.column_value = column_value
Packit Service 0535c1
Packit Service 0535c1
    def __str__(self):
Packit Service 0535c1
        return f"{self.__dict__}"
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
class NmstateOvsdbPlugin(NmstatePlugin):
Packit Service 0535c1
    def __init__(self):
Packit Service 0535c1
        self._schema = None
Packit Service 0535c1
        self._idl = None
Packit Service 0535c1
        self._transaction = None
Packit Service 0535c1
        self._seq_no = 0
Packit Service 0535c1
        self._load_schema()
Packit Service 0535c1
        self._connect_to_ovs_db()
Packit Service 0535c1
Packit Service 0535c1
    def unload(self):
Packit Service 0535c1
        if self._transaction:
Packit Service 0535c1
            self._transaction.abort()
Packit Service 0535c1
            self._transaction = None
Packit Service 0535c1
        if self._idl:
Packit Service 0535c1
            self._idl.close()
Packit Service 0535c1
            self._idl = None
Packit Service 0535c1
Packit Service 0535c1
    def _load_schema(self):
Packit Service 0535c1
        schema_path = os.environ.get(
Packit Service 0535c1
            "OVS_SCHEMA_PATH", DEFAULT_OVS_SCHEMA_PATH
Packit Service 0535c1
        )
Packit Service 0535c1
        if not os.path.exists(schema_path):
Packit Service 0535c1
            raise NmstateValueError(
Packit Service 0535c1
                f"OVS schema file {schema_path} does not exist, "
Packit Service 0535c1
                "please define the correct one via "
Packit Service 0535c1
                "environment variable 'OVS_SCHEMA_PATH'"
Packit Service 0535c1
            )
Packit Service 0535c1
        if not os.access(schema_path, os.R_OK):
Packit Service 0535c1
            raise NmstatePermissionError(
Packit Service 0535c1
                f"Has no read permission to OVS schema file {schema_path}"
Packit Service 0535c1
            )
Packit Service 0535c1
        self._schema = SchemaHelper(schema_path)
Packit Service 0535c1
        self._schema.register_columns(
Packit Service 0535c1
            "Interface", [OvsDB.EXTERNAL_IDS, "name"]
Packit Service 0535c1
        )
Packit Service 0535c1
        self._schema.register_columns("Bridge", [OvsDB.EXTERNAL_IDS, "name"])
Packit Service 0535c1
Packit Service 0535c1
    def _connect_to_ovs_db(self):
Packit Service 0535c1
        socket_path = os.environ.get(
Packit Service 0535c1
            "OVS_DB_UNIX_SOCKET_PATH", DEFAULT_OVS_DB_SOCKET_PATH
Packit Service 0535c1
        )
Packit Service 0535c1
        if not os.path.exists(socket_path):
Packit Service 0535c1
            raise NmstateValueError(
Packit Service 0535c1
                f"OVS database socket file {socket_path} does not exist, "
Packit Service 0535c1
                "please start the OVS daemon or define the socket path via "
Packit Service 0535c1
                "environment variable 'OVS_DB_UNIX_SOCKET_PATH'"
Packit Service 0535c1
            )
Packit Service 0535c1
        if not os.access(socket_path, os.R_OK):
Packit Service 0535c1
            raise NmstatePermissionError(
Packit Service 0535c1
                f"Has no read permission to OVS db socket file {socket_path}"
Packit Service 0535c1
            )
Packit Service 0535c1
Packit Service 0535c1
        self._idl = Idl(f"unix:{socket_path}", self._schema)
Packit Service 0535c1
        self.refresh_content()
Packit Service 0535c1
        if not self._idl.has_ever_connected():
Packit Service 0535c1
            self._idl = None
Packit Service 0535c1
            raise NmstatePluginError("Failed to connect to OVS DB")
Packit Service 0535c1
Packit Service 0535c1
    def refresh_content(self):
Packit Service 0535c1
        if self._idl:
Packit Service 0535c1
            timeout_end = time.time() + TIMEOUT
Packit Service 0535c1
            self._idl.run()
Packit Service 0535c1
            if self._idl.change_seqno == self._seq_no and self._seq_no:
Packit Service 0535c1
                return
Packit Service 0535c1
            while True:
Packit Service 0535c1
                changed = self._idl.run()
Packit Service 0535c1
                cur_seq_no = self._idl.change_seqno
Packit Service 0535c1
                if cur_seq_no != self._seq_no or changed:
Packit Service 0535c1
                    self._seq_no = cur_seq_no
Packit Service 0535c1
                    return
Packit Service 0535c1
                poller = ovs.poller.Poller()
Packit Service 0535c1
                self._idl.wait(poller)
Packit Service 0535c1
                poller.timer_wait(TIMEOUT * 1000)
Packit Service 0535c1
                poller.block()
Packit Service 0535c1
                if time.time() > timeout_end:
Packit Service 0535c1
                    raise NmstateTimeoutError(
Packit Service 0535c1
                        f"Plugin {self.name} timeout({TIMEOUT} "
Packit Service 0535c1
                        "seconds) when refresh OVS database connection"
Packit Service 0535c1
                    )
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def name(self):
Packit Service 0535c1
        return "nmstate-plugin-ovsdb"
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def priority(self):
Packit Service 0535c1
        return NmstatePlugin.DEFAULT_PRIORITY + 1
Packit Service 0535c1
Packit Service 0535c1
    @property
Packit Service 0535c1
    def plugin_capabilities(self):
Packit Service 0535c1
        return NmstatePlugin.PLUGIN_CAPABILITY_IFACE
Packit Service 0535c1
Packit Service 0535c1
    def get_interfaces(self):
Packit Service 0535c1
        ifaces = []
Packit Service 0535c1
        for row in list(self._idl.tables["Interface"].rows.values()) + list(
Packit Service 0535c1
            self._idl.tables["Bridge"].rows.values()
Packit Service 0535c1
        ):
Packit Service 0535c1
            ifaces.append(
Packit Service 0535c1
                {
Packit Service 0535c1
                    Interface.NAME: row.name,
Packit Service 0535c1
                    OvsDB.OVS_DB_SUBTREE: {
Packit Service 0535c1
                        OvsDB.EXTERNAL_IDS: row.external_ids
Packit Service 0535c1
                    },
Packit Service 0535c1
                }
Packit Service 0535c1
            )
Packit Service 0535c1
        return ifaces
Packit Service 0535c1
Packit Service 0535c1
    def apply_changes(self, net_state, save_to_disk):
Packit Service 5463fa
        # State might changed after other plugin invoked apply_changes()
Packit Service 0535c1
        self.refresh_content()
Packit Service 5463fa
        cur_iface_to_ext_ids = {}
Packit Service 5463fa
        for iface_info in self.get_interfaces():
Packit Service 5463fa
            cur_iface_to_ext_ids[iface_info[Interface.NAME]] = iface_info[
Packit Service 5463fa
                OvsDB.OVS_DB_SUBTREE
Packit Service 5463fa
            ][OvsDB.EXTERNAL_IDS]
Packit Service 5463fa
Packit Service 0535c1
        pending_changes = []
Packit Service 0535c1
        for iface in net_state.ifaces.values():
Packit Service 0535c1
            if not iface.is_changed and not iface.is_desired:
Packit Service 0535c1
                continue
Packit Service 0535c1
            if not iface.is_up:
Packit Service 0535c1
                continue
Packit Service 0535c1
            if iface.type == OVSBridge.TYPE:
Packit Service 0535c1
                table_name = "Bridge"
Packit Service 0535c1
            elif iface.type == OVSInterface.TYPE:
Packit Service 0535c1
                table_name = "Interface"
Packit Service 0535c1
            else:
Packit Service 0535c1
                continue
Packit Service 5463fa
            ids_after_nm_applied = cur_iface_to_ext_ids.get(iface.name, {})
Packit Service 5463fa
            ids_before_nm_applied = (
Packit Service 5463fa
                iface.to_dict()
Packit Service 5463fa
                .get(OvsDB.OVS_DB_SUBTREE, {})
Packit Service 5463fa
                .get(OvsDB.EXTERNAL_IDS, {})
Packit Service 5463fa
            )
Packit Service 5463fa
            original_desire_ids = iface.original_dict.get(
Packit Service 5463fa
                OvsDB.OVS_DB_SUBTREE, {}
Packit Service 5463fa
            ).get(OvsDB.EXTERNAL_IDS)
Packit Service 5463fa
Packit Service 5463fa
            desire_ids = []
Packit Service 5463fa
Packit Service 5463fa
            if original_desire_ids is None:
Packit Service 5463fa
                desire_ids = ids_before_nm_applied
Packit Service 5463fa
            else:
Packit Service 5463fa
                desire_ids = original_desire_ids
Packit Service 5463fa
Packit Service 5463fa
            # should include external_id created by NetworkManager.
Packit Service 5463fa
            if NM_EXTERNAL_ID in ids_after_nm_applied:
Packit Service 5463fa
                desire_ids[NM_EXTERNAL_ID] = ids_after_nm_applied[
Packit Service 5463fa
                    NM_EXTERNAL_ID
Packit Service 5463fa
                ]
Packit Service 5463fa
            if desire_ids != ids_after_nm_applied:
Packit Service 5463fa
                pending_changes.append(
Packit Service 5463fa
                    _generate_db_change_external_ids(
Packit Service 5463fa
                        table_name, iface.name, desire_ids
Packit Service 5463fa
                    )
Packit Service 5463fa
                )
Packit Service 0535c1
        if pending_changes:
Packit Service 0535c1
            if not save_to_disk:
Packit Service 0535c1
                raise NmstateNotImplementedError(
Packit Service 0535c1
                    "ovsdb plugin does not support memory only changes"
Packit Service 0535c1
                )
Packit Service 0535c1
            elif self._idl:
Packit Service 0535c1
                self._start_transaction()
Packit Service 0535c1
                self._db_write(pending_changes)
Packit Service 0535c1
                self._commit_transaction()
Packit Service 0535c1
Packit Service 0535c1
    def _db_write(self, changes):
Packit Service 0535c1
        changes_index = {change.row_name: change for change in changes}
Packit Service 0535c1
        changed_tables = set(change.table_name for change in changes)
Packit Service 0535c1
        for changed_table in changed_tables:
Packit Service 0535c1
            for row in self._idl.tables[changed_table].rows.values():
Packit Service 0535c1
                if row.name in changes_index:
Packit Service 0535c1
                    change = changes_index[row.name]
Packit Service 0535c1
                    setattr(row, change.column_name, change.column_value)
Packit Service 0535c1
Packit Service 0535c1
    def _start_transaction(self):
Packit Service 0535c1
        self._transaction = Transaction(self._idl)
Packit Service 0535c1
Packit Service 0535c1
    def _commit_transaction(self):
Packit Service 0535c1
        if self._transaction:
Packit Service 0535c1
            status = self._transaction.commit()
Packit Service 0535c1
            timeout_end = time.time() + TIMEOUT
Packit Service 0535c1
            while status == Transaction.INCOMPLETE:
Packit Service 0535c1
                self._idl.run()
Packit Service 0535c1
                poller = ovs.poller.Poller()
Packit Service 0535c1
                self._idl.wait(poller)
Packit Service 0535c1
                self._transaction.wait(poller)
Packit Service 0535c1
                poller.timer_wait(TIMEOUT * 1000)
Packit Service 0535c1
                poller.block()
Packit Service 0535c1
                if time.time() > timeout_end:
Packit Service 0535c1
                    raise NmstateTimeoutError(
Packit Service 0535c1
                        f"Plugin {self.name} timeout({TIMEOUT} "
Packit Service 0535c1
                        "seconds) when commit OVS database transaction"
Packit Service 0535c1
                    )
Packit Service 0535c1
                status = self._transaction.commit()
Packit Service 0535c1
Packit Service 0535c1
            if status == Transaction.SUCCESS:
Packit Service 0535c1
                self.refresh_content()
Packit Service 0535c1
Packit Service 0535c1
            transaction_error = self._transaction.get_error()
Packit Service 0535c1
            self._transaction = None
Packit Service 0535c1
Packit Service 0535c1
            if status not in (Transaction.SUCCESS, Transaction.UNCHANGED):
Packit Service 0535c1
                raise NmstatePluginError(
Packit Service 0535c1
                    f"Plugin {self.name} failure on commiting OVS database "
Packit Service 0535c1
                    f"transaction: status: {status} "
Packit Service 0535c1
                    f"error: {transaction_error}"
Packit Service 0535c1
                )
Packit Service 0535c1
        else:
Packit Service 0535c1
            raise NmstatePluginError(
Packit Service 0535c1
                "BUG: _commit_transaction() invoked with "
Packit Service 0535c1
                "self._transaction is None"
Packit Service 0535c1
            )
Packit Service 0535c1
Packit Service 0535c1
Packit Service 5463fa
def _generate_db_change_external_ids(table_name, iface_name, desire_ids):
Packit Service 0535c1
    if desire_ids and not isinstance(desire_ids, dict):
Packit Service 0535c1
        raise NmstateValueError("Invalid external_ids, should be dictionary")
Packit Service 0535c1
Packit Service 5463fa
    # Convert all value to string
Packit Service 5463fa
    for key, value in desire_ids.items():
Packit Service 5463fa
        desire_ids[key] = str(value)
Packit Service 0f5335
Packit Service 5463fa
    return _Changes(table_name, OvsDB.EXTERNAL_IDS, iface_name, desire_ids)
Packit Service 0535c1
Packit Service 0535c1
Packit Service 0535c1
NMSTATE_PLUGIN = NmstateOvsdbPlugin