diff --git a/doc/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json b/doc/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json new file mode 100644 index 0000000000..2970994fad --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json @@ -0,0 +1,14 @@ +{ + "keypair": { + "fingerprint": "44:fe:29:6e:23:14:b9:53:5b:65:82:58:1c:fe:5a:c3", + "name": "keypair-6638abdb-c4e8-407c-ba88-c8dd7cc3c4f1", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1HTrHCbb9NawNLSV8N6tSa8i637+EC2dA+lsdHHfQlT54t+N0nHhJPlKWDLhc579j87vp6RDFriFJ/smsTnDnf64O12z0kBaJpJPH2zXrBkZFK6q2rmxydURzX/z0yLSCP77SFJ0fdXWH2hMsAusflGyryHGX20n+mZK6mDrxVzGxEz228dwQ5G7Az5OoZDWygH2pqPvKjkifRw0jwUKf3BbkP0QvANACOk26cv16mNFpFJfI1N3OC5lUsZQtKGR01ptJoWijYKccqhkAKuo902tg/qup58J5kflNm7I61sy1mJon6SGqNUSfoQagqtBH6vd/tU1jnlwZ03uUroAL Generated-by-Nova\n", + "user_id": "fake", + "deleted": false, + "created_at": "2014-05-07T12:06:13.681238", + "updated_at": null, + "deleted_at": null, + "id": 1 + } +} diff --git a/doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json b/doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json new file mode 100644 index 0000000000..c511353376 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json @@ -0,0 +1,7 @@ +{ + "keypair": { + "name": "keypair-d20a3d59-9433-4b79-8726-20b431d89c78", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json b/doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json new file mode 100644 index 0000000000..ca1a70b334 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json @@ -0,0 +1,9 @@ +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "keypair-803a1926-af78-4b05-902a-1d6f7a8d9d3e", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova", + "user_id": "fake" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json b/doc/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json new file mode 100644 index 0000000000..3d5fe045a0 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json @@ -0,0 +1,12 @@ +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", + "name": "keypair-50ca852e-273f-4cdc-8949-45feba200837", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n" + } + } + ] +} \ No newline at end of file diff --git a/doc/v3/api_samples/keypairs/v2.2/keypairs-post-req.json b/doc/v3/api_samples/keypairs/v2.2/keypairs-post-req.json new file mode 100644 index 0000000000..ebd4ae54c1 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.2/keypairs-post-req.json @@ -0,0 +1,6 @@ +{ + "keypair": { + "name": "keypair-ab9ff2e6-a6d7-4915-a241-044c369c07f9", + "type": "ssh" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json b/doc/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json new file mode 100644 index 0000000000..a1634bd123 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json @@ -0,0 +1,10 @@ +{ + "keypair": { + "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", + "name": "keypair-50ca852e-273f-4cdc-8949-45feba200837", + "type": "ssh", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEApBdzF+fTq5QbN3R+QlO5TZr6W64GcUqcho5ZxPBZZIq53P1K\ndtpaY856ManqEwME1tN+JOw8+mmCK2RpkMHtk5BNPOMqr5Y+OQ5MqI/eX1v7GWnJ\ntHGTbi+vRDmxBh3aa3xiUGo66c9tjUKAg/ExQfFr/vKJvTR/S3urPlj3vfFgu+yi\n8PKoH0LGyHsviWsD1peDuu2XS+ca8qbkY3yD1o4Mv1R/OSF4P2fxjjWdp8R4EkoT\nJMKkhRAgAuS9zxwftPv9djP4opHWrRUlRo6bh75CzrN6Hu5uh5Tn5bkifOQcy1gW\n772vd6pBpi4OGQHPKz4djvmCLAVBzSyzDP6EKQIDAQABAoIBAQCB+tU/ZXKlIe+h\nMNTmoz1QfOe+AY625Rwx9cakGqMk4kKyC62VkgcxshfXCToSjzyhEuyEQOFYloT2\n7FY2xXb0gcS861Efv0pQlcQhbbz/GnQ/wC13ktPu3zTdPTm9l54xsFiMTGmYVaf4\n0mnMmhyjmKIsVGDJEDGZUD/oZj7wJGOFha5M4FZrZlJIrEZC0rGGlcC0kGF2no6B\nj1Mu7HjyK3pTKf4dlp+jeRikUF5Pct+qT+rcv2rZ3fl3inxtlLEwZeFPbp/njf/U\nIGxFzZsuLmiFlsJar6M5nEckTB3p25maWWaR8/0jvJRgsPnuoUrUoGDq87DMKCdk\nlw6by9fRAoGBANhnS9ko7Of+ntqIFR7xOG9p/oPATztgHkFxe4GbQ0leaDRTx3vE\ndQmUCnn24xtyVECaI9a4IV+LP1npw8niWUJ4pjgdAlkF4cCTu9sN+cBO15SfdACI\nzD1DaaHmpFCAWlpTo68VWlvWll6i2ncCkRJR1+q/C/yQz7asvl4AakElAoGBAMId\nxqMT2Sy9xLuHsrAoMUvBOkwaMYZH+IAb4DvUDjVIiKWjmonrmopS5Lpb+ALBKqZe\neVfD6HwWQqGwCFItToaEkZvrNfTapoNCHWWg001D49765UV5lMrArDbM1vXtFfM4\nDRYM6+Y6o/6QH8EBgXtyBxcYthIDBM3wBJa67xG1AoGAKTm8fFlMkIG0N4N3Kpbf\nnnH915GaRoBwIx2AXtd6QQ7oIRfYx95MQY/fUw7SgxcLr+btbulTCkWXwwRClUI2\nqPAdElGMcfMp56r9PaTy8EzUyu55heSJrB4ckIhEw0VAcTa/1wnlVduSd+LkZYmq\no2fOD11n5iycNXvBJF1F4LUCgYAMaRbwCi7SW3eefbiA5rDwJPRzNSGBckyC9EVL\nzezynyaNYH5a3wNMYKxa9dJPasYtSND9OXs9o7ay26xMhLUGiKc+jrUuaGRI9Asp\nGjUoNXT2JphN7s4CgHsCLep4YqYKnMTJah4S5CDj/5boIg6DM/EcGupZEHRYLkY8\n1MrAGQKBgQCi9yeC39ctLUNn+Ix604gttWWChdt3ozufTZ7HybJOSRA9Gh3iD5gm\nzlz0xqpGShKpOY2k+ftvja0poMdGeJLt84P3r2q01IgI7w0LmOj5m0W10dHysH27\nBWpCnHdBJMxnBsMRPoM4MKkmKWD9l5PSTCTWtkIpsyuDCko6D9UwZA==\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n", + "user_id": "fake" + } +} \ No newline at end of file diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 41e2ba1282..d75970bc8e 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -38,6 +38,8 @@ from nova import exception REST_API_VERSION_HISTORY = """REST API Version History: * 2.1 - Initial version. Equivalent to v2.0 code + * 2.2 - Adds (keypair) type parameter for os-keypairs plugin + Fixes success status code for create/delete a keypair method """ # The minimum and maximum versions of the API supported @@ -46,7 +48,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.1" +_MAX_API_VERSION = "2.2" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/contrib/keypairs.py b/nova/api/openstack/compute/contrib/keypairs.py index cbecdf674d..5b89ba4ba3 100644 --- a/nova/api/openstack/compute/contrib/keypairs.py +++ b/nova/api/openstack/compute/contrib/keypairs.py @@ -107,11 +107,8 @@ class KeypairController(object): authorize(context, action='show') try: - # Since this method returns the whole object, functional test - # test_keypairs_get is failing, receiving an unexpected field - # 'type', which was added to the keypair object. - # TODO(claudiub): Revert the changes in the next commit, which will - # enable nova-api to return the keypair type. + # The return object needs to be a dict in order to pop the 'type' + # field, since it is incompatible with API version <= 2.1. keypair = self.api.get_key_pair(context, context.user_id, id) keypair = self._filter_keypair(keypair, created_at=True, deleted=True, deleted_at=True, diff --git a/nova/api/openstack/compute/plugins/v3/keypairs.py b/nova/api/openstack/compute/plugins/v3/keypairs.py index 9f96358310..a0511aa59d 100644 --- a/nova/api/openstack/compute/plugins/v3/keypairs.py +++ b/nova/api/openstack/compute/plugins/v3/keypairs.py @@ -25,6 +25,7 @@ from nova.api import validation from nova.compute import api as compute_api from nova import exception from nova.i18n import _ +from nova.objects import keypair as keypair_obj ALIAS = 'os-keypairs' @@ -39,6 +40,8 @@ class KeypairController(wsgi.Controller): self.api = compute_api.KeypairAPI() def _filter_keypair(self, keypair, **attrs): + # TODO(claudiub): After v2 and v2.1 is no longer supported, + # keypair_type can be added to the clean dict below clean = { 'name': keypair.name, 'public_key': keypair.public_key, @@ -48,9 +51,28 @@ class KeypairController(wsgi.Controller): clean[attr] = keypair[attr] return clean - # TODO(oomichi): Here should be 201(Created) instead of 200 by v2.1 - # +microversions because the keypair creation finishes when returning - # a response. + @wsgi.Controller.api_version("2.2") + @wsgi.response(201) + @extensions.expected_errors((400, 403, 409)) + @validation.schema(keypairs.create_v22) + def create(self, req, body): + """Create or import keypair. + + Sending name will generate a key and return private_key + and fingerprint. + + Keypair will have the type ssh or x509, specified by key_type. + + You can send a public_key to add an existing ssh/x509 key. + + params: keypair object with: + name (required) - string + public_key (optional) - string + key_type (optional) - string + """ + return self._create(req, body, type=True) + + @wsgi.Controller.api_version("2.1", "2.1") # noqa @extensions.expected_errors((400, 403, 409)) @validation.schema(keypairs.create) def create(self, req, body): @@ -59,29 +81,34 @@ class KeypairController(wsgi.Controller): Sending name will generate a key and return private_key and fingerprint. - You can send a public_key to add an existing ssh key + You can send a public_key to add an existing ssh key. params: keypair object with: name (required) - string public_key (optional) - string """ + return self._create(req, body) + def _create(self, req, body, **keypair_filters): context = req.environ['nova.context'] authorize(context, action='create') params = body['keypair'] name = params['name'] + key_type = params.get('key_type', keypair_obj.KEYPAIR_TYPE_SSH) try: if 'public_key' in params: keypair = self.api.import_key_pair(context, context.user_id, name, - params['public_key']) - keypair = self._filter_keypair(keypair, user_id=True) + params['public_key'], key_type) + keypair = self._filter_keypair(keypair, user_id=True, + **keypair_filters) else: keypair, private_key = self.api.create_key_pair( - context, context.user_id, name) - keypair = self._filter_keypair(keypair, user_id=True) + context, context.user_id, name, key_type) + keypair = self._filter_keypair(keypair, user_id=True, + **keypair_filters) keypair['private_key'] = private_key return {'keypair': keypair} @@ -94,12 +121,19 @@ class KeypairController(wsgi.Controller): except exception.KeyPairExists as exc: raise webob.exc.HTTPConflict(explanation=exc.format_message()) - # TODO(oomichi): Here should be 204(No Content) instead of 202 by v2.1 - # +microversions because the resource keypair has been deleted completely - # when returning a response. + @wsgi.Controller.api_version("2.1", "2.1") @wsgi.response(202) @extensions.expected_errors(404) def delete(self, req, id): + self._delete(req, id) + + @wsgi.Controller.api_version("2.2") # noqa + @wsgi.response(204) + @extensions.expected_errors(404) + def delete(self, req, id): + self._delete(req, id) + + def _delete(self, req, id): """Delete a keypair with a given name.""" context = req.environ['nova.context'] authorize(context, action='delete') @@ -108,23 +142,29 @@ class KeypairController(wsgi.Controller): except exception.KeypairNotFound as exc: raise webob.exc.HTTPNotFound(explanation=exc.format_message()) + @wsgi.Controller.api_version("2.2") @extensions.expected_errors(404) def show(self, req, id): + return self._show(req, id, type=True) + + @wsgi.Controller.api_version("2.1", "2.1") # noqa + @extensions.expected_errors(404) + def show(self, req, id): + return self._show(req, id) + + def _show(self, req, id, **keypair_filters): """Return data for the given key name.""" context = req.environ['nova.context'] authorize(context, action='show') try: - # Since this method returns the whole object, functional test - # test_keypairs_get is failing, receiving an unexpected field - # 'type', which was added to the keypair object. - # TODO(claudiub): Revert the changes in the next commit, which will - # enable nova-api to return the keypair type. + # The return object needs to be a dict in order to pop the 'type' + # field, if the api_version < 2.2. keypair = self.api.get_key_pair(context, context.user_id, id) keypair = self._filter_keypair(keypair, created_at=True, deleted=True, deleted_at=True, id=True, user_id=True, - updated_at=True) + updated_at=True, **keypair_filters) except exception.KeypairNotFound as exc: raise webob.exc.HTTPNotFound(explanation=exc.format_message()) # TODO(oomichi): It is necessary to filter a response of keypair with @@ -132,15 +172,25 @@ class KeypairController(wsgi.Controller): # behaviors in this keypair resource. return {'keypair': keypair} + @wsgi.Controller.api_version("2.2") @extensions.expected_errors(()) def index(self, req): + return self._index(req, type=True) + + @wsgi.Controller.api_version("2.1", "2.1") # noqa + @extensions.expected_errors(()) + def index(self, req): + return self._index(req) + + def _index(self, req, **keypair_filters): """List of keypairs for a user.""" context = req.environ['nova.context'] authorize(context, action='index') key_pairs = self.api.get_key_pairs(context, context.user_id) rval = [] for key_pair in key_pairs: - rval.append({'keypair': self._filter_keypair(key_pair)}) + rval.append({'keypair': self._filter_keypair(key_pair, + **keypair_filters)}) return {'keypairs': rval} diff --git a/nova/api/openstack/compute/schemas/v3/keypairs.py b/nova/api/openstack/compute/schemas/v3/keypairs.py index 8d4c9f2d23..e9dfd93705 100644 --- a/nova/api/openstack/compute/schemas/v3/keypairs.py +++ b/nova/api/openstack/compute/schemas/v3/keypairs.py @@ -32,6 +32,24 @@ create = { 'additionalProperties': False, } +create_v22 = { + 'type': 'object', + 'properties': { + 'keypair': { + 'type': 'object', + 'properties': { + 'name': parameter_types.name, + 'type': {'type': 'string'}, + 'public_key': {'type': 'string'}, + }, + 'required': ['name'], + 'additionalProperties': False, + }, + }, + 'required': ['keypair'], + 'additionalProperties': False, +} + server_create = { 'key_name': parameter_types.name, } diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 2a79956b29..4b2f567c88 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -20,3 +20,18 @@ user documentation. If no version is specified then the API will behave as if a version request of v2.1 was requested. + +- **2.2** + + Added Keypair type. + + A user can request the creation of a certain 'type' of keypair (ssh or x509) + in the os-keypairs plugin + + If no keypair type is specified, then the default 'ssh' type of keypair is + created. + + Fixes status code for os-keypairs create method from 200 to 201 + + Fixes status code for os-keypairs delete method from 202 to 204 + diff --git a/nova/compute/api.py b/nova/compute/api.py index 3059f92aeb..7fbe8a113a 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -3730,7 +3730,11 @@ class KeypairAPI(base.Base): notify = self.get_notifier() notify.info(context, 'keypair.%s' % event_suffix, payload) - def _validate_new_key_pair(self, context, user_id, key_name): + def _validate_new_key_pair(self, context, user_id, key_name, key_type): + if key_type is not keypair_obj.KEYPAIR_TYPE_SSH: + raise exception.InvalidKeypair( + reason=_('Specified Keypair type "%s" is invalid') % key_type) + safe_chars = "_- " + string.digits + string.ascii_letters clean_value = "".join(x for x in key_name if x in safe_chars) if clean_value != key_name: @@ -3752,9 +3756,10 @@ class KeypairAPI(base.Base): raise exception.KeypairLimitExceeded() @wrap_exception() - def import_key_pair(self, context, user_id, key_name, public_key): + def import_key_pair(self, context, user_id, key_name, public_key, + key_type=keypair_obj.KEYPAIR_TYPE_SSH): """Import a key pair using an existing public key.""" - self._validate_new_key_pair(context, user_id, key_name) + self._validate_new_key_pair(context, user_id, key_name, key_type) self._notify(context, 'import.start', key_name) @@ -3763,7 +3768,7 @@ class KeypairAPI(base.Base): keypair = objects.KeyPair(context) keypair.user_id = user_id keypair.name = key_name - keypair.type = keypair_obj.KEYPAIR_TYPE_SSH + keypair.type = key_type keypair.fingerprint = fingerprint keypair.public_key = public_key keypair.create() @@ -3773,9 +3778,10 @@ class KeypairAPI(base.Base): return keypair @wrap_exception() - def create_key_pair(self, context, user_id, key_name): + def create_key_pair(self, context, user_id, key_name, + key_type=keypair_obj.KEYPAIR_TYPE_SSH): """Create a new key pair.""" - self._validate_new_key_pair(context, user_id, key_name) + self._validate_new_key_pair(context, user_id, key_name, key_type) self._notify(context, 'create.start', key_name) @@ -3784,7 +3790,7 @@ class KeypairAPI(base.Base): keypair = objects.KeyPair(context) keypair.user_id = user_id keypair.name = key_name - keypair.type = keypair_obj.KEYPAIR_TYPE_SSH + keypair.type = key_type keypair.fingerprint = fingerprint keypair.public_key = public_key keypair.create() diff --git a/nova/tests/functional/api_samples_test_base.py b/nova/tests/functional/api_samples_test_base.py index 9a414ba050..e71df95e7c 100644 --- a/nova/tests/functional/api_samples_test_base.py +++ b/nova/tests/functional/api_samples_test_base.py @@ -270,6 +270,7 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase): 'public_key': 'ssh-rsa[ a-zA-Z0-9/+=]*' 'Generated-by-Nova', 'fingerprint': '([0-9a-f]{2}:){15}[0-9a-f]{2}', + 'keypair_type': 'ssh|x509', 'host': self._get_host(), 'host_name': '[0-9a-z]{32}', 'glance_host': self._get_glance_host(), diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json.tpl new file mode 100644 index 0000000000..e2e8dee070 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json.tpl @@ -0,0 +1,14 @@ +{ + "keypair": { + "public_key": "%(public_key)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "fingerprint": "%(fingerprint)s", + "user_id": "fake", + "deleted": false, + "created_at": "%(strtime)s", + "updated_at": null, + "deleted_at": null, + "id": 1 + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json.tpl new file mode 100644 index 0000000000..fc93c93603 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json.tpl @@ -0,0 +1,7 @@ +{ + "keypair": { + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s" + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json.tpl new file mode 100644 index 0000000000..01b22b0e40 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "keypair": { + "fingerprint": "%(fingerprint)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json.tpl new file mode 100644 index 0000000000..8e0963bc7a --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json.tpl @@ -0,0 +1,12 @@ +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "%(fingerprint)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s" + } + } + ] +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-req.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-req.json.tpl new file mode 100644 index 0000000000..03ac7dcd7e --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-req.json.tpl @@ -0,0 +1,6 @@ +{ + "keypair": { + "name": "%(keypair_name)s", + "type": "%(keypair_type)s" + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json.tpl new file mode 100644 index 0000000000..2645fa9aa0 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json.tpl @@ -0,0 +1,10 @@ +{ + "keypair": { + "fingerprint": "%(fingerprint)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "private_key": "%(private_key)s", + "public_key": "%(public_key)s", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/v3/test_keypairs.py b/nova/tests/functional/v3/test_keypairs.py index 3d0f5ec0eb..f42d06319b 100644 --- a/nova/tests/functional/v3/test_keypairs.py +++ b/nova/tests/functional/v3/test_keypairs.py @@ -15,30 +15,44 @@ import uuid +from nova.objects import keypair as keypair_obj from nova.tests.functional.v3 import api_sample_base class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3): + request_api_version = None sample_dir = "keypairs" + expected_delete_status_code = 202 + expected_post_status_code = 200 def generalize_subs(self, subs, vanilla_regexes): subs['keypair_name'] = 'keypair-[0-9a-f-]+' return subs def test_keypairs_post(self, public_key=None): + return self._check_keypairs_post(public_key, + api_version=self.request_api_version) + + def _check_keypairs_post(self, public_key, **kwargs): """Get api sample of key pairs post request.""" key_name = 'keypair-' + str(uuid.uuid4()) - response = self._do_post('os-keypairs', 'keypairs-post-req', - {'keypair_name': key_name}) + subs = dict(keypair_name=key_name, **kwargs) + response = self._do_post('os-keypairs', 'keypairs-post-req', subs, + api_version=self.request_api_version) + subs = self._get_regexes() subs['keypair_name'] = '(%s)' % key_name - self._verify_response('keypairs-post-resp', subs, response, 200) + self._verify_response('keypairs-post-resp', subs, response, + self.expected_post_status_code) # NOTE(maurosr): return the key_name is necessary cause the # verification returns the label of the last compared information in # the response, not necessarily the key name. return key_name def test_keypairs_import_key_post(self): + self._check_keypairs_import_key_post() + + def _check_keypairs_import_key_post(self, **kwargs): # Get api sample of key pairs post to import user's key. key_name = 'keypair-' + str(uuid.uuid4()) subs = { @@ -49,16 +63,19 @@ class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3): "9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc" "pSxsIbECHw== Generated-by-Nova" } + subs.update(**kwargs) response = self._do_post('os-keypairs', 'keypairs-import-post-req', - subs) + subs, api_version=self.request_api_version) subs = self._get_regexes() subs['keypair_name'] = '(%s)' % key_name - self._verify_response('keypairs-import-post-resp', subs, response, 200) + self._verify_response('keypairs-import-post-resp', subs, response, + self.expected_post_status_code) def test_keypairs_list(self): # Get api sample of key pairs list request. key_name = self.test_keypairs_post() - response = self._do_get('os-keypairs') + response = self._do_get('os-keypairs', + api_version=self.request_api_version) subs = self._get_regexes() subs['keypair_name'] = '(%s)' % key_name self._verify_response('keypairs-list-resp', subs, response, 200) @@ -66,7 +83,30 @@ class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3): def test_keypairs_get(self): # Get api sample of key pairs get request. key_name = self.test_keypairs_post() - response = self._do_get('os-keypairs/%s' % key_name) + response = self._do_get('os-keypairs/%s' % key_name, + api_version=self.request_api_version) subs = self._get_regexes() subs['keypair_name'] = '(%s)' % key_name self._verify_response('keypairs-get-resp', subs, response, 200) + + def test_keypairs_delete(self): + # Get api sample of key pairs delete request. + key_name = self.test_keypairs_post() + response = self._do_delete('os-keypairs/%s' % key_name, + api_version=self.request_api_version) + self.assertEqual(self.expected_delete_status_code, + response.status_code) + + +class KeyPairsV22SampleJsonTest(KeyPairsSampleJsonTest): + request_api_version = '2.2' + expected_post_status_code = 201 + expected_delete_status_code = 204 + + def test_keypairs_post(self, public_key=None): + return self._check_keypairs_post( + public_key, keypair_type=keypair_obj.KEYPAIR_TYPE_SSH) + + def test_keypairs_import_key_post(self): + self._check_keypairs_import_key_post( + keypair_type=keypair_obj.KEYPAIR_TYPE_SSH) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py index e0bd55e072..d96567e06f 100644 --- a/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py +++ b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py @@ -18,6 +18,7 @@ import webob from nova.api.openstack.compute.contrib import keypairs as keypairs_v2 from nova.api.openstack.compute.plugins.v3 import keypairs as keypairs_v21 +from nova.api.openstack import wsgi as os_wsgi from nova import db from nova import exception from nova.openstack.common import policy as common_policy @@ -62,6 +63,7 @@ def db_key_pair_create_duplicate(context, keypair): class KeypairsTestV21(test.TestCase): base_url = '/v2/fake' validation_error = exception.ValidationError + wsgi_api_version = os_wsgi.DEFAULT_API_VERSION def _setup_app_and_controller(self): self.app_server = fakes.wsgi_app_v21(init_only=('os-keypairs', @@ -85,7 +87,7 @@ class KeypairsTestV21(test.TestCase): osapi_compute_ext_list=['Keypairs']) self._setup_app_and_controller() - self.req = fakes.HTTPRequest.blank('') + self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) def test_keypair_list(self): res_dict = self.controller.index(self.req) @@ -97,6 +99,7 @@ class KeypairsTestV21(test.TestCase): res_dict = self.controller.create(self.req, body=body) self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) self.assertTrue(len(res_dict['keypair']['private_key']) > 0) + self._assert_keypair_type(res_dict) def _test_keypair_create_bad_request_case(self, body, @@ -161,6 +164,7 @@ class KeypairsTestV21(test.TestCase): # FIXME(ja): sholud we check that public_key was sent to create? self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) self.assertNotIn('private_key', res_dict['keypair']) + self._assert_keypair_type(res_dict) def test_keypair_import_quota_limit(self): @@ -234,7 +238,8 @@ class KeypairsTestV21(test.TestCase): def _db_key_pair_get(context, user_id, name): return dict(test_keypair.fake_keypair, - name='foo', public_key='XXX', fingerprint='YYY') + name='foo', public_key='XXX', fingerprint='YYY', + type='ssh') self.stubs.Set(db, "key_pair_get", _db_key_pair_get) @@ -242,6 +247,7 @@ class KeypairsTestV21(test.TestCase): self.assertEqual('foo', res_dict['keypair']['name']) self.assertEqual('XXX', res_dict['keypair']['public_key']) self.assertEqual('YYY', res_dict['keypair']['fingerprint']) + self._assert_keypair_type(res_dict) def test_keypair_show_not_found(self): @@ -283,6 +289,9 @@ class KeypairsTestV21(test.TestCase): self.assertIn('key_name', server_dict) self.assertEqual(server_dict['key_name'], '') + def _assert_keypair_type(self, res_dict): + self.assertNotIn('type', res_dict['keypair']) + class KeypairPolicyTestV21(test.TestCase): KeyPairController = keypairs_v21.KeypairController() @@ -293,7 +302,8 @@ class KeypairPolicyTestV21(test.TestCase): def _db_key_pair_get(context, user_id, name): return dict(test_keypair.fake_keypair, - name='foo', public_key='XXX', fingerprint='YYY') + name='foo', public_key='XXX', fingerprint='YYY', + type='ssh') self.stubs.Set(db, "key_pair_get", _db_key_pair_get) @@ -365,15 +375,7 @@ class KeypairPolicyTestV21(test.TestCase): rules = {self.policy_path + ':delete': common_policy.parse_rule('')} policy.set_rules(rules) - res = self.KeyPairController.delete(self.req, 'FAKE') - - # NOTE: on v2.1, http status code is set as wsgi_code of API - # method instead of status_int in a response object. - if isinstance(self.KeyPairController, keypairs_v21.KeypairController): - status_int = self.KeyPairController.delete.wsgi_code - else: - status_int = res.status_int - self.assertEqual(202, status_int) + self.KeyPairController.delete(self.req, 'FAKE') class KeypairsTestV2(KeypairsTestV21): @@ -384,6 +386,19 @@ class KeypairsTestV2(KeypairsTestV21): self.controller = keypairs_v2.KeypairController() +class KeypairsTestV22(KeypairsTestV21): + wsgi_api_version = '2.2' + + def test_keypair_list(self): + res_dict = self.controller.index(self.req) + expected = {'keypairs': [{'keypair': dict(keypair_data, name='FAKE', + type='ssh')}]} + self.assertEqual(expected, res_dict) + + def _assert_keypair_type(self, res_dict): + self.assertEqual('ssh', res_dict['keypair']['type']) + + class KeypairPolicyTestV2(KeypairPolicyTestV21): KeyPairController = keypairs_v2.KeypairController() policy_path = 'compute_extension:keypairs' diff --git a/nova/tests/unit/compute/test_keypairs.py b/nova/tests/unit/compute/test_keypairs.py index 30c3c8c35c..c40f8bcc93 100644 --- a/nova/tests/unit/compute/test_keypairs.py +++ b/nova/tests/unit/compute/test_keypairs.py @@ -21,6 +21,7 @@ from nova.compute import api as compute_api from nova import context from nova import db from nova import exception +from nova.objects import keypair as keypair_obj from nova import quota from nova.tests.unit.compute import test_compute from nova.tests.unit import fake_notifier @@ -46,6 +47,7 @@ class KeypairAPITestCase(test_compute.BaseTestCase): 'HJAXVI+oCiyMMfffoTq16M1xfV58JstgtTqAXG+ZFpicGajREU' 'E/E3hO5MGgcHmyzIrWHKpe1n3oEGuz') self.fingerprint = '4e:48:c6:a0:4a:f9:dd:b5:4c:85:54:5a:af:43:47:5a' + self.keypair_type = keypair_obj.KEYPAIR_TYPE_SSH self.key_destroyed = False def _keypair_db_call_stubs(self): @@ -108,12 +110,13 @@ class CreateImportSharedTestMixIn(object): up by the test runner unless they are part of a 'concrete' test case. """ - def assertKeyNameRaises(self, exc_class, expected_message, name): + def assertKeypairRaises(self, exc_class, expected_message, name): func = getattr(self.keypair_api, self.func_name) args = [] if self.func_name == 'import_key_pair': args.append(self.pub_key) + args.append(self.keypair_type) exc = self.assertRaises(exc_class, func, self.ctxt, self.ctxt.user_id, name, *args) @@ -121,7 +124,7 @@ class CreateImportSharedTestMixIn(object): def assertInvalidKeypair(self, expected_message, name): msg = 'Keypair data is invalid: %s' % expected_message - self.assertKeyNameRaises(exception.InvalidKeypair, msg, name) + self.assertKeypairRaises(exception.InvalidKeypair, msg, name) def test_name_too_short(self): msg = ('Keypair name must be string and between 1 ' @@ -137,6 +140,11 @@ class CreateImportSharedTestMixIn(object): msg = "Keypair name contains unsafe characters" self.assertInvalidKeypair(msg, '* BAD CHARACTERS! *') + def test_invalid_keypair_type(self): + self.keypair_type = 'fakey_type' + msg = 'Specified Keypair type "fakey_type" is invalid' + self.assertInvalidKeypair(msg, 'test') + def test_already_exists(self): def db_key_pair_create_duplicate(context, keypair): raise exception.KeyPairExists(key_name=keypair.get('name', '')) @@ -145,7 +153,7 @@ class CreateImportSharedTestMixIn(object): msg = ("Key pair '%(key_name)s' already exists." % {'key_name': self.existing_key_name}) - self.assertKeyNameRaises(exception.KeyPairExists, msg, + self.assertKeypairRaises(exception.KeyPairExists, msg, self.existing_key_name) def test_quota_limit(self): @@ -155,7 +163,7 @@ class CreateImportSharedTestMixIn(object): self.stubs.Set(QUOTAS, "count", fake_quotas_count) msg = "Maximum number of key pairs exceeded" - self.assertKeyNameRaises(exception.KeypairLimitExceeded, msg, 'foo') + self.assertKeypairRaises(exception.KeypairLimitExceeded, msg, 'foo') class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn): @@ -165,6 +173,7 @@ class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn): keypair, private_key = self.keypair_api.create_key_pair( self.ctxt, self.ctxt.user_id, 'foo') self.assertEqual('foo', keypair['name']) + self.assertEqual(self.keypair_type, keypair['type']) self._check_notifications() @@ -180,6 +189,7 @@ class ImportKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn): self.assertEqual('foo', keypair['name']) self.assertEqual(self.fingerprint, keypair['fingerprint']) self.assertEqual(self.pub_key, keypair['public_key']) + self.assertEqual(self.keypair_type, keypair['type']) self._check_notifications(action='import') def test_bad_key_data(self):