diff --git a/nova/pci/pci_device.py b/nova/pci/pci_device.py new file mode 100644 index 000000000000..4ee2ef6660ff --- /dev/null +++ b/nova/pci/pci_device.py @@ -0,0 +1,121 @@ +# Copyright 2014 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. + +import copy +import functools + +from nova import exception + + +def check_device_status(dev_status=None): + """Decorator to check device status before changing it.""" + + if dev_status is not None and not isinstance(dev_status, set): + dev_status = set(dev_status) + + def outer(f): + @functools.wraps(f) + def inner(devobj, instance=None): + if devobj['status'] not in dev_status: + raise exception.PciDeviceInvalidStatus( + compute_node_id=devobj.compute_node_id, + address=devobj.address, status=devobj.status, + hopestatus=dev_status) + if instance: + return f(devobj, instance) + else: + return f(devobj) + return inner + return outer + + +@check_device_status(dev_status=['available']) +def claim(devobj, instance): + devobj.status = 'claimed' + devobj.instance_uuid = instance['uuid'] + + +@check_device_status(dev_status=['available', 'claimed']) +def allocate(devobj, instance): + if devobj.status == 'claimed' and devobj.instance_uuid != instance['uuid']: + raise exception.PciDeviceInvalidOwner( + compute_node_id=devobj.compute_node_id, + address=devobj.address, owner=devobj.instance_uuid, + hopeowner=instance['uuid']) + + devobj.status = 'allocated' + devobj.instance_uuid = instance['uuid'] + + # Notes(yjiang5): remove this check when instance object for + # compute manager is finished + if isinstance(instance, dict): + if 'pci_devices' not in instance: + instance['pci_devices'] = [] + instance['pci_devices'].append(copy.copy(devobj)) + else: + instance.pci_devices.objects.append(copy.copy(devobj)) + + +@check_device_status(dev_status=['available']) +def remove(devobj): + devobj.status = 'removed' + devobj.instance_uuid = None + + +@check_device_status(dev_status=['claimed', 'allocated']) +def free(devobj, instance=None): + if instance and devobj.instance_uuid != instance['uuid']: + raise exception.PciDeviceInvalidOwner( + compute_node_id=devobj.compute_node_id, + address=devobj.address, owner=devobj.instance_uuid, + hopeowner=instance['uuid']) + old_status = devobj.status + devobj.status = 'available' + devobj.instance_uuid = None + if old_status == 'allocated' and instance: + # Notes(yjiang5): remove this check when instance object for + # compute manager is finished + existed = next((dev for dev in instance['pci_devices'] + if dev.id == devobj.id)) + if isinstance(instance, dict): + instance['pci_devices'].remove(existed) + else: + instance.pci_devices.objects.remove(existed) + + +def update_device(devobj, dev_dict): + """Sync the content from device dictionary to device object. + + The resource tracker updates the available devices periodically. + To avoid meaningless syncs with the database, we update the device + object only if a value changed. + """ + + # Note(yjiang5): status/instance_uuid should only be updated by + # functions like claim/allocate etc. The id is allocated by + # database. The extra_info is created by the object. + no_changes = ('status', 'instance_uuid', 'id', 'extra_info') + map(lambda x: dev_dict.pop(x, None), + [key for key in no_changes]) + + for k, v in dev_dict.items(): + if k in devobj.fields.keys(): + devobj[k] = v + else: + # Note (yjiang5) extra_info.update does not update + # obj_what_changed, set it explicitely + extra_info = devobj.extra_info + extra_info.update({k: v}) + devobj.extra_info = extra_info diff --git a/nova/tests/pci/test_pci_device.py b/nova/tests/pci/test_pci_device.py new file mode 100644 index 000000000000..4b5cc1e37d96 --- /dev/null +++ b/nova/tests/pci/test_pci_device.py @@ -0,0 +1,119 @@ +# Copyright 2014 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. + +from nova import context +from nova import exception +from nova.objects import instance +from nova.objects import pci_device as pci_device_obj +from nova.pci import pci_device +from nova import test + + +dev_dict = { + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': None, + 'id': 1, + 'compute_node_id': 1, + 'address': 'a', + 'vendor_id': 'v', + 'product_id': 'p', + 'dev_type': 't', + 'status': 'available', + 'dev_id': 'i', + 'label': 'l', + 'instance_uuid': None, + 'extra_info': '{}', + } + + +class PciDeviceTestCase(test.TestCase): + def setUp(self): + super(PciDeviceTestCase, self).setUp() + self.ctxt = context.get_admin_context() + self.inst = instance.Instance() + self.inst.uuid = 'fake-inst-uuid' + self.inst.pci_devices = pci_device_obj.PciDeviceList() + self.devobj = pci_device_obj.PciDevice._from_db_object( + self.ctxt, + pci_device_obj.PciDevice(), + dev_dict) + + def test_claim_device(self): + pci_device.claim(self.devobj, self.inst) + self.assertEqual(self.devobj.status, 'claimed') + self.assertEqual(self.devobj.instance_uuid, + self.inst.uuid) + self.assertEqual(len(self.inst.pci_devices), 0) + + def test_claim_device_fail(self): + self.devobj.status = 'allocated' + self.assertRaises(exception.PciDeviceInvalidStatus, + pci_device.claim, self.devobj, self.inst) + + def test_allocate_device(self): + pci_device.claim(self.devobj, self.inst) + pci_device.allocate(self.devobj, self.inst) + self.assertEqual(self.devobj.status, 'allocated') + self.assertEqual(self.devobj.instance_uuid, 'fake-inst-uuid') + self.assertEqual(len(self.inst.pci_devices), 1) + self.assertEqual(self.inst.pci_devices[0]['vendor_id'], 'v') + self.assertEqual(self.inst.pci_devices[0]['status'], 'allocated') + + def test_allocacte_device_fail_status(self): + self.devobj.status = 'removed' + self.assertRaises(exception.PciDeviceInvalidStatus, + pci_device.allocate, + self.devobj, + self.inst) + + def test_allocacte_device_fail_owner(self): + inst_2 = instance.Instance() + inst_2.uuid = 'fake-inst-uuid-2' + pci_device.claim(self.devobj, self.inst) + self.assertRaises(exception.PciDeviceInvalidOwner, + pci_device.allocate, + self.devobj, inst_2) + + def test_free_claimed_device(self): + pci_device.claim(self.devobj, self.inst) + pci_device.free(self.devobj, self.inst) + self.assertEqual(self.devobj.status, 'available') + self.assertIsNone(self.devobj.instance_uuid) + + def test_free_allocated_device(self): + pci_device.claim(self.devobj, self.inst) + pci_device.allocate(self.devobj, self.inst) + self.assertEqual(len(self.inst.pci_devices), 1) + pci_device.free(self.devobj, self.inst) + self.assertEqual(len(self.inst.pci_devices), 0) + self.assertEqual(self.devobj.status, 'available') + self.assertIsNone(self.devobj.instance_uuid) + + def test_free_device_fail(self): + self.devobj.status = 'removed' + self.assertRaises(exception.PciDeviceInvalidStatus, + pci_device.free, self.devobj) + + def test_remove_device(self): + pci_device.remove(self.devobj) + self.assertEqual(self.devobj.status, 'removed') + self.assertIsNone(self.devobj.instance_uuid) + + def test_remove_device_fail(self): + pci_device.claim(self.devobj, self.inst) + self.assertRaises(exception.PciDeviceInvalidStatus, + pci_device.remove, self.devobj)