|
23 | 23 | from oslo_config import cfg
|
24 | 24 | from oslo_log import log as logging
|
25 | 25 | from oslo_serialization import jsonutils
|
| 26 | +from oslo_utils.fixture import uuidsentinel as uuids |
26 | 27 | from oslo_utils import units
|
27 | 28 |
|
28 | 29 | import nova
|
@@ -275,9 +276,7 @@ def fake_create(cls, xml, host):
|
275 | 276 | self.assertNotIn('binding:profile', port)
|
276 | 277 |
|
277 | 278 | # create a server using the VF via neutron
|
278 |
| - flavor_id = self._create_flavor() |
279 | 279 | self._create_server(
|
280 |
| - flavor_id=flavor_id, |
281 | 280 | networks=[
|
282 | 281 | {'port': base.LibvirtNeutronFixture.network_4_port_1['id']},
|
283 | 282 | ],
|
@@ -548,9 +547,7 @@ def test_create_server_after_change_in_nonsriov_pf_to_sriov_pf(self):
|
548 | 547 | self.neutron.create_port({'port': self.neutron.network_4_port_1})
|
549 | 548 |
|
550 | 549 | # create a server using the VF via neutron
|
551 |
| - flavor_id = self._create_flavor() |
552 | 550 | self._create_server(
|
553 |
| - flavor_id=flavor_id, |
554 | 551 | networks=[
|
555 | 552 | {'port': base.LibvirtNeutronFixture.network_4_port_1['id']},
|
556 | 553 | ],
|
@@ -672,6 +669,222 @@ def test_detach_direct_physical(self):
|
672 | 669 | self.neutron.sriov_pf_port2['id'])
|
673 | 670 |
|
674 | 671 |
|
| 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 | + |
675 | 888 | class PCIServersTest(_PCIServersTestBase):
|
676 | 889 |
|
677 | 890 | ADMIN_API = True
|
|
0 commit comments