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:
parent
09239fc2ea
commit
a755e5d9f2
@ -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
|
||||
====================
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.91",
|
||||
"version": "2.92",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.91",
|
||||
"version": "2.92",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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': {},
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"keypair": {
|
||||
"name": "%(keypair_name)s",
|
||||
"type": "%(keypair_type)s",
|
||||
"public_key": "%(public_key)s",
|
||||
"user_id": "%(user_id)s"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"keypair": {
|
||||
"name": "%(keypair_name)s",
|
||||
"type": "%(keypair_type)s",
|
||||
"user_id": "%(user_id)s"
|
||||
}
|
||||
}
|
@ -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')
|
||||
|
@ -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",
|
||||
|
@ -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')
|
||||
|
@ -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'])
|
||||
|
@ -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', ''))
|
||||
|
@ -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][_- ]``
|
Loading…
x
Reference in New Issue
Block a user