[OVN] Off-path SmartNIC DPU Port Binding with OVN

Traditionally it has been the CMSs, in OpenStacks case Nova's,
responsibility to create Virtual Interfaces (VIFs) as part of
instance life cycle, and subsequently manage plug/unplug operations
on the Open vSwitch integration bridge.

With the advent of SmartNIC DPUs which are connected to multiple
distinct CPUs we can have a topology where the instance runs on one
host and Open vSwitch and OVN runs on a different host, the
SmartNIC DPU control plane CPU.

One of the main use cases for having this topology is security
where we treat the hypervisor host as untrusted and prohibit
direct communication between the hypervisor host and the SmartNIC
DPU control plane host.  In addition to that control facilities
such as switchdev devices are only visible from the SmartNIC DPU
control plane CPUs.

Adds support for binding ports of type VNIC_REMOTE_MANAGED by
looking up chassis based on serial number that Nova provides in
the binding_profile.

Information required by the OVN controller to successfully look up
and plug representor port is provided as options on the LSP as
defined by the representor plug provider documentation [0][1].

0: https://docs.ovn.org/en/stable/topics/vif-plug-providers/vif-plug-providers.html
1: https://github.com/ovn-org/ovn-vif/blob/main/Documentation/topics/vif-plug-providers/vif-plug-representor.rst
Partial-Bug: #1932154
Depends-On: I496db96ea40da3bee5b81bcee1edc79e1f46b541
Depends-On: I83a128a260acdd8bf78fede566af6881b8b82a9c
Change-Id: Icc6c2d0f7f8f5cc94997db6244175a8e8884789f
This commit is contained in:
Frode Nordahl 2021-09-14 14:33:29 +02:00
parent ce96e502fa
commit 7d64d0c116
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
13 changed files with 579 additions and 57 deletions

View File

@ -50,7 +50,7 @@ msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
neutron-lib==2.18.0
neutron-lib==2.20.0
openstacksdk==0.31.2
os-client-config==1.28.0
os-ken==2.2.0

View File

@ -49,31 +49,6 @@ OVN_LIVENESS_CHECK_EXT_ID_KEY = 'neutron:liveness_check_at'
METADATA_LIVENESS_CHECK_EXT_ID_KEY = 'neutron:metadata_liveness_check_at'
OVN_PORT_BINDING_PROFILE = portbindings.PROFILE
# Port Binding Profile data validation
#
# To allow for validating multiple parameter sets that may contain some of the
# same keys, you can specify for which vnic_type and capability the parameter
# set is valid for.
#
# By leaving vnic_type and capability to the default of 'None' any parameter
# set that has a key which is present in the port binding data will be used for
# validation.
#
# The param_set type is Dict[str,Optional[List[any]]] where the key is used to
# match keys in the port binding data. A value of 'None' means not to check
# type for this key, when a list of type classes is provided the data will be
# validated to be of one of the listed types.
OVNPortBindingProfileParamSet = collections.namedtuple(
'OVNPortBindingProfileParamSet', ['param_set', 'vnic_type', 'capability'])
OVN_PORT_BINDING_PROFILE_PARAMS = [
OVNPortBindingProfileParamSet({'parent_name': [str],
'tag': [int]},
None, None),
OVNPortBindingProfileParamSet({'vtep-physical-switch': [str],
'vtep-logical-switch': [str]},
None, None),
]
MIGRATING_ATTR = 'migrating_to'
OVN_ROUTER_PORT_OPTION_KEYS = ['router-port', 'nat-addresses']
OVN_GATEWAY_CHASSIS_KEY = 'redirect-chassis'
@ -295,6 +270,69 @@ UNKNOWN_ADDR = 'unknown'
PORT_CAP_SWITCHDEV = 'switchdev'
PORT_CAP_PARAM = 'capabilities'
VIF_DETAILS_PCI_VENDOR_INFO = 'pci_vendor_info'
VIF_DETAILS_PCI_SLOT = 'pci_slot'
VIF_DETAILS_PHYSICAL_NETWORK = 'physical_network'
VIF_DETAILS_CARD_SERIAL_NUMBER = 'card_serial_number'
VIF_DETAILS_PF_MAC_ADDRESS = 'pf_mac_address'
VIF_DETAILS_VF_NUM = 'vf_num'
# Port Binding Profile data validation
#
# To allow for validating multiple parameter sets that may contain some of the
# same keys, you can specify for which vnic_type and capability the parameter
# set is valid for.
#
# By leaving vnic_type and capability to the default of 'None' any parameter
# set that has a key which is present in the port binding data will be used for
# validation.
#
# The param_set type is Dict[str,Optional[List[any]]] where the key is used to
# match keys in the port binding data. A value of 'None' means not to check
# type for this key, when a list of type classes is provided the data will be
# validated to be of one of the listed types.
OVNPortBindingProfileParamSet = collections.namedtuple(
'OVNPortBindingProfileParamSet', ['param_set', 'vnic_type', 'capability'])
OVN_PORT_BINDING_PROFILE_PARAMS = [
OVNPortBindingProfileParamSet({'parent_name': [str],
'tag': [int]},
None, None),
OVNPortBindingProfileParamSet({'vtep-physical-switch': [str],
'vtep-logical-switch': [str]},
None, None),
# For the two supported switchdev modes the data provided in the binding
# profile is similar to what is used for Legacy SR-IOV. However, the
# `physical_network` value type is Union[str,None]. When a port is
# attached to a project network backed by an overlay (tunneled) network the
# value will be 'None'. For the case of ports attached to a project
# network backed by VLAN the value will be of type `str` and set to the
# value provided in the `physical_network` tag in the Nova PCI Passthrough
# configuration.
#
# Note that while the OVN driver provides services to Legacy SR-IOV
# instances through the creation of external ports for DHCP and Metadata,
# it does not bind the instance ports themselves. Thus there is no
# parameter set for them here.
#
# Switchdev capable device exposed on the hypervisor host.
OVNPortBindingProfileParamSet({VIF_DETAILS_PCI_VENDOR_INFO: [str],
VIF_DETAILS_PCI_SLOT: [str],
VIF_DETAILS_PHYSICAL_NETWORK: [str,
type(None)]},
portbindings.VNIC_DIRECT,
PORT_CAP_SWITCHDEV),
# SmartNIC DPU. Switchdev capable device exposed on the SmartNIC DPU
# control plane CPUs.
OVNPortBindingProfileParamSet({VIF_DETAILS_PCI_VENDOR_INFO: [str],
VIF_DETAILS_PCI_SLOT: [str],
VIF_DETAILS_PHYSICAL_NETWORK: [str,
type(None)],
VIF_DETAILS_CARD_SERIAL_NUMBER: [str],
VIF_DETAILS_PF_MAC_ADDRESS: [str],
VIF_DETAILS_VF_NUM: [int]},
portbindings.VNIC_REMOTE_MANAGED,
None),
]
# The name of the port security group attribute is currently not in neutron nor
# neutron-lib api definitions or constants. To avoid importing the extension
@ -308,6 +346,11 @@ LSP_TYPE_EXTERNAL = 'external'
LSP_TYPE_LOCALPORT = 'localport'
LSP_OPTIONS_VIRTUAL_PARENTS_KEY = 'virtual-parents'
LSP_OPTIONS_VIRTUAL_IP_KEY = 'virtual-ip'
LSP_OPTIONS_VIF_PLUG_TYPE_KEY = 'vif-plug-type'
LSP_OPTIONS_VIF_PLUG_MTU_REQUEST_KEY = 'vif-plug-mtu-request'
LSP_OPTIONS_VIF_PLUG_REPRESENTOR_PF_MAC_KEY = 'vif-plug:representor:pf-mac'
LSP_OPTIONS_VIF_PLUG_REPRESENTOR_VF_NUM_KEY = 'vif-plug:representor:vf-num'
LSP_OPTIONS_REQUESTED_CHASSIS_KEY = 'requested-chassis'
LSP_OPTIONS_MCAST_FLOOD_REPORTS = 'mcast_flood_reports'
LSP_OPTIONS_MCAST_FLOOD = 'mcast_flood'
@ -332,6 +375,7 @@ NEUTRON_AVAILABILITY_ZONES = 'neutron-availability-zones'
OVN_CMS_OPTIONS = 'ovn-cms-options'
CMS_OPT_CHASSIS_AS_GW = 'enable-chassis-as-gw'
CMS_OPT_AVAILABILITY_ZONES = 'availability-zones'
CMS_OPT_CARD_SERIAL_NUMBER = 'card-serial-number'
# OVN vlan transparency option
VLAN_PASSTHRU = 'vlan-passthru'
@ -347,4 +391,5 @@ OVN_SUPPORTED_VNIC_TYPES = [portbindings.VNIC_NORMAL,
portbindings.VNIC_DIRECT_PHYSICAL,
portbindings.VNIC_MACVTAP,
portbindings.VNIC_VHOST_VDPA,
portbindings.VNIC_REMOTE_MANAGED,
]

View File

@ -926,14 +926,29 @@ class OVNMechanismDriver(api.MechanismDriver):
# cannot be found.
chassis_physnets = []
try:
# The PortContext host property contains special handling that
# we need to take into account, thus passing both the port Dict
# and the PortContext instance so that the helper can decide
# which to use.
bind_host = self._ovn_client.determine_bind_host(
port,
port_context=context)
datapath_type, iface_types, chassis_physnets = (
self.sb_ovn.get_chassis_data_for_ml2_bind_port(context.host))
self.sb_ovn.get_chassis_data_for_ml2_bind_port(bind_host))
iface_types = iface_types.split(',') if iface_types else []
except RuntimeError:
LOG.debug('Refusing to bind port %(port_id)s due to '
'no OVN chassis for host: %(host)s',
{'port_id': port['id'], 'host': context.host})
{'port_id': port['id'], 'host': bind_host})
return
except n_exc.InvalidInput as e:
# The port binding profile is validated both on port creation and
# update. The new rules apply to a VNIC type previously not
# consumed by the OVN mechanism driver, so this should never
# happen.
LOG.error('Validation of binding profile unexpectedly failed '
'while attempting to bind port %s', port['id'])
raise e
for segment_to_bind in context.segments_to_bind:
network_type = segment_to_bind['network_type']
@ -944,7 +959,7 @@ class OVNMechanismDriver(api.MechanismDriver):
'segmentation ID %(segmentation_id)s, '
'physical network %(physical_network)s',
{'port_id': port['id'],
'host': context.host,
'host': bind_host,
'network_type': network_type,
'segmentation_id': segmentation_id,
'physical_network': physical_network})
@ -967,7 +982,7 @@ class OVNMechanismDriver(api.MechanismDriver):
'%(chassis_physnets)s not supporting '
'physical network: %(physical_network)s',
{'port_id': port['id'],
'host': context.host,
'host': bind_host,
'chassis_physnets': chassis_physnets,
'physical_network': physical_network})
else:

View File

@ -858,6 +858,21 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
# preference patch (as part of external ids) merges.
return [c.name for c in self.chassis_list().execute(check_error=True)]
def get_chassis_by_card_serial_from_cms_options(self,
card_serial_number):
for ch in self.chassis_list().execute(check_error=True):
if ('{}={}'
.format(ovn_const.CMS_OPT_CARD_SERIAL_NUMBER,
card_serial_number)
in ch.external_ids.get(
ovn_const.OVN_CMS_OPTIONS, '').split(',')):
return ch
msg = _('Chassis with %s %s %s does not exist'
) % (ovn_const.OVN_CMS_OPTIONS,
ovn_const.CMS_OPT_CARD_SERIAL_NUMBER,
card_serial_number)
raise RuntimeError(msg)
def get_chassis_data_for_ml2_bind_port(self, hostname):
try:
cmd = self.db_find_rows('Chassis', ('hostname', '=', hostname))

View File

@ -222,9 +222,52 @@ class OVNClient(object):
if lsp.name != port['id'] and
virtual_ip in utils.get_ovn_port_addresses(lsp)]
def determine_bind_host(self, port, port_context=None):
"""Determine which host the port should be bound to.
Traditionally it has been Nova's responsibility to create Virtual
Interfaces (VIFs) as part of instance life cycle, and subsequently
manage plug/unplug operations on the Open vSwitch integration bridge.
For the traditional topology the bind host will be the same as the
hypervisor hosting the instance.
With the advent of SmartNIC DPUs which are connected to multiple
distinct CPUs we can have a topology where the instance runs on one
host and Open vSwitch and OVN runs on a different host, the SmartNIC
DPU control plane CPU. In the SmartNIC DPU topology the bind host will
be different than the hypervisor host.
This helper accepts both a port Dict and optionally a PortContext
instance so that it can be used both before and after a port is bound.
:param port: Port Dictionary
:type port: Dict[str,any]
:param port_context: PortContext instance describing the port
:type port_context: api.PortContext
:returns: FQDN or Hostname to bind port to.
:rtype: str
:raises: n_exc.InvalidInput, RuntimeError
"""
# Note that we use port_context.host below when called from bind_port
port = port_context.current if port_context else port
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
if vnic_type != portbindings.VNIC_REMOTE_MANAGED:
# The ``PortContext`` ``host`` property contains handling of
# special cases.
return port_context.host if port_context else port.get(
portbindings.HOST_ID, '')
binding_prof = utils.validate_and_get_data_from_binding_profile(port)
if ovn_const.VIF_DETAILS_CARD_SERIAL_NUMBER in binding_prof:
return self._sb_idl.get_chassis_by_card_serial_from_cms_options(
binding_prof[
ovn_const.VIF_DETAILS_CARD_SERIAL_NUMBER]).hostname
return ''
def _get_port_options(self, port):
context = n_context.get_admin_context()
binding_prof = utils.validate_and_get_data_from_binding_profile(port)
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
vtep_physical_switch = binding_prof.get('vtep-physical-switch')
port_type = ''
@ -301,8 +344,22 @@ class OVNClient(object):
# HA Chassis Group will bind the port to the highest
# priority Chassis
if port_type != ovn_const.LSP_TYPE_EXTERNAL:
options.update({'requested-chassis':
port.get(portbindings.HOST_ID, '')})
if (vnic_type == portbindings.VNIC_REMOTE_MANAGED and
ovn_const.VIF_DETAILS_PF_MAC_ADDRESS in binding_prof):
port_net = self._plugin.get_network(
context, port['network_id'])
options.update({
ovn_const.LSP_OPTIONS_VIF_PLUG_TYPE_KEY: 'representor',
ovn_const.LSP_OPTIONS_VIF_PLUG_MTU_REQUEST_KEY: str(
port_net['mtu']),
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_PF_MAC_KEY: (
binding_prof.get(
ovn_const.VIF_DETAILS_PF_MAC_ADDRESS)),
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_VF_NUM_KEY: str(
binding_prof.get(ovn_const.VIF_DETAILS_VF_NUM))})
options.update({
ovn_const.LSP_OPTIONS_REQUESTED_CHASSIS_KEY: (
self.determine_bind_host(port))})
# TODO(lucasagomes): Enable the mcast_flood_reports by default,
# according to core OVN developers it shouldn't cause any harm

View File

@ -61,6 +61,9 @@ class TestPortBinding(base.TestOVNFunctionalBase):
self.ovs_host = 'ovs-host'
self.dpdk_host = 'dpdk-host'
self.invalid_dpdk_host = 'invalid-host'
self.insecure_host = 'insecure-host'
self.smartnic_dpu_host = 'smartnic-dpu-host'
self.smartnic_dpu_serial = 'fake-smartnic-dpu-serial'
self.add_fake_chassis(self.ovs_host)
self.add_fake_chassis(
self.dpdk_host,
@ -71,32 +74,39 @@ class TestPortBinding(base.TestOVNFunctionalBase):
self.invalid_dpdk_host,
external_ids={'datapath-type': 'netdev',
'iface-types': 'dummy,dummy-internal,geneve,vxlan'})
self.add_fake_chassis(
self.smartnic_dpu_host,
external_ids={ovn_const.OVN_CMS_OPTIONS: '{}={}'.format(
ovn_const.CMS_OPT_CARD_SERIAL_NUMBER,
self.smartnic_dpu_serial)})
self.n1 = self._make_network(self.fmt, 'n1', True)
res = self._create_subnet(self.fmt, self.n1['network']['id'],
'10.0.0.0/24')
self.deserialize(self.fmt, res)
def _create_or_update_port(self, port_id=None, hostname=None):
def _create_or_update_port(self, port_id=None, hostname=None,
vnic_type=None, binding_profile=None):
port_data = {'port': {}}
if hostname:
port_data['port']['device_id'] = uuidutils.generate_uuid()
port_data['port']['device_owner'] = 'compute:None'
port_data['port']['binding:host_id'] = hostname
if vnic_type:
port_data['port'][portbindings.VNIC_TYPE] = vnic_type
if binding_profile:
port_data['port'][portbindings.PROFILE] = binding_profile
if port_id is None:
port_data = {
'port': {'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id}}
if hostname:
port_data['port']['device_id'] = uuidutils.generate_uuid()
port_data['port']['device_owner'] = 'compute:None'
port_data['port']['binding:host_id'] = hostname
port_data['port'].update({
'network_id': self.n1['network']['id'],
'tenant_id': self._tenant_id})
port_req = self.new_create_request('ports', port_data, self.fmt)
port_res = port_req.get_response(self.api)
p = self.deserialize(self.fmt, port_res)
port_id = p['port']['id']
else:
port_data = {
'port': {'device_id': uuidutils.generate_uuid(),
'device_owner': 'compute:None',
'binding:host_id': hostname}}
port_req = self.new_update_request('ports', port_data, port_id,
self.fmt)
port_res = port_req.get_response(self.api)
@ -104,16 +114,32 @@ class TestPortBinding(base.TestOVNFunctionalBase):
return port_id
def _verify_vif_details(self, port_id, expected_host_name,
expected_vif_type, expected_vif_details):
def _port_show(self, port_id):
port_req = self.new_show_request('ports', port_id)
port_res = port_req.get_response(self.api)
p = self.deserialize(self.fmt, port_res)
return self.deserialize(self.fmt, port_res)
def _verify_vif_details(self, port_id, expected_host_name,
expected_vif_type, expected_vif_details):
p = self._port_show(port_id)
self.assertEqual(expected_host_name, p['port']['binding:host_id'])
self.assertEqual(expected_vif_type, p['port']['binding:vif_type'])
self.assertEqual(expected_vif_details,
p['port']['binding:vif_details'])
def _find_port_row(self, port_id):
cmd = self.nb_api.db_find_rows(
'Logical_Switch_Port', ('name', '=', port_id))
rows = cmd.execute(check_error=True)
return rows[0] if rows else None
def _verify_lsp_details(self, port_id, lsp_options):
ovn_lsp = self._find_port_row(port_id)
for key, value in lsp_options.items():
self.assertEqual(
value,
ovn_lsp.options[key])
def test_port_binding_create_port(self):
port_id = self._create_or_update_port(hostname=self.ovs_host)
self._verify_vif_details(port_id, self.ovs_host, 'ovs',
@ -130,6 +156,38 @@ class TestPortBinding(base.TestOVNFunctionalBase):
self._verify_vif_details(port_id, self.invalid_dpdk_host, 'ovs',
OVS_VIF_DETAILS)
def test_port_binding_create_remote_managed_port(self):
pci_vendor_info = 'fake-pci-vendor-info'
pci_slot = 'fake-pci-slot'
physical_network = None
pf_mac_address = 'fake-pf-mac'
vf_num = 42
port_id = self._create_or_update_port(
hostname=self.insecure_host,
vnic_type=portbindings.VNIC_REMOTE_MANAGED,
binding_profile={
ovn_const.VIF_DETAILS_PCI_VENDOR_INFO: pci_vendor_info,
ovn_const.VIF_DETAILS_PCI_SLOT: pci_slot,
ovn_const.VIF_DETAILS_PHYSICAL_NETWORK: physical_network,
ovn_const.VIF_DETAILS_CARD_SERIAL_NUMBER: (
self.smartnic_dpu_serial),
ovn_const.VIF_DETAILS_PF_MAC_ADDRESS: pf_mac_address,
ovn_const.VIF_DETAILS_VF_NUM: vf_num,
})
self._verify_vif_details(port_id, self.insecure_host, 'ovs',
OVS_VIF_DETAILS)
self._verify_lsp_details(port_id, {
ovn_const.LSP_OPTIONS_REQUESTED_CHASSIS_KEY: (
self.smartnic_dpu_host),
ovn_const.LSP_OPTIONS_VIF_PLUG_TYPE_KEY: 'representor',
ovn_const.LSP_OPTIONS_VIF_PLUG_MTU_REQUEST_KEY: str(
self.n1['network']['mtu']),
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_PF_MAC_KEY: (
pf_mac_address),
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_VF_NUM_KEY: str(vf_num),
})
def test_port_binding_update_port(self):
port_id = self._create_or_update_port()
self._verify_vif_details(port_id, '', 'unbound', {})
@ -151,6 +209,42 @@ class TestPortBinding(base.TestOVNFunctionalBase):
self._verify_vif_details(port_id, self.invalid_dpdk_host, 'ovs',
OVS_VIF_DETAILS)
def test_port_binding_update_remote_managed_port(self):
port_id = self._create_or_update_port(
vnic_type=portbindings.VNIC_REMOTE_MANAGED)
self._verify_vif_details(port_id, '', 'unbound', {})
pci_vendor_info = 'fake-pci-vendor-info'
pci_slot = 'fake-pci-slot'
physical_network = None
pf_mac_address = 'fake-pf-mac'
vf_num = 42
port_id = self._create_or_update_port(
port_id=port_id,
hostname=self.insecure_host,
vnic_type=portbindings.VNIC_REMOTE_MANAGED,
binding_profile={
ovn_const.VIF_DETAILS_PCI_VENDOR_INFO: pci_vendor_info,
ovn_const.VIF_DETAILS_PCI_SLOT: pci_slot,
ovn_const.VIF_DETAILS_PHYSICAL_NETWORK: physical_network,
ovn_const.VIF_DETAILS_CARD_SERIAL_NUMBER: (
self.smartnic_dpu_serial),
ovn_const.VIF_DETAILS_PF_MAC_ADDRESS: pf_mac_address,
ovn_const.VIF_DETAILS_VF_NUM: vf_num,
})
self._verify_vif_details(port_id, self.insecure_host, 'ovs',
OVS_VIF_DETAILS)
self._verify_lsp_details(port_id, {
ovn_const.LSP_OPTIONS_REQUESTED_CHASSIS_KEY: (
self.smartnic_dpu_host),
ovn_const.LSP_OPTIONS_VIF_PLUG_TYPE_KEY: 'representor',
ovn_const.LSP_OPTIONS_VIF_PLUG_MTU_REQUEST_KEY: str(
self.n1['network']['mtu']),
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_PF_MAC_KEY: (
pf_mac_address),
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_VF_NUM_KEY: str(vf_num),
})
class TestPortBindingOverTcp(TestPortBinding):
def get_ovsdb_server_protocol(self):

View File

@ -540,6 +540,50 @@ class TestValidateAndGetDataFromBindingProfile(base.BaseTestCase):
utils.validate_and_get_data_from_binding_profile(
{constants.OVN_PORT_BINDING_PROFILE: expect}))
binding_profile = {
constants.PORT_CAP_PARAM: [constants.PORT_CAP_SWITCHDEV],
'pci_vendor_info': 'dead:beef',
'pci_slot': '0000:ca:fe.42',
'physical_network': 'physnet1',
}
expect = binding_profile.copy()
del(expect[constants.PORT_CAP_PARAM])
self.assertDictEqual(
expect,
utils.validate_and_get_data_from_binding_profile(
{portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT,
constants.OVN_PORT_BINDING_PROFILE: binding_profile}))
binding_profile = {
constants.PORT_CAP_PARAM: [constants.PORT_CAP_SWITCHDEV],
'pci_vendor_info': 'dead:beef',
'pci_slot': '0000:ca:fe.42',
'physical_network': None,
}
expect = binding_profile.copy()
del(expect[constants.PORT_CAP_PARAM])
self.assertDictEqual(
expect,
utils.validate_and_get_data_from_binding_profile(
{portbindings.VNIC_TYPE: portbindings.VNIC_DIRECT,
constants.OVN_PORT_BINDING_PROFILE: binding_profile}))
expect = {
'pci_vendor_info': 'dead:beef',
'pci_slot': '0000:ca:fe.42',
'physical_network': 'physnet1',
'card_serial_number': 'AB2000X00042',
'pf_mac_address': '00:53:00:00:00:42',
'vf_num': 42,
}
self.assertDictEqual(
utils.validate_and_get_data_from_binding_profile(
{portbindings.VNIC_TYPE: portbindings.VNIC_REMOTE_MANAGED,
constants.OVN_PORT_BINDING_PROFILE: expect}),
expect)
def test_unknown_profile_items_pruned(self):
# Confirm that unknown profile items are pruned
self.assertEqual(

View File

@ -181,6 +181,7 @@ class FakeOvsdbSbOvnIdl(object):
self.chassis_list = mock.MagicMock()
self.is_table_present = mock.Mock()
self.is_table_present.return_value = False
self.get_chassis_by_card_serial_from_cms_options = mock.Mock()
class FakeOvsdbTransaction(object):
@ -835,7 +836,8 @@ class FakeChassis(object):
@staticmethod
def create(attrs=None, az_list=None, chassis_as_gw=False,
bridge_mappings=None, rp_bandwidths=None,
rp_inventory_defaults=None, rp_hypervisors=None):
rp_inventory_defaults=None, rp_hypervisors=None,
card_serial_number=None):
cms_opts = []
if az_list:
cms_opts.append("%s=%s" % (ovn_const.CMS_OPT_AVAILABILITY_ZONES,
@ -863,6 +865,10 @@ class FakeChassis(object):
elif rp_hypervisors == '': # Test wrongly defined parameter
cms_opts.append('%s=' % ovn_const.RP_HYPERVISORS)
if card_serial_number:
cms_opts.append('%s=%s' % (ovn_const.CMS_OPT_CARD_SERIAL_NUMBER,
card_serial_number))
external_ids = {}
if cms_opts:
external_ids[ovn_const.OVN_CMS_OPTIONS] = ','.join(cms_opts)
@ -870,7 +876,7 @@ class FakeChassis(object):
if bridge_mappings:
external_ids['ovn-bridge-mappings'] = ','.join(bridge_mappings)
attrs = {
chassis_attrs = {
'encaps': [],
'external_ids': external_ids,
'hostname': '',
@ -881,5 +887,5 @@ class FakeChassis(object):
'vtep_logical_switches': []}
# Overwrite default attributes.
attrs.update(attrs)
return type('Chassis', (object, ), attrs)
chassis_attrs.update(attrs or {})
return type('Chassis', (object, ), chassis_attrs)

View File

@ -792,3 +792,61 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
lb_row = self._find_ovsdb_fake_row(self.lb_table, 'name', 'lb_2')
lb = self.nb_ovn_idl.get_floatingip_in_nat_or_lb(fip_id)
self.assertEqual(lb['_uuid'], lb_row.uuid)
class TestSBImplIdlOvnBase(TestDBImplIdlOvn):
fake_set = {
'chassis': [
{
'hostname': 'fake-smartnic-dpu-chassis.fqdn',
'external_ids': {
ovn_const.OVN_CMS_OPTIONS: (
'firstoption,'
'card-serial-number=fake-serial,'
'thirdoption'),
},
},
],
}
fake_associations = {}
def setUp(self):
super(TestSBImplIdlOvnBase, self).setUp()
self.chassis_table = fakes.FakeOvsdbTable.create_one_ovsdb_table()
self._tables = {}
self._tables['Chassis'] = self.chassis_table
with mock.patch.object(impl_idl_ovn.OvsdbSbOvnIdl, 'from_worker',
return_value=mock.Mock()):
with mock.patch.object(ovs_idl.Backend, 'autocreate_indices',
create=True):
impl_idl_ovn.OvsdbSbOvnIdl.ovsdb_connection = None
self.sb_ovn_idl = impl_idl_ovn.OvsdbSbOvnIdl(mock.MagicMock())
self.sb_ovn_idl.idl.tables = self._tables
def _load_sb_db(self):
# Load Chassis
fake_chassis = TestSBImplIdlOvnBase.fake_set['chassis']
self._load_ovsdb_fake_rows(self.chassis_table, fake_chassis)
class TestSBImplIdlOvnGetChassisByCardSerialFromCMSOptions(
TestSBImplIdlOvnBase):
def test_chassis_not_found(self):
self._load_sb_db()
self.assertRaises(
RuntimeError,
self.sb_ovn_idl.get_chassis_by_card_serial_from_cms_options,
'non-existent')
def test_chassis_found(self):
self._load_sb_db()
self.assertEqual(
'fake-smartnic-dpu-chassis.fqdn',
self.sb_ovn_idl.get_chassis_by_card_serial_from_cms_options(
'fake-serial').hostname)

View File

@ -0,0 +1,122 @@
# Copyright 2022 Canonical
# All Rights Reserved.
#
# 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.common.ovn import constants
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
from neutron.tests import base
from neutron.tests.unit import fake_resources as fakes
from neutron_lib.api.definitions import portbindings
class TestOVNClientBase(base.BaseTestCase):
def setUp(self):
super(TestOVNClientBase, self).setUp()
self.nb_idl = mock.MagicMock()
self.sb_idl = mock.MagicMock()
self.ovn_client = ovn_client.OVNClient(self.nb_idl, self.sb_idl)
class TestOVNClientDetermineBindHost(TestOVNClientBase):
def setUp(self):
super(TestOVNClientDetermineBindHost, self).setUp()
self.get_chassis_by_card_serial_from_cms_options = (
self.sb_idl.get_chassis_by_card_serial_from_cms_options)
self.fake_smartnic_hostname = 'fake-chassis-hostname'
self.get_chassis_by_card_serial_from_cms_options.return_value = (
fakes.FakeChassis.create(
attrs={'hostname': self.fake_smartnic_hostname}))
def test_vnic_normal_unbound_port(self):
self.assertEqual(
'',
self.ovn_client.determine_bind_host({}))
def test_vnic_normal_bound_port(self):
port = {
portbindings.HOST_ID: 'fake-binding-host-id',
}
self.assertEqual(
'fake-binding-host-id',
self.ovn_client.determine_bind_host(port))
def test_vnic_normal_port_context(self):
context = mock.MagicMock()
context.host = 'fake-binding-host-id'
self.assertEqual(
'fake-binding-host-id',
self.ovn_client.determine_bind_host({}, port_context=context))
def test_vnic_remote_managed_unbound_port_no_binding_profile(self):
port = {
portbindings.VNIC_TYPE: portbindings.VNIC_REMOTE_MANAGED,
}
self.assertEqual(
'',
self.ovn_client.determine_bind_host(port))
def test_vnic_remote_managed_unbound_port(self):
port = {
portbindings.VNIC_TYPE: portbindings.VNIC_REMOTE_MANAGED,
constants.OVN_PORT_BINDING_PROFILE: {
constants.VIF_DETAILS_PCI_VENDOR_INFO: 'fake-pci-vendor-info',
constants.VIF_DETAILS_PCI_SLOT: 'fake-pci-slot',
constants.VIF_DETAILS_PHYSICAL_NETWORK: None,
constants.VIF_DETAILS_CARD_SERIAL_NUMBER: 'fake-serial',
constants.VIF_DETAILS_PF_MAC_ADDRESS: 'fake-pf-mac',
constants.VIF_DETAILS_VF_NUM: 42,
},
}
self.assertEqual(
self.fake_smartnic_hostname,
self.ovn_client.determine_bind_host(port))
def test_vnic_remote_managed_bound_port(self):
port = {
portbindings.VNIC_TYPE: portbindings.VNIC_REMOTE_MANAGED,
portbindings.HOST_ID: 'fake-binding-host-id',
constants.OVN_PORT_BINDING_PROFILE: {
constants.VIF_DETAILS_PCI_VENDOR_INFO: 'fake-pci-vendor-info',
constants.VIF_DETAILS_PCI_SLOT: 'fake-pci-slot',
constants.VIF_DETAILS_PHYSICAL_NETWORK: None,
constants.VIF_DETAILS_CARD_SERIAL_NUMBER: 'fake-serial',
constants.VIF_DETAILS_PF_MAC_ADDRESS: 'fake-pf-mac',
constants.VIF_DETAILS_VF_NUM: 42,
},
}
self.assertEqual(
self.fake_smartnic_hostname,
self.ovn_client.determine_bind_host(port))
def test_vnic_remote_managed_port_context(self):
context = mock.MagicMock()
context.current = {
portbindings.VNIC_TYPE: portbindings.VNIC_REMOTE_MANAGED,
constants.OVN_PORT_BINDING_PROFILE: {
constants.VIF_DETAILS_PCI_VENDOR_INFO: 'fake-pci-vendor-info',
constants.VIF_DETAILS_PCI_SLOT: 'fake-pci-slot',
constants.VIF_DETAILS_PHYSICAL_NETWORK: None,
constants.VIF_DETAILS_CARD_SERIAL_NUMBER: 'fake-serial',
constants.VIF_DETAILS_PF_MAC_ADDRESS: 'fake-pf-mac',
constants.VIF_DETAILS_VF_NUM: 42,
},
}
context.host = 'fake-binding-host-id'
self.assertEqual(
self.fake_smartnic_hostname,
self.ovn_client.determine_bind_host({}, port_context=context))

View File

@ -1246,6 +1246,36 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase):
portbindings.VIF_TYPE_OVS,
self.mech_driver.vif_details[portbindings.VIF_TYPE_OVS])
def _test_bind_port_remote_managed(self, fake_segments):
fake_serial = 'fake-serial'
fake_port = fakes.FakePort.create_one_port(
attrs={'binding:vnic_type': 'remote-managed',
'binding:profile': {
'pci_vendor_info': 'fake-pci-vendor-info',
'pci_slot': 'fake-pci-slot',
'physical_network': fake_segments[0][
'physical_network'],
'card_serial_number': fake_serial,
'pf_mac_address': '00:53:00:00:00:42',
'vf_num': 42}}).info()
fake_smartnic_dpu = 'fake-smartnic-dpu'
ch_smartnic_dpu = fakes.FakeChassis.create(
attrs={'hostname': fake_smartnic_dpu},
card_serial_number=fake_serial)
self.sb_ovn.get_chassis_by_card_serial_from_cms_options.\
return_value = ch_smartnic_dpu
fake_host = 'host'
fake_port_context = fakes.FakePortContext(
fake_port, fake_host, fake_segments)
self.mech_driver.bind_port(fake_port_context)
self.sb_ovn.get_chassis_data_for_ml2_bind_port.assert_called_once_with(
fake_smartnic_dpu)
fake_port_context.set_binding.assert_called_once_with(
fake_segments[0]['id'],
portbindings.VIF_TYPE_OVS,
self.mech_driver.vif_details[portbindings.VIF_TYPE_OVS])
def test_bind_port_vdpa(self):
segment_attrs = {'network_type': 'geneve',
'physical_network': None,
@ -1284,6 +1314,24 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase):
[fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()]
self._test_bind_port_sriov(fake_segments)
def test_bind_remote_managed_port_geneve(self):
"""Test binding a REMOTE_MANAGED port to a geneve segment."""
segment_attrs = {'network_type': 'geneve',
'physical_network': None,
'segmentation_id': 1023}
fake_segments = \
[fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()]
self._test_bind_port_remote_managed(fake_segments)
def test_bind_remote_managed_port_vlan(self):
"""Test binding a REMOTE_MANAGED port to a geneve segment."""
segment_attrs = {'network_type': 'vlan',
'physical_network': 'fake-physnet',
'segmentation_id': 42}
fake_segments = \
[fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()]
self._test_bind_port_remote_managed(fake_segments)
def test_bind_port_vlan(self):
segment_attrs = {'network_type': 'vlan',
'physical_network': 'fake-physnet',
@ -3431,9 +3479,10 @@ class TestOVNMechanismDriverSecurityGroup(MechDriverSetupBase,
_, kwargs = self.mech_driver.nb_ovn.create_lswitch_port.call_args
self.assertEqual(
1, self.mech_driver.nb_ovn.create_lswitch_port.call_count)
self.assertEqual(ovn_const.LSP_TYPE_EXTERNAL, kwargs['type'])
self.assertEqual(fake_grp, kwargs['ha_chassis_group'])
sync_mock.assert_called_once_with(mock.ANY, net_id, mock.ANY)
if vnic_type in ovn_const.EXTERNAL_PORT_TYPES:
self.assertEqual(ovn_const.LSP_TYPE_EXTERNAL, kwargs['type'])
self.assertEqual(fake_grp, kwargs['ha_chassis_group'])
sync_mock.assert_called_once_with(mock.ANY, net_id, mock.ANY)
def test_create_port_with_vnic_direct(self):
self._test_create_port_with_vnic_type(portbindings.VNIC_DIRECT)
@ -3446,6 +3495,14 @@ class TestOVNMechanismDriverSecurityGroup(MechDriverSetupBase,
self._test_create_port_with_vnic_type(
portbindings.VNIC_MACVTAP)
def test_create_port_with_vnic_remote_managed(self):
self._test_create_port_with_vnic_type(
portbindings.VNIC_REMOTE_MANAGED)
# Confirm LSP options are not populated when there is no binding
# profile yet.
_, kwargs = self.mech_driver.nb_ovn.create_lswitch_port.call_args
self.assertNotIn('vif-plug-type', kwargs['options'])
def test_update_port_with_sgs(self):
with self.network() as n, self.subnet(n):
sg1 = self._create_empty_sg('sg1')
@ -3802,8 +3859,10 @@ class TestOVNVVirtualPort(OVNMechanismDriverTestCase):
self.fmt, {'network': self.net},
'10.0.0.1', '10.0.0.0/24')['subnet']
@mock.patch.object(ovn_client.OVNClient, 'determine_bind_host')
@mock.patch.object(ovn_client.OVNClient, 'get_virtual_port_parents')
def test_create_port_with_virtual_type_and_options(self, mock_get_parents):
def test_create_port_with_virtual_type_and_options(
self, mock_get_parents, mock_determine_bind_host):
fake_parents = ['parent-0', 'parent-1']
mock_get_parents.return_value = fake_parents
port = {'id': 'virt-port',

View File

@ -0,0 +1,7 @@
---
features:
- |
Add support for VNIC type ``remote-managed`` in OVN. The OVN driver can now
bind remote managed ports to SmartNIC DPUs. SmartNIC DPU portbinding
requires OVN version 21.12 or above, compiled with OVN VIF version 21.12 or
above.

View File

@ -16,7 +16,7 @@ Jinja2>=2.10 # BSD License (3 clause)
keystonemiddleware>=5.1.0 # Apache-2.0
netaddr>=0.7.18 # BSD
netifaces>=0.10.4 # MIT
neutron-lib>=2.18.0 # Apache-2.0
neutron-lib>=2.20.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
tenacity>=6.0.0 # Apache-2.0
SQLAlchemy>=1.4.23 # MIT