From c39e550716e89de83a31bc39412678bbf7c426bf Mon Sep 17 00:00:00 2001 From: ShunliZhou Date: Thu, 21 Sep 2017 10:35:24 +0800 Subject: [PATCH] port pci stats from nova to zun Change-Id: I93ce4fdec7e6d870ee4e57e73374089ba27becfe Partially-Implements: blueprint support-pcipassthroughfilter --- zun/common/exception.py | 6 + zun/conf/__init__.py | 2 + zun/conf/pci.py | 122 ++++++++++++ zun/pci/stats.py | 308 +++++++++++++++++++++++++++++++ zun/tests/unit/pci/fakes.py | 38 ++++ zun/tests/unit/pci/test_stats.py | 290 +++++++++++++++++++++++++++++ 6 files changed, 766 insertions(+) create mode 100644 zun/conf/pci.py create mode 100644 zun/pci/stats.py create mode 100644 zun/tests/unit/pci/fakes.py create mode 100644 zun/tests/unit/pci/test_stats.py diff --git a/zun/common/exception.py b/zun/common/exception.py index d88bda3d3..aa698675a 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -578,6 +578,12 @@ class PciDeviceNotFound(NotFound): message = _("PCI Device %(node_id)s:%(address)s not found.") +class PciDevicePoolEmpty(ZunException): + message = _( + "Attempt to consume PCI device %(compute_node_uuid)s:%(address)s " + "from empty pool") + + class CapsuleAlreadyExists(ResourceExists): message = _("A capsule with %(field)s %(value)s already exists.") diff --git a/zun/conf/__init__.py b/zun/conf/__init__.py index 67051aa4a..03ce8e6d2 100644 --- a/zun/conf/__init__.py +++ b/zun/conf/__init__.py @@ -25,6 +25,7 @@ from zun.conf import network from zun.conf import neutron_client from zun.conf import nova_client from zun.conf import path +from zun.conf import pci from zun.conf import profiler from zun.conf import scheduler from zun.conf import services @@ -51,3 +52,4 @@ profiler.register_opts(CONF) neutron_client.register_opts(CONF) network.register_opts(CONF) websocket_proxy.register_opts(CONF) +pci.register_opts(CONF) diff --git a/zun/conf/pci.py b/zun/conf/pci.py new file mode 100644 index 000000000..e1930fff5 --- /dev/null +++ b/zun/conf/pci.py @@ -0,0 +1,122 @@ +# Copyright (c) 2017 Intel, Inc. +# Copyright (c) 2017 OpenStack Foundation +# 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 oslo_config import cfg + +pci_group = cfg.OptGroup( + name='pci', + title='PCI passthrough options') + +pci_opts = [ + cfg.MultiStrOpt('alias', + default=[], + help=""" +An alias for a PCI passthrough device requirement. + +Possible Values: + +* A list of JSON values which describe the aliases. For example: + + alias = { + "name": "QuickAssist", + "product_id": "0443", + "vendor_id": "8086", + "device_type": "PCI" + } + + defines an alias for the Intel QuickAssist card. (multi valued). Valid key + values are : + + * "name": Name of the PCI alias. + * "product_id": Product ID of the device in hexadecimal. + * "vendor_id": Vendor ID of the device in hexadecimal. + * "device_type": Type of PCI device. Valid values are: "type-PCI", + "PF" and "VF". +"""), + cfg.MultiStrOpt('passthrough_whitelist', + default=[], + help=""" +White list of PCI devices available to VMs. + +Possible values: + +* A JSON dictionary which describe a whitelisted PCI device. It should take + the following format: + + ["vendor_id": "",] ["product_id": "",] + ["address": "[[[[]:]]:][][.[]]" | + "devname": "",] + {"": "",} + + Where '[' indicates zero or one occurrences, '{' indicates zero or multiple + occurrences, and '|' mutually exclusive options. Note that any missing + fields are automatically wildcarded. + + Valid key values are : + + * "vendor_id": Vendor ID of the device in hexadecimal. + * "product_id": Product ID of the device in hexadecimal. + * "address": PCI address of the device. + * "devname": Device name of the device (for e.g. interface name). Not all + PCI devices have a name. + * "": Additional and used for matching PCI devices. + Supported : "physical_network". + + The address key supports traditional glob style and regular expression + syntax. Valid examples are: + + passthrough_whitelist = {"devname":"eth0", + "physical_network":"physnet"} + passthrough_whitelist = {"address":"*:0a:00.*"} + passthrough_whitelist = {"address":":0a:00.", + "physical_network":"physnet1"} + passthrough_whitelist = {"vendor_id":"1137", + "product_id":"0071"} + passthrough_whitelist = {"vendor_id":"1137", + "product_id":"0071", + "address": "0000:0a:00.1", + "physical_network":"physnet1"} + passthrough_whitelist = {"address":{"domain": ".*", + "bus": "02", "slot": "01", + "function": "[2-7]"}, + "physical_network":"physnet1"} + passthrough_whitelist = {"address":{"domain": ".*", + "bus": "02", "slot": "0[1-2]", + "function": ".*"}, + "physical_network":"physnet1"} + + The following are invalid, as they specify mutually exclusive options: + + passthrough_whitelist = {"devname":"eth0", + "physical_network":"physnet", + "address":"*:0a:00.*"} + +* A JSON list of JSON dictionaries corresponding to the above format. For + example: + + passthrough_whitelist = [{"product_id":"0001", "vendor_id":"8086"}, + {"product_id":"0002", "vendor_id":"8086"}] +""") +] + + +def register_opts(conf): + conf.register_group(pci_group) + conf.register_opts(pci_opts, group=pci_group) + + +def list_opts(): + return {pci_group: pci_opts} diff --git a/zun/pci/stats.py b/zun/pci/stats.py new file mode 100644 index 000000000..c74feae33 --- /dev/null +++ b/zun/pci/stats.py @@ -0,0 +1,308 @@ +# Copyright (c) 2017 Intel, Inc. +# Copyright (c) 2017 OpenStack Foundation +# 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. + +import copy + +from oslo_config import cfg +from oslo_log import log as logging +import six + +from zun.common import exception +from zun.objects import fields +from zun.objects import pci_device_pool +from zun.pci import utils +from zun.pci import whitelist + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class PciDeviceStats(object): + + """PCI devices summary information. + + According to the PCI SR-IOV spec, a PCI physical function can have up to + 256 PCI virtual functions, thus the number of assignable PCI functions in + a cloud can be big. The scheduler needs to know all device availability + information in order to determine which compute hosts can support a PCI + request. Passing individual virtual device information to the scheduler + does not scale, so we provide summary information. + + Usually the virtual functions provided by a host PCI device have the same + value for most properties, like vendor_id, product_id and class type. + The PCI stats class summarizes this information for the scheduler. + + The pci stats information is maintained exclusively by compute node + resource tracker and updated to database. The scheduler fetches the + information and selects the compute node accordingly. If a compute + node is selected, the resource tracker allocates the devices to the + instance and updates the pci stats information. + + This summary information will be helpful for cloud management also. + """ + + pool_keys = ['product_id', 'vendor_id', 'numa_node', 'dev_type'] + + def __init__(self, stats=None, dev_filter=None): + super(PciDeviceStats, self).__init__() + # NOTE(sbauza): Stats are a PCIDevicePoolList object + self.pools = [pci_pool.to_dict() + for pci_pool in stats] if stats else [] + self.pools.sort(key=lambda item: len(item)) + self.dev_filter = dev_filter or whitelist.Whitelist( + CONF.pci.passthrough_whitelist) + + def _equal_properties(self, dev, entry, matching_keys): + return all(dev.get(prop) == entry.get(prop) + for prop in matching_keys) + + def _find_pool(self, dev_pool): + """Return the first pool that matches dev.""" + for pool in self.pools: + pool_keys = pool.copy() + del pool_keys['count'] + del pool_keys['devices'] + equal = self._equal_properties(dev_pool, pool_keys, + dev_pool.keys()) + if (len(pool_keys.keys()) == len(dev_pool.keys()) and equal): + return pool + + def _create_pool_keys_from_dev(self, dev): + """create a stats pool dict that this dev is supposed to be part of + + Note that this pool dict contains the stats pool's keys and their + values. 'count' and 'devices' are not included. + """ + # Don't add a device that doesn't have a matching device spec. + # This can happen during initial sync up with the controller + devspec = self.dev_filter.get_devspec(dev) + if not devspec: + return + tags = devspec.get_tags() + pool = {k: getattr(dev, k) for k in self.pool_keys} + if tags: + pool.update(tags) + return pool + + def add_device(self, dev): + """Add a device to its matching pool.""" + dev_pool = self._create_pool_keys_from_dev(dev) + if dev_pool: + pool = self._find_pool(dev_pool) + if not pool: + dev_pool['count'] = 0 + dev_pool['devices'] = [] + self.pools.append(dev_pool) + self.pools.sort(key=lambda item: len(item)) + pool = dev_pool + pool['count'] += 1 + pool['devices'].append(dev) + + @staticmethod + def _decrease_pool_count(pool_list, pool, count=1): + """Decrement pool's size by count. + + If pool becomes empty, remove pool from pool_list. + """ + if pool['count'] > count: + pool['count'] -= count + count = 0 + else: + count -= pool['count'] + pool_list.remove(pool) + return count + + def remove_device(self, dev): + """Remove one device from the first pool that it matches.""" + dev_pool = self._create_pool_keys_from_dev(dev) + if dev_pool: + pool = self._find_pool(dev_pool) + if not pool: + raise exception.PciDevicePoolEmpty( + compute_node_uuid=dev.compute_node_uuid, + address=dev.address) + pool['devices'].remove(dev) + self._decrease_pool_count(self.pools, pool) + + def get_free_devs(self): + free_devs = [] + for pool in self.pools: + free_devs.extend(pool['devices']) + return free_devs + + def consume_requests(self, pci_requests, numa_cells=None): + alloc_devices = [] + for request in pci_requests: + count = request.count + spec = request.spec + # For now, keep the same algorithm as during scheduling: + # a spec may be able to match multiple pools. + pools = self._filter_pools_for_spec(self.pools, spec) + if numa_cells: + pools = self._filter_pools_for_numa_cells(pools, numa_cells) + pools = self._filter_non_requested_pfs(request, pools) + # Failed to allocate the required number of devices + # Return the devices already allocated back to their pools + if sum([pool['count'] for pool in pools]) < count: + LOG.error("Failed to allocate PCI devices for container." + " Unassigning devices back to pools." + " This should not happen, since the scheduler" + " should have accurate information, and allocation" + " during claims is controlled via a hold" + " on the compute node semaphore") + for d in range(len(alloc_devices)): + self.add_device(alloc_devices.pop()) + return None + for pool in pools: + if pool['count'] >= count: + num_alloc = count + else: + num_alloc = pool['count'] + count -= num_alloc + pool['count'] -= num_alloc + for d in range(num_alloc): + pci_dev = pool['devices'].pop() + self._handle_device_dependents(pci_dev) + pci_dev.request_id = request.request_id + alloc_devices.append(pci_dev) + if count == 0: + break + return alloc_devices + + def _handle_device_dependents(self, pci_dev): + """Remove device dependents or a parent from pools. + + In case the device is a PF, all of it's dependent VFs should + be removed from pools count, if these are present. + When the device is a VF, it's parent PF pool count should be + decreased, unless it is no longer in a pool. + """ + if pci_dev.dev_type == fields.PciDeviceType.SRIOV_PF: + vfs_list = pci_dev.child_devices + if vfs_list: + for vf in vfs_list: + self.remove_device(vf) + elif pci_dev.dev_type == fields.PciDeviceType.SRIOV_VF: + try: + parent = pci_dev.parent_device + # Make sure not to decrease PF pool count if this parent has + # been already removed from pools + if parent in self.get_free_devs(): + self.remove_device(parent) + except exception.PciDeviceNotFound: + return + + @staticmethod + def _filter_pools_for_spec(pools, request_specs): + return [pool for pool in pools + if utils.pci_device_prop_match(pool, request_specs)] + + @staticmethod + def _filter_pools_for_numa_cells(pools, numa_cells): + # Some systems don't report numa node info for pci devices, in + # that case None is reported in pci_device.numa_node, by adding None + # to numa_cells we allow assigning those devices to instances with + # numa topology + numa_cells = [None] + [cell.id for cell in numa_cells] + # filter out pools which numa_node is not included in numa_cells + return [pool for pool in pools if any(utils.pci_device_prop_match( + pool, [{'numa_node': cell}]) for cell in numa_cells)] + + def _filter_non_requested_pfs(self, request, matching_pools): + # Remove SRIOV_PFs from pools, unless it has been explicitly requested + # This is especially needed in cases where PFs and VFs has the same + # product_id. + if all(spec.get('dev_type') != fields.PciDeviceType.SRIOV_PF for + spec in request.spec): + matching_pools = self._filter_pools_for_pfs(matching_pools) + return matching_pools + + @staticmethod + def _filter_pools_for_pfs(pools): + return [pool for pool in pools + if not pool.get('dev_type') == fields.PciDeviceType.SRIOV_PF] + + def _apply_request(self, pools, request, numa_cells=None): + # NOTE(vladikr): This code maybe open to race conditions. + # Two concurrent requests may succeed when called support_requests + # because this method does not remove related devices from the pools + count = request.count + matching_pools = self._filter_pools_for_spec(pools, request.spec) + if numa_cells: + matching_pools = self._filter_pools_for_numa_cells(matching_pools, + numa_cells) + matching_pools = self._filter_non_requested_pfs(request, + matching_pools) + if sum([pool['count'] for pool in matching_pools]) < count: + return False + else: + for pool in matching_pools: + count = self._decrease_pool_count(pools, pool, count) + if not count: + break + return True + + def support_requests(self, requests, numa_cells=None): + """Check if the pci requests can be met. + + Scheduler checks compute node's PCI stats to decide if a + container can be scheduled into the node. Support does not + mean real allocation. + If numa_cells is provided then only devices contained in + those nodes are considered. + """ + # note (yjiang5): this function has high possibility to fail, + # so no exception should be triggered for performance reason. + pools = copy.deepcopy(self.pools) + return all([self._apply_request(pools, r, numa_cells) + for r in requests]) + + def apply_requests(self, requests, numa_cells=None): + """Apply PCI requests to the PCI stats. + + This is used in container creation, when the scheduler has to + maintain how the resources are consumed by the container. + If numa_cells is provided then only devices contained in + those nodes are considered. + """ + if not all([self._apply_request(self.pools, r, numa_cells) + for r in requests]): + raise exception.PciDeviceRequestFailed(requests=requests) + + def __iter__(self): + # 'devices' shouldn't be part of stats + pools = [] + for pool in self.pools: + tmp = {k: v for k, v in pool.items() if k != 'devices'} + pools.append(tmp) + return iter(pools) + + def clear(self): + """Clear all the stats maintained.""" + self.pools = [] + + def __eq__(self, other): + return self.pools == other.pools + + if six.PY2: + def __ne__(self, other): + return not (self == other) + + def to_device_pools_obj(self): + """Return the contents of the pools as a PciDevicePoolList object.""" + stats = [x for x in self] + return pci_device_pool.from_pci_stats(stats) diff --git a/zun/tests/unit/pci/fakes.py b/zun/tests/unit/pci/fakes.py new file mode 100644 index 000000000..e02ba4d15 --- /dev/null +++ b/zun/tests/unit/pci/fakes.py @@ -0,0 +1,38 @@ +# Copyright (c) 2017 OpenStack Foundation +# 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. + +import functools + +import mock + +from zun.pci import whitelist + + +def fake_pci_whitelist(): + devspec = mock.Mock() + devspec.get_tags.return_value = None + patcher = mock.patch.object(whitelist.Whitelist, 'get_devspec', + return_value=devspec) + patcher.start() + return patcher + + +def patch_pci_whitelist(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + patcher = fake_pci_whitelist() + f(self, *args, **kwargs) + patcher.stop() + return wrapper diff --git a/zun/tests/unit/pci/test_stats.py b/zun/tests/unit/pci/test_stats.py new file mode 100644 index 000000000..e679097ac --- /dev/null +++ b/zun/tests/unit/pci/test_stats.py @@ -0,0 +1,290 @@ +# Copyright (c) 2017 OpenStack Foundation +# 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. + +import mock +from oslo_config import cfg + +from zun.common import exception +from zun import objects +from zun.objects import fields +from zun.pci import stats +from zun.pci import whitelist +from zun.tests import base +from zun.tests.unit.pci import fakes + +CONF = cfg.CONF +fake_pci_1 = { + 'compute_node_uuid': 1, + 'address': '0000:00:00.1', + 'product_id': 'p1', + 'vendor_id': 'v1', + 'status': 'available', + 'extra_k1': 'v1', + 'request_id': None, + 'numa_node': 0, + 'dev_type': fields.PciDeviceType.STANDARD, + 'parent_addr': None, + } + + +fake_pci_2 = dict(fake_pci_1, vendor_id='v2', + product_id='p2', + address='0000:00:00.2', + numa_node=1) + + +fake_pci_3 = dict(fake_pci_1, address='0000:00:00.3') + +fake_pci_4 = dict(fake_pci_1, vendor_id='v3', + product_id='p3', + address='0000:00:00.3', + numa_node=None) + + +class PciDeviceStatsTestCase(base.TestCase): + def _create_fake_devs(self): + self.fake_dev_1 = objects.PciDevice.create(None, fake_pci_1) + self.fake_dev_2 = objects.PciDevice.create(None, fake_pci_2) + self.fake_dev_3 = objects.PciDevice.create(None, fake_pci_3) + self.fake_dev_4 = objects.PciDevice.create(None, fake_pci_4) + + for dev in [self.fake_dev_1, self.fake_dev_2, + self.fake_dev_3, self.fake_dev_4]: + self.pci_stats.add_device(dev) + + def setUp(self): + super(PciDeviceStatsTestCase, self).setUp() + self.pci_stats = stats.PciDeviceStats() + # The following two calls need to be made before adding the devices. + patcher = fakes.fake_pci_whitelist() + self.addCleanup(patcher.stop) + self._create_fake_devs() + + def test_add_device(self): + self.assertEqual(len(self.pci_stats.pools), 3) + self.assertEqual(set([d['vendor_id'] for d in self.pci_stats]), + set(['v1', 'v2', 'v3'])) + self.assertEqual(set([d['count'] for d in self.pci_stats]), + set([1, 2])) + + def test_remove_device(self): + self.pci_stats.remove_device(self.fake_dev_2) + self.assertEqual(len(self.pci_stats.pools), 2) + self.assertEqual(self.pci_stats.pools[0]['count'], 2) + self.assertEqual(self.pci_stats.pools[0]['vendor_id'], 'v1') + + def test_remove_device_exception(self): + self.pci_stats.remove_device(self.fake_dev_2) + self.assertRaises(exception.PciDevicePoolEmpty, + self.pci_stats.remove_device, + self.fake_dev_2) + + def test_pci_stats_equivalent(self): + pci_stats2 = stats.PciDeviceStats() + for dev in [self.fake_dev_1, + self.fake_dev_2, + self.fake_dev_3, + self.fake_dev_4]: + pci_stats2.add_device(dev) + self.assertEqual(self.pci_stats, pci_stats2) + + def test_pci_stats_not_equivalent(self): + pci_stats2 = stats.PciDeviceStats() + for dev in [self.fake_dev_1, + self.fake_dev_2, + self.fake_dev_3]: + pci_stats2.add_device(dev) + self.assertNotEqual(self.pci_stats, pci_stats2) + + def test_object_create(self): + m = self.pci_stats.to_device_pools_obj() + new_stats = stats.PciDeviceStats(m) + + self.assertEqual(len(new_stats.pools), 3) + self.assertEqual(set([d['count'] for d in new_stats]), + set([1, 2])) + self.assertEqual(set([d['vendor_id'] for d in new_stats]), + set(['v1', 'v2', 'v3'])) + + @mock.patch( + 'zun.pci.whitelist.Whitelist._parse_white_list_from_config') + def test_white_list_parsing(self, mock_whitelist_parse): + white_list = '{"product_id":"0001", "vendor_id":"8086"}' + CONF.set_override('passthrough_whitelist', white_list, 'pci') + pci_stats = stats.PciDeviceStats() + pci_stats.add_device(self.fake_dev_2) + pci_stats.remove_device(self.fake_dev_2) + self.assertEqual(1, mock_whitelist_parse.call_count) + + +class PciDeviceStatsWithTagsTestCase(base.TestCase): + + def setUp(self): + super(PciDeviceStatsWithTagsTestCase, self).setUp() + white_list = ['{"vendor_id":"1137","product_id":"0071",' + '"address":"*:0a:00.*","physical_network":"physnet1"}', + '{"vendor_id":"1137","product_id":"0072"}'] + self.config(passthrough_whitelist=white_list, group='pci') + dev_filter = whitelist.Whitelist(white_list) + self.pci_stats = stats.PciDeviceStats(dev_filter=dev_filter) + + def _create_pci_devices(self): + self.pci_tagged_devices = [] + for dev in range(4): + pci_dev = {'compute_node_uuid': 1, + 'address': '0000:0a:00.%d' % dev, + 'vendor_id': '1137', + 'product_id': '0071', + 'status': 'available', + 'request_id': None, + 'dev_type': 'PCI', + 'parent_addr': None, + 'numa_node': 0} + self.pci_tagged_devices.append(objects.PciDevice.create(None, + pci_dev)) + + self.pci_untagged_devices = [] + for dev in range(3): + pci_dev = {'compute_node_uuid': 1, + 'address': '0000:0b:00.%d' % dev, + 'vendor_id': '1137', + 'product_id': '0072', + 'status': 'available', + 'request_id': None, + 'dev_type': 'PCI', + 'parent_addr': None, + 'numa_node': 0} + self.pci_untagged_devices.append(objects.PciDevice.create(None, + pci_dev)) + + for dev in self.pci_tagged_devices: + self.pci_stats.add_device(dev) + + for dev in self.pci_untagged_devices: + self.pci_stats.add_device(dev) + + def _assertPoolContent(self, pool, vendor_id, product_id, count, **tags): + self.assertEqual(vendor_id, pool['vendor_id']) + self.assertEqual(product_id, pool['product_id']) + self.assertEqual(count, pool['count']) + if tags: + for k, v in tags.items(): + self.assertEqual(v, pool[k]) + + def _assertPools(self): + # Pools are ordered based on the number of keys. 'product_id', + # 'vendor_id' are always part of the keys. When tags are present, + # they are also part of the keys. In this test class, we have + # two pools with the second one having the tag 'physical_network' + # and the value 'physnet1' + self.assertEqual(2, len(self.pci_stats.pools)) + self._assertPoolContent(self.pci_stats.pools[0], '1137', '0072', + len(self.pci_untagged_devices)) + self.assertEqual(self.pci_untagged_devices, + self.pci_stats.pools[0]['devices']) + self._assertPoolContent(self.pci_stats.pools[1], '1137', '0071', + len(self.pci_tagged_devices), + physical_network='physnet1') + self.assertEqual(self.pci_tagged_devices, + self.pci_stats.pools[1]['devices']) + + def test_add_devices(self): + self._create_pci_devices() + self._assertPools() + + def test_add_device_no_devspec(self): + self._create_pci_devices() + pci_dev = {'compute_node_uuid': 1, + 'address': '0000:0c:00.1', + 'vendor_id': '2345', + 'product_id': '0172', + 'status': 'available', + 'parent_addr': None, + 'request_id': None} + pci_dev_obj = objects.PciDevice.create(None, pci_dev) + self.pci_stats.add_device(pci_dev_obj) + # There should be no change + self.assertIsNone( + self.pci_stats._create_pool_keys_from_dev(pci_dev_obj)) + self._assertPools() + + def test_remove_device_no_devspec(self): + self._create_pci_devices() + pci_dev = {'compute_node_uuid': 1, + 'address': '0000:0c:00.1', + 'vendor_id': '2345', + 'product_id': '0172', + 'status': 'available', + 'parent_addr': None, + 'request_id': None} + pci_dev_obj = objects.PciDevice.create(None, pci_dev) + self.pci_stats.remove_device(pci_dev_obj) + # There should be no change + self.assertIsNone( + self.pci_stats._create_pool_keys_from_dev(pci_dev_obj)) + self._assertPools() + + def test_remove_device(self): + self._create_pci_devices() + dev1 = self.pci_untagged_devices.pop() + self.pci_stats.remove_device(dev1) + dev2 = self.pci_tagged_devices.pop() + self.pci_stats.remove_device(dev2) + self._assertPools() + + +class PciDeviceVFPFStatsTestCase(base.TestCase): + + def setUp(self): + super(PciDeviceVFPFStatsTestCase, self).setUp() + white_list = ['{"vendor_id":"8086","product_id":"1528"}', + '{"vendor_id":"8086","product_id":"1515"}'] + self.config(passthrough_whitelist=white_list, group='pci') + self.pci_stats = stats.PciDeviceStats() + + def _create_pci_devices(self, vf_product_id=1515, pf_product_id=1528): + self.sriov_pf_devices = [] + for dev in range(2): + pci_dev = {'compute_node_uuid': 1, + 'address': '0000:81:00.%d' % dev, + 'vendor_id': '8086', + 'product_id': '%d' % pf_product_id, + 'status': 'available', + 'request_id': None, + 'dev_type': fields.PciDeviceType.SRIOV_PF, + 'parent_addr': None, + 'numa_node': 0} + dev_obj = objects.PciDevice.create(None, pci_dev) + dev_obj.child_devices = [] + self.sriov_pf_devices.append(dev_obj) + + self.sriov_vf_devices = [] + for dev in range(8): + pci_dev = {'compute_node_uuid': 1, + 'address': '0000:81:10.%d' % dev, + 'vendor_id': '8086', + 'product_id': '%d' % vf_product_id, + 'status': 'available', + 'request_id': None, + 'dev_type': fields.PciDeviceType.SRIOV_VF, + 'parent_addr': '0000:81:00.%d' % int(dev / 4), + 'numa_node': 0} + dev_obj = objects.PciDevice.create(None, pci_dev) + dev_obj.parent_device = self.sriov_pf_devices[int(dev / 4)] + dev_obj.parent_device.child_devices.append(dev_obj) + self.sriov_vf_devices.append(dev_obj) + + list(map(self.pci_stats.add_device, self.sriov_pf_devices)) + list(map(self.pci_stats.add_device, self.sriov_vf_devices))