[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:
parent
ce96e502fa
commit
7d64d0c116
@ -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
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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))
|
@ -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',
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user