diff --git a/nova/objects/instance.py b/nova/objects/instance.py new file mode 100644 index 000000000000..836d78c082e2 --- /dev/null +++ b/nova/objects/instance.py @@ -0,0 +1,238 @@ +# Copyright 2013 IBM Corp. +# +# 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 db +from nova import notifications +from nova.objects import base +from nova.objects import utils as obj_utils +from nova import utils + +from oslo.config import cfg + + +CONF = cfg.CONF + + +class Instance(base.NovaObject): + fields = { + 'id': int, + + 'user_id': obj_utils.str_or_none, + 'project_id': obj_utils.str_or_none, + + 'image_ref': obj_utils.str_or_none, + 'kernel_id': obj_utils.str_or_none, + 'ramdisk_id': obj_utils.str_or_none, + 'hostname': obj_utils.str_or_none, + + 'launch_index': obj_utils.int_or_none, + 'key_name': obj_utils.str_or_none, + 'key_data': obj_utils.str_or_none, + + 'power_state': obj_utils.int_or_none, + 'vm_state': obj_utils.str_or_none, + 'task_state': obj_utils.str_or_none, + + 'memory_mb': obj_utils.int_or_none, + 'vcpus': obj_utils.int_or_none, + 'root_gb': obj_utils.int_or_none, + 'ephemeral_gb': obj_utils.int_or_none, + + 'host': obj_utils.str_or_none, + 'node': obj_utils.str_or_none, + + 'instance_type_id': obj_utils.int_or_none, + + 'user_data': obj_utils.str_or_none, + + 'reservation_id': obj_utils.str_or_none, + + 'scheduled_at': obj_utils.datetime_or_none, + 'launched_at': obj_utils.datetime_or_none, + 'terminated_at': obj_utils.datetime_or_none, + + 'availability_zone': obj_utils.str_or_none, + + 'display_name': obj_utils.str_or_none, + 'display_description': obj_utils.str_or_none, + + 'launched_on': obj_utils.str_or_none, + 'locked': bool, + + 'os_type': obj_utils.str_or_none, + 'architecture': obj_utils.str_or_none, + 'vm_mode': obj_utils.str_or_none, + 'uuid': obj_utils.str_or_none, + + 'root_device_name': obj_utils.str_or_none, + 'default_ephemeral_device': obj_utils.str_or_none, + 'default_swap_device': obj_utils.str_or_none, + 'config_drive': obj_utils.str_or_none, + + 'access_ip_v4': obj_utils.ip_or_none(4), + 'access_ip_v6': obj_utils.ip_or_none(6), + + 'auto_disk_config': bool, + 'progress': obj_utils.int_or_none, + + 'shutdown_terminate': bool, + 'disable_terminate': bool, + + 'cell_name': obj_utils.str_or_none, + + 'metadata': dict, + 'system_metadata': dict, + + } + + @property + def name(self): + try: + base_name = CONF.instance_name_template % self.id + except TypeError: + # Support templates like "uuid-%(uuid)s", etc. + info = {} + # NOTE(russellb): Don't use self.iteritems() here, as it will + # result in infinite recursion on the name property. + for key in self.fields: + # prevent recursion if someone specifies %(name)s + # %(name)s will not be valid. + if key == 'name': + continue + info[key] = self[key] + try: + base_name = CONF.instance_name_template % info + except KeyError: + base_name = self.uuid + return base_name + + def _attr_access_ip_v4_to_primitive(self): + if self.access_ip_v4 is not None: + return str(self.access_ip_v4) + else: + return None + + def _attr_access_ip_v6_to_primitive(self): + if self.access_ip_v6 is not None: + return str(self.access_ip_v6) + else: + return None + + _attr_scheduled_at_to_primitive = obj_utils.dt_serializer('scheduled_at') + _attr_launched_at_to_primitive = obj_utils.dt_serializer('launched_at') + _attr_terminated_at_to_primitive = obj_utils.dt_serializer('terminated_at') + + _attr_scheduled_at_from_primitive = obj_utils.dt_deserializer + _attr_launched_at_from_primitive = obj_utils.dt_deserializer + _attr_terminated_at_from_primitive = obj_utils.dt_deserializer + + @staticmethod + def _from_db_object(instance, db_inst, expected_attrs=None): + """Method to help with migration to objects. + + Converts a database entity to a formal object. + """ + if expected_attrs is None: + expected_attrs = [] + # Most of the field names match right now, so be quick + for field in instance.fields: + if field in ['metadata', 'system_metadata']: + continue + instance[field] = db_inst[field] + + if 'metadata' in expected_attrs: + instance['metadata'] = utils.metadata_to_dict(db_inst['metadata']) + if 'system_metadata' in expected_attrs: + instance['system_metadata'] = utils.metadata_to_dict( + db_inst['system_metadata']) + + instance.obj_reset_changes() + return instance + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid=None, expected_attrs=None): + if expected_attrs is None: + expected_attrs = [] + + # Construct DB-specific columns from generic expected_attrs + columns_to_join = [] + if 'metadata' in expected_attrs: + columns_to_join.append('metadata') + if 'system_metadata' in expected_attrs: + columns_to_join.append('system_metadata') + + db_inst = db.instance_get_by_uuid(context, uuid, + columns_to_join) + return Instance._from_db_object(cls(), db_inst, expected_attrs) + + @base.remotable + def save(self, context, expected_task_state=None): + """Save updates to this instance + + Column-wise updates will be made based on the result of + self.what_changed(). If expected_task_state is provided, + it will be checked against the in-database copy of the + instance before updates are made. + :param context: Security context + :param expected_task_state: Optional tuple of valid task states + for the instance to be in. + """ + updates = {} + changes = self.obj_what_changed() + for field in changes: + updates[field] = self[field] + if expected_task_state is not None: + updates['expected_task_state'] = expected_task_state + old_ref, inst_ref = db.instance_update_and_get_original(context, + self.uuid, + updates) + + expected_attrs = [] + for attr in ('metadata', 'system_metadata'): + if hasattr(self, base.get_attrname(attr)): + expected_attrs.append(attr) + Instance._from_db_object(self, inst_ref, expected_attrs) + if 'vm_state' in changes or 'task_state' in changes: + notifications.send_update(context, old_ref, inst_ref) + + self.obj_reset_changes() + + @base.remotable + def refresh(self, context): + extra = [] + for field in ['system_metadata', 'metadata']: + if hasattr(self, base.get_attrname(field)): + extra.append(field) + current = self.__class__.get_by_uuid(context, uuid=self.uuid, + expected_attrs=extra) + for field in self.fields: + if (hasattr(self, base.get_attrname(field)) and + self[field] != current[field]): + self[field] = current[field] + + def obj_load(self, attrname): + extra = [] + if attrname == 'system_metadata': + extra.append('system_metadata') + elif attrname == 'metadata': + extra.append('metadata') + + if not extra: + raise Exception('Cannot load "%s" from instance' % attrname) + + # NOTE(danms): This could be optimized to just load the bits we need + instance = self.__class__.get_by_uuid(self._context, + uuid=self.uuid, + expected_attrs=extra) + self[attrname] = instance[attrname] diff --git a/nova/tests/objects/test_instance.py b/nova/tests/objects/test_instance.py new file mode 100644 index 000000000000..8136a4f1c33d --- /dev/null +++ b/nova/tests/objects/test_instance.py @@ -0,0 +1,186 @@ +# Copyright 2013 IBM Corp. +# +# 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 datetime +import iso8601 +import netaddr + +from nova import context +from nova import db +from nova.objects import instance +from nova.openstack.common import timeutils +from nova.tests.api.openstack import fakes +from nova.tests.objects import test_objects + + +class _TestInstanceObject(object): + @property + def fake_instance(self): + fake_instance = fakes.stub_instance(id=2, + access_ipv4='1.2.3.4', + access_ipv6='::1') + fake_instance['scheduled_at'] = None + fake_instance['terminated_at'] = None + fake_instance['deleted_at'] = None + fake_instance['created_at'] = None + fake_instance['updated_at'] = None + fake_instance['launched_at'] = ( + fake_instance['launched_at'].replace( + tzinfo=iso8601.iso8601.Utc(), microsecond=0)) + return fake_instance + + def test_datetime_deserialization(self): + red_letter_date = timeutils.parse_isotime( + timeutils.isotime(datetime.datetime(1955, 11, 5))) + inst = instance.Instance() + inst.uuid = 'fake-uuid' + inst.launched_at = red_letter_date + primitive = inst.obj_to_primitive() + expected = {'nova_object.name': 'Instance', + 'nova_object.namespace': 'nova', + 'nova_object.version': '1.0', + 'nova_object.data': + {'uuid': 'fake-uuid', + 'launched_at': '1955-11-05T00:00:00Z'}, + 'nova_object.changes': ['uuid', 'launched_at']} + self.assertEqual(primitive, expected) + inst2 = instance.Instance.obj_from_primitive(primitive) + self.assertTrue(isinstance(inst2.launched_at, + datetime.datetime)) + self.assertEqual(inst2.launched_at, red_letter_date) + + def test_ip_deserialization(self): + inst = instance.Instance() + inst.uuid = 'fake-uuid' + inst.access_ip_v4 = '1.2.3.4' + inst.access_ip_v6 = '::1' + primitive = inst.obj_to_primitive() + expected = {'nova_object.name': 'Instance', + 'nova_object.namespace': 'nova', + 'nova_object.version': '1.0', + 'nova_object.data': + {'uuid': 'fake-uuid', + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': '::1'}, + 'nova_object.changes': ['uuid', 'access_ip_v6', + 'access_ip_v4']} + self.assertEqual(primitive, expected) + inst2 = instance.Instance.obj_from_primitive(primitive) + self.assertTrue(isinstance(inst2.access_ip_v4, netaddr.IPAddress)) + self.assertTrue(isinstance(inst2.access_ip_v6, netaddr.IPAddress)) + self.assertEqual(inst2.access_ip_v4, netaddr.IPAddress('1.2.3.4')) + self.assertEqual(inst2.access_ip_v6, netaddr.IPAddress('::1')) + + def test_get_without_expected(self): + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_by_uuid') + db.instance_get_by_uuid(ctxt, 'uuid', []).AndReturn(self.fake_instance) + self.mox.ReplayAll() + inst = instance.Instance.get_by_uuid(ctxt, uuid='uuid') + # Make sure these weren't loaded + self.assertFalse(hasattr(inst, '_metadata')) + self.assertFalse(hasattr(inst, '_system_metadata')) + self.assertRemotes() + + def test_get_with_expected(self): + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_by_uuid') + db.instance_get_by_uuid( + ctxt, 'uuid', + ['metadata', 'system_metadata']).AndReturn(self.fake_instance) + self.mox.ReplayAll() + inst = instance.Instance.get_by_uuid( + ctxt, uuid='uuid', expected_attrs=['metadata', 'system_metadata']) + self.assertTrue(hasattr(inst, '_metadata')) + self.assertTrue(hasattr(inst, '_system_metadata')) + self.assertRemotes() + + def test_load(self): + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_by_uuid') + fake_uuid = self.fake_instance['uuid'] + db.instance_get_by_uuid(ctxt, fake_uuid, []).AndReturn( + self.fake_instance) + fake_inst2 = dict(self.fake_instance, + system_metadata=[{'key': 'foo', 'value': 'bar'}]) + db.instance_get_by_uuid(ctxt, fake_uuid, ['system_metadata'] + ).AndReturn(fake_inst2) + self.mox.ReplayAll() + inst = instance.Instance.get_by_uuid(ctxt, uuid=fake_uuid) + self.assertFalse(hasattr(inst, '_system_metadata')) + sys_meta = inst.system_metadata + self.assertEqual(sys_meta, {'foo': 'bar'}) + self.assertTrue(hasattr(inst, '_system_metadata')) + # Make sure we don't run load again + sys_meta2 = inst.system_metadata + self.assertEqual(sys_meta2, {'foo': 'bar'}) + self.assertRemotes() + + def test_get_remote(self): + # isotime doesn't have microseconds and is always UTC + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_by_uuid') + fake_instance = self.fake_instance + db.instance_get_by_uuid(ctxt, 'fake-uuid', []).AndReturn( + fake_instance) + self.mox.ReplayAll() + inst = instance.Instance.get_by_uuid(ctxt, uuid='fake-uuid') + self.assertEqual(inst.id, fake_instance['id']) + self.assertEqual(inst.launched_at, fake_instance['launched_at']) + self.assertEqual(str(inst.access_ip_v4), + fake_instance['access_ip_v4']) + self.assertEqual(str(inst.access_ip_v6), + fake_instance['access_ip_v6']) + self.assertRemotes() + + def test_refresh(self): + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_by_uuid') + fake_uuid = self.fake_instance['uuid'] + db.instance_get_by_uuid(ctxt, fake_uuid, []).AndReturn( + dict(self.fake_instance, host='orig-host')) + db.instance_get_by_uuid(ctxt, fake_uuid, []).AndReturn( + dict(self.fake_instance, host='new-host')) + self.mox.ReplayAll() + inst = instance.Instance.get_by_uuid(ctxt, uuid=fake_uuid) + self.assertEqual(inst.host, 'orig-host') + inst.refresh() + self.assertEqual(inst.host, 'new-host') + self.assertRemotes() + + def test_save(self): + ctxt = context.get_admin_context() + fake_inst = dict(self.fake_instance, host='oldhost') + fake_uuid = fake_inst['uuid'] + self.mox.StubOutWithMock(db, 'instance_get_by_uuid') + self.mox.StubOutWithMock(db, 'instance_update_and_get_original') + db.instance_get_by_uuid(ctxt, fake_uuid, []).AndReturn(fake_inst) + db.instance_update_and_get_original( + ctxt, fake_uuid, {'user_data': 'foo'}).AndReturn( + (fake_inst, dict(fake_inst, host='newhost'))) + self.mox.ReplayAll() + inst = instance.Instance.get_by_uuid(ctxt, uuid=fake_uuid) + inst.user_data = 'foo' + inst.save() + self.assertEqual(inst.host, 'newhost') + + +class TestInstanceObject(test_objects._LocalTest, + _TestInstanceObject): + pass + + +class TestRemoteInstanceObject(test_objects._RemoteTest, + _TestInstanceObject): + pass