diff --git a/api-ref/source/v1/instances.inc b/api-ref/source/v1/instances.inc index 5f7f7811..c6b9228a 100644 --- a/api-ref/source/v1/instances.inc +++ b/api-ref/source/v1/instances.inc @@ -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** diff --git a/api-ref/source/v1/parameters.yaml b/api-ref/source/v1/parameters.yaml index 3bf00f53..72f254dc 100644 --- a/api-ref/source/v1/parameters.yaml +++ b/api-ref/source/v1/parameters.yaml @@ -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 diff --git a/api-ref/source/v1/samples/instances/instance-create-req.json b/api-ref/source/v1/samples/instances/instance-create-req.json index b664a8a3..79e2057f 100644 --- a/api-ref/source/v1/samples/instances/instance-create-req.json +++ b/api-ref/source/v1/samples/instances/instance-create-req.json @@ -22,5 +22,6 @@ "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" } ], - "user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==" + "user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "key_name": "test_key" } diff --git a/mogan/api/controllers/v1/instances.py b/mogan/api/controllers/v1/instances.py index b303875c..cd5e0786 100644 --- a/mogan/api/controllers/v1/instances.py +++ b/mogan/api/controllers/v1/instances.py @@ -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) diff --git a/mogan/api/controllers/v1/schemas/instances.py b/mogan/api/controllers/v1/schemas/instances.py index 6e4f2b68..c7d6f067 100644 --- a/mogan/api/controllers/v1/schemas/instances.py +++ b/mogan/api/controllers/v1/schemas/instances.py @@ -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, diff --git a/mogan/engine/api.py b/mogan/engine/api.py index decfa8c8..738d49af 100644 --- a/mogan/engine/api.py +++ b/mogan/engine/api.py @@ -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): diff --git a/mogan/engine/flows/create_instance.py b/mogan/engine/flows/create_instance.py index 6865d881..c3bf392e 100644 --- a/mogan/engine/flows/create_instance.py +++ b/mogan/engine/flows/create_instance.py @@ -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': {} } diff --git a/mogan/engine/manager.py b/mogan/engine/manager.py index 27240acf..3547f8c4 100644 --- a/mogan/engine/manager.py +++ b/mogan/engine/manager.py @@ -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, diff --git a/mogan/engine/metadata.py b/mogan/engine/metadata.py index ebbd8caa..7e1999d0 100644 --- a/mogan/engine/metadata.py +++ b/mogan/engine/metadata.py @@ -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 diff --git a/mogan/engine/rpcapi.py b/mogan/engine/rpcapi.py index f276b66b..5b98f684 100644 --- a/mogan/engine/rpcapi.py +++ b/mogan/engine/rpcapi.py @@ -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) diff --git a/mogan/tests/unit/engine/test_engine_api.py b/mogan/tests/unit/engine/test_engine_api.py index 68cbf820..97726efd 100644 --- a/mogan/tests/unit/engine/test_engine_api.py +++ b/mogan/tests/unit/engine/test_engine_api.py @@ -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) diff --git a/mogan/tests/unit/engine/test_rpcapi.py b/mogan/tests/unit/engine/test_rpcapi.py index d3478838..546ea7ad 100644 --- a/mogan/tests/unit/engine/test_rpcapi.py +++ b/mogan/tests/unit/engine/test_rpcapi.py @@ -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)