diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index 60504d9146a..e21da2c8d5e 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -1663,6 +1663,20 @@ #matchmaker_heartbeat_ttl=600 +# +# Options defined in nova.pci.pci_request +# + +# An alias for a PCI passthrough device requirement. This +# allows users to specify the alias in the extra_spec for a +# flavor, without needing to repeat all the PCI property +# requirements. For example: pci_alias = { "name": +# "QuicAssist", "product_id": "0443", "vendor_id": "8086", +# "device_type": "ACCEL" } defines an alias for the Intel +# QuickAssist card. (multi valued) (multi valued) +#pci_alias= + + # # Options defined in nova.scheduler.driver # diff --git a/nova/exception.py b/nova/exception.py index 14f52bd7e85..b6e1c090709 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1399,3 +1399,11 @@ class PciDevicePoolEmpty(NovaException): msg_fmt = _( "Attempt to consume PCI Device %(compute_node_id)s:%(address)s " "from empty pool") + + +class PciInvalidAlias(NovaException): + msg_fmt = _("Invalid PCI alias definition: %(reason)s") + + +class PciRequestAliasNotDefined(NovaException): + msg_fmt = _("PCI alias %(alias)s is not defined") diff --git a/nova/pci/pci_request.py b/nova/pci/pci_request.py new file mode 100644 index 00000000000..e25a7c2e6a2 --- /dev/null +++ b/nova/pci/pci_request.py @@ -0,0 +1,233 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Intel Corporation +# 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. +# @author: Yongli He, Intel Corporation. + +""" Example of a PCI alias: + pci_alias = '{ + "name": "QuicAssist", + "product_id": "0443", + "vendor_id": "8086", + "device_type": "ACCEL", + }' + + Aliases with the same name and the same device_type are OR operation: + pci_alias = '{ + "name": "QuicAssist", + "product_id": "0442", + "vendor_id": "8086", + "device_type": "ACCEL", + }' + These 2 aliases define a device request meaning: vendor_id is "8086" and + product id is "0442" or "0443". + """ + +import copy +import jsonschema + +from nova import exception +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova.pci import pci_utils +from nova import utils +from oslo.config import cfg + +pci_alias_opts = [ + cfg.MultiStrOpt('pci_alias', + default=[], + help='An alias for a PCI passthrough device requirement. ' + 'This allows users to specify the alias in the ' + 'extra_spec for a flavor, without needing to repeat ' + 'all the PCI property requirements. For example: ' + 'pci_alias = ' + '{ "name": "QuicAssist", ' + ' "product_id": "0443", ' + ' "vendor_id": "8086", ' + ' "device_type": "ACCEL" ' + '} ' + 'defines an alias for the Intel QuickAssist card. ' + '(multi valued)' + ) +] + +CONF = cfg.CONF +CONF.register_opts(pci_alias_opts) + +LOG = logging.getLogger(__name__) + + +_ALIAS_DEV_TYPE = ['NIC', 'ACCEL', 'GPU'] +_ALIAS_CAP_TYPE = ['pci'] +_ALIAS_SCHEMA = { + "type": "object", + "additionalProperties": False, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256, + }, + "capability_type": { + "type": "string", + "enum": _ALIAS_CAP_TYPE, + }, + "product_id": { + "type": "string", + "pattern": pci_utils.PCI_VENDOR_PATTERN, + }, + "vendor_id": { + "type": "string", + "pattern": pci_utils.PCI_VENDOR_PATTERN, + }, + "device_type": { + "type": "string", + "enum": _ALIAS_DEV_TYPE, + }, + }, + "required": ["name"], +} + + +def _get_alias_from_config(): + """Parse and validate PCI aliases from the nova config.""" + jaliases = CONF.pci_alias + aliases = {} # map alias name to alias spec list + try: + for jsonspecs in jaliases: + spec = jsonutils.loads(jsonspecs) + jsonschema.validate(spec, _ALIAS_SCHEMA) + name = spec.pop("name") + if name not in aliases: + aliases[name] = [spec] + else: + if aliases[name][0]["device_type"] == spec["device_type"]: + aliases[name].append(spec) + else: + reason = "Device type mismatch for alias '%s'" % name + raise exception.PciInvalidAlias(reason=reason) + + except exception.PciInvalidAlias: + raise + except Exception as e: + raise exception.PciInvalidAlias(reason=str(e)) + + return aliases + + +def _translate_alias_to_requests(alias_spec): + """Generate complete pci requests from pci aliases in extra_spec.""" + + pci_aliases = _get_alias_from_config() + + pci_requests = [] # list of a specs dict + alias_spec = alias_spec.replace(' ', '') + for name, count in [spec.split(':') for spec in alias_spec.split(',')]: + if name not in pci_aliases: + raise exception.PciRequestAliasNotDefined(alias=name) + else: + request = {'count': int(count), + 'spec': copy.deepcopy(pci_aliases[name]), + 'alias_name': name} + pci_requests.append(request) + return pci_requests + + +def get_pci_requests_from_flavor(flavor): + """Get flavor's pci request. + + The pci_passthrough:alias scope in flavor extra_specs + describes the flavor's pci requests, the key is + 'pci_passthrough:alias' and the value has format + 'alias_name_x:count, alias_name_y:count, ... '. The alias_name is + defined in 'pci_alias' configurations. + + The flavor's requirement is translated into pci requests list, + each entry in the list is a dictionary. The dictionary has + three keys. The 'specs' gives the pci device properties + requirement, the 'count' gives the number of devices, and the + optional 'alias_name' is the corresponding alias definition name. + + Example: + Assume alias configuration is: + {'vendor_id':'8086', + 'device_id':'1502', + 'name':'alias_1'} + + The flavor extra specs includes: 'pci_passthrough:alias': 'alias_1:2'. + + The returned pci_requests are: + pci_requests = [{'count':2, + 'specs': [{'vendor_id':'8086', + 'device_id':'1502'}], + 'alias_name': 'alias_1'}] + + :param flavor: the flavor to be checked + :returns: a list of pci requests + """ + if 'extra_specs' not in flavor: + return [] + + pci_requests = [] + if 'pci_passthrough:alias' in flavor['extra_specs']: + pci_requests = _translate_alias_to_requests( + flavor['extra_specs']['pci_passthrough:alias']) + return pci_requests + + +def get_instance_pci_requests(instance, prefix=""): + """Get instance's pci allocation requirement. + + After a flavor's pci requirement is translated into pci requests, + the requests are kept in instance's system metadata to avoid + future flavor access and translation. This function get the + pci requests from instance system metadata directly. + + As save_flavor_pci_info(), the prefix can be used to stash + information about another flavor for later use, like in resize. + """ + + if 'system_metadata' not in instance: + return [] + system_metadata = utils.instance_sys_meta(instance) + pci_requests = system_metadata.get('%spci_requests' % prefix) + + if not pci_requests: + return [] + return jsonutils.loads(pci_requests) + + +def save_flavor_pci_info(metadata, instance_type, prefix=''): + """Save flavor's pci information to metadata. + + To reduce flavor access and pci request translation, the + translated pci requests are saved into instance's system + metadata. + + As save_flavor_info(), the prefix can be used to stash information + about another flavor for later use, like in resize. + """ + pci_requests = get_pci_requests_from_flavor(instance_type) + if pci_requests: + to_key = '%spci_requests' % prefix + metadata[to_key] = jsonutils.dumps(pci_requests) + + +def delete_flavor_pci_info(metadata, *prefixes): + """Delete pci requests information from instance's system_metadata.""" + for prefix in prefixes: + to_key = '%spci_requests' % prefix + if to_key in metadata: + del metadata[to_key] diff --git a/nova/pci/pci_utils.py b/nova/pci/pci_utils.py index e5ce4a10bc4..bdf1e72fe43 100644 --- a/nova/pci/pci_utils.py +++ b/nova/pci/pci_utils.py @@ -22,6 +22,7 @@ from nova import exception +PCI_VENDOR_PATTERN = "^(hex{4})$".replace("hex", "[\da-fA-F]") _PCI_ADDRESS_PATTERN = ("^(hex{4}):(hex{2}):(hex{2}).(oct{1})$". replace("hex", "[\da-fA-F]"). replace("oct", "[0-7]")) diff --git a/nova/tests/pci/test_pci_request.py b/nova/tests/pci/test_pci_request.py new file mode 100644 index 00000000000..051fe4d927e --- /dev/null +++ b/nova/tests/pci/test_pci_request.py @@ -0,0 +1,297 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Intel Corporation +# 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. +# @author: Yongli He, Intel Corporation. + +"""Tests for PCI request.""" + +from nova import exception +from nova.openstack.common import jsonutils +from nova.pci import pci_request as pci_request +from nova import test + + +_fake_alias1 = """{ + "name": "QuicAssist", + "capability_type": "pci", + "product_id": "4443", + "vendor_id": "8086", + "device_type": "ACCEL" + }""" + +_fake_alias11 = """{ + "name": "QuicAssist", + "capability_type": "pci", + "product_id": "4444", + "vendor_id": "8086", + "device_type": "ACCEL" + }""" + +_fake_alias2 = """{ + "name": "xxx", + "capability_type": "pci", + "product_id": "1111", + "vendor_id": "1111", + "device_type": "N" + }""" + +_fake_alias3 = """{ + "name": "IntelNIC", + "capability_type": "pci", + "product_id": "1111", + "vendor_id": "8086", + "device_type": "NIC" + }""" + + +class AliasTestCase(test.TestCase): + + def setUp(self): + super(AliasTestCase, self).setUp() + + def test_good_alias(self): + self.flags(pci_alias=[_fake_alias1]) + als = pci_request._get_alias_from_config() + self.assertEqual(type(als['QuicAssist']), list) + expect_dict = { + "capability_type": "pci", + "product_id": "4443", + "vendor_id": "8086", + "device_type": "ACCEL" + } + self.assertEqual(expect_dict, als['QuicAssist'][0]) + + def test_multispec_alias(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias11]) + als = pci_request._get_alias_from_config() + self.assertEqual(type(als['QuicAssist']), list) + expect_dict1 = { + "capability_type": "pci", + "product_id": "4443", + "vendor_id": "8086", + "device_type": "ACCEL" + } + expect_dict2 = { + "capability_type": "pci", + "product_id": "4444", + "vendor_id": "8086", + "device_type": "ACCEL" + } + + self.assertEqual(expect_dict1, als['QuicAssist'][0]) + self.assertEqual(expect_dict2, als['QuicAssist'][1]) + + def test_wrong_type_aliase(self): + self.flags(pci_alias=[_fake_alias2]) + self.assertRaises(exception.PciInvalidAlias, + pci_request._get_alias_from_config) + + def test_wrong_product_id_aliase(self): + self.flags(pci_alias=[ + """{ + "name": "xxx", + "capability_type": "pci", + "product_id": "g111", + "vendor_id": "1111", + "device_type": "NIC" + }"""]) + self.assertRaises(exception.PciInvalidAlias, + pci_request._get_alias_from_config) + + def test_wrong_vendor_id_aliase(self): + self.flags(pci_alias=[ + """{ + "name": "xxx", + "capability_type": "pci", + "product_id": "1111", + "vendor_id": "0xg111", + "device_type": "NIC" + }"""]) + self.assertRaises(exception.PciInvalidAlias, + pci_request._get_alias_from_config) + + def test_wrong_cap_type_aliase(self): + self.flags(pci_alias=[ + """{ + "name": "xxx", + "capability_type": "usb", + "product_id": "1111", + "vendor_id": "8086", + "device_type": "NIC" + }"""]) + self.assertRaises(exception.PciInvalidAlias, + pci_request._get_alias_from_config) + + def test_dup_aliase(self): + self.flags(pci_alias=[ + """{ + "name": "xxx", + "capability_type": "pci", + "product_id": "1111", + "vendor_id": "8086", + "device_type": "NIC" + }""", + """{ + "name": "xxx", + "capability_type": "pci", + "product_id": "1111", + "vendor_id": "8086", + "device_type": "ACCEL" + }"""]) + self.assertRaises( + exception.PciInvalidAlias, + pci_request._get_alias_from_config) + + def test_aliase_2_request(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + expect_request = [ + {'count': 3, + 'spec': [{'vendor_id': '8086', 'product_id': '4443', + 'device_type': 'ACCEL', + 'capability_type': 'pci'}], + 'alias_name': 'QuicAssist'}, + + {'count': 1, + 'spec': [{'vendor_id': '8086', 'product_id': '1111', + 'device_type': "NIC", + 'capability_type': 'pci'}], + 'alias_name': 'IntelNIC'}, ] + + requests = pci_request._translate_alias_to_requests( + "QuicAssist : 3, IntelNIC: 1") + self.assertEqual(set([p['count'] for p in requests]), set([1, 3])) + exp_real = zip(expect_request, requests) + for exp, real in exp_real: + self.assertEqual(real, exp) + + def test_aliase_2_request_invalid(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + self.assertRaises(exception.PciRequestAliasNotDefined, + pci_request._translate_alias_to_requests, + "QuicAssistX : 3") + + def test_get_pci_requests_from_flavor(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + expect_request = [ + {'count': 3, + 'spec': [{'vendor_id': '8086', 'product_id': '4443', + 'device_type': "ACCEL", + 'capability_type': 'pci'}], + 'alias_name': 'QuicAssist'}, + + {'count': 1, + 'spec': [{'vendor_id': '8086', 'product_id': '1111', + 'device_type': "NIC", + 'capability_type': 'pci'}], + 'alias_name': 'IntelNIC'}, ] + + flavor = {'extra_specs': {"pci_passthrough:alias": + "QuicAssist:3, IntelNIC: 1"}} + requests = pci_request.get_pci_requests_from_flavor(flavor) + self.assertEqual(set([p['count'] for p in requests]), set([1, 3])) + exp_real = zip(expect_request, requests) + for exp, real in exp_real: + self.assertEqual(real, exp) + + def test_get_pci_requests_from_flavor_no_extra_spec(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + flavor = {} + requests = pci_request.get_pci_requests_from_flavor(flavor) + self.assertEqual([], requests) + + def test_get_instance_pci_requests_no_meta(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + instance = {} + requests = pci_request.get_instance_pci_requests(instance) + self.assertEqual([], requests) + + def test_get_instance_pci_requests_no_request(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + instance = {'system_metadata': {'a': 'b'}} + requests = pci_request.get_instance_pci_requests(instance) + self.assertEqual([], requests) + + def test_get_instance_pci_requests(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + expect_request = [{ + 'count': 3, + 'spec': [{'vendor_id': '8086', 'product_id': '4443', + 'device_type': "ACCEL", + 'capability_type': 'pci'}], + 'alias_name': 'QuicAssist'}] + + instance = {"system_metadata": {"pci_requests": + jsonutils.dumps(expect_request)}} + requests = pci_request.get_instance_pci_requests(instance) + exp_real = zip(expect_request, requests) + for exp, real in exp_real: + self.assertEqual(real, exp) + + def test_get_instance_pci_requests_prefix(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + expect_request = [{ + 'count': 3, + 'spec': [{'vendor_id': '8086', 'product_id': '4443', + 'device_type': "ACCEL", + 'capability_type': 'pci'}], + 'alias_name': 'QuicAssist'}] + + instance = {"system_metadata": {"new_pci_requests": + jsonutils.dumps(expect_request)}} + requests = pci_request.get_instance_pci_requests(instance, 'new_') + exp_real = zip(expect_request, requests) + for exp, real in exp_real: + self.assertEqual(real, exp) + + def test_save_flavor_pci_info(self): + self.flags(pci_alias=[_fake_alias1, _fake_alias3]) + expect_request = [ + {'count': 3, + 'spec': [{'vendor_id': '8086', 'product_id': '4443', + 'device_type': "ACCEL", + 'capability_type': 'pci'}], + 'alias_name': 'QuicAssist'}, + + {'count': 1, + 'spec': [{'vendor_id': '8086', 'product_id': '1111', + 'device_type': "NIC", + 'capability_type': 'pci'}], + 'alias_name': 'IntelNIC'}, ] + + flavor = {'extra_specs': {"pci_passthrough:alias": + "QuicAssist:3, IntelNIC: 1"}} + + meta = {} + pci_request.save_flavor_pci_info(meta, flavor) + + real = jsonutils.loads(meta['pci_requests']) + exp_real = zip(expect_request, real) + for exp, real in exp_real: + self.assertEqual(real, exp) + + meta = {} + pci_request.save_flavor_pci_info(meta, flavor, "old_") + real = jsonutils.loads(meta['old_pci_requests']) + exp_real = zip(expect_request, real) + for exp, real in exp_real: + self.assertEqual(real, exp) + + def test_delete_flavor_pci_info(self): + meta = {"pci_requests": "fake", "old_pci_requests": "fake"} + pci_request.delete_flavor_pci_info(meta, '') + self.assertTrue('pci_requests' not in meta) + pci_request.delete_flavor_pci_info(meta, 'old_') + self.assertTrue('old_pci_requests' not in meta)