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
- user_data: user_data
- personality: personality
- key_name: key_name
**Example Create Instance: JSON request**

View File

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

View File

@ -22,5 +22,6 @@
"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')
image_uuid = instance.get('image_uuid')
user_data = instance.get('user_data')
key_name = instance.get('key_name')
personality = instance.pop('personality', None)
injected_files = []
@ -605,6 +606,7 @@ class InstanceController(InstanceControllerBase):
requested_networks=requested_networks,
user_data=user_data,
injected_files=injected_files,
key_name=key_name,
min_count=min_count,
max_count=max_count)
except exception.InstanceTypeNotFound:
@ -616,6 +618,10 @@ class InstanceController(InstanceControllerBase):
msg = (_("Requested image %s could not be found") % image_uuid)
raise wsme.exc.ClientSideError(
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:
raise wsme.exc.ClientSideError(
e.message, status_code=http_client.FORBIDDEN)

View File

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

View File

@ -79,7 +79,7 @@ class API(object):
image_uuid, name, description,
availability_zone, extra,
requested_networks, user_data,
max_count):
key_name, max_count):
"""Verify all the input parameters"""
if user_data:
@ -100,6 +100,13 @@ class API(object):
requested_networks,
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 = {
'image_uuid': image_uuid,
'status': states.BUILDING,
@ -114,7 +121,7 @@ class API(object):
'availability_zone': availability_zone}
# 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):
"""Apply the template to instance name.
@ -242,16 +249,18 @@ class API(object):
def _create_instance(self, context, instance_type, image_uuid,
name, description, availability_zone, extra,
requested_networks, user_data, injected_files,
min_count, max_count):
key_name, min_count, max_count):
"""Verify all the input parameters"""
# Verify the specified image exists
if image_uuid:
self._get_image(context, image_uuid)
base_options, max_net_count = self._validate_and_build_base_options(
context, instance_type, image_uuid, name, description,
availability_zone, extra, requested_networks, user_data, max_count)
base_options, max_net_count, key_pair = \
self._validate_and_build_base_options(
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
# user adjusted for any network quota constraints, including
@ -289,6 +298,7 @@ class API(object):
requested_networks,
user_data,
decoded_files,
key_pair,
request_spec,
filter_properties=None)
@ -297,7 +307,8 @@ class API(object):
def create(self, context, instance_type, image_uuid,
name=None, description=None, availability_zone=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
Sending instance information to the engine and will handle
@ -316,7 +327,8 @@ class API(object):
image_uuid, name, description,
availability_zone, extra,
requested_networks, user_data,
injected_files, min_count, max_count)
injected_files, key_name,
min_count, max_count)
def _delete_instance(self, context, instance):

View File

@ -51,7 +51,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
def __init__(self, engine_rpcapi):
requires = ['filter_properties', 'request_spec', 'instance',
'requested_networks', 'user_data', 'injected_files',
'context']
'key_pair', 'context']
super(OnFailureRescheduleTask, self).__init__(addons=[ACTION],
requires=requires)
self.engine_rpcapi = engine_rpcapi
@ -69,7 +69,8 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
pass
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."""
create_instance = self.engine_rpcapi.create_instance
@ -95,6 +96,7 @@ class OnFailureRescheduleTask(flow_utils.MoganTask):
return create_instance(context, instance, requested_networks,
user_data=user_data,
injected_files=injected_files,
key_pair=key_pair,
request_spec=request_spec,
filter_properties=filter_properties)
@ -210,17 +212,17 @@ class GenerateConfigDriveTask(flow_utils.MoganTask):
"""Generate ConfigDrive value the instance."""
def __init__(self):
requires = ['instance', 'user_data', 'injected_files', 'configdrive',
'context']
requires = ['instance', 'user_data', 'injected_files', 'key_pair',
'configdrive', 'context']
super(GenerateConfigDriveTask, self).__init__(addons=[ACTION],
requires=requires)
def _generate_configdrive(self, context, instance, user_data=None,
files=None):
files=None, key_pair=None):
"""Generate a config drive."""
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 configdrive.ConfigDriveBuilder(instance_md=i_meta) as cdb:
@ -236,12 +238,13 @@ class GenerateConfigDriveTask(flow_utils.MoganTask):
compressed.seek(0)
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):
try:
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:
with excutils.save_and_reraise_exception():
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,
injected_files, ports, request_spec, filter_properties):
injected_files, key_pair, ports, request_spec,
filter_properties):
"""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,
'user_data': user_data,
'injected_files': injected_files,
'key_pair': key_pair,
'ports': ports,
'configdrive': {}
}

View File

@ -342,7 +342,7 @@ class EngineManager(base_manager.BaseEngineManager):
@wrap_instance_fault
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):
"""Perform a deployment."""
LOG.debug("Starting instance...", instance=instance)
@ -393,6 +393,7 @@ class EngineManager(base_manager.BaseEngineManager):
requested_networks,
user_data,
injected_files,
key_pair,
node['ports'],
request_spec,
filter_properties,

View File

@ -50,7 +50,8 @@ class InvalidMetadataPath(Exception):
class InstanceMetadata(object):
"""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
collection. Methods after that should not cause time delays due to
network operations or lengthy cpu operations.
@ -75,6 +76,7 @@ class InstanceMetadata(object):
self.uuid = instance.uuid
self.content = {}
self.files = []
self.keypair = key_pair
# 'content' is passed in from the configdrive code in
# mogan/engine/flows/create_instance.py. That's how we get the
@ -111,6 +113,17 @@ class InstanceMetadata(object):
if 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['name'] = self.instance.name
metadata['availability_zone'] = self.availability_zone

View File

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

View File

@ -50,7 +50,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
instance_type = self._create_instance_type()
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.context,
instance_type=instance_type,
@ -61,6 +61,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
extra={'k1', 'v1'},
requested_networks=None,
user_data=None,
key_name=None,
max_count=2)
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({'k1', 'v1'}, base_opts['extra'])
self.assertEqual('test_az', base_opts['availability_zone'])
self.assertEqual(None, key_pair)
@mock.patch.object(objects.Instance, 'create')
def test__provision_instances(self, mock_inst_create):
@ -109,7 +111,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
'availability_zone': 'test_az'}
min_count = 1
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_create.return_value = mock.MagicMock()
mock_list_az.return_value = {'availability_zones': ['test_az']}
@ -136,7 +138,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
mock_validate.assert_called_once_with(
self.context, instance_type, 'fake-uuid', 'fake-name',
'fake-descritpion', 'test_az', {'k1', 'v1'}, requested_networks,
None, max_count)
None, None, max_count)
self.assertTrue(mock_create.called)
self.assertTrue(mock_get_image.called)
res = self.dbapi._get_quota_usages(self.context, self.project_id)
@ -180,7 +182,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
'availability_zone': 'test_az'}
min_count = 11
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_list_az.return_value = {'availability_zones': ['test_az']}
requested_networks = [{'uuid': 'fake'}]
@ -198,6 +200,7 @@ class ComputeAPIUnitTest(base.DbTestCase):
requested_networks,
None,
None,
None,
min_count,
max_count)

View File

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