diff --git a/neutron_taas/services/taas/service_drivers/ovn/__init__.py b/neutron_taas/services/taas/service_drivers/ovn/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_taas/services/taas/service_drivers/ovn/helper.py b/neutron_taas/services/taas/service_drivers/ovn/helper.py new file mode 100644 index 00000000..9db6dbe8 --- /dev/null +++ b/neutron_taas/services/taas/service_drivers/ovn/helper.py @@ -0,0 +1,124 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import queue +import threading + +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from oslo_config import cfg +from oslo_log import helpers as log_helpers +from oslo_log import log as logging +from ovs.stream import Stream + +from neutron_taas.services.taas.service_drivers.ovn.ovsdb import impl_idl_taas + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +class TaasOvnProviderHelper(): + + def __init__(self): + ovn_conf.register_opts() + self._requests = queue.Queue() + self._helper_thread = threading.Thread(target=self._request_handler) + self._helper_thread.daemon = True + self._check_and_set_ssl_files() + self._taas_mirror_func_map = { + 'mirror_del': self.mirror_del, + 'mirror_add': self.mirror_add, + } + self._subscribe() + self._helper_thread.start() + + def _subscribe(self): + registry.subscribe(self._post_fork_initialize, + resources.PROCESS, + events.AFTER_INIT) + + def _post_fork_initialize(self, resource, event, trigger, payload=None): + self.ovn_nbdb = impl_idl_taas.OvnNbIdlForTaas() + self.ovn_nbdb_api = self.ovn_nbdb.start() + + def _check_and_set_ssl_files(self): + priv_key_file = CONF.ovn.ovn_nb_private_key + cert_file = CONF.ovn.ovn_nb_certificate + ca_cert_file = CONF.ovn.ovn_nb_ca_cert + + if priv_key_file: + Stream.ssl_set_private_key_file(priv_key_file) + + if cert_file: + Stream.ssl_set_certificate_file(cert_file) + + if ca_cert_file: + Stream.ssl_set_ca_cert_file(ca_cert_file) + + def _request_handler(self): + while True: + request = self._requests.get() + request_type = request['type'] + if request_type == 'exit': + break + + request_handler = self._taas_mirror_func_map.get(request_type) + try: + if request_handler: + request_handler(request['info']) + self._requests.task_done() + except Exception: + # If any unexpected exception happens we don't want the + # notify_loop to exit. + LOG.exception('Unexpected exception in request_handler') + + def _execute_commands(self, commands): + with self.ovn_nbdb_api.transaction(check_error=True) as txn: + for command in commands: + txn.add(command) + + def shutdown(self): + self._requests.put({'type': 'exit'}) + self._helper_thread.join() + self.ovn_nbdb.stop() + del self.ovn_nbdb_api + + def add_request(self, req): + self._requests.put(req) + + @log_helpers.log_method_call + def mirror_del(self, request): + port_id = request.pop('port_id') + ovn_port = self.ovn_nbdb_api.lookup('Logical_Switch_Port', port_id) + mirror = self.ovn_nbdb_api.mirror_get( + request['name']).execute(check_error=True) + self.ovn_nbdb_api.lsp_detach_mirror( + ovn_port.name, mirror.uuid, + if_exist=True).execute(check_error=True) + self.ovn_nbdb_api.mirror_del( + mirror.uuid).execute(check_error=True) + + @log_helpers.log_method_call + def mirror_add(self, request): + port_id = request.pop('port_id') + ovn_port = self.ovn_nbdb_api.lookup('Logical_Switch_Port', port_id) + + mirror = self.ovn_nbdb_api.mirror_add( + **request).execute(check_error=True) + self.ovn_nbdb_api.lsp_attach_mirror( + ovn_port.name, mirror.uuid, + may_exist=True).execute(check_error=True) + + return mirror diff --git a/neutron_taas/services/taas/service_drivers/ovn/ovsdb/__init__.py b/neutron_taas/services/taas/service_drivers/ovn/ovsdb/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_taas/services/taas/service_drivers/ovn/ovsdb/impl_idl_taas.py b/neutron_taas/services/taas/service_drivers/ovn/ovsdb/impl_idl_taas.py new file mode 100644 index 00000000..eaeb4404 --- /dev/null +++ b/neutron_taas/services/taas/service_drivers/ovn/ovsdb/impl_idl_taas.py @@ -0,0 +1,153 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import atexit +import contextlib +import tenacity + +from neutron.common.ovn import exceptions as ovn_exceptions +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron_lib import exceptions as n_exc +from oslo_config import cfg +from oslo_log import log +from ovsdbapp.backend import ovs_idl +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp.backend.ovs_idl import transaction as idl_trans +from ovsdbapp.schema.ovn_northbound import impl_idl as nb_impl_idl + + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +class Backend(ovs_idl.Backend): + lookup_table = {} + ovsdb_connection = None + + def __init__(self, connection): + ovn_conf.register_opts() + self.ovsdb_connection = connection + super().__init__(connection) + + def start_connection(self, connection): + try: + self.ovsdb_connection.start() + except Exception as e: + connection_exception = OvsdbConnectionUnavailable( + db_schema=self.schema, error=e) + LOG.exception(connection_exception) + raise connection_exception from e + + @property + def idl(self): + return self.ovsdb_connection.idl + + @property + def tables(self): + return self.idl.tables + + _tables = tables + + def is_table_present(self, table_name): + return table_name in self._tables + + def is_col_present(self, table_name, col_name): + return self.is_table_present(table_name) and ( + col_name in self._tables[table_name].columns) + + def create_transaction(self, check_error=False, log_errors=True): + return idl_trans.Transaction( + self, self.ovsdb_connection, self.ovsdb_connection.timeout, + check_error, log_errors) + + # Check for a column match in the table. If not found do a retry with + # a stop delay of 10 secs. This function would be useful if the caller + # wants to verify for the presence of a particular row in the table + # with the column match before doing any transaction. + # Eg. We can check if Logical_Switch row is present before adding a + # logical switch port to it. + @tenacity.retry(retry=tenacity.retry_if_exception_type(RuntimeError), + wait=tenacity.wait_exponential(), + stop=tenacity.stop_after_delay(10), + reraise=True) + def check_for_row_by_value_and_retry(self, table, column, match): + try: + idlutils.row_by_value(self.idl, table, column, match) + except idlutils.RowNotFound as e: + msg = (_("%(match)s does not exist in %(column)s of %(table)s") + % {'match': match, 'column': column, 'table': table}) + raise RuntimeError(msg) from e + + +class OvsdbConnectionUnavailable(n_exc.ServiceUnavailable): + message = _("OVS database connection to %(db_schema)s failed with error: " + "'%(error)s'. Verify that the OVS and OVN services are " + "available and that the 'ovn_nb_connection' and " + "'ovn_sb_connection' configuration options are correct.") + + +class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): + def __init__(self, connection): + super().__init__(connection) + self.idl._session.reconnect.set_probe_interval( + ovn_conf.get_ovn_ovsdb_probe_interval()) + + @contextlib.contextmanager + def transaction(self, check_error=False, log_errors=True, nested=True, + **kwargs): + """A wrapper on the ovsdbapp transaction to work with revisions. + + This method is just a wrapper around the ovsdbapp transaction + to handle revision conflicts correctly. + """ + try: + with super().transaction(check_error, log_errors, nested, + **kwargs) as t: + yield t + except ovn_exceptions.RevisionConflict as e: + LOG.info('Transaction aborted. Reason: %s', e) + + +class OvnNbIdlForTaas(connection.OvsdbIdl): + + SCHEMA = "OVN_Northbound" + TABLES = ('Logical_Switch_Port', 'Mirror') + + def __init__(self): + ovn_conf.register_opts() + self.conn_string = ovn_conf.get_ovn_nb_connection() + helper = self._get_ovsdb_helper(self.conn_string) + for table in OvnNbIdlForTaas.TABLES: + helper.register_table(table) + super().__init__(self.conn_string, helper) + atexit.register(self.stop) + + @tenacity.retry( + wait=tenacity.wait_exponential(18), + reraise=True) + def _get_ovsdb_helper(self, connection_string): + return idlutils.get_schema_helper(connection_string, self.SCHEMA) + + def start(self): + self.conn = connection.Connection(self, timeout=180) + return OvsdbNbOvnIdl(self.conn) + + def stop(self): + # Close the running connection if it has been initalized + if hasattr(self, 'conn'): + if not self.conn.stop(timeout=180): + LOG.debug("Connection terminated to OvnNb " + "but a thread is still alive") + del self.conn + # Close the idl session + self.close() diff --git a/neutron_taas/services/taas/service_drivers/ovn/taas_ovn.py b/neutron_taas/services/taas/service_drivers/ovn/taas_ovn.py new file mode 100644 index 00000000..46cb121a --- /dev/null +++ b/neutron_taas/services/taas/service_drivers/ovn/taas_ovn.py @@ -0,0 +1,111 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.api.definitions import tap_mirror as tap_m_api_def +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from neutron_taas.services.taas import service_drivers +from neutron_taas.services.taas.service_drivers.ovn import helper + + +LOG = logging.getLogger(__name__) + + +class TaasOvnDriver(service_drivers.TaasBaseDriver): + """Taas OVN Service Driver class""" + + more_supported_extension_aliases = [tap_m_api_def.ALIAS] + + def __init__(self, service_plugin): + LOG.debug("Loading Taas OVN Driver.") + super().__init__(service_plugin) + self._ovn_helper = helper.TaasOvnProviderHelper() + + def __del__(self): + self._ovn_helper.shutdown() + + @log_helpers.log_method_call + def create_tap_service_precommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def create_tap_service_postcommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def delete_tap_service_precommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def delete_tap_service_postcommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def create_tap_flow_precommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def create_tap_flow_postcommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def delete_tap_flow_precommit(self, context): + """Send tap flow deletion RPC message to agent.""" + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def delete_tap_flow_postcommit(self, context): + LOG.warning("Not implemented") + + @log_helpers.log_method_call + def create_tap_mirror_precommit(self, context): + pass + + @log_helpers.log_method_call + def create_tap_mirror_postcommit(self, context): + LOG.info('create_tap_mirror_postcommit %s', context.tap_mirror) + t_m = context.tap_mirror + type = 'erspan' if 'erspan' in t_m['mirror_type'] else 'gre' + directions = t_m['directions'] + for direction, tunnel_id in directions.items(): + mirror_port_name = 'tm_%s_%s' % (direction.lower(), t_m['id'][0:6]) + ovn_direction = ('from-lport' if direction == 'OUT' + else 'to-lport') + request = {'type': 'mirror_add', + 'info': {'name': mirror_port_name, + 'direction_filter': ovn_direction, + 'dest': t_m['remote_ip'], + 'mirror_type': type, + 'index': int(tunnel_id), + 'port_id': t_m['port_id']}} + self._ovn_helper.add_request(request) + + @log_helpers.log_method_call + def delete_tap_mirror_precommit(self, context): + LOG.info('delete_tap_mirror_precommit %s', context.tap_mirror) + t_m = context.tap_mirror + directions = t_m['directions'] + for direction, tunnel_id in directions.items(): + mirror_port_name = 'tm_%s_%s' % (direction.lower(), t_m['id'][0:6]) + request = { + 'type': 'mirror_del', + 'info': {'id': t_m['id'], + 'name': mirror_port_name, + 'sink': t_m['remote_ip'], + 'port_id': t_m['port_id']} + } + self._ovn_helper.add_request(request) + + @log_helpers.log_method_call + def delete_tap_mirror_postcommit(self, context): + pass diff --git a/neutron_taas/tests/unit/services/ovn/__init__.py b/neutron_taas/tests/unit/services/ovn/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_taas/tests/unit/services/ovn/ovsdb/__init__.py b/neutron_taas/tests/unit/services/ovn/ovsdb/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_taas/tests/unit/services/ovn/ovsdb/schema_files/ovn-nb.ovsschema b/neutron_taas/tests/unit/services/ovn/ovsdb/schema_files/ovn-nb.ovsschema new file mode 100644 index 00000000..e103360e --- /dev/null +++ b/neutron_taas/tests/unit/services/ovn/ovsdb/schema_files/ovn-nb.ovsschema @@ -0,0 +1,665 @@ +{ + "name": "OVN_Northbound", + "version": "7.1.0", + "cksum": "217362582 33949", + "tables": { + "NB_Global": { + "columns": { + "name": {"type": "string"}, + "nb_cfg": {"type": {"key": "integer"}}, + "nb_cfg_timestamp": {"type": {"key": "integer"}}, + "sb_cfg": {"type": {"key": "integer"}}, + "sb_cfg_timestamp": {"type": {"key": "integer"}}, + "hv_cfg": {"type": {"key": "integer"}}, + "hv_cfg_timestamp": {"type": {"key": "integer"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "connections": { + "type": {"key": {"type": "uuid", + "refTable": "Connection"}, + "min": 0, + "max": "unlimited"}}, + "ssl": { + "type": {"key": {"type": "uuid", + "refTable": "SSL"}, + "min": 0, "max": 1}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "ipsec": {"type": "boolean"}}, + "maxRows": 1, + "isRoot": true}, + "Copp": { + "columns": { + "name": {"type": "string"}, + "meters": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Logical_Switch": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Switch_Port", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "acls": {"type": {"key": {"type": "uuid", + "refTable": "ACL", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "qos_rules": {"type": {"key": {"type": "uuid", + "refTable": "QoS", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "load_balancer": {"type": {"key": {"type": "uuid", + "refTable": "Load_Balancer", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "load_balancer_group": { + "type": {"key": {"type": "uuid", + "refTable": "Load_Balancer_Group"}, + "min": 0, + "max": "unlimited"}}, + "dns_records": {"type": {"key": {"type": "uuid", + "refTable": "DNS", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "copp": {"type": {"key": {"type": "uuid", "refTable": "Copp", + "refType": "weak"}, + "min": 0, "max": 1}}, + "other_config": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "forwarding_groups": { + "type": {"key": {"type": "uuid", + "refTable": "Forwarding_Group", + "refType": "strong"}, + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Logical_Switch_Port": { + "columns": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "parent_name": {"type": {"key": "string", "min": 0, "max": 1}}, + "tag_request": { + "type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 4095}, + "min": 0, "max": 1}}, + "tag": { + "type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 4095}, + "min": 0, "max": 1}}, + "addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "dynamic_addresses": {"type": {"key": "string", + "min": 0, + "max": 1}}, + "port_security": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "up": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "dhcpv4_options": {"type": {"key": {"type": "uuid", + "refTable": "DHCP_Options", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "dhcpv6_options": {"type": {"key": {"type": "uuid", + "refTable": "DHCP_Options", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "mirror_rules": {"type": {"key": {"type": "uuid", + "refTable": "Mirror", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "ha_chassis_group": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis_Group", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "Forwarding_Group": { + "columns": { + "name": {"type": "string"}, + "vip": {"type": "string"}, + "vmac": {"type": "string"}, + "liveness": {"type": "boolean"}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "child_port": {"type": {"key": "string", + "min": 1, "max": "unlimited"}}}, + "isRoot": false}, + "Address_Set": { + "columns": { + "name": {"type": "string"}, + "addresses": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Port_Group": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Switch_Port", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "acls": {"type": {"key": {"type": "uuid", + "refTable": "ACL", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Load_Balancer": { + "columns": { + "name": {"type": "string"}, + "vips": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "protocol": { + "type": {"key": {"type": "string", + "enum": ["set", ["tcp", "udp", "sctp"]]}, + "min": 0, "max": 1}}, + "health_check": {"type": { + "key": {"type": "uuid", + "refTable": "Load_Balancer_Health_Check", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "ip_port_mappings": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "selection_fields": { + "type": {"key": {"type": "string", + "enum": ["set", + ["eth_src", "eth_dst", "ip_src", "ip_dst", + "tp_src", "tp_dst"]]}, + "min": 0, "max": "unlimited"}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Load_Balancer_Group": { + "columns": { + "name": {"type": "string"}, + "load_balancer": {"type": {"key": {"type": "uuid", + "refTable": "Load_Balancer", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Load_Balancer_Health_Check": { + "columns": { + "vip": {"type": "string"}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "ACL": { + "columns": { + "name": {"type": {"key": {"type": "string", + "maxLength": 63}, + "min": 0, "max": 1}}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "direction": {"type": {"key": {"type": "string", + "enum": ["set", ["from-lport", "to-lport"]]}}}, + "match": {"type": "string"}, + "action": {"type": {"key": {"type": "string", + "enum": ["set", + ["allow", "allow-related", + "allow-stateless", "drop", + "reject", "pass"]]}}}, + "log": {"type": "boolean"}, + "severity": {"type": {"key": {"type": "string", + "enum": ["set", + ["alert", "warning", + "notice", "info", + "debug"]]}, + "min": 0, "max": 1}}, + "meter": {"type": {"key": "string", "min": 0, "max": 1}}, + "label": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 4294967295}}}, + "tier": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 3}}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "QoS": { + "columns": { + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "direction": {"type": {"key": {"type": "string", + "enum": ["set", ["from-lport", "to-lport"]]}}}, + "match": {"type": "string"}, + "action": {"type": {"key": {"type": "string", + "enum": ["set", ["dscp"]]}, + "value": {"type": "integer", + "minInteger": 0, + "maxInteger": 63}, + "min": 0, "max": "unlimited"}}, + "bandwidth": {"type": {"key": {"type": "string", + "enum": ["set", ["rate", + "burst"]]}, + "value": {"type": "integer", + "minInteger": 1, + "maxInteger": 4294967295}, + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "Mirror": { + "columns": { + "name": {"type": "string"}, + "filter": {"type": {"key": {"type": "string", + "enum": ["set", ["from-lport", + "to-lport", + "both"]]}}}, + "sink":{"type": "string"}, + "type": {"type": {"key": {"type": "string", + "enum": ["set", ["gre", + "erspan", + "local"]]}}}, + "index": {"type": "integer"}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Meter": { + "columns": { + "name": {"type": "string"}, + "unit": {"type": {"key": {"type": "string", + "enum": ["set", ["kbps", "pktps"]]}}}, + "bands": {"type": {"key": {"type": "uuid", + "refTable": "Meter_Band", + "refType": "strong"}, + "min": 1, + "max": "unlimited"}}, + "fair": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "Meter_Band": { + "columns": { + "action": {"type": {"key": {"type": "string", + "enum": ["set", ["drop"]]}}}, + "rate": {"type": {"key": {"type": "integer", + "minInteger": 1, + "maxInteger": 4294967295}}}, + "burst_size": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 4294967295}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "Logical_Router": { + "columns": { + "name": {"type": "string"}, + "ports": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Port", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "static_routes": {"type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Static_Route", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "policies": { + "type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Policy", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "nat": {"type": {"key": {"type": "uuid", + "refTable": "NAT", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "load_balancer": {"type": {"key": {"type": "uuid", + "refTable": "Load_Balancer", + "refType": "weak"}, + "min": 0, + "max": "unlimited"}}, + "load_balancer_group": { + "type": {"key": {"type": "uuid", + "refTable": "Load_Balancer_Group"}, + "min": 0, + "max": "unlimited"}}, + "copp": {"type": {"key": {"type": "uuid", "refTable": "Copp", + "refType": "weak"}, + "min": 0, "max": 1}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Logical_Router_Port": { + "columns": { + "name": {"type": "string"}, + "gateway_chassis": { + "type": {"key": {"type": "uuid", + "refTable": "Gateway_Chassis", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "ha_chassis_group": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis_Group", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "options": { + "type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "networks": {"type": {"key": "string", + "min": 1, + "max": "unlimited"}}, + "mac": {"type": "string"}, + "peer": {"type": {"key": "string", "min": 0, "max": 1}}, + "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, + "ipv6_ra_configs": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "ipv6_prefix": {"type": {"key": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "status": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "Logical_Router_Static_Route": { + "columns": { + "route_table": {"type": "string"}, + "ip_prefix": {"type": "string"}, + "policy": {"type": {"key": {"type": "string", + "enum": ["set", ["src-ip", + "dst-ip"]]}, + "min": 0, "max": 1}}, + "nexthop": {"type": "string"}, + "output_port": {"type": {"key": "string", "min": 0, "max": 1}}, + "bfd": {"type": {"key": {"type": "uuid", "refTable": "BFD", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "Logical_Router_Policy": { + "columns": { + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "match": {"type": "string"}, + "action": {"type": { + "key": {"type": "string", + "enum": ["set", ["allow", "drop", "reroute"]]}}}, + "nexthop": {"type": {"key": "string", "min": 0, "max": 1}}, + "nexthops": {"type": { + "key": "string", "min": 0, "max": "unlimited"}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "NAT": { + "columns": { + "external_ip": {"type": "string"}, + "external_mac": {"type": {"key": "string", + "min": 0, "max": 1}}, + "external_port_range": {"type": "string"}, + "logical_ip": {"type": "string"}, + "logical_port": {"type": {"key": "string", + "min": 0, "max": 1}}, + "type": {"type": {"key": {"type": "string", + "enum": ["set", ["dnat", + "snat", + "dnat_and_snat" + ]]}}}, + "allowed_ext_ips": {"type": { + "key": {"type": "uuid", "refTable": "Address_Set", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "exempted_ext_ips": {"type": { + "key": {"type": "uuid", "refTable": "Address_Set", + "refType": "strong"}, + "min": 0, + "max": 1}}, + "gateway_port": { + "type": {"key": {"type": "uuid", + "refTable": "Logical_Router_Port", + "refType": "weak"}, + "min": 0, + "max": 1}}, + "options": {"type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "DHCP_Options": { + "columns": { + "cidr": {"type": "string"}, + "options": {"type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": true}, + "Connection": { + "columns": { + "target": {"type": "string"}, + "max_backoff": {"type": {"key": {"type": "integer", + "minInteger": 1000}, + "min": 0, + "max": 1}}, + "inactivity_probe": {"type": {"key": "integer", + "min": 0, + "max": 1}}, + "other_config": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "is_connected": {"type": "boolean", "ephemeral": true}, + "status": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}, + "ephemeral": true}}, + "indexes": [["target"]]}, + "DNS": { + "columns": { + "records": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "isRoot": true}, + "SSL": { + "columns": { + "private_key": {"type": "string"}, + "certificate": {"type": "string"}, + "ca_cert": {"type": "string"}, + "bootstrap_ca_cert": {"type": "boolean"}, + "ssl_protocols": {"type": "string"}, + "ssl_ciphers": {"type": "string"}, + "external_ids": {"type": {"key": "string", + "value": "string", + "min": 0, + "max": "unlimited"}}}, + "maxRows": 1}, + "Gateway_Chassis": { + "columns": { + "name": {"type": "string"}, + "chassis_name": {"type": "string"}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": false}, + "HA_Chassis": { + "columns": { + "chassis_name": {"type": "string"}, + "priority": {"type": {"key": {"type": "integer", + "minInteger": 0, + "maxInteger": 32767}}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "isRoot": false}, + "HA_Chassis_Group": { + "columns": { + "name": {"type": "string"}, + "ha_chassis": { + "type": {"key": {"type": "uuid", + "refTable": "HA_Chassis", + "refType": "strong"}, + "min": 0, + "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["name"]], + "isRoot": true}, + "BFD": { + "columns": { + "logical_port": {"type": "string"}, + "dst_ip": {"type": "string"}, + "min_tx": {"type": {"key": {"type": "integer", + "minInteger": 1}, + "min": 0, "max": 1}}, + "min_rx": {"type": {"key": {"type": "integer"}, + "min": 0, "max": 1}}, + "detect_mult": {"type": {"key": {"type": "integer", + "minInteger": 1}, + "min": 0, "max": 1}}, + "status": { + "type": {"key": {"type": "string", + "enum": ["set", ["down", "init", "up", + "admin_down"]]}, + "min": 0, "max": 1}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "options": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["logical_port", "dst_ip"]], + "isRoot": true}, + "Static_MAC_Binding": { + "columns": { + "logical_port": {"type": "string"}, + "ip": {"type": "string"}, + "mac": {"type": "string"}, + "override_dynamic_mac": {"type": "boolean"}}, + "indexes": [["logical_port", "ip"]], + "isRoot": true}, + "Chassis_Template_Var": { + "columns": { + "chassis": {"type": "string"}, + "variables": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}, + "external_ids": { + "type": {"key": "string", "value": "string", + "min": 0, "max": "unlimited"}}}, + "indexes": [["chassis"]], + "isRoot": true} + } +} diff --git a/neutron_taas/tests/unit/services/ovn/ovsdb/test_impl_idl_taas.py b/neutron_taas/tests/unit/services/ovn/ovsdb/test_impl_idl_taas.py new file mode 100644 index 00000000..1eced728 --- /dev/null +++ b/neutron_taas/tests/unit/services/ovn/ovsdb/test_impl_idl_taas.py @@ -0,0 +1,74 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +from unittest import mock + +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron.tests import base +from ovs.db import idl as ovs_idl +from ovsdbapp.backend import ovs_idl as real_ovs_idl +from ovsdbapp.backend.ovs_idl import idlutils + +from neutron_taas.services.taas.service_drivers.ovn.ovsdb import impl_idl_taas + + +basedir = os.path.dirname(os.path.abspath(__file__)) +schema_files = { + 'OVN_Northbound': os.path.join(basedir, + 'schema_files', 'ovn-nb.ovsschema'), +} + + +class TestOvnNbIdlForTaas(base.BaseTestCase): + + def setUp(self): + super().setUp() + ovn_conf.register_opts() + self.mock_gsh = mock.patch.object( + idlutils, 'get_schema_helper', + side_effect=lambda x, y: ovs_idl.SchemaHelper( + location=schema_files['OVN_Northbound'])).start() + self.idl_taas = impl_idl_taas.OvnNbIdlForTaas() + + def test__get_ovsdb_helper(self): + self.mock_gsh.reset_mock() + self.idl_taas._get_ovsdb_helper('foo') + self.mock_gsh.assert_called_once_with('foo', 'OVN_Northbound') + + @mock.patch.object(real_ovs_idl.Backend, 'autocreate_indices', mock.Mock(), + create=True) + def test_start(self): + with mock.patch('ovsdbapp.backend.ovs_idl.connection.Connection', + side_effect=lambda x, timeout: mock.Mock()): + idl_taas_1 = impl_idl_taas.OvnNbIdlForTaas() + ret_taas_1 = idl_taas_1.start() + id1 = id(ret_taas_1.ovsdb_connection) + idl_taas_2 = impl_idl_taas.OvnNbIdlForTaas() + ret_taas_2 = idl_taas_2.start() + id2 = id(ret_taas_2.ovsdb_connection) + self.assertNotEqual(id1, id2) + + @mock.patch('ovsdbapp.backend.ovs_idl.connection.Connection') + def test_stop(self, mock_conn): + mock_conn.stop.return_value = False + with mock.patch.object(self.idl_taas, 'close') as mock_close: + self.idl_taas.start() + self.idl_taas.stop() + mock_close.assert_called_once_with() + + @mock.patch('ovsdbapp.backend.ovs_idl.connection.Connection') + def test_stop_no_connection(self, mock_conn): + mock_conn.stop.return_value = False + with mock.patch.object(self.idl_taas, 'close') as mock_close: + self.idl_taas.stop() + mock_close.assert_called_once_with() diff --git a/neutron_taas/tests/unit/services/ovn/test_helper.py b/neutron_taas/tests/unit/services/ovn/test_helper.py new file mode 100644 index 00000000..94dd9cfe --- /dev/null +++ b/neutron_taas/tests/unit/services/ovn/test_helper.py @@ -0,0 +1,95 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf +from neutron.tests import base +from neutron_lib.callbacks import events +from neutron_lib.callbacks import resources + +from neutron_taas.services.taas.service_drivers.ovn import helper + + +class TestTaasOvnProviderHelper(base.BaseTestCase): + + def setUp(self): + super().setUp() + ovn_conf.register_opts() + + ovn_nb_idl = mock.patch( + 'neutron_taas.services.taas.service_drivers.ovn.ovsdb.' + 'impl_idl_taas.OvnNbIdlForTaas') + self.mock_ovn_nb_idl = ovn_nb_idl.start() + mock.patch( + 'ovsdbapp.backend.ovs_idl.idlutils.get_schema_helper').start() + self.helper = helper.TaasOvnProviderHelper() + self.helper._post_fork_initialize( + resources.PROCESS, events.AFTER_INIT, None) + + self.ovn_nbdb_api = mock.patch.object(self.helper, 'ovn_nbdb_api') + self.ovn_nbdb_api.start() + add_req_thread = mock.patch.object(helper.TaasOvnProviderHelper, + 'add_request') + self.mock_add_request = add_req_thread.start() + + def test_mirror_add(self): + port_id = '1234' + name = 'foo_mirror' + dest_ip = '10.92.10.5' + type = 'gre' + tunnel_id = 101 + direction = 'to-lport' + + self.helper.mirror_add({ + 'name': name, + 'direction_filter': direction, + 'dest': dest_ip, + 'mirror_type': type, + 'index': tunnel_id, + 'port_id': port_id + }) + + self.helper.ovn_nbdb_api.lookup.assert_called_once_with( + 'Logical_Switch_Port', port_id) + self.helper.ovn_nbdb_api.mirror_add.assert_called_once_with( + name=name, + direction_filter=direction, + dest=dest_ip, + mirror_type=type, + index=tunnel_id + ) + self.helper.ovn_nbdb_api.lsp_attach_mirror.assert_called_once() + + def test_mirror_del(self): + port_id = '1234' + name = 'foo_mirror' + dest_ip = '10.92.10.5' + type = 'gre' + tunnel_id = 101 + direction = 'to-lport' + + self.helper.mirror_del({ + 'port_id': port_id, + 'name': name, + 'direction_filter': direction, + 'dest': dest_ip, + 'mirror_type': type, + 'index': tunnel_id, + 'port_id': port_id + }) + + self.helper.ovn_nbdb_api.lookup.assert_called_once_with( + 'Logical_Switch_Port', port_id) + self.helper.ovn_nbdb_api.mirror_get.assert_called_once_with(name) + self.helper.ovn_nbdb_api.lsp_detach_mirror.assert_called_once() + self.helper.ovn_nbdb_api.mirror_del.assert_called_once() diff --git a/neutron_taas/tests/unit/services/ovn/test_taas_ovn.py b/neutron_taas/tests/unit/services/ovn/test_taas_ovn.py new file mode 100644 index 00000000..96a543f4 --- /dev/null +++ b/neutron_taas/tests/unit/services/ovn/test_taas_ovn.py @@ -0,0 +1,130 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +from unittest import mock + +from neutron.tests import base +from oslo_utils import uuidutils + +from neutron_taas.services.taas.service_drivers.ovn import helper +from neutron_taas.services.taas.service_drivers.ovn import taas_ovn + + +class FakeMirrorContext(): + def __init__(self, tap_mirror): + self._tap_mirror = tap_mirror + + @property + def tap_mirror(self): + return self._tap_mirror + + +class TestTaasOvnDriver(base.BaseTestCase): + + def setUp(self): + super().setUp() + self.driver = taas_ovn.TaasOvnDriver('tapmirror') + add_req_thread = mock.patch.object(helper.TaasOvnProviderHelper, + 'add_request') + self.mock_add_request = add_req_thread.start() + helper_mock = mock.patch.object(helper.TaasOvnProviderHelper, + 'shutdown') + helper_mock.start() + + self.tap_mirror_dict = { + 'mirror_type': 'gre', + 'directions': {'IN': 101}, + 'id': uuidutils.generate_uuid(), + 'remote_ip': '10.92.10.5', + 'port_id': uuidutils.generate_uuid() + } + self.multi_dir_t_mirror = copy.deepcopy(self.tap_mirror_dict) + self.multi_dir_t_mirror['directions'] = {'IN': 101, 'OUT': 102} + + def test_create_tap_mirror_postcommit(self): + ctx = FakeMirrorContext(self.tap_mirror_dict) + self.driver.create_tap_mirror_postcommit(ctx) + expected_dict = { + 'type': 'mirror_add', + 'info': { + 'name': mock.ANY, + 'direction_filter': 'to-lport', + 'dest': self.tap_mirror_dict['remote_ip'], + 'mirror_type': self.tap_mirror_dict['mirror_type'], + 'index': self.tap_mirror_dict['directions']['IN'], + 'port_id': self.tap_mirror_dict['port_id'], + } + } + self.mock_add_request.assert_called_once_with(expected_dict) + + def test_create_tap_mirror_postcommit_multi_dir(self): + ctx = FakeMirrorContext(self.multi_dir_t_mirror) + self.driver.create_tap_mirror_postcommit(ctx) + + expected_in_call = { + 'type': 'mirror_add', + 'info': { + 'name': mock.ANY, + 'direction_filter': 'to-lport', + 'dest': self.tap_mirror_dict['remote_ip'], + 'mirror_type': self.tap_mirror_dict['mirror_type'], + 'index': self.tap_mirror_dict['directions']['IN'], + 'port_id': self.tap_mirror_dict['port_id'], + } + } + expected_out_call = copy.deepcopy(expected_in_call) + expected_out_call['info']['direction_filter'] = 'from-lport' + out_dir_tun_id = self.multi_dir_t_mirror['directions']['OUT'] + expected_out_call['info']['index'] = out_dir_tun_id + + expected_calls = [ + mock.call(expected_in_call), + mock.call(expected_out_call) + ] + + self.mock_add_request.assert_has_calls(expected_calls) + + def test_delete_tap_mirror_precommit(self): + ctx = FakeMirrorContext(self.tap_mirror_dict) + self.driver.delete_tap_mirror_precommit(ctx) + + expected_dict = { + 'type': 'mirror_del', + 'info': { + 'id': self.tap_mirror_dict['id'], + 'name': mock.ANY, + 'sink': self.tap_mirror_dict['remote_ip'], + 'port_id': self.tap_mirror_dict['port_id']} + } + self.mock_add_request.assert_called_once_with(expected_dict) + + def test_delete_tap_mirror_precommit_multi_dir(self): + ctx = FakeMirrorContext(self.multi_dir_t_mirror) + self.driver.delete_tap_mirror_precommit(ctx) + + expected_call = { + 'type': 'mirror_del', + 'info': { + 'id': self.tap_mirror_dict['id'], + 'name': mock.ANY, + 'sink': self.tap_mirror_dict['remote_ip'], + 'port_id': self.tap_mirror_dict['port_id'], + } + } + + expected_calls = [ + mock.call(expected_call), + mock.call(expected_call) + ] + + self.mock_add_request.assert_has_calls(expected_calls) diff --git a/releasenotes/notes/tap-mirror-with-ovn-driver-ff26bc79d3d411be.yaml b/releasenotes/notes/tap-mirror-with-ovn-driver-ff26bc79d3d411be.yaml new file mode 100644 index 00000000..fc3de294 --- /dev/null +++ b/releasenotes/notes/tap-mirror-with-ovn-driver-ff26bc79d3d411be.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add possibility to create (CRUD) ``tap_mirrors`` with ``OVN`` backend. + At least ``OVN`` ``v22.12.0`` is necessary to create mirrors. + Other tap-as-a-service APIs (tap-service and tap-flow) are not part + of this effort + (https://specs.openstack.org/openstack/neutron-specs/specs/2023.2/erspan-for-tap-as-a-service.html)