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
|
.. 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
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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': {},
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
% 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')
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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', ''))
|
||||||
|
|
|
@ -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…
Reference in New Issue