diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index e21da2c8d5e2..1aa5ba5550ba 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -1677,6 +1677,16 @@ #pci_alias= +# +# Options defined in nova.pci.pci_whitelist +# + +# White list of PCI devices available to VMs. For example: +# pci_passthrough_whitelist = [{"vendor_id": "8086", +# "product_id": "0443"}] (multi valued) +#pci_passthrough_whitelist= + + # # Options defined in nova.scheduler.driver # diff --git a/nova/exception.py b/nova/exception.py index bebede55cd0b..85b5fc643c8f 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1419,3 +1419,7 @@ class MissingParameter(NovaException): ec2_code = 'MissingParameter' msg_fmt = _("Not enough parameters: %(reason)s") code = 400 + + +class PciConfigInvalidWhitelist(Invalid): + mst_fmt = _("Invalid PCI devices Whitelist config %(reason)s") diff --git a/nova/pci/pci_whitelist.py b/nova/pci/pci_whitelist.py new file mode 100644 index 000000000000..b11c816b151f --- /dev/null +++ b/nova/pci/pci_whitelist.py @@ -0,0 +1,115 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 Intel, Inc. +# Copyright (c) 2013 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 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 oslo.config import cfg + +pci_opts = [cfg.MultiStrOpt('pci_passthrough_whitelist', + default=[], + help='White list of PCI devices available to VMs. ' + 'For example: pci_passthrough_whitelist = ' + '[{"vendor_id": "8086", "product_id": "0443"}]' + ) + ] +CONF = cfg.CONF +CONF.register_opts(pci_opts) + +LOG = logging.getLogger(__name__) + + +_PCI_VENDOR_PATTERN = "^(hex{4})$".replace("hex", "[\da-fA-F]") +_WHITELIST_SCHEMA = { + "type": "array", + "items": + { + "type": "object", + "additionalProperties": False, + "properties": { + "product_id": { + "type": "string", + "pattern": _PCI_VENDOR_PATTERN + }, + "vendor_id": { + "type": "string", + "pattern": _PCI_VENDOR_PATTERN + }, + }, + "required": ["product_id", "vendor_id"] + } +} + + +class PciHostDevicesWhiteList(object): + + """White list class to decide assignable pci devices. + + Not all devices on compute node can be assigned to guest, the + cloud administrator decides the devices that can be assigned + based on vendor_id or product_id etc. If no white list specified, + no device will be assignable. + """ + + def _parse_white_list_from_config(self, whitelists): + """Parse and validate the pci whitelist from the nova config.""" + specs = [] + try: + for jsonspecs in whitelists: + spec = jsonutils.loads(jsonspecs) + jsonschema.validate(spec, _WHITELIST_SCHEMA) + specs.extend(spec) + except Exception as e: + raise exception.PciConfigInvalidWhitelist(reason=str(e)) + + return specs + + def __init__(self, whitelist_spec=None): + """White list constructor + + For example, followed json string specifies that devices whose + vendor_id is '8086' and product_id is '1520' can be assigned + to guest. + '[{"product_id":"1520", "vendor_id":"8086"}]' + + :param whitelist_spec: A json string for a list of dictionaries, + each dictionary specifies the pci device + properties requirement. + """ + super(PciHostDevicesWhiteList, self).__init__() + if whitelist_spec: + self.spec = self._parse_white_list_from_config(whitelist_spec) + else: + self.spec = None + + def device_assignable(self, dev): + """Check if a device can be assigned to a guest. + + :param dev: A dictionary describing the device properties + """ + if self.spec is None: + return False + return pci_utils.pci_device_prop_match(dev, self.spec) + + +def get_pci_devices_filter(): + return PciHostDevicesWhiteList(CONF.pci_passthrough_whitelist) diff --git a/nova/tests/pci/test_pci_whitelist.py b/nova/tests/pci/test_pci_whitelist.py new file mode 100644 index 000000000000..17cbfff58689 --- /dev/null +++ b/nova/tests/pci/test_pci_whitelist.py @@ -0,0 +1,99 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 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 nova import exception +from nova.objects import pci_device +from nova.pci import pci_whitelist +from nova import test + + +dev_dict = { + 'compute_node_id': 1, + 'address': 'a', + 'product_id': '0001', + 'vendor_id': '8086', + 'status': 'available', + } + + +class PciHostDevicesWhiteListTestCase(test.TestCase): + def setUp(self): + super(PciHostDevicesWhiteListTestCase, self).setUp() + + def test_whitelist_wrong_format(self): + white_list = '[{"vendor_x_id":"8086", "product_id":"0001"}]' + self.assertRaises( + exception.PciConfigInvalidWhitelist, + pci_whitelist.PciHostDevicesWhiteList, white_list + ) + + white_list = '[{"vendor_id":"80863", "product_id":"0001"}]' + self.assertRaises( + exception.PciConfigInvalidWhitelist, + pci_whitelist.PciHostDevicesWhiteList, white_list + ) + + def test_whitelist_missed_fields(self): + white_list = '[{"vendor_id":"80863"}]' + self.assertRaises( + exception.PciConfigInvalidWhitelist, + pci_whitelist.PciHostDevicesWhiteList, white_list + ) + + def test_whitelist(self): + white_list = '[{"product_id":"0001", "vendor_id":"8086"}]' + parsed = pci_whitelist.PciHostDevicesWhiteList([white_list]) + self.assertEqual(parsed.spec, [{'vendor_id': '8086', + 'product_id': '0001'}]) + + def test_whitelist_empty(self): + dev = pci_device.PciDevice.create(dev_dict) + parsed = pci_whitelist.PciHostDevicesWhiteList() + self.assertEqual(parsed.device_assignable(dev), False) + + def test_whitelist_multiple(self): + white_list_1 = '[{"product_id":"0001", "vendor_id":"8086"}]' + white_list_2 = '[{"product_id":"0002", "vendor_id":"8087"}]' + parsed = pci_whitelist.PciHostDevicesWhiteList( + [white_list_1, white_list_2]) + self.assertEqual(parsed.spec, + [{'vendor_id': '8086', 'product_id': '0001'}, + {'vendor_id': '8087', 'product_id': '0002'}]) + + def test_device_assignable(self): + dev = pci_device.PciDevice.create(dev_dict) + white_list = '[{"product_id":"0001", "vendor_id":"8086"}]' + parsed = pci_whitelist.PciHostDevicesWhiteList([white_list]) + self.assertEqual(parsed.device_assignable(dev), True) + + def test_device_assignable_multiple(self): + dev = pci_device.PciDevice.create(dev_dict) + white_list_1 = '[{"product_id":"0001", "vendor_id":"8086"}]' + white_list_2 = '[{"product_id":"0002", "vendor_id":"8087"}]' + parsed = pci_whitelist.PciHostDevicesWhiteList( + [white_list_1, white_list_2]) + self.assertEqual(parsed.device_assignable(dev), True) + dev.vendor_id = '8087' + dev.product_id = '0002' + self.assertEqual(parsed.device_assignable(dev), True) + + def test_get_pci_devices_filter(self): + white_list_1 = '[{"product_id":"0001", "vendor_id":"8086"}]' + self.flags(pci_passthrough_whitelist=[white_list_1]) + pci_filter = pci_whitelist.get_pci_devices_filter() + dev = pci_device.PciDevice.create(dev_dict) + self.assertEqual(pci_filter.device_assignable(dev), True)