summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/objects/instance.py238
-rw-r--r--nova/tests/objects/test_instance.py186
2 files changed, 424 insertions, 0 deletions
diff --git a/nova/objects/instance.py b/nova/objects/instance.py
new file mode 100644
index 000000000..836d78c08
--- /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 000000000..8136a4f1c
--- /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