Add support for injecting keypairs

Change-Id: I6aa4f8ca970ca70e7e2279e2436e94939e065e1f
This commit is contained in:
Zhenguo Niu 2017-04-20 20:07:05 +08:00
parent 0d00ed2fa8
commit a7224e4ba9
12 changed files with 76 additions and 25 deletions

View File

@ -40,6 +40,7 @@ Request
- networks.port_type: network_port_type - networks.port_type: network_port_type
- user_data: user_data - user_data: user_data
- personality: personality - personality: personality
- key_name: key_name
**Example Create Instance: JSON request** **Example Create Instance: JSON request**

View File

@ -219,6 +219,12 @@ instance_uuid:
in: body in: body
required: true required: true
type: string type: string
key_name:
description: |
Key pair name.
in: body
required: false
type: string
keypair_fingerprint: keypair_fingerprint:
in: body in: body
required: true required: true

View File

@ -22,5 +22,6 @@
"contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
} }
], ],
"user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==" "user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==",
"key_name": "test_key"
} }

View File

@ -583,6 +583,7 @@ class InstanceController(InstanceControllerBase):
instance_type_uuid = instance.get('instance_type_uuid') instance_type_uuid = instance.get('instance_type_uuid')
image_uuid = instance.get('image_uuid') image_uuid = instance.get('image_uuid')
user_data = instance.get('user_data') user_data = instance.get('user_data')
key_name = instance.get('key_name')
personality = instance.pop('personality', None) personality = instance.pop('personality', None)
injected_files = [] injected_files = []
@ -605,6 +606,7 @@ class InstanceController(InstanceControllerBase):
requested_networks=requested_networks, requested_networks=requested_networks,
user_data=user_data, user_data=user_data,
injected_files=injected_files, injected_files=injected_files,
key_name=key_name,
min_count=min_count, min_count=min_count,
max_count=max_count) max_count=max_count)
except exception.InstanceTypeNotFound: except exception.InstanceTypeNotFound:
@ -616,6 +618,10 @@ class InstanceController(InstanceControllerBase):
msg = (_("Requested image %s could not be found") % image_uuid) msg = (_("Requested image %s could not be found") % image_uuid)
raise wsme.exc.ClientSideError( raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST) msg, status_code=http_client.BAD_REQUEST)
except exception.KeypairNotFound:
msg = (_("Invalid key_name %s provided.") % key_name)
raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST)
except exception.PortLimitExceeded as e: except exception.PortLimitExceeded as e:
raise wsme.exc.ClientSideError( raise wsme.exc.ClientSideError(
e.message, status_code=http_client.FORBIDDEN) e.message, status_code=http_client.FORBIDDEN)

View File

@ -39,6 +39,7 @@ create_instance = {
}, },
'user_data': {'type': 'string', 'format': 'base64'}, 'user_data': {'type': 'string', 'format': 'base64'},
'personality': parameter_types.personality, 'personality': parameter_types.personality,
'key_name': parameter_types.name,
'min_count': {'type': 'integer', 'minimum': 1}, 'min_count': {'type': 'integer', 'minimum': 1},
'max_count': {'type': 'integer', 'minimum': 1}, 'max_count': {'type': 'integer', 'minimum': 1},
'extra': parameter_types.extra, 'extra': parameter_types.extra,

View File

@ -79,7 +79,7 @@ class API(object):
image_uuid, name, description, image_uuid, name, description,
availability_zone, extra, availability_zone, extra,
requested_networks, user_data, requested_networks, user_data,
max_count): key_name, max_count):
"""Verify all the input parameters""" """Verify all the input parameters"""
if user_data: if user_data:
@ -100,6 +100,13 @@ class API(object):
requested_networks, requested_networks,
max_count) max_count)
if key_name is not None:
key_pair = objects.KeyPair.get_by_name(context,
context.user_id,
key_name)
else:
key_pair = None
base_options = { base_options = {
'image_uuid': image_uuid, 'image_uuid': image_uuid,
'status': states.BUILDING, 'status': states.BUILDING,
@ -114,7 +121,7 @@ class API(object):
'availability_zone': availability_zone} 'availability_zone': availability_zone}
# return the validated options # return the validated options
return base_options, max_network_count return base_options, max_network_count, key_pair
def _new_instance_name_from_template(self, uuid, name, index): def _new_instance_name_from_template(self, uuid, name, index):
"""Apply the template to instance name. """Apply the template to instance name.
@ -242,16 +249,18 @@ class API(object):
def _create_instance(self, context, instance_type, image_uuid, def _create_instance(self, context, instance_type, image_uuid,
name, description, availability_zone, extra, name, description, availability_zone, extra,
requested_networks, user_data, injected_files, requested_networks, user_data, injected_files,
min_count, max_count): key_name, min_count, max_count):
"""Verify all the input parameters""" """Verify all the input parameters"""
# Verify the specified image exists # Verify the specified image exists
if image_uuid: if image_uuid:
self._get_image(context, image_uuid) self._get_image(context, image_uuid)
base_options, max_net_count = self._validate_and_build_base_options( base_options, max_net_count, key_pair = \
context, instance_type, image_uuid, name, description, self._validate_and_build_base_options(
availability_zone, extra, requested_networks, user_data, max_count) context, instance_type, image_uuid, name, description,
availability_zone, extra, requested_networks, user_data,
key_name, max_count)
# max_net_count is the maximum number of instances requested by the # max_net_count is the maximum number of instances requested by the
# user adjusted for any network quota constraints, including # user adjusted for any network quota constraints, including
@ -289,6 +298,7 @@ class API(object):
requested_networks, requested_networks,
user_data, user_data,
decoded_files, decoded_files,
key_pair,
request_spec, request_spec,
filter_properties=None) filter_properties=None)
@ -297,7 +307,8 @@ class API(object):
def create(self, context, instance_type, image_uuid, def create(self, context, instance_type, image_uuid,
name=None, description=None, availability_zone=None, name=None, description=None, availability_zone=None,
extra=None, requested_networks=None, user_data=None, extra=None, requested_networks=None, user_data=None,
injected_files=None, min_count=None, max_count=None): injected_files=None, key_name=None, min_count=None,
max_count=None):
"""Provision instances """Provision instances
Sending instance information to the engine and will handle Sending instance information to the engine and will handle
@ -316,7 +327,8 @@ class API(object):
image_uuid, name, description, image_uuid, name, description,
availability_zone, extra, availability_zone, extra,
requested_networks, user_data, requested_networks, user_data,
injected_files, min_count, max_count) injected_files, key_name,
min_count, max_count)
def _delete_instance(self, context, instance): def _delete_instance(self, context, instance):

View File

@ -51,7 +51,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
def __init__(self, engine_rpcapi): def __init__(self, engine_rpcapi):
requires = ['filter_properties', 'request_spec', 'instance', requires = ['filter_properties', 'request_spec', 'instance',
'requested_networks', 'user_data', 'injected_files', 'requested_networks', 'user_data', 'injected_files',
'context'] 'key_pair', 'context']
super(OnFailureRescheduleTask, self).__init__(addons=[ACTION], super(OnFailureRescheduleTask, self).__init__(addons=[ACTION],
requires=requires) requires=requires)
self.engine_rpcapi = engine_rpcapi self.engine_rpcapi = engine_rpcapi
@ -69,7 +69,8 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
pass pass
def _reschedule(self, context, cause, request_spec, filter_properties, def _reschedule(self, context, cause, request_spec, filter_properties,
instance, requested_networks, user_data, injected_files): instance, requested_networks, user_data, injected_files,
key_pair):
"""Actions that happen during the rescheduling attempt occur here.""" """Actions that happen during the rescheduling attempt occur here."""
create_instance = self.engine_rpcapi.create_instance create_instance = self.engine_rpcapi.create_instance
@ -95,6 +96,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
return create_instance(context, instance, requested_networks, return create_instance(context, instance, requested_networks,
user_data=user_data, user_data=user_data,
injected_files=injected_files, injected_files=injected_files,
key_pair=key_pair,
request_spec=request_spec, request_spec=request_spec,
filter_properties=filter_properties) filter_properties=filter_properties)
@ -210,17 +212,17 @@ class GenerateConfigDriveTask(flow_utils.MoganTask):
"""Generate ConfigDrive value the instance.""" """Generate ConfigDrive value the instance."""
def __init__(self): def __init__(self):
requires = ['instance', 'user_data', 'injected_files', 'configdrive', requires = ['instance', 'user_data', 'injected_files', 'key_pair',
'context'] 'configdrive', 'context']
super(GenerateConfigDriveTask, self).__init__(addons=[ACTION], super(GenerateConfigDriveTask, self).__init__(addons=[ACTION],
requires=requires) requires=requires)
def _generate_configdrive(self, context, instance, user_data=None, def _generate_configdrive(self, context, instance, user_data=None,
files=None): files=None, key_pair=None):
"""Generate a config drive.""" """Generate a config drive."""
i_meta = instance_metadata.InstanceMetadata( i_meta = instance_metadata.InstanceMetadata(
instance, content=files, user_data=user_data) instance, content=files, user_data=user_data, key_pair=key_pair)
with tempfile.NamedTemporaryFile() as uncompressed: with tempfile.NamedTemporaryFile() as uncompressed:
with configdrive.ConfigDriveBuilder(instance_md=i_meta) as cdb: with configdrive.ConfigDriveBuilder(instance_md=i_meta) as cdb:
@ -236,12 +238,13 @@ class GenerateConfigDriveTask(flow_utils.MoganTask):
compressed.seek(0) compressed.seek(0)
return base64.b64encode(compressed.read()) return base64.b64encode(compressed.read())
def execute(self, context, instance, user_data, injected_files, def execute(self, context, instance, user_data, injected_files, key_pair,
configdrive): configdrive):
try: try:
configdrive['value'] = self._generate_configdrive( configdrive['value'] = self._generate_configdrive(
context, instance, user_data=user_data, files=injected_files) context, instance, user_data=user_data, files=injected_files,
key_pair=key_pair)
except Exception as e: except Exception as e:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
msg = ("Failed to build configdrive: %s" % msg = ("Failed to build configdrive: %s" %
@ -284,7 +287,8 @@ class CreateInstanceTask(flow_utils.MoganTask):
def get_flow(context, manager, instance, requested_networks, user_data, def get_flow(context, manager, instance, requested_networks, user_data,
injected_files, ports, request_spec, filter_properties): injected_files, key_pair, ports, request_spec,
filter_properties):
"""Constructs and returns the manager entrypoint flow """Constructs and returns the manager entrypoint flow
@ -310,6 +314,7 @@ def get_flow(context, manager, instance, requested_networks, user_data,
'requested_networks': requested_networks, 'requested_networks': requested_networks,
'user_data': user_data, 'user_data': user_data,
'injected_files': injected_files, 'injected_files': injected_files,
'key_pair': key_pair,
'ports': ports, 'ports': ports,
'configdrive': {} 'configdrive': {}
} }

View File

@ -342,7 +342,7 @@ class EngineManager(base_manager.BaseEngineManager):
@wrap_instance_fault @wrap_instance_fault
def create_instance(self, context, instance, requested_networks, def create_instance(self, context, instance, requested_networks,
user_data, injected_files, request_spec=None, user_data, injected_files, key_pair, request_spec=None,
filter_properties=None): filter_properties=None):
"""Perform a deployment.""" """Perform a deployment."""
LOG.debug("Starting instance...", instance=instance) LOG.debug("Starting instance...", instance=instance)
@ -393,6 +393,7 @@ class EngineManager(base_manager.BaseEngineManager):
requested_networks, requested_networks,
user_data, user_data,
injected_files, injected_files,
key_pair,
node['ports'], node['ports'],
request_spec, request_spec,
filter_properties, filter_properties,

View File

@ -50,7 +50,8 @@ class InvalidMetadataPath(Exception):
class InstanceMetadata(object): class InstanceMetadata(object):
"""Instance metadata.""" """Instance metadata."""
def __init__(self, instance, content=None, user_data=None, extra_md=None): def __init__(self, instance, content=None, user_data=None,
key_pair=None, extra_md=None):
"""Creation of this object should basically cover all time consuming """Creation of this object should basically cover all time consuming
collection. Methods after that should not cause time delays due to collection. Methods after that should not cause time delays due to
network operations or lengthy cpu operations. network operations or lengthy cpu operations.
@ -75,6 +76,7 @@ class InstanceMetadata(object):
self.uuid = instance.uuid self.uuid = instance.uuid
self.content = {} self.content = {}
self.files = [] self.files = []
self.keypair = key_pair
# 'content' is passed in from the configdrive code in # 'content' is passed in from the configdrive code in
# mogan/engine/flows/create_instance.py. That's how we get the # mogan/engine/flows/create_instance.py. That's how we get the
@ -111,6 +113,17 @@ class InstanceMetadata(object):
if self.extra_md: if self.extra_md:
metadata.update(self.extra_md) metadata.update(self.extra_md)
if self.keypair:
metadata['public_keys'] = {
self.keypair.name: self.keypair.public_key,
}
metadata['keys'] = [
{'name': self.keypair.name,
'type': self.keypair.type,
'data': self.keypair.public_key}
]
metadata['hostname'] = self.hostname metadata['hostname'] = self.hostname
metadata['name'] = self.instance.name metadata['name'] = self.instance.name
metadata['availability_zone'] = self.availability_zone metadata['availability_zone'] = self.availability_zone

View File

@ -50,7 +50,7 @@ class EngineAPI(object):
serializer=serializer) serializer=serializer)
def create_instance(self, context, instance, requested_networks, def create_instance(self, context, instance, requested_networks,
user_data, injected_files, request_spec, user_data, injected_files, key_pair, request_spec,
filter_properties): filter_properties):
"""Signal to engine service to perform a deployment.""" """Signal to engine service to perform a deployment."""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host) cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
@ -58,6 +58,7 @@ class EngineAPI(object):
requested_networks=requested_networks, requested_networks=requested_networks,
user_data=user_data, user_data=user_data,
injected_files=injected_files, injected_files=injected_files,
key_pair=key_pair,
request_spec=request_spec, request_spec=request_spec,
filter_properties=filter_properties) filter_properties=filter_properties)

View File

@ -50,7 +50,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
instance_type = self._create_instance_type() instance_type = self._create_instance_type()
mock_check_nets.return_value = 3 mock_check_nets.return_value = 3
base_opts, max_network_count = \ base_opts, max_network_count, key_pair = \
self.engine_api._validate_and_build_base_options( self.engine_api._validate_and_build_base_options(
self.context, self.context,
instance_type=instance_type, instance_type=instance_type,
@ -61,6 +61,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
extra={'k1', 'v1'}, extra={'k1', 'v1'},
requested_networks=None, requested_networks=None,
user_data=None, user_data=None,
key_name=None,
max_count=2) max_count=2)
self.assertEqual('fake-user', base_opts['user_id']) self.assertEqual('fake-user', base_opts['user_id'])
@ -69,6 +70,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
self.assertEqual(instance_type.uuid, base_opts['instance_type_uuid']) self.assertEqual(instance_type.uuid, base_opts['instance_type_uuid'])
self.assertEqual({'k1', 'v1'}, base_opts['extra']) self.assertEqual({'k1', 'v1'}, base_opts['extra'])
self.assertEqual('test_az', base_opts['availability_zone']) self.assertEqual('test_az', base_opts['availability_zone'])
self.assertEqual(None, key_pair)
@mock.patch.object(objects.Instance, 'create') @mock.patch.object(objects.Instance, 'create')
def test__provision_instances(self, mock_inst_create): def test__provision_instances(self, mock_inst_create):
@ -109,7 +111,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
'availability_zone': 'test_az'} 'availability_zone': 'test_az'}
min_count = 1 min_count = 1
max_count = 2 max_count = 2
mock_validate.return_value = (base_options, max_count) mock_validate.return_value = (base_options, max_count, None)
mock_get_image.side_effect = None mock_get_image.side_effect = None
mock_create.return_value = mock.MagicMock() mock_create.return_value = mock.MagicMock()
mock_list_az.return_value = {'availability_zones': ['test_az']} mock_list_az.return_value = {'availability_zones': ['test_az']}
@ -136,7 +138,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
mock_validate.assert_called_once_with( mock_validate.assert_called_once_with(
self.context, instance_type, 'fake-uuid', 'fake-name', self.context, instance_type, 'fake-uuid', 'fake-name',
'fake-descritpion', 'test_az', {'k1', 'v1'}, requested_networks, 'fake-descritpion', 'test_az', {'k1', 'v1'}, requested_networks,
None, max_count) None, None, max_count)
self.assertTrue(mock_create.called) self.assertTrue(mock_create.called)
self.assertTrue(mock_get_image.called) self.assertTrue(mock_get_image.called)
res = self.dbapi._get_quota_usages(self.context, self.project_id) res = self.dbapi._get_quota_usages(self.context, self.project_id)
@ -180,7 +182,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
'availability_zone': 'test_az'} 'availability_zone': 'test_az'}
min_count = 11 min_count = 11
max_count = 20 max_count = 20
mock_validate.return_value = (base_options, max_count) mock_validate.return_value = (base_options, max_count, None)
mock_get_image.side_effect = None mock_get_image.side_effect = None
mock_list_az.return_value = {'availability_zones': ['test_az']} mock_list_az.return_value = {'availability_zones': ['test_az']}
requested_networks = [{'uuid': 'fake'}] requested_networks = [{'uuid': 'fake'}]
@ -198,6 +200,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
requested_networks, requested_networks,
None, None,
None, None,
None,
min_count, min_count,
max_count) max_count)

View File

@ -109,6 +109,7 @@ class RPCAPITestCase(base.DbTestCase):
requested_networks=[], requested_networks=[],
user_data=None, user_data=None,
injected_files=None, injected_files=None,
key_pair=None,
request_spec=None, request_spec=None,
filter_properties=None) filter_properties=None)