diff --git a/api-ref/source/os-keypairs.inc b/api-ref/source/os-keypairs.inc index e03e7d91aafc..76099fad165e 100644 --- a/api-ref/source/os-keypairs.inc +++ b/api-ref/source/os-keypairs.inc @@ -44,12 +44,16 @@ Response .. literalinclude:: ../../doc/api_samples/os-keypairs/v2.35/keypairs-list-resp.json :language: javascript -Create Or Import Keypair -======================== +Import (or create) Keypair +========================== .. rest_method:: POST /os-keypairs -Generates or imports a keypair. +Imports (or generates) a keypair. + +.. warning:: + + Generating a keypair is no longer possible starting from version 2.92. Normal response codes: 200, 201 @@ -65,7 +69,7 @@ Request .. rest_parameters:: parameters.yaml - keypair: keypair - - name: keypair_name + - name: keypair_name_in - public_key: keypair_public_key_in - type: keypair_type_in - user_id: keypair_userid_in @@ -75,6 +79,11 @@ Request .. literalinclude:: ../../doc/api_samples/os-keypairs/v2.10/keypairs-import-post-req.json :language: javascript +**Example Import Keypair (v2.92): JSON request** + +.. literalinclude:: ../../doc/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json + :language: javascript + Response -------- @@ -93,6 +102,11 @@ Response .. literalinclude:: ../../doc/api_samples/os-keypairs/v2.10/keypairs-import-post-resp.json :language: javascript +**Example Import Keypair (v2.92): JSON response** + +.. literalinclude:: ../../doc/api_samples/os-keypairs/v2.92/keypairs-import-post-resp.json + :language: javascript + Show Keypair Details ==================== diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 9853ad23f120..63f0f58963fa 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -4410,11 +4410,23 @@ keypair_links: required: false min_version: 2.35 keypair_name: + in: body + required: true + type: string + description: | + The name for the keypair. +keypair_name_in: in: body required: true type: string description: | A name for the keypair which will be used to reference it later. + + .. note:: + + Since microversion 2.92, allowed characters are ASCII letters + ``[a-zA-Z]``, digits ``[0-9]`` and the following special + characters: ``[@._- ]``. keypair_private_key: description: | If you do not provide a public key on create, a new keypair will @@ -4424,6 +4436,7 @@ keypair_private_key: in: body required: false type: string + max_version: 2.91 keypair_public_key: description: | The keypair public key. @@ -4432,10 +4445,11 @@ keypair_public_key: type: string keypair_public_key_in: description: | - The public ssh key to import. If you omit this value, a keypair is - generated for you. + The public ssh key to import. + Was optional before microversion 2.92 : if you were omitting this value, a + keypair was generated for you. in: body - required: false + required: true type: string keypair_type: in: body diff --git a/doc/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json b/doc/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json new file mode 100644 index 000000000000..72600f836859 --- /dev/null +++ b/doc/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json @@ -0,0 +1,8 @@ +{ + "keypair": { + "name": "me.and.myself@this.nice.domain.com mooh.", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova", + "user_id": "fake" + } +} diff --git a/doc/api_samples/os-keypairs/v2.92/keypairs-import-post-resp.json b/doc/api_samples/os-keypairs/v2.92/keypairs-import-post-resp.json new file mode 100644 index 000000000000..b828798c5eae --- /dev/null +++ b/doc/api_samples/os-keypairs/v2.92/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": "me.and.myself@this.nice.domain.com mooh.", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova", + "user_id": "fake" + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 1c17bd42a4f3..6e98517b617b 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.91", + "version": "2.92", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index bd609a25e433..5fdd20ae611b 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.91", + "version": "2.92", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 4345e2c914f6..a3a8b1f41e86 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -249,6 +249,9 @@ REST_API_VERSION_HISTORY = """REST API Version History: server responses regardless of policy configuration. * 2.91 - Add support to unshelve instance to a specific host and to pin/unpin AZ. + * 2.92 - Drop generation of keypair, add keypair name validation on + ``POST /os-keypairs`` and allow including @ and dot (.) characters + in keypair name. """ # The minimum and maximum versions of the API supported @@ -257,7 +260,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.91' +_MAX_API_VERSION = '2.92' DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which are related to network, images and baremetal diff --git a/nova/api/openstack/compute/keypairs.py b/nova/api/openstack/compute/keypairs.py index 65dacfa638d0..40b702bdb5ab 100644 --- a/nova/api/openstack/compute/keypairs.py +++ b/nova/api/openstack/compute/keypairs.py @@ -43,15 +43,19 @@ class KeypairController(wsgi.Controller): @wsgi.Controller.api_version("2.10") @wsgi.response(201) @wsgi.expected_errors((400, 403, 409)) - @validation.schema(keypairs.create_v210) + @validation.schema(keypairs.create_v210, "2.10", "2.91") + @validation.schema(keypairs.create_v292, "2.92") def create(self, req, body): """Create or import keypair. + Keypair generations are allowed until version 2.91. + Afterwards, only imports are allowed. + A policy check restricts users from creating keys for other users params: keypair object with: name (required) - string - public_key (optional) - string + public_key (optional or required if >=2.92) - string type (optional) - string user_id (optional) - string """ @@ -114,6 +118,8 @@ class KeypairController(wsgi.Controller): context, user_id, name, params['public_key'], key_type_value) else: + # public_key is a required field starting with 2.92 so this + # generation should only happen with older versions. keypair, private_key = self.api.create_key_pair( context, user_id, name, key_type_value) keypair['private_key'] = private_key diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index b1dfadf3fb7a..b65e50c62f6a 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -1211,3 +1211,11 @@ responses is now visible to all users. Previously this was an admin-only field. Add support to unshelve instance to a specific host. Add support to pin a server to an availability zone or unpin a server from any availability zone. + +.. _microversion 2.92: + +2.92 +---- + +The ``POST /os-keypairs`` API now forbids to generate a keypair and allows new +safe characters, specifically '@' and '.' (dot character). diff --git a/nova/api/openstack/compute/schemas/keypairs.py b/nova/api/openstack/compute/schemas/keypairs.py index 7ebd3c7433ed..74b992c3e364 100644 --- a/nova/api/openstack/compute/schemas/keypairs.py +++ b/nova/api/openstack/compute/schemas/keypairs.py @@ -23,7 +23,7 @@ create = { 'keypair': { 'type': 'object', 'properties': { - 'name': parameter_types.name, + 'name': parameter_types.keypair_name_special_chars, 'public_key': {'type': 'string'}, }, 'required': ['name'], @@ -46,7 +46,7 @@ create_v22 = { 'keypair': { 'type': 'object', 'properties': { - 'name': parameter_types.name, + 'name': parameter_types.keypair_name_special_chars, 'type': { 'type': 'string', 'enum': ['ssh', 'x509'] @@ -67,7 +67,7 @@ create_v210 = { 'keypair': { 'type': 'object', 'properties': { - 'name': parameter_types.name, + 'name': parameter_types.keypair_name_special_chars, 'type': { 'type': 'string', 'enum': ['ssh', 'x509'] @@ -83,6 +83,11 @@ create_v210 = { 'additionalProperties': False, } +create_v292 = copy.deepcopy(create_v210) +create_v292['properties']['keypair']['properties']['name'] = (parameter_types. + keypair_name_special_chars_292) +create_v292['properties']['keypair']['required'] = ['name', 'public_key'] + index_query_schema_v20 = { 'type': 'object', 'properties': {}, diff --git a/nova/api/validation/parameter_types.py b/nova/api/validation/parameter_types.py index 79badb7d142f..bdb3ad3c8378 100644 --- a/nova/api/validation/parameter_types.py +++ b/nova/api/validation/parameter_types.py @@ -290,7 +290,7 @@ fqdn = { name = { # NOTE: Nova v2.1 API contains some 'name' parameters such - # as keypair, server, flavor, aggregate and so on. They are + # as server, flavor, aggregate and so on. They are # stored in the DB and Nova specific parameters. # This definition is used for all their parameters. 'type': 'string', 'minLength': 1, 'maxLength': 255, @@ -304,6 +304,18 @@ az_name = { } +keypair_name_special_chars = {'allOf': [name, { + 'type': 'string', 'minLength': 1, 'maxLength': 255, + 'format': 'keypair_name_20' +}]} + + +keypair_name_special_chars_292 = {'allOf': [name, { + 'type': 'string', 'minLength': 1, 'maxLength': 255, + 'format': 'keypair_name_292' +}]} + + az_name_with_leading_trailing_spaces = { 'type': 'string', 'minLength': 1, 'maxLength': 255, 'format': 'az_name_with_leading_trailing_spaces' diff --git a/nova/api/validation/validators.py b/nova/api/validation/validators.py index ed2f211eee9d..b0e9478d35e2 100644 --- a/nova/api/validation/validators.py +++ b/nova/api/validation/validators.py @@ -17,6 +17,7 @@ Internal implementation of request Body validating middleware. """ import re +import string import jsonschema from jsonschema import exceptions as jsonschema_exc @@ -153,6 +154,28 @@ def _validate_az_name(instance): raise exception.InvalidName(reason=regex.reason) +@jsonschema.FormatChecker.cls_checks('keypair_name_20', + exception.InvalidName) +def _validate_keypair_name_20(keypair_name): + safe_chars = "_- " + string.digits + string.ascii_letters + return _validate_keypair_name(keypair_name, safe_chars) + + +@jsonschema.FormatChecker.cls_checks('keypair_name_292', + exception.InvalidName) +def _validate_keypair_name_292(keypair_name): + safe_chars = "@._- " + string.digits + string.ascii_letters + return _validate_keypair_name(keypair_name, safe_chars) + + +def _validate_keypair_name(keypair_name, safe_chars): + clean_value = "".join(x for x in keypair_name if x in safe_chars) + if clean_value != keypair_name: + reason = _("Only expected characters: [%s]") % safe_chars + raise exception.InvalidName(reason=reason) + return True + + def _soft_validate_additional_properties(validator, additional_properties_value, instance, diff --git a/nova/compute/api.py b/nova/compute/api.py index 8e2063fd4cf5..112a318f1d37 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -22,7 +22,6 @@ networking and storage of VMs, and compute hosts on which they run).""" import collections import functools import re -import string import typing as ty from castellan import key_manager @@ -6694,19 +6693,7 @@ class KeypairAPI: } self.notifier.info(context, 'keypair.%s' % event_suffix, payload) - def _validate_new_key_pair(self, context, user_id, key_name, 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: - raise exception.InvalidKeypair( - reason=_("Keypair name contains unsafe characters")) - - try: - utils.check_string_length(key_name, min_length=1, max_length=255) - except exception.InvalidInput: - raise exception.InvalidKeypair( - reason=_('Keypair name must be string and between ' - '1 and 255 characters long')) + def _check_key_pair_quotas(self, context, user_id, key_name, key_type): try: objects.Quotas.check_deltas(context, {'key_pairs': 1}, user_id) local_limit.enforce_db_limit(context, local_limit.KEY_PAIRS, @@ -6720,7 +6707,7 @@ class KeypairAPI: 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, key_type) + self._check_key_pair_quotas(context, user_id, key_name, key_type) self._notify(context, 'import.start', key_name) @@ -6755,7 +6742,7 @@ class KeypairAPI: 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, key_type) + self._check_key_pair_quotas(context, user_id, key_name, key_type) keypair = objects.KeyPair(context) keypair.user_id = user_id diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json.tpl new file mode 100644 index 000000000000..03e60c013312 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-import-post-req.json.tpl @@ -0,0 +1,8 @@ +{ + "keypair": { + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s", + "user_id": "%(user_id)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-import-post-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-import-post-resp.json.tpl new file mode 100644 index 000000000000..30d3fa969daf --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/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": "%(user_id)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-post-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-post-req.json.tpl new file mode 100644 index 000000000000..f6a6d47b56ec --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-keypairs/v2.92/keypairs-post-req.json.tpl @@ -0,0 +1,7 @@ +{ + "keypair": { + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "user_id": "%(user_id)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/test_keypairs.py b/nova/tests/functional/api_sample_tests/test_keypairs.py index eab88f61e1a1..a121b98449ae 100644 --- a/nova/tests/functional/api_sample_tests/test_keypairs.py +++ b/nova/tests/functional/api_sample_tests/test_keypairs.py @@ -319,3 +319,66 @@ class KeyPairsV235SampleJsonTest(api_sample_base.ApiSampleTestBaseV21): % keypairs_user2[1]) subs = {'keypair_name': keypairs_user2[2]} self._verify_response('keypairs-list-user2-resp', subs, response, 200) + + +class KeyPairsV292SampleJsonTest(api_sample_base.ApiSampleTestBaseV21): + ADMIN_API = True + sample_dir = 'os-keypairs' + microversion = '2.92' + expected_post_status_code = 201 + scenarios = [('v2_92', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(KeyPairsV292SampleJsonTest, self).setUp() + self.api.microversion = self.microversion + + # NOTE(sbauza): This method is stupidly needed for _verify_response(). + # See the TODO(sdague) above. + def generalize_subs(self, subs, vanilla_regexes): + subs['keypair_name'] = '[0-9a-zA-Z-_.@ ]+' + return subs + + def test_keypairs_post_no_longer_supported(self): + subs = { + 'keypair_name': 'foo', + 'keypair_type': keypair_obj.KEYPAIR_TYPE_SSH, + 'user_id': 'fake' + } + response = self._do_post('os-keypairs', 'keypairs-post-req', subs) + self.assertEqual(400, response.status_code) + + def test_keypairs_import_key_invalid_name(self): + public_key = fake_crypto.get_ssh_public_key() + subs = { + 'keypair_name': '!nvalid=name|', + 'keypair_type': keypair_obj.KEYPAIR_TYPE_SSH, + 'user_id': 'fake', + 'public_key': public_key, + } + response = self._do_post('os-keypairs', 'keypairs-import-post-req', + subs) + self.assertEqual(400, response.status_code) + + def _test_keypairs_import_key_post(self, name=None): + if not name: + name = 'keypair-' + uuids.fake + public_key = fake_crypto.get_ssh_public_key() + params = { + 'keypair_name': name, + 'keypair_type': keypair_obj.KEYPAIR_TYPE_SSH, + 'user_id': 'fake', + 'public_key': public_key, + } + response = self._do_post('os-keypairs', 'keypairs-import-post-req', + params) + # NOTE(sbauza): We do some crazy regexp change in _verify_response() so + # we only need to pass the keypair name. + subs = {'keypair_name': name} + self._verify_response('keypairs-import-post-resp', subs, response, + self.expected_post_status_code) + + def test_keypairs_import_key_post(self): + self._test_keypairs_import_key_post() + + def test_keypairs_import_key_special_characters(self): + self._test_keypairs_import_key_post(name='my-key@ my.host') diff --git a/nova/tests/functional/notification_sample_tests/test_keypair.py b/nova/tests/functional/notification_sample_tests/test_keypair.py index b2481f1b2a43..01c59b0f3629 100644 --- a/nova/tests/functional/notification_sample_tests/test_keypair.py +++ b/nova/tests/functional/notification_sample_tests/test_keypair.py @@ -16,7 +16,12 @@ from nova.tests.functional.notification_sample_tests \ class TestKeypairNotificationSample( notification_sample_base.NotificationSampleTestBase): + api_major_version = 'v2.1' + microversion = 'latest' + def test_keypair_create_delete(self): + # Keypair generation is no longer supported with 2.92 microversion. + self.api.microversion = '2.91' keypair_req = { "keypair": { "name": "my-key", diff --git a/nova/tests/functional/regressions/test_bug_1843708.py b/nova/tests/functional/regressions/test_bug_1843708.py index 2ae725a5eb74..2eda92125bfb 100644 --- a/nova/tests/functional/regressions/test_bug_1843708.py +++ b/nova/tests/functional/regressions/test_bug_1843708.py @@ -15,6 +15,7 @@ from nova import context from nova import objects from nova.tests.functional import integrated_helpers +from nova.tests.unit import fake_crypto class RebuildWithKeypairTestCase(integrated_helpers._IntegratedTestBase): @@ -26,14 +27,19 @@ class RebuildWithKeypairTestCase(integrated_helpers._IntegratedTestBase): microversion = 'latest' def test_rebuild_with_keypair(self): + pub_key1 = fake_crypto.get_ssh_public_key() + keypair_req = { 'keypair': { 'name': 'test-key1', 'type': 'ssh', + 'public_key': pub_key1, }, } keypair1 = self.api.post_keypair(keypair_req) + pub_key2 = fake_crypto.get_ssh_public_key() keypair_req['keypair']['name'] = 'test-key2' + keypair_req['keypair']['public_key'] = pub_key2 keypair2 = self.api.post_keypair(keypair_req) server = self._build_server(networks='none') diff --git a/nova/tests/unit/api/openstack/compute/test_keypairs.py b/nova/tests/unit/api/openstack/compute/test_keypairs.py index 30e95fb21de5..c6f59b615bb5 100644 --- a/nova/tests/unit/api/openstack/compute/test_keypairs.py +++ b/nova/tests/unit/api/openstack/compute/test_keypairs.py @@ -37,6 +37,8 @@ keypair_data = { FAKE_UUID = 'b48316c5-71e8-45e4-9884-6c78055b9b13' +keypair_name_2_92_compatible = 'my-key@ my.host' + def fake_keypair(name): return dict(test_keypair.fake_keypair, @@ -110,16 +112,22 @@ class KeypairsTestV21(test.TestCase): self.assertGreater(len(res_dict['keypair']['private_key']), 0) self._assert_keypair_type(res_dict) - def _test_keypair_create_bad_request_case(self, - body, - exception): - self.assertRaises(exception, - self.controller.create, self.req, body=body) + def _test_keypair_create_bad_request_case( + self, body, exception, error_msg=None + ): + if error_msg: + self.assertRaisesRegex(exception, error_msg, + self.controller.create, + self.req, body=body) + else: + self.assertRaises(exception, + self.controller.create, self.req, body=body) def test_keypair_create_with_empty_name(self): body = {'keypair': {'name': ''}} self._test_keypair_create_bad_request_case(body, - self.validation_error) + self.validation_error, + 'is too short') def test_keypair_create_with_name_too_long(self): body = { @@ -128,7 +136,8 @@ class KeypairsTestV21(test.TestCase): } } self._test_keypair_create_bad_request_case(body, - self.validation_error) + self.validation_error, + 'is too long') def test_keypair_create_with_name_leading_trailing_spaces(self): body = { @@ -136,8 +145,10 @@ class KeypairsTestV21(test.TestCase): 'name': ' test ' } } + expected_msg = 'Can not start or end with whitespace.' self._test_keypair_create_bad_request_case(body, - self.validation_error) + self.validation_error, + expected_msg) def test_keypair_create_with_name_leading_trailing_spaces_compat_mode( self): @@ -152,8 +163,21 @@ class KeypairsTestV21(test.TestCase): 'name': 'test/keypair' } } + expected_msg = 'Only expected characters' self._test_keypair_create_bad_request_case(body, - webob.exc.HTTPBadRequest) + self.validation_error, + expected_msg) + + def test_keypair_create_with_special_characters(self): + body = { + 'keypair': { + 'name': keypair_name_2_92_compatible + } + } + expected_msg = 'Only expected characters' + self._test_keypair_create_bad_request_case(body, + self.validation_error, + expected_msg) def test_keypair_import_bad_key(self): body = { @@ -167,8 +191,10 @@ class KeypairsTestV21(test.TestCase): def test_keypair_create_with_invalid_keypair_body(self): body = {'alpha': {'name': 'create_test'}} + expected_msg = "'keypair' is a required property" self._test_keypair_create_bad_request_case(body, - self.validation_error) + self.validation_error, + expected_msg) def test_keypair_import(self): body = { @@ -470,3 +496,82 @@ class KeypairsTestV275(test.TestCase): version='2.75', use_admin_context=True) self.assertRaises(exception.ValidationError, self.controller.delete, req, 1) + + +class KeypairsTestV292(test.TestCase): + wsgi_api_version = '2.92' + wsgi_old_api_version = '2.91' + + def setUp(self): + super(KeypairsTestV292, self).setUp() + self.controller = keypairs_v21.KeypairController() + self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version) + self.old_req = fakes.HTTPRequest.blank( + '', version=self.wsgi_old_api_version) + + def test_keypair_create_no_longer_supported(self): + body = { + 'keypair': { + 'name': keypair_name_2_92_compatible, + } + } + self.assertRaises(exception.ValidationError, self.controller.create, + self.req, body=body) + + def test_keypair_create_works_with_old_version(self): + body = { + 'keypair': { + 'name': 'fake', + } + } + res_dict = self.controller.create(self.old_req, body=body) + self.assertEqual('fake', res_dict['keypair']['name']) + self.assertGreater(len(res_dict['keypair']['private_key']), 0) + + def test_keypair_import_works_with_new_version(self): + body = { + 'keypair': { + 'name': 'fake', + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA' + 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4' + 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y' + 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi' + 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj' + 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1' + 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc' + 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB' + 'bHkXa6OciiJDvkRzJXzf', + } + } + res_dict = self.controller.create(self.req, body=body) + self.assertEqual('fake', res_dict['keypair']['name']) + self.assertNotIn('private_key', res_dict['keypair']) + + def test_keypair_create_refuses_special_chars_with_old_version(self): + body = { + 'keypair': { + 'name': keypair_name_2_92_compatible, + } + } + self.assertRaises(exception.ValidationError, self.controller.create, + self.old_req, body=body) + + def test_keypair_import_with_special_characters(self): + body = { + 'keypair': { + 'name': keypair_name_2_92_compatible, + 'public_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBYIznA' + 'x9D7118Q1VKGpXy2HDiKyUTM8XcUuhQpo0srqb9rboUp4' + 'a9NmCwpWpeElDLuva707GOUnfaBAvHBwsRXyxHJjRaI6Y' + 'Qj2oLJwqvaSaWUbyT1vtryRqy6J3TecN0WINY71f4uymi' + 'MZP0wby4bKBcYnac8KiCIlvkEl0ETjkOGUq8OyWRmn7lj' + 'j5SESEUdBP0JnuTFKddWTU/wD6wydeJaUhBTqOlHn0kX1' + 'GyqoNTE1UEhcM5ZRWgfUZfTjVyDF2kGj3vJLCJtJ8LoGc' + 'j7YaN4uPg1rBle+izwE/tLonRrds+cev8p6krSSrxWOwB' + 'bHkXa6OciiJDvkRzJXzf', + } + } + + res_dict = self.controller.create(self.req, body=body) + self.assertEqual(keypair_name_2_92_compatible, + res_dict['keypair']['name']) diff --git a/nova/tests/unit/compute/test_keypairs.py b/nova/tests/unit/compute/test_keypairs.py index 0ea20a60efcc..b0d6eb46dc72 100644 --- a/nova/tests/unit/compute/test_keypairs.py +++ b/nova/tests/unit/compute/test_keypairs.py @@ -123,24 +123,6 @@ class CreateImportSharedTestMixIn(object): name, *args) self.assertIn(expected_message, str(exc)) - def assertInvalidKeypair(self, expected_message, name): - msg = 'Keypair data is invalid: %s' % expected_message - self.assertKeypairRaises(exception.InvalidKeypair, msg, name) - - def test_name_too_short(self): - msg = ('Keypair name must be string and between 1 ' - 'and 255 characters long') - self.assertInvalidKeypair(msg, '') - - def test_name_too_long(self): - msg = ('Keypair name must be string and between 1 ' - 'and 255 characters long') - self.assertInvalidKeypair(msg, 'x' * 256) - - def test_invalid_chars(self): - msg = "Keypair name contains unsafe characters" - self.assertInvalidKeypair(msg, '* BAD CHARACTERS! *') - def test_already_exists(self): def db_key_pair_create_duplicate(context, keypair): raise exception.KeyPairExists(key_name=keypair.get('name', '')) diff --git a/releasenotes/notes/bp-keypair-generation-removal-3004a8643dcd1fd9.yaml b/releasenotes/notes/bp-keypair-generation-removal-3004a8643dcd1fd9.yaml new file mode 100644 index 000000000000..7adbeb460155 --- /dev/null +++ b/releasenotes/notes/bp-keypair-generation-removal-3004a8643dcd1fd9.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + The 2.92 microversion makes the following changes: + + * Make public_key a mandatory parameter for keypair creation. This means + that by this microversion, Nova will stop to support automatic keypair + generations. Only imports will be possible. + * Allow 2 new special characters: '@' and '.' (dot), + in addition to the existing constraints of ``[a-z][A-Z][0-9][_- ]``