From c836f425bcc8092bb34b4d8be43638ea58e169ca Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Thu, 12 Feb 2015 20:20:02 +1030 Subject: [PATCH] Adds keypair type in nova-api X509 certificates are used by Windows for passwordless authentication (WinRM) in a way which can be considered consistent with the usage of SSH keys on Linux, as both are based on public / private keypairs. Enables nova-api to return the keypair type, updates nova-api version to reflect the changes and updates the unit and functional tests to validate the API changes. Unit tests have been updated to ensure that the keypair type is not being returned on previous API versions. Note: x509 keypair implementation is added in the next commit. DocImpact - See nova/api/openstack/rest_api_version_history.rst for details APIImpact Depends-On: Id5b210d7afe5c0a590abcbd42b9ff85b071a5c55 Co-Authored-By: Chris Yeoh Partially implements: blueprint keypair-x509-certificates Change-Id: I215662f2f92a01921a866c3218031787a9eaf915 --- .../keypairs/v2.2/keypairs-get-resp.json | 14 +++ .../v2.2/keypairs-import-post-req.json | 7 ++ .../v2.2/keypairs-import-post-resp.json | 9 ++ .../keypairs/v2.2/keypairs-list-resp.json | 12 +++ .../keypairs/v2.2/keypairs-post-req.json | 6 ++ .../keypairs/v2.2/keypairs-post-resp.json | 10 +++ nova/api/openstack/api_version_request.py | 4 +- .../api/openstack/compute/contrib/keypairs.py | 7 +- .../openstack/compute/plugins/v3/keypairs.py | 86 +++++++++++++++---- .../openstack/compute/schemas/v3/keypairs.py | 18 ++++ .../openstack/rest_api_version_history.rst | 15 ++++ nova/compute/api.py | 20 +++-- .../tests/functional/api_samples_test_base.py | 1 + .../keypairs/v2.2/keypairs-get-resp.json.tpl | 14 +++ .../v2.2/keypairs-import-post-req.json.tpl | 7 ++ .../v2.2/keypairs-import-post-resp.json.tpl | 9 ++ .../keypairs/v2.2/keypairs-list-resp.json.tpl | 12 +++ .../keypairs/v2.2/keypairs-post-req.json.tpl | 6 ++ .../keypairs/v2.2/keypairs-post-resp.json.tpl | 10 +++ nova/tests/functional/v3/test_keypairs.py | 54 ++++++++++-- .../compute/contrib/test_keypairs.py | 39 ++++++--- nova/tests/unit/compute/test_keypairs.py | 18 +++- 22 files changed, 324 insertions(+), 54 deletions(-) create mode 100644 doc/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json create mode 100644 doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json create mode 100644 doc/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json create mode 100644 doc/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json create mode 100644 doc/v3/api_samples/keypairs/v2.2/keypairs-post-req.json create mode 100644 doc/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-req.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-import-post-resp.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-req.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json.tpl 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 000000000000..2970994fadf2 --- /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 000000000000..c51135337607 --- /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 000000000000..ca1a70b334c1 --- /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 000000000000..3d5fe045a023 --- /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 000000000000..ebd4ae54c1e5 --- /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 000000000000..a1634bd12305 --- /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 41e2ba12824d..d75970bc8eb2 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 cbecdf674de3..5b89ba4ba3f2 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 9f9635831076..a0511aa59d34 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 8d4c9f2d23bb..e9dfd9370534 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 2a79956b29c1..4b2f567c8818 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 3059f92aebe8..7fbe8a113afe 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 9a414ba05077..e71df95e7c72 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 000000000000..e2e8dee070a6 --- /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 000000000000..fc93c9360379 --- /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 000000000000..01b22b0e400c --- /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 000000000000..8e0963bc7a39 --- /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 000000000000..03ac7dcd7e2a --- /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 000000000000..2645fa9aa0c2 --- /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 3d0f5ec0eb55..f42d06319bd1 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 e0bd55e072b5..d96567e06f8c 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 30c3c8c35c68..c40f8bcc9379 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):