api: Drop generating a keypair and add special chars to naming

As agreed in the spec, we will both drop the generation support for a keypair
but we'll also accept @ (at) and . (dot) chars in the keyname, all of them in
the same API microversion.

Rebased the work from I5de15935e83823afa545a250cf84f6a7a37036b4

APIImpact

Implements: blueprint keypair-generation-removal
Co-Authored-By: Nicolas Parquet <nicolas.parquet@gandi.net>

Change-Id: I6a7c71fb4385348c87067543d0454f302907395e
This commit is contained in:
Sylvain Bauza 2022-07-08 17:23:01 +02:00
parent 09239fc2ea
commit a755e5d9f2
22 changed files with 344 additions and 60 deletions

View File

@ -44,12 +44,16 @@ Response
.. literalinclude:: ../../doc/api_samples/os-keypairs/v2.35/keypairs-list-resp.json .. literalinclude:: ../../doc/api_samples/os-keypairs/v2.35/keypairs-list-resp.json
:language: javascript :language: javascript
Create Or Import Keypair Import (or create) Keypair
======================== ==========================
.. rest_method:: POST /os-keypairs .. 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 Normal response codes: 200, 201
@ -65,7 +69,7 @@ Request
.. rest_parameters:: parameters.yaml .. rest_parameters:: parameters.yaml
- keypair: keypair - keypair: keypair
- name: keypair_name - name: keypair_name_in
- public_key: keypair_public_key_in - public_key: keypair_public_key_in
- type: keypair_type_in - type: keypair_type_in
- user_id: keypair_userid_in - user_id: keypair_userid_in
@ -75,6 +79,11 @@ Request
.. literalinclude:: ../../doc/api_samples/os-keypairs/v2.10/keypairs-import-post-req.json .. literalinclude:: ../../doc/api_samples/os-keypairs/v2.10/keypairs-import-post-req.json
:language: javascript :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 Response
-------- --------
@ -93,6 +102,11 @@ Response
.. literalinclude:: ../../doc/api_samples/os-keypairs/v2.10/keypairs-import-post-resp.json .. literalinclude:: ../../doc/api_samples/os-keypairs/v2.10/keypairs-import-post-resp.json
:language: javascript :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 Show Keypair Details
==================== ====================

View File

@ -4410,11 +4410,23 @@ keypair_links:
required: false required: false
min_version: 2.35 min_version: 2.35
keypair_name: keypair_name:
in: body
required: true
type: string
description: |
The name for the keypair.
keypair_name_in:
in: body in: body
required: true required: true
type: string type: string
description: | description: |
A name for the keypair which will be used to reference it later. 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: keypair_private_key:
description: | description: |
If you do not provide a public key on create, a new keypair will If you do not provide a public key on create, a new keypair will
@ -4424,6 +4436,7 @@ keypair_private_key:
in: body in: body
required: false required: false
type: string type: string
max_version: 2.91
keypair_public_key: keypair_public_key:
description: | description: |
The keypair public key. The keypair public key.
@ -4432,10 +4445,11 @@ keypair_public_key:
type: string type: string
keypair_public_key_in: keypair_public_key_in:
description: | description: |
The public ssh key to import. If you omit this value, a keypair is The public ssh key to import.
generated for you. Was optional before microversion 2.92 : if you were omitting this value, a
keypair was generated for you.
in: body in: body
required: false required: true
type: string type: string
keypair_type: keypair_type:
in: body in: body

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.91", "version": "2.92",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.91", "version": "2.92",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -249,6 +249,9 @@ REST_API_VERSION_HISTORY = """REST API Version History:
server responses regardless of policy configuration. server responses regardless of policy configuration.
* 2.91 - Add support to unshelve instance to a specific host and * 2.91 - Add support to unshelve instance to a specific host and
to pin/unpin AZ. 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 # 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 # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = '2.1' _MIN_API_VERSION = '2.1'
_MAX_API_VERSION = '2.91' _MAX_API_VERSION = '2.92'
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal

View File

@ -43,15 +43,19 @@ class KeypairController(wsgi.Controller):
@wsgi.Controller.api_version("2.10") @wsgi.Controller.api_version("2.10")
@wsgi.response(201) @wsgi.response(201)
@wsgi.expected_errors((400, 403, 409)) @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): def create(self, req, body):
"""Create or import keypair. """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 A policy check restricts users from creating keys for other users
params: keypair object with: params: keypair object with:
name (required) - string name (required) - string
public_key (optional) - string public_key (optional or required if >=2.92) - string
type (optional) - string type (optional) - string
user_id (optional) - string user_id (optional) - string
""" """
@ -114,6 +118,8 @@ class KeypairController(wsgi.Controller):
context, user_id, name, params['public_key'], context, user_id, name, params['public_key'],
key_type_value) key_type_value)
else: 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( keypair, private_key = self.api.create_key_pair(
context, user_id, name, key_type_value) context, user_id, name, key_type_value)
keypair['private_key'] = private_key keypair['private_key'] = private_key

View File

@ -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 unshelve instance to a specific host.
Add support to pin a server to an availability zone or unpin a server from any availability zone. 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).

View File

@ -23,7 +23,7 @@ create = {
'keypair': { 'keypair': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': parameter_types.name, 'name': parameter_types.keypair_name_special_chars,
'public_key': {'type': 'string'}, 'public_key': {'type': 'string'},
}, },
'required': ['name'], 'required': ['name'],
@ -46,7 +46,7 @@ create_v22 = {
'keypair': { 'keypair': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': parameter_types.name, 'name': parameter_types.keypair_name_special_chars,
'type': { 'type': {
'type': 'string', 'type': 'string',
'enum': ['ssh', 'x509'] 'enum': ['ssh', 'x509']
@ -67,7 +67,7 @@ create_v210 = {
'keypair': { 'keypair': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': parameter_types.name, 'name': parameter_types.keypair_name_special_chars,
'type': { 'type': {
'type': 'string', 'type': 'string',
'enum': ['ssh', 'x509'] 'enum': ['ssh', 'x509']
@ -83,6 +83,11 @@ create_v210 = {
'additionalProperties': False, '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 = { index_query_schema_v20 = {
'type': 'object', 'type': 'object',
'properties': {}, 'properties': {},

View File

@ -290,7 +290,7 @@ fqdn = {
name = { name = {
# NOTE: Nova v2.1 API contains some 'name' parameters such # 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. # stored in the DB and Nova specific parameters.
# This definition is used for all their parameters. # This definition is used for all their parameters.
'type': 'string', 'minLength': 1, 'maxLength': 255, '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 = { az_name_with_leading_trailing_spaces = {
'type': 'string', 'minLength': 1, 'maxLength': 255, 'type': 'string', 'minLength': 1, 'maxLength': 255,
'format': 'az_name_with_leading_trailing_spaces' 'format': 'az_name_with_leading_trailing_spaces'

View File

@ -17,6 +17,7 @@ Internal implementation of request Body validating middleware.
""" """
import re import re
import string
import jsonschema import jsonschema
from jsonschema import exceptions as jsonschema_exc from jsonschema import exceptions as jsonschema_exc
@ -153,6 +154,28 @@ def _validate_az_name(instance):
raise exception.InvalidName(reason=regex.reason) 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, def _soft_validate_additional_properties(validator,
additional_properties_value, additional_properties_value,
instance, instance,

View File

@ -22,7 +22,6 @@ networking and storage of VMs, and compute hosts on which they run)."""
import collections import collections
import functools import functools
import re import re
import string
import typing as ty import typing as ty
from castellan import key_manager from castellan import key_manager
@ -6694,19 +6693,7 @@ class KeypairAPI:
} }
self.notifier.info(context, 'keypair.%s' % event_suffix, payload) self.notifier.info(context, 'keypair.%s' % event_suffix, payload)
def _validate_new_key_pair(self, context, user_id, key_name, key_type): def _check_key_pair_quotas(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'))
try: try:
objects.Quotas.check_deltas(context, {'key_pairs': 1}, user_id) objects.Quotas.check_deltas(context, {'key_pairs': 1}, user_id)
local_limit.enforce_db_limit(context, local_limit.KEY_PAIRS, 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, def import_key_pair(self, context, user_id, key_name, public_key,
key_type=keypair_obj.KEYPAIR_TYPE_SSH): key_type=keypair_obj.KEYPAIR_TYPE_SSH):
"""Import a key pair using an existing public key.""" """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) self._notify(context, 'import.start', key_name)
@ -6755,7 +6742,7 @@ class KeypairAPI:
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): key_type=keypair_obj.KEYPAIR_TYPE_SSH):
"""Create a new key pair.""" """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 = objects.KeyPair(context)
keypair.user_id = user_id keypair.user_id = user_id

View File

@ -0,0 +1,8 @@
{
"keypair": {
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"public_key": "%(public_key)s",
"user_id": "%(user_id)s"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,7 @@
{
"keypair": {
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"user_id": "%(user_id)s"
}
}

View File

@ -319,3 +319,66 @@ class KeyPairsV235SampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
% keypairs_user2[1]) % keypairs_user2[1])
subs = {'keypair_name': keypairs_user2[2]} subs = {'keypair_name': keypairs_user2[2]}
self._verify_response('keypairs-list-user2-resp', subs, response, 200) 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')

View File

@ -16,7 +16,12 @@ from nova.tests.functional.notification_sample_tests \
class TestKeypairNotificationSample( class TestKeypairNotificationSample(
notification_sample_base.NotificationSampleTestBase): notification_sample_base.NotificationSampleTestBase):
api_major_version = 'v2.1'
microversion = 'latest'
def test_keypair_create_delete(self): def test_keypair_create_delete(self):
# Keypair generation is no longer supported with 2.92 microversion.
self.api.microversion = '2.91'
keypair_req = { keypair_req = {
"keypair": { "keypair": {
"name": "my-key", "name": "my-key",

View File

@ -15,6 +15,7 @@
from nova import context from nova import context
from nova import objects from nova import objects
from nova.tests.functional import integrated_helpers from nova.tests.functional import integrated_helpers
from nova.tests.unit import fake_crypto
class RebuildWithKeypairTestCase(integrated_helpers._IntegratedTestBase): class RebuildWithKeypairTestCase(integrated_helpers._IntegratedTestBase):
@ -26,14 +27,19 @@ class RebuildWithKeypairTestCase(integrated_helpers._IntegratedTestBase):
microversion = 'latest' microversion = 'latest'
def test_rebuild_with_keypair(self): def test_rebuild_with_keypair(self):
pub_key1 = fake_crypto.get_ssh_public_key()
keypair_req = { keypair_req = {
'keypair': { 'keypair': {
'name': 'test-key1', 'name': 'test-key1',
'type': 'ssh', 'type': 'ssh',
'public_key': pub_key1,
}, },
} }
keypair1 = self.api.post_keypair(keypair_req) keypair1 = self.api.post_keypair(keypair_req)
pub_key2 = fake_crypto.get_ssh_public_key()
keypair_req['keypair']['name'] = 'test-key2' keypair_req['keypair']['name'] = 'test-key2'
keypair_req['keypair']['public_key'] = pub_key2
keypair2 = self.api.post_keypair(keypair_req) keypair2 = self.api.post_keypair(keypair_req)
server = self._build_server(networks='none') server = self._build_server(networks='none')

View File

@ -37,6 +37,8 @@ keypair_data = {
FAKE_UUID = 'b48316c5-71e8-45e4-9884-6c78055b9b13' FAKE_UUID = 'b48316c5-71e8-45e4-9884-6c78055b9b13'
keypair_name_2_92_compatible = 'my-key@ my.host'
def fake_keypair(name): def fake_keypair(name):
return dict(test_keypair.fake_keypair, return dict(test_keypair.fake_keypair,
@ -110,16 +112,22 @@ class KeypairsTestV21(test.TestCase):
self.assertGreater(len(res_dict['keypair']['private_key']), 0) self.assertGreater(len(res_dict['keypair']['private_key']), 0)
self._assert_keypair_type(res_dict) self._assert_keypair_type(res_dict)
def _test_keypair_create_bad_request_case(self, def _test_keypair_create_bad_request_case(
body, self, body, exception, error_msg=None
exception): ):
self.assertRaises(exception, if error_msg:
self.controller.create, self.req, body=body) 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): def test_keypair_create_with_empty_name(self):
body = {'keypair': {'name': ''}} body = {'keypair': {'name': ''}}
self._test_keypair_create_bad_request_case(body, 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): def test_keypair_create_with_name_too_long(self):
body = { body = {
@ -128,7 +136,8 @@ class KeypairsTestV21(test.TestCase):
} }
} }
self._test_keypair_create_bad_request_case(body, 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): def test_keypair_create_with_name_leading_trailing_spaces(self):
body = { body = {
@ -136,8 +145,10 @@ class KeypairsTestV21(test.TestCase):
'name': ' test ' 'name': ' test '
} }
} }
expected_msg = 'Can not start or end with whitespace.'
self._test_keypair_create_bad_request_case(body, 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( def test_keypair_create_with_name_leading_trailing_spaces_compat_mode(
self): self):
@ -152,8 +163,21 @@ class KeypairsTestV21(test.TestCase):
'name': 'test/keypair' 'name': 'test/keypair'
} }
} }
expected_msg = 'Only expected characters'
self._test_keypair_create_bad_request_case(body, 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): def test_keypair_import_bad_key(self):
body = { body = {
@ -167,8 +191,10 @@ class KeypairsTestV21(test.TestCase):
def test_keypair_create_with_invalid_keypair_body(self): def test_keypair_create_with_invalid_keypair_body(self):
body = {'alpha': {'name': 'create_test'}} body = {'alpha': {'name': 'create_test'}}
expected_msg = "'keypair' is a required property"
self._test_keypair_create_bad_request_case(body, self._test_keypair_create_bad_request_case(body,
self.validation_error) self.validation_error,
expected_msg)
def test_keypair_import(self): def test_keypair_import(self):
body = { body = {
@ -470,3 +496,82 @@ class KeypairsTestV275(test.TestCase):
version='2.75', use_admin_context=True) version='2.75', use_admin_context=True)
self.assertRaises(exception.ValidationError, self.controller.delete, self.assertRaises(exception.ValidationError, self.controller.delete,
req, 1) 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'])

View File

@ -123,24 +123,6 @@ class CreateImportSharedTestMixIn(object):
name, *args) name, *args)
self.assertIn(expected_message, str(exc)) 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 test_already_exists(self):
def db_key_pair_create_duplicate(context, keypair): def db_key_pair_create_duplicate(context, keypair):
raise exception.KeyPairExists(key_name=keypair.get('name', '')) raise exception.KeyPairExists(key_name=keypair.get('name', ''))

View File

@ -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][_- ]``