Add keypairs to Instance object

This makes us load keypairs from instance_extra if requested, falling
back to pulling the keypairs by name from the database if we weren't
booted with our own keypair information.

Related to blueprint cells-keypairs-api-db

Change-Id: I23a81ff1698e5ba73e1a087082682fd8689c1987
This commit is contained in:
Dan Smith 2016-05-04 11:45:38 -07:00
parent 0b36cf34e4
commit e83842b80b
6 changed files with 127 additions and 30 deletions

View File

@ -37,7 +37,6 @@ from nova import context
from nova import network
from nova.network.security_group import openstack_driver
from nova import objects
from nova.objects import keypair as keypair_obj
from nova import utils
from nova.virt import netutils
@ -299,20 +298,20 @@ class InstanceMetadata(object):
metadata.update(self.extra_md)
if self.network_config:
metadata['network_config'] = self.network_config
if self.instance.key_name:
metadata['public_keys'] = {
self.instance.key_name: self.instance.key_data
}
if self.instance.key_name:
if cells_opts.get_cell_type() == 'compute':
cells_api = cells_rpcapi.CellsAPI()
keypair = cells_api.get_keypair_at_top(
context.get_admin_context(), self.instance.user_id,
self.instance.key_name)
else:
keypair = keypair_obj.KeyPair.get_by_name(
context.get_admin_context(), self.instance.user_id,
self.instance.key_name)
keypair = self.instance.keypairs[0]
metadata['public_keys'] = {
keypair.name: keypair.public_key,
}
metadata['keys'] = [
{'name': keypair.name,
'type': keypair.type,

View File

@ -47,7 +47,8 @@ _INSTANCE_OPTIONAL_NON_COLUMN_FIELDS = ['fault', 'flavor', 'old_flavor',
'new_flavor', 'ec2_ids']
# These are fields that are optional and in instance_extra
_INSTANCE_EXTRA_FIELDS = ['numa_topology', 'pci_requests',
'flavor', 'vcpu_model', 'migration_context']
'flavor', 'vcpu_model', 'migration_context',
'keypairs']
# These are fields that can be specified as expected_attrs
INSTANCE_OPTIONAL_ATTRS = (_INSTANCE_OPTIONAL_JOINED_FIELDS +
@ -91,7 +92,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
base.NovaObjectDictCompat):
# Version 2.0: Initial version
# Version 2.1: Added services
VERSION = '2.1'
# Version 2.2: Added keypairs
VERSION = '2.2'
fields = {
'id': fields.IntegerField(),
@ -189,7 +191,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
'vcpu_model': fields.ObjectField('VirtCPUModel', nullable=True),
'ec2_ids': fields.ObjectField('EC2Ids'),
'migration_context': fields.ObjectField('MigrationContext',
nullable=True)
nullable=True),
'keypairs': fields.ObjectField('KeyPairList'),
}
obj_extra_fields = ['name']
@ -197,6 +200,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
def obj_make_compatible(self, primitive, target_version):
super(Instance, self).obj_make_compatible(primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (2, 2) and 'keypairs' in primitive:
del primitive['keypairs']
if target_version < (2, 1) and 'services' in primitive:
del primitive['services']
@ -339,6 +344,9 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
db_inst['extra'].get('migration_context'))
else:
instance.migration_context = None
if 'keypairs' in expected_attrs:
if have_extra:
instance._load_keypairs(db_inst['extra'].get('keypairs'))
if 'info_cache' in expected_attrs:
if db_inst.get('info_cache') is None:
instance.info_cache = None
@ -457,6 +465,11 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
'new': new,
}
updates['extra']['flavor'] = jsonutils.dumps(flavor_info)
keypairs = updates.pop('keypairs', None)
if keypairs:
expected_attrs.append('keypairs')
updates['extra']['keypairs'] = jsonutils.dumps(
keypairs.obj_to_primitive())
vcpu_model = updates.pop('vcpu_model', None)
expected_attrs.append('vcpu_model')
if vcpu_model:
@ -589,6 +602,10 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
else:
objects.MigrationContext._destroy(context, self.uuid)
def _save_keypairs(self, context):
# NOTE(danms): Read-only so no need to save this.
pass
@base.remotable
def save(self, expected_vm_state=None,
expected_task_state=None, admin_state_reset=False):
@ -842,6 +859,34 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
self.migration_context = objects.MigrationContext.obj_from_db_obj(
db_context)
def _load_keypairs(self, db_keypairs=_NO_DATA_SENTINEL):
if db_keypairs is _NO_DATA_SENTINEL:
inst = objects.Instance.get_by_uuid(self._context, self.uuid,
expected_attrs=['keypairs'])
if 'keypairs' in inst:
self.keypairs = inst.keypairs
self.keypairs.obj_reset_changes(recursive=True)
self.obj_reset_changes(['keypairs'])
return
# NOTE(danms): We need to load from the old location by name
# if we don't have them in extra
self.keypairs = objects.KeyPairList(objects=[])
try:
key = objects.KeyPair.get_by_name(self._context,
self.user_id,
self.key_name)
self.keypairs.objects.append(key)
except exception.KeypairNotFound:
pass
# NOTE(danms): If we loaded from legacy, we leave the keypairs
# attribute dirty in hopes someone else will save it for us
elif db_keypairs:
self.keypairs = objects.KeyPairList.obj_from_primitive(
jsonutils.loads(db_keypairs))
self.obj_reset_changes(['keypairs'])
def apply_migration_context(self):
if self.migration_context:
self.numa_topology = self.migration_context.new_numa_topology
@ -912,6 +957,10 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
self._load_ec2_ids()
elif attrname == 'migration_context':
self._load_migration_context()
elif attrname == 'keypairs':
# NOTE(danms): Let keypairs control its own destiny for
# resetting changes.
return self._load_keypairs()
elif attrname == 'security_groups':
self._load_security_groups()
elif attrname == 'pci_devices':

View File

@ -85,7 +85,7 @@ def fake_db_instance(**updates):
db_instance[name] = None
elif field.default != fields.UnspecifiedDefault:
db_instance[name] = field.default
elif name in ['flavor', 'ec2_ids']:
elif name in ['flavor', 'ec2_ids', 'keypairs']:
pass
else:
raise Exception('fake_db_instance needs help with %s' % name)
@ -120,6 +120,7 @@ def fake_instance_obj(context, obj_instance_class=None, **updates):
inst = obj_instance_class._from_db_object(context,
obj_instance_class(), fake_db_instance(**updates),
expected_attrs=expected_attrs)
inst.keypairs = objects.KeyPairList(objects=[])
if flavor:
inst.flavor = flavor
inst.old_flavor = None

View File

@ -142,10 +142,11 @@ class _TestInstanceObject(object):
exp_cols.remove('vcpu_model')
exp_cols.remove('ec2_ids')
exp_cols.remove('migration_context')
exp_cols.remove('keypairs')
exp_cols = list(filter(lambda x: 'flavor' not in x, exp_cols))
exp_cols.extend(['extra', 'extra.numa_topology', 'extra.pci_requests',
'extra.flavor', 'extra.vcpu_model',
'extra.migration_context'])
'extra.migration_context', 'extra.keypairs'])
fake_topology = (test_instance_numa_topology.
fake_db_topology['numa_topology'])
@ -158,6 +159,10 @@ class _TestInstanceObject(object):
test_vcpu_model.fake_vcpumodel.obj_to_primitive())
fake_mig_context = jsonutils.dumps(
test_mig_ctxt.fake_migration_context_obj.obj_to_primitive())
fake_keypairlist = objects.KeyPairList(objects=[
objects.KeyPair(name='foo')])
fake_keypairs = jsonutils.dumps(
fake_keypairlist.obj_to_primitive())
fake_service = {'created_at': None, 'updated_at': None,
'deleted_at': None, 'deleted': False, 'id': 123,
'host': 'fake-host', 'binary': 'nova-fake',
@ -174,6 +179,7 @@ class _TestInstanceObject(object):
'flavor': fake_flavor,
'vcpu_model': fake_vcpu_model,
'migration_context': fake_mig_context,
'keypairs': fake_keypairs,
})
db.instance_get_by_uuid(
self.context, 'uuid',
@ -190,6 +196,7 @@ class _TestInstanceObject(object):
for attr in instance.INSTANCE_OPTIONAL_ATTRS:
self.assertTrue(inst.obj_attr_is_set(attr))
self.assertEqual(123, inst.services[0].id)
self.assertEqual('foo', inst.keypairs[0].name)
def test_lazy_load_services_on_deleted_instance(self):
# We should avoid trying to hit the database to reload the instance
@ -235,6 +242,46 @@ class _TestInstanceObject(object):
self.assertRaises(exception.ObjectActionError,
inst.obj_load_attr, 'foo')
def test_create_and_load_keypairs_from_extra(self):
inst = objects.Instance(context=self.context,
user_id=self.context.user_id,
project_id=self.context.project_id)
inst.keypairs = objects.KeyPairList(objects=[
objects.KeyPair(name='foo')])
inst.create()
inst = objects.Instance.get_by_uuid(self.context, inst.uuid,
expected_attrs=['keypairs'])
self.assertEqual('foo', inst.keypairs[0].name)
def test_lazy_load_keypairs_from_extra(self):
inst = objects.Instance(context=self.context,
user_id=self.context.user_id,
project_id=self.context.project_id)
inst.keypairs = objects.KeyPairList(objects=[
objects.KeyPair(name='foo')])
inst.create()
inst = objects.Instance.get_by_uuid(self.context, inst.uuid)
self.assertNotIn('keypairs', inst)
self.assertEqual('foo', inst.keypairs[0].name)
self.assertNotIn('keypairs', inst.obj_what_changed())
@mock.patch('nova.objects.KeyPair.get_by_name')
def test_lazy_load_keypairs_from_legacy(self, mock_get):
mock_get.return_value = objects.KeyPair(name='foo')
inst = objects.Instance(context=self.context,
user_id=self.context.user_id,
key_name='foo',
project_id=self.context.project_id)
inst.create()
inst = objects.Instance.get_by_uuid(self.context, inst.uuid)
self.assertNotIn('keypairs', inst)
self.assertEqual('foo', inst.keypairs[0].name)
self.assertIn('keypairs', inst.obj_what_changed())
def test_get_remote(self):
# isotime doesn't have microseconds and is always UTC
self.mox.StubOutWithMock(db, 'instance_get_by_uuid')

View File

@ -1132,7 +1132,7 @@ object_data = {
'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502',
'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d',
'ImageMetaProps': '1.12-6a132dee47931447bf86c03c7006d96c',
'Instance': '2.1-416fdd0dfc33dfa12ff2cfdd8cc32e17',
'Instance': '2.2-ab450ec9c1f4d429755c48d492b823f0',
'InstanceAction': '1.1-f9f293e526b66fca0d05c3b3a2d13914',
'InstanceActionEvent': '1.1-e56a64fa4710e43ef7af2ad9d6028b33',
'InstanceActionEventList': '1.1-13d92fb953030cdbfee56481756e02be',

View File

@ -85,6 +85,9 @@ def fake_inst_obj(context):
system_metadata={},
security_groups=objects.SecurityGroupList(),
availability_zone=None)
inst.keypairs = objects.KeyPairList(objects=[
fake_keypair_obj(inst.key_name, inst.key_data)])
nwinfo = network_model.NetworkInfo([])
inst.info_cache = objects.InstanceInfoCache(context=context,
instance_uuid=inst.uuid,
@ -394,15 +397,13 @@ class MetadataTestCase(test.TestCase):
@mock.patch.object(base64, 'b64encode', lambda data: FAKE_SEED)
@mock.patch('nova.cells.rpcapi.CellsAPI.get_keypair_at_top')
@mock.patch.object(objects.KeyPair, 'get_by_name')
@mock.patch.object(jsonutils, 'dump_as_bytes')
def _test_as_json_with_options(self, mock_json_dump_as_bytes,
mock_keypair, mock_cells_keypair,
mock_cells_keypair,
is_cells=False, os_version=base.GRIZZLY):
if is_cells:
self.flags(enable=True, group='cells')
self.flags(cell_type='compute', group='cells')
mock_keypair = mock_cells_keypair
instance = self.instance
keypair = self.keypair
@ -435,14 +436,15 @@ class MetadataTestCase(test.TestCase):
if md._check_os_version(base.LIBERTY, os_version):
expected_metadata['project_id'] = instance.project_id
mock_keypair.return_value = keypair
mock_cells_keypair.return_value = keypair
md._metadata_as_json(os_version, 'non useless path parameter')
if instance.key_name:
mock_keypair.assert_called_once_with(mock.ANY,
instance.user_id,
instance.key_name)
self.assertIsInstance(mock_keypair.call_args[0][0],
context.RequestContext)
if is_cells:
mock_cells_keypair.assert_called_once_with(mock.ANY,
instance.user_id,
instance.key_name)
self.assertIsInstance(mock_cells_keypair.call_args[0][0],
context.RequestContext)
self.assertEqual(md.md_mimetype, base.MIME_TYPE_APPLICATION_JSON)
mock_json_dump_as_bytes.assert_called_once_with(expected_metadata)
@ -552,19 +554,18 @@ class OpenStackMetadataTestCase(test.TestCase):
self.assertEqual(found, content)
def test_x509_keypair(self):
# check if the x509 content is set, if the keypair type is x509.
fakes.stub_out_key_pair_funcs(self.stubs, type='x509')
inst = self.instance.obj_clone()
expected = {'name': self.instance['key_name'],
'type': 'x509',
'data': 'public_key'}
inst.keypairs[0].name = expected['name']
inst.keypairs[0].type = expected['type']
inst.keypairs[0].public_key = expected['data']
mdinst = fake_InstanceMetadata(self.stubs, inst)
mdjson = mdinst.lookup("/openstack/2012-08-10/meta_data.json")
mddict = jsonutils.loads(mdjson)
# keypair is stubbed-out, so it's public_key is 'public_key'.
expected = {'name': self.instance['key_name'],
'type': 'x509',
'data': 'public_key'}
self.assertEqual([expected], mddict['keys'])
def test_extra_md(self):