Skip to content

Commit 70e7aff

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "tests: Add functional test for vDPA device"
2 parents c49bd42 + 22fcfcd commit 70e7aff

File tree

6 files changed

+483
-102
lines changed

6 files changed

+483
-102
lines changed

nova/pci/stats.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def _filter_pools(self, pools, request, numa_cells):
441441

442442
if after_count < before_count:
443443
LOG.debug(
444-
'Dropped %d devices due to mismatched PCI attribute(s)',
444+
'Dropped %d device(s) due to mismatched PCI attribute(s)',
445445
before_count - after_count
446446
)
447447

@@ -458,7 +458,7 @@ def _filter_pools(self, pools, request, numa_cells):
458458

459459
if after_count < before_count:
460460
LOG.debug(
461-
'Dropped %d devices as they are on the wrong NUMA node(s)',
461+
'Dropped %d device(s) as they are on the wrong NUMA node(s)',
462462
before_count - after_count
463463
)
464464

@@ -474,7 +474,7 @@ def _filter_pools(self, pools, request, numa_cells):
474474

475475
if after_count < before_count:
476476
LOG.debug(
477-
'Dropped %d devices as they are PFs which we have not '
477+
'Dropped %d device(s) as they are PFs which we have not '
478478
'requested',
479479
before_count - after_count
480480
)
@@ -491,7 +491,7 @@ def _filter_pools(self, pools, request, numa_cells):
491491

492492
if after_count < before_count:
493493
LOG.debug(
494-
'Dropped %d devices as they are VDPA devices which we have '
494+
'Dropped %d device(s) as they are VDPA devices which we have '
495495
'not requested',
496496
before_count - after_count
497497
)

nova/tests/functional/integrated_helpers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,24 @@ def _reboot_server(self, server, hard=False, expected_state='ACTIVE'):
413413
fake_notifier.wait_for_versioned_notifications('instance.reboot.end')
414414
return self._wait_for_state_change(server, expected_state)
415415

416+
def _attach_interface(self, server, port_uuid):
417+
"""attach a neutron port to a server."""
418+
body = {
419+
"interfaceAttachment": {
420+
"port_id": port_uuid
421+
}
422+
}
423+
attachment = self.api.attach_interface(server['id'], body)
424+
fake_notifier.wait_for_versioned_notifications(
425+
'instance.interface_attach.end')
426+
return attachment
427+
428+
def _detach_interface(self, server, port_uuid):
429+
"""detach a neutron port form a server."""
430+
self.api.detach_interface(server['id'], port_uuid)
431+
fake_notifier.wait_for_versioned_notifications(
432+
'instance.interface_detach.end')
433+
416434
def _rebuild_server(self, server, image_uuid, expected_state='ACTIVE'):
417435
"""Rebuild a server."""
418436
self.api.post_server_action(

nova/tests/functional/libvirt/base.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def _setup_scheduler_service(self):
8585
return self.start_service('scheduler')
8686

8787
def _get_connection(
88-
self, host_info=None, pci_info=None, mdev_info=None,
88+
self, host_info=None, pci_info=None, mdev_info=None, vdpa_info=None,
8989
libvirt_version=None, qemu_version=None, hostname=None,
9090
):
9191
if not host_info:
@@ -107,12 +107,14 @@ def _get_connection(
107107
host_info=host_info,
108108
pci_info=pci_info,
109109
mdev_info=mdev_info,
110+
vdpa_info=vdpa_info,
110111
hostname=hostname)
111112
return fake_connection
112113

113114
def start_compute(
114115
self, hostname='compute1', host_info=None, pci_info=None,
115-
mdev_info=None, libvirt_version=None, qemu_version=None,
116+
mdev_info=None, vdpa_info=None, libvirt_version=None,
117+
qemu_version=None,
116118
):
117119
"""Start a compute service.
118120
@@ -129,8 +131,8 @@ def start_compute(
129131

130132
def _start_compute(hostname, host_info):
131133
fake_connection = self._get_connection(
132-
host_info, pci_info, mdev_info, libvirt_version, qemu_version,
133-
hostname,
134+
host_info, pci_info, mdev_info, vdpa_info, libvirt_version,
135+
qemu_version, hostname,
134136
)
135137
# This is fun. Firstly we need to do a global'ish mock so we can
136138
# actually start the service.
@@ -299,8 +301,8 @@ class LibvirtNeutronFixture(nova_fixtures.NeutronFixture):
299301
'subnet_id': subnet_4['id']
300302
}
301303
],
302-
'binding:vif_type': 'hw_veb',
303304
'binding:vif_details': {'vlan': 42},
305+
'binding:vif_type': 'hw_veb',
304306
'binding:vnic_type': 'direct',
305307
}
306308

nova/tests/functional/libvirt/test_pci_sriov_servers.py

Lines changed: 217 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from oslo_config import cfg
2424
from oslo_log import log as logging
2525
from oslo_serialization import jsonutils
26+
from oslo_utils.fixture import uuidsentinel as uuids
2627
from oslo_utils import units
2728

2829
import nova
@@ -275,9 +276,7 @@ def fake_create(cls, xml, host):
275276
self.assertNotIn('binding:profile', port)
276277

277278
# create a server using the VF via neutron
278-
flavor_id = self._create_flavor()
279279
self._create_server(
280-
flavor_id=flavor_id,
281280
networks=[
282281
{'port': base.LibvirtNeutronFixture.network_4_port_1['id']},
283282
],
@@ -548,9 +547,7 @@ def test_create_server_after_change_in_nonsriov_pf_to_sriov_pf(self):
548547
self.neutron.create_port({'port': self.neutron.network_4_port_1})
549548

550549
# create a server using the VF via neutron
551-
flavor_id = self._create_flavor()
552550
self._create_server(
553-
flavor_id=flavor_id,
554551
networks=[
555552
{'port': base.LibvirtNeutronFixture.network_4_port_1['id']},
556553
],
@@ -672,6 +669,222 @@ def test_detach_direct_physical(self):
672669
self.neutron.sriov_pf_port2['id'])
673670

674671

672+
class VDPAServersTest(_PCIServersTestBase):
673+
674+
# this is needed for os_compute_api:os-migrate-server:migrate policy
675+
ADMIN_API = True
676+
microversion = 'latest'
677+
678+
# Whitelist both the PF and VF; in reality, you probably wouldn't do this
679+
# but we want to make sure that the PF is correctly taken off the table
680+
# once any VF is used
681+
PCI_PASSTHROUGH_WHITELIST = [jsonutils.dumps(x) for x in (
682+
{
683+
'vendor_id': '15b3',
684+
'product_id': '101d',
685+
'physical_network': 'physnet4',
686+
},
687+
{
688+
'vendor_id': '15b3',
689+
'product_id': '101e',
690+
'physical_network': 'physnet4',
691+
},
692+
)]
693+
# No need for aliases as these test will request SRIOV via neutron
694+
PCI_ALIAS = []
695+
696+
NUM_PFS = 1
697+
NUM_VFS = 4
698+
699+
FAKE_LIBVIRT_VERSION = 6_009_000 # 6.9.0
700+
FAKE_QEMU_VERSION = 5_001_000 # 5.1.0
701+
702+
def setUp(self):
703+
super().setUp()
704+
705+
# The ultimate base class _IntegratedTestBase uses NeutronFixture but
706+
# we need a bit more intelligent neutron for these tests. Applying the
707+
# new fixture here means that we re-stub what the previous neutron
708+
# fixture already stubbed.
709+
self.neutron = self.useFixture(base.LibvirtNeutronFixture(self))
710+
711+
def start_compute(self):
712+
vf_ratio = self.NUM_VFS // self.NUM_PFS
713+
714+
pci_info = fakelibvirt.HostPCIDevicesInfo(
715+
num_pci=0, num_pfs=0, num_vfs=0)
716+
vdpa_info = fakelibvirt.HostVDPADevicesInfo()
717+
718+
pci_info.add_device(
719+
dev_type='PF',
720+
bus=0x6,
721+
slot=0x0,
722+
function=0,
723+
iommu_group=40, # totally arbitrary number
724+
numa_node=0,
725+
vf_ratio=vf_ratio,
726+
vend_id='15b3',
727+
vend_name='Mellanox Technologies',
728+
prod_id='101d',
729+
prod_name='MT2892 Family [ConnectX-6 Dx]',
730+
driver_name='mlx5_core')
731+
732+
for idx in range(self.NUM_VFS):
733+
vf = pci_info.add_device(
734+
dev_type='VF',
735+
bus=0x6,
736+
slot=0x0,
737+
function=idx + 1,
738+
iommu_group=idx + 41, # totally arbitrary number + offset
739+
numa_node=0,
740+
vf_ratio=vf_ratio,
741+
parent=(0x6, 0x0, 0),
742+
vend_id='15b3',
743+
vend_name='Mellanox Technologies',
744+
prod_id='101e',
745+
prod_name='ConnectX Family mlx5Gen Virtual Function',
746+
driver_name='mlx5_core')
747+
vdpa_info.add_device(f'vdpa_vdpa{idx}', idx, vf)
748+
749+
return super().start_compute(
750+
pci_info=pci_info, vdpa_info=vdpa_info,
751+
libvirt_version=self.FAKE_LIBVIRT_VERSION,
752+
qemu_version=self.FAKE_QEMU_VERSION)
753+
754+
def create_vdpa_port(self):
755+
vdpa_port = {
756+
'id': uuids.vdpa_port,
757+
'network_id': self.neutron.network_4['id'],
758+
'status': 'ACTIVE',
759+
'mac_address': 'b5:bc:2e:e7:51:ee',
760+
'fixed_ips': [
761+
{
762+
'ip_address': '192.168.4.6',
763+
'subnet_id': self.neutron.subnet_4['id']
764+
}
765+
],
766+
'binding:vif_details': {},
767+
'binding:vif_type': 'ovs',
768+
'binding:vnic_type': 'vdpa',
769+
}
770+
771+
# create the port
772+
self.neutron.create_port({'port': vdpa_port})
773+
return vdpa_port
774+
775+
def test_create_server(self):
776+
"""Create an instance using a neutron-provisioned vDPA VIF."""
777+
778+
orig_create = nova.virt.libvirt.guest.Guest.create
779+
780+
def fake_create(cls, xml, host):
781+
tree = etree.fromstring(xml)
782+
elem = tree.find('./devices/interface/[@type="vdpa"]')
783+
784+
# compare source device
785+
# the MAC address is derived from the neutron port, while the
786+
# source dev path assumes we attach vDPA devs in order
787+
expected = """
788+
<interface type="vdpa">
789+
<mac address="b5:bc:2e:e7:51:ee"/>
790+
<model type="virtio"/>
791+
<source dev="/dev/vhost-vdpa-3"/>
792+
</interface>"""
793+
actual = etree.tostring(elem, encoding='unicode')
794+
795+
self.assertXmlEqual(expected, actual)
796+
797+
return orig_create(xml, host)
798+
799+
self.stub_out(
800+
'nova.virt.libvirt.guest.Guest.create',
801+
fake_create,
802+
)
803+
804+
hostname = self.start_compute()
805+
num_pci = self.NUM_PFS + self.NUM_VFS
806+
807+
# both the PF and VF with vDPA capabilities (dev_type=vdpa) should have
808+
# been counted
809+
self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci)
810+
811+
# create the port
812+
vdpa_port = self.create_vdpa_port()
813+
814+
# ensure the binding details are currently unset
815+
port = self.neutron.show_port(vdpa_port['id'])['port']
816+
self.assertNotIn('binding:profile', port)
817+
818+
# create a server using the vDPA device via neutron
819+
self._create_server(networks=[{'port': vdpa_port['id']}])
820+
821+
# ensure there is one less VF available and that the PF is no longer
822+
# usable
823+
self.assertPCIDeviceCounts(hostname, total=num_pci, free=num_pci - 2)
824+
825+
# ensure the binding details sent to "neutron" were correct
826+
port = self.neutron.show_port(vdpa_port['id'])['port']
827+
self.assertIn('binding:profile', port)
828+
self.assertEqual(
829+
{
830+
'pci_vendor_info': '15b3:101e',
831+
'pci_slot': '0000:06:00.4',
832+
'physical_network': 'physnet4',
833+
},
834+
port['binding:profile'],
835+
)
836+
837+
def _test_common(self, op, *args, **kwargs):
838+
self.start_compute()
839+
840+
# create the port and a server, with the port attached to the server
841+
vdpa_port = self.create_vdpa_port()
842+
server = self._create_server(networks=[{'port': vdpa_port['id']}])
843+
844+
# attempt the unsupported action and ensure it fails
845+
ex = self.assertRaises(
846+
client.OpenStackApiException,
847+
op, server, *args, **kwargs)
848+
self.assertIn(
849+
'not supported for instance with vDPA ports',
850+
ex.response.text)
851+
852+
def test_attach_interface(self):
853+
self.start_compute()
854+
855+
# create the port and a server, but don't attach the port to the server
856+
# yet
857+
vdpa_port = self.create_vdpa_port()
858+
server = self._create_server(networks='none')
859+
860+
# attempt to attach the port to the server
861+
ex = self.assertRaises(
862+
client.OpenStackApiException,
863+
self._attach_interface, server, vdpa_port['id'])
864+
self.assertIn(
865+
'not supported for instance with vDPA ports',
866+
ex.response.text)
867+
868+
def test_detach_interface(self):
869+
self._test_common(self._detach_interface, uuids.vdpa_port)
870+
871+
def test_shelve(self):
872+
self._test_common(self._shelve_server)
873+
874+
def test_suspend(self):
875+
self._test_common(self._suspend_server)
876+
877+
def test_evacute(self):
878+
self._test_common(self._evacuate_server)
879+
880+
def test_resize(self):
881+
flavor_id = self._create_flavor()
882+
self._test_common(self._resize_server, flavor_id)
883+
884+
def test_cold_migrate(self):
885+
self._test_common(self._migrate_server)
886+
887+
675888
class PCIServersTest(_PCIServersTestBase):
676889

677890
ADMIN_API = True

0 commit comments

Comments
 (0)