TAAS tap-mirror OVN driver

Depends-On: https://review.opendev.org/c/openstack/ovsdbapp/+/890328
Related-Bug: #2015471
Change-Id: I6007a2a1644bbe432d7a8abaeaa8194a4eb2bfed
This commit is contained in:
elajkat 2023-06-30 09:55:31 +02:00
parent faa98ef97c
commit 8c1b33d50a
12 changed files with 1364 additions and 0 deletions

View File

@ -0,0 +1,125 @@
# 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 oslo_config import cfg
from oslo_log import helpers as log_helpers
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_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(object):
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

View File

@ -0,0 +1,154 @@
# 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 oslo_config import cfg
from oslo_log import log
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 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()

View File

@ -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 oslo_log import helpers as log_helpers
from oslo_log import log as logging
from neutron_taas.extensions import _tap_mirror
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_mirror.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

View File

@ -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}
}
}

View File

@ -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()

View File

@ -0,0 +1,96 @@
# 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_lib.callbacks import events
from neutron_lib.callbacks import resources
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron.tests import base
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()

View File

@ -0,0 +1,131 @@
# 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 oslo_utils import uuidutils
from neutron.tests import base
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)

View File

@ -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)