Adds keypair type in nova-api

X509 certificates are used by Windows for passwordless
authentication (WinRM) in a way which can be considered
consistent with the usage of SSH keys on Linux, as both
are based on public / private keypairs.

Enables nova-api to return the keypair type, updates
nova-api version to reflect the changes and updates the
unit and functional tests to validate the API changes.

Unit tests have been updated to ensure that the keypair
type is not being returned on previous API versions.

Note: x509 keypair implementation is added in the next
commit.

DocImpact - See nova/api/openstack/rest_api_version_history.rst
for details

APIImpact

Depends-On: Id5b210d7afe5c0a590abcbd42b9ff85b071a5c55

Co-Authored-By: Chris Yeoh <cyeoh@au1.ibm.com>
Partially implements: blueprint keypair-x509-certificates

Change-Id: I215662f2f92a01921a866c3218031787a9eaf915
This commit is contained in:
Chris Yeoh 2015-02-12 20:20:02 +10:30 committed by Sean Dague
parent 21a422e426
commit c836f425bc
22 changed files with 324 additions and 54 deletions

View File

@ -0,0 +1,14 @@
{
"keypair": {
"fingerprint": "44:fe:29:6e:23:14:b9:53:5b:65:82:58:1c:fe:5a:c3",
"name": "keypair-6638abdb-c4e8-407c-ba88-c8dd7cc3c4f1",
"type": "ssh",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1HTrHCbb9NawNLSV8N6tSa8i637+EC2dA+lsdHHfQlT54t+N0nHhJPlKWDLhc579j87vp6RDFriFJ/smsTnDnf64O12z0kBaJpJPH2zXrBkZFK6q2rmxydURzX/z0yLSCP77SFJ0fdXWH2hMsAusflGyryHGX20n+mZK6mDrxVzGxEz228dwQ5G7Az5OoZDWygH2pqPvKjkifRw0jwUKf3BbkP0QvANACOk26cv16mNFpFJfI1N3OC5lUsZQtKGR01ptJoWijYKccqhkAKuo902tg/qup58J5kflNm7I61sy1mJon6SGqNUSfoQagqtBH6vd/tU1jnlwZ03uUroAL Generated-by-Nova\n",
"user_id": "fake",
"deleted": false,
"created_at": "2014-05-07T12:06:13.681238",
"updated_at": null,
"deleted_at": null,
"id": 1
}
}

View File

@ -0,0 +1,7 @@
{
"keypair": {
"name": "keypair-d20a3d59-9433-4b79-8726-20b431d89c78",
"type": "ssh",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova"
}
}

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": "keypair-803a1926-af78-4b05-902a-1d6f7a8d9d3e",
"type": "ssh",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova",
"user_id": "fake"
}
}

View File

@ -0,0 +1,12 @@
{
"keypairs": [
{
"keypair": {
"fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd",
"name": "keypair-50ca852e-273f-4cdc-8949-45feba200837",
"type": "ssh",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n"
}
}
]
}

View File

@ -0,0 +1,6 @@
{
"keypair": {
"name": "keypair-ab9ff2e6-a6d7-4915-a241-044c369c07f9",
"type": "ssh"
}
}

View File

@ -0,0 +1,10 @@
{
"keypair": {
"fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd",
"name": "keypair-50ca852e-273f-4cdc-8949-45feba200837",
"type": "ssh",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEApBdzF+fTq5QbN3R+QlO5TZr6W64GcUqcho5ZxPBZZIq53P1K\ndtpaY856ManqEwME1tN+JOw8+mmCK2RpkMHtk5BNPOMqr5Y+OQ5MqI/eX1v7GWnJ\ntHGTbi+vRDmxBh3aa3xiUGo66c9tjUKAg/ExQfFr/vKJvTR/S3urPlj3vfFgu+yi\n8PKoH0LGyHsviWsD1peDuu2XS+ca8qbkY3yD1o4Mv1R/OSF4P2fxjjWdp8R4EkoT\nJMKkhRAgAuS9zxwftPv9djP4opHWrRUlRo6bh75CzrN6Hu5uh5Tn5bkifOQcy1gW\n772vd6pBpi4OGQHPKz4djvmCLAVBzSyzDP6EKQIDAQABAoIBAQCB+tU/ZXKlIe+h\nMNTmoz1QfOe+AY625Rwx9cakGqMk4kKyC62VkgcxshfXCToSjzyhEuyEQOFYloT2\n7FY2xXb0gcS861Efv0pQlcQhbbz/GnQ/wC13ktPu3zTdPTm9l54xsFiMTGmYVaf4\n0mnMmhyjmKIsVGDJEDGZUD/oZj7wJGOFha5M4FZrZlJIrEZC0rGGlcC0kGF2no6B\nj1Mu7HjyK3pTKf4dlp+jeRikUF5Pct+qT+rcv2rZ3fl3inxtlLEwZeFPbp/njf/U\nIGxFzZsuLmiFlsJar6M5nEckTB3p25maWWaR8/0jvJRgsPnuoUrUoGDq87DMKCdk\nlw6by9fRAoGBANhnS9ko7Of+ntqIFR7xOG9p/oPATztgHkFxe4GbQ0leaDRTx3vE\ndQmUCnn24xtyVECaI9a4IV+LP1npw8niWUJ4pjgdAlkF4cCTu9sN+cBO15SfdACI\nzD1DaaHmpFCAWlpTo68VWlvWll6i2ncCkRJR1+q/C/yQz7asvl4AakElAoGBAMId\nxqMT2Sy9xLuHsrAoMUvBOkwaMYZH+IAb4DvUDjVIiKWjmonrmopS5Lpb+ALBKqZe\neVfD6HwWQqGwCFItToaEkZvrNfTapoNCHWWg001D49765UV5lMrArDbM1vXtFfM4\nDRYM6+Y6o/6QH8EBgXtyBxcYthIDBM3wBJa67xG1AoGAKTm8fFlMkIG0N4N3Kpbf\nnnH915GaRoBwIx2AXtd6QQ7oIRfYx95MQY/fUw7SgxcLr+btbulTCkWXwwRClUI2\nqPAdElGMcfMp56r9PaTy8EzUyu55heSJrB4ckIhEw0VAcTa/1wnlVduSd+LkZYmq\no2fOD11n5iycNXvBJF1F4LUCgYAMaRbwCi7SW3eefbiA5rDwJPRzNSGBckyC9EVL\nzezynyaNYH5a3wNMYKxa9dJPasYtSND9OXs9o7ay26xMhLUGiKc+jrUuaGRI9Asp\nGjUoNXT2JphN7s4CgHsCLep4YqYKnMTJah4S5CDj/5boIg6DM/EcGupZEHRYLkY8\n1MrAGQKBgQCi9yeC39ctLUNn+Ix604gttWWChdt3ozufTZ7HybJOSRA9Gh3iD5gm\nzlz0xqpGShKpOY2k+ftvja0poMdGeJLt84P3r2q01IgI7w0LmOj5m0W10dHysH27\nBWpCnHdBJMxnBsMRPoM4MKkmKWD9l5PSTCTWtkIpsyuDCko6D9UwZA==\n-----END RSA PRIVATE KEY-----\n",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n",
"user_id": "fake"
}
}

View File

@ -38,6 +38,8 @@ from nova import exception
REST_API_VERSION_HISTORY = """REST API Version History:
* 2.1 - Initial version. Equivalent to v2.0 code
* 2.2 - Adds (keypair) type parameter for os-keypairs plugin
Fixes success status code for create/delete a keypair method
"""
# The minimum and maximum versions of the API supported
@ -46,7 +48,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.1"
_MAX_API_VERSION = "2.2"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -107,11 +107,8 @@ class KeypairController(object):
authorize(context, action='show')
try:
# Since this method returns the whole object, functional test
# test_keypairs_get is failing, receiving an unexpected field
# 'type', which was added to the keypair object.
# TODO(claudiub): Revert the changes in the next commit, which will
# enable nova-api to return the keypair type.
# The return object needs to be a dict in order to pop the 'type'
# field, since it is incompatible with API version <= 2.1.
keypair = self.api.get_key_pair(context, context.user_id, id)
keypair = self._filter_keypair(keypair, created_at=True,
deleted=True, deleted_at=True,

View File

@ -25,6 +25,7 @@ from nova.api import validation
from nova.compute import api as compute_api
from nova import exception
from nova.i18n import _
from nova.objects import keypair as keypair_obj
ALIAS = 'os-keypairs'
@ -39,6 +40,8 @@ class KeypairController(wsgi.Controller):
self.api = compute_api.KeypairAPI()
def _filter_keypair(self, keypair, **attrs):
# TODO(claudiub): After v2 and v2.1 is no longer supported,
# keypair_type can be added to the clean dict below
clean = {
'name': keypair.name,
'public_key': keypair.public_key,
@ -48,9 +51,28 @@ class KeypairController(wsgi.Controller):
clean[attr] = keypair[attr]
return clean
# TODO(oomichi): Here should be 201(Created) instead of 200 by v2.1
# +microversions because the keypair creation finishes when returning
# a response.
@wsgi.Controller.api_version("2.2")
@wsgi.response(201)
@extensions.expected_errors((400, 403, 409))
@validation.schema(keypairs.create_v22)
def create(self, req, body):
"""Create or import keypair.
Sending name will generate a key and return private_key
and fingerprint.
Keypair will have the type ssh or x509, specified by key_type.
You can send a public_key to add an existing ssh/x509 key.
params: keypair object with:
name (required) - string
public_key (optional) - string
key_type (optional) - string
"""
return self._create(req, body, type=True)
@wsgi.Controller.api_version("2.1", "2.1") # noqa
@extensions.expected_errors((400, 403, 409))
@validation.schema(keypairs.create)
def create(self, req, body):
@ -59,29 +81,34 @@ class KeypairController(wsgi.Controller):
Sending name will generate a key and return private_key
and fingerprint.
You can send a public_key to add an existing ssh key
You can send a public_key to add an existing ssh key.
params: keypair object with:
name (required) - string
public_key (optional) - string
"""
return self._create(req, body)
def _create(self, req, body, **keypair_filters):
context = req.environ['nova.context']
authorize(context, action='create')
params = body['keypair']
name = params['name']
key_type = params.get('key_type', keypair_obj.KEYPAIR_TYPE_SSH)
try:
if 'public_key' in params:
keypair = self.api.import_key_pair(context,
context.user_id, name,
params['public_key'])
keypair = self._filter_keypair(keypair, user_id=True)
params['public_key'], key_type)
keypair = self._filter_keypair(keypair, user_id=True,
**keypair_filters)
else:
keypair, private_key = self.api.create_key_pair(
context, context.user_id, name)
keypair = self._filter_keypair(keypair, user_id=True)
context, context.user_id, name, key_type)
keypair = self._filter_keypair(keypair, user_id=True,
**keypair_filters)
keypair['private_key'] = private_key
return {'keypair': keypair}
@ -94,12 +121,19 @@ class KeypairController(wsgi.Controller):
except exception.KeyPairExists as exc:
raise webob.exc.HTTPConflict(explanation=exc.format_message())
# TODO(oomichi): Here should be 204(No Content) instead of 202 by v2.1
# +microversions because the resource keypair has been deleted completely
# when returning a response.
@wsgi.Controller.api_version("2.1", "2.1")
@wsgi.response(202)
@extensions.expected_errors(404)
def delete(self, req, id):
self._delete(req, id)
@wsgi.Controller.api_version("2.2") # noqa
@wsgi.response(204)
@extensions.expected_errors(404)
def delete(self, req, id):
self._delete(req, id)
def _delete(self, req, id):
"""Delete a keypair with a given name."""
context = req.environ['nova.context']
authorize(context, action='delete')
@ -108,23 +142,29 @@ class KeypairController(wsgi.Controller):
except exception.KeypairNotFound as exc:
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
@wsgi.Controller.api_version("2.2")
@extensions.expected_errors(404)
def show(self, req, id):
return self._show(req, id, type=True)
@wsgi.Controller.api_version("2.1", "2.1") # noqa
@extensions.expected_errors(404)
def show(self, req, id):
return self._show(req, id)
def _show(self, req, id, **keypair_filters):
"""Return data for the given key name."""
context = req.environ['nova.context']
authorize(context, action='show')
try:
# Since this method returns the whole object, functional test
# test_keypairs_get is failing, receiving an unexpected field
# 'type', which was added to the keypair object.
# TODO(claudiub): Revert the changes in the next commit, which will
# enable nova-api to return the keypair type.
# The return object needs to be a dict in order to pop the 'type'
# field, if the api_version < 2.2.
keypair = self.api.get_key_pair(context, context.user_id, id)
keypair = self._filter_keypair(keypair, created_at=True,
deleted=True, deleted_at=True,
id=True, user_id=True,
updated_at=True)
updated_at=True, **keypair_filters)
except exception.KeypairNotFound as exc:
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
# TODO(oomichi): It is necessary to filter a response of keypair with
@ -132,15 +172,25 @@ class KeypairController(wsgi.Controller):
# behaviors in this keypair resource.
return {'keypair': keypair}
@wsgi.Controller.api_version("2.2")
@extensions.expected_errors(())
def index(self, req):
return self._index(req, type=True)
@wsgi.Controller.api_version("2.1", "2.1") # noqa
@extensions.expected_errors(())
def index(self, req):
return self._index(req)
def _index(self, req, **keypair_filters):
"""List of keypairs for a user."""
context = req.environ['nova.context']
authorize(context, action='index')
key_pairs = self.api.get_key_pairs(context, context.user_id)
rval = []
for key_pair in key_pairs:
rval.append({'keypair': self._filter_keypair(key_pair)})
rval.append({'keypair': self._filter_keypair(key_pair,
**keypair_filters)})
return {'keypairs': rval}

View File

@ -32,6 +32,24 @@ create = {
'additionalProperties': False,
}
create_v22 = {
'type': 'object',
'properties': {
'keypair': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'type': {'type': 'string'},
'public_key': {'type': 'string'},
},
'required': ['name'],
'additionalProperties': False,
},
},
'required': ['keypair'],
'additionalProperties': False,
}
server_create = {
'key_name': parameter_types.name,
}

View File

@ -20,3 +20,18 @@ user documentation.
If no version is specified then the API will behave as if a version
request of v2.1 was requested.
- **2.2**
Added Keypair type.
A user can request the creation of a certain 'type' of keypair (ssh or x509)
in the os-keypairs plugin
If no keypair type is specified, then the default 'ssh' type of keypair is
created.
Fixes status code for os-keypairs create method from 200 to 201
Fixes status code for os-keypairs delete method from 202 to 204

View File

@ -3730,7 +3730,11 @@ class KeypairAPI(base.Base):
notify = self.get_notifier()
notify.info(context, 'keypair.%s' % event_suffix, payload)
def _validate_new_key_pair(self, context, user_id, key_name):
def _validate_new_key_pair(self, context, user_id, key_name, key_type):
if key_type is not keypair_obj.KEYPAIR_TYPE_SSH:
raise exception.InvalidKeypair(
reason=_('Specified Keypair type "%s" is invalid') % key_type)
safe_chars = "_- " + string.digits + string.ascii_letters
clean_value = "".join(x for x in key_name if x in safe_chars)
if clean_value != key_name:
@ -3752,9 +3756,10 @@ class KeypairAPI(base.Base):
raise exception.KeypairLimitExceeded()
@wrap_exception()
def import_key_pair(self, context, user_id, key_name, public_key):
def import_key_pair(self, context, user_id, key_name, public_key,
key_type=keypair_obj.KEYPAIR_TYPE_SSH):
"""Import a key pair using an existing public key."""
self._validate_new_key_pair(context, user_id, key_name)
self._validate_new_key_pair(context, user_id, key_name, key_type)
self._notify(context, 'import.start', key_name)
@ -3763,7 +3768,7 @@ class KeypairAPI(base.Base):
keypair = objects.KeyPair(context)
keypair.user_id = user_id
keypair.name = key_name
keypair.type = keypair_obj.KEYPAIR_TYPE_SSH
keypair.type = key_type
keypair.fingerprint = fingerprint
keypair.public_key = public_key
keypair.create()
@ -3773,9 +3778,10 @@ class KeypairAPI(base.Base):
return keypair
@wrap_exception()
def create_key_pair(self, context, user_id, key_name):
def create_key_pair(self, context, user_id, key_name,
key_type=keypair_obj.KEYPAIR_TYPE_SSH):
"""Create a new key pair."""
self._validate_new_key_pair(context, user_id, key_name)
self._validate_new_key_pair(context, user_id, key_name, key_type)
self._notify(context, 'create.start', key_name)
@ -3784,7 +3790,7 @@ class KeypairAPI(base.Base):
keypair = objects.KeyPair(context)
keypair.user_id = user_id
keypair.name = key_name
keypair.type = keypair_obj.KEYPAIR_TYPE_SSH
keypair.type = key_type
keypair.fingerprint = fingerprint
keypair.public_key = public_key
keypair.create()

View File

@ -270,6 +270,7 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
'public_key': 'ssh-rsa[ a-zA-Z0-9/+=]*'
'Generated-by-Nova',
'fingerprint': '([0-9a-f]{2}:){15}[0-9a-f]{2}',
'keypair_type': 'ssh|x509',
'host': self._get_host(),
'host_name': '[0-9a-z]{32}',
'glance_host': self._get_glance_host(),

View File

@ -0,0 +1,14 @@
{
"keypair": {
"public_key": "%(public_key)s",
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"fingerprint": "%(fingerprint)s",
"user_id": "fake",
"deleted": false,
"created_at": "%(strtime)s",
"updated_at": null,
"deleted_at": null,
"id": 1
}
}

View File

@ -0,0 +1,7 @@
{
"keypair": {
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"public_key": "%(public_key)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": "fake"
}
}

View File

@ -0,0 +1,12 @@
{
"keypairs": [
{
"keypair": {
"fingerprint": "%(fingerprint)s",
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"public_key": "%(public_key)s"
}
}
]
}

View File

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

View File

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

View File

@ -15,30 +15,44 @@
import uuid
from nova.objects import keypair as keypair_obj
from nova.tests.functional.v3 import api_sample_base
class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3):
request_api_version = None
sample_dir = "keypairs"
expected_delete_status_code = 202
expected_post_status_code = 200
def generalize_subs(self, subs, vanilla_regexes):
subs['keypair_name'] = 'keypair-[0-9a-f-]+'
return subs
def test_keypairs_post(self, public_key=None):
return self._check_keypairs_post(public_key,
api_version=self.request_api_version)
def _check_keypairs_post(self, public_key, **kwargs):
"""Get api sample of key pairs post request."""
key_name = 'keypair-' + str(uuid.uuid4())
response = self._do_post('os-keypairs', 'keypairs-post-req',
{'keypair_name': key_name})
subs = dict(keypair_name=key_name, **kwargs)
response = self._do_post('os-keypairs', 'keypairs-post-req', subs,
api_version=self.request_api_version)
subs = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name
self._verify_response('keypairs-post-resp', subs, response, 200)
self._verify_response('keypairs-post-resp', subs, response,
self.expected_post_status_code)
# NOTE(maurosr): return the key_name is necessary cause the
# verification returns the label of the last compared information in
# the response, not necessarily the key name.
return key_name
def test_keypairs_import_key_post(self):
self._check_keypairs_import_key_post()
def _check_keypairs_import_key_post(self, **kwargs):
# Get api sample of key pairs post to import user's key.
key_name = 'keypair-' + str(uuid.uuid4())
subs = {
@ -49,16 +63,19 @@ class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3):
"9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc"
"pSxsIbECHw== Generated-by-Nova"
}
subs.update(**kwargs)
response = self._do_post('os-keypairs', 'keypairs-import-post-req',
subs)
subs, api_version=self.request_api_version)
subs = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name
self._verify_response('keypairs-import-post-resp', subs, response, 200)
self._verify_response('keypairs-import-post-resp', subs, response,
self.expected_post_status_code)
def test_keypairs_list(self):
# Get api sample of key pairs list request.
key_name = self.test_keypairs_post()
response = self._do_get('os-keypairs')
response = self._do_get('os-keypairs',
api_version=self.request_api_version)
subs = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name
self._verify_response('keypairs-list-resp', subs, response, 200)
@ -66,7 +83,30 @@ class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3):
def test_keypairs_get(self):
# Get api sample of key pairs get request.
key_name = self.test_keypairs_post()
response = self._do_get('os-keypairs/%s' % key_name)
response = self._do_get('os-keypairs/%s' % key_name,
api_version=self.request_api_version)
subs = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name
self._verify_response('keypairs-get-resp', subs, response, 200)
def test_keypairs_delete(self):
# Get api sample of key pairs delete request.
key_name = self.test_keypairs_post()
response = self._do_delete('os-keypairs/%s' % key_name,
api_version=self.request_api_version)
self.assertEqual(self.expected_delete_status_code,
response.status_code)
class KeyPairsV22SampleJsonTest(KeyPairsSampleJsonTest):
request_api_version = '2.2'
expected_post_status_code = 201
expected_delete_status_code = 204
def test_keypairs_post(self, public_key=None):
return self._check_keypairs_post(
public_key, keypair_type=keypair_obj.KEYPAIR_TYPE_SSH)
def test_keypairs_import_key_post(self):
self._check_keypairs_import_key_post(
keypair_type=keypair_obj.KEYPAIR_TYPE_SSH)

View File

@ -18,6 +18,7 @@ import webob
from nova.api.openstack.compute.contrib import keypairs as keypairs_v2
from nova.api.openstack.compute.plugins.v3 import keypairs as keypairs_v21
from nova.api.openstack import wsgi as os_wsgi
from nova import db
from nova import exception
from nova.openstack.common import policy as common_policy
@ -62,6 +63,7 @@ def db_key_pair_create_duplicate(context, keypair):
class KeypairsTestV21(test.TestCase):
base_url = '/v2/fake'
validation_error = exception.ValidationError
wsgi_api_version = os_wsgi.DEFAULT_API_VERSION
def _setup_app_and_controller(self):
self.app_server = fakes.wsgi_app_v21(init_only=('os-keypairs',
@ -85,7 +87,7 @@ class KeypairsTestV21(test.TestCase):
osapi_compute_ext_list=['Keypairs'])
self._setup_app_and_controller()
self.req = fakes.HTTPRequest.blank('')
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)
def test_keypair_list(self):
res_dict = self.controller.index(self.req)
@ -97,6 +99,7 @@ class KeypairsTestV21(test.TestCase):
res_dict = self.controller.create(self.req, body=body)
self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
self.assertTrue(len(res_dict['keypair']['private_key']) > 0)
self._assert_keypair_type(res_dict)
def _test_keypair_create_bad_request_case(self,
body,
@ -161,6 +164,7 @@ class KeypairsTestV21(test.TestCase):
# FIXME(ja): sholud we check that public_key was sent to create?
self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
self.assertNotIn('private_key', res_dict['keypair'])
self._assert_keypair_type(res_dict)
def test_keypair_import_quota_limit(self):
@ -234,7 +238,8 @@ class KeypairsTestV21(test.TestCase):
def _db_key_pair_get(context, user_id, name):
return dict(test_keypair.fake_keypair,
name='foo', public_key='XXX', fingerprint='YYY')
name='foo', public_key='XXX', fingerprint='YYY',
type='ssh')
self.stubs.Set(db, "key_pair_get", _db_key_pair_get)
@ -242,6 +247,7 @@ class KeypairsTestV21(test.TestCase):
self.assertEqual('foo', res_dict['keypair']['name'])
self.assertEqual('XXX', res_dict['keypair']['public_key'])
self.assertEqual('YYY', res_dict['keypair']['fingerprint'])
self._assert_keypair_type(res_dict)
def test_keypair_show_not_found(self):
@ -283,6 +289,9 @@ class KeypairsTestV21(test.TestCase):
self.assertIn('key_name', server_dict)
self.assertEqual(server_dict['key_name'], '')
def _assert_keypair_type(self, res_dict):
self.assertNotIn('type', res_dict['keypair'])
class KeypairPolicyTestV21(test.TestCase):
KeyPairController = keypairs_v21.KeypairController()
@ -293,7 +302,8 @@ class KeypairPolicyTestV21(test.TestCase):
def _db_key_pair_get(context, user_id, name):
return dict(test_keypair.fake_keypair,
name='foo', public_key='XXX', fingerprint='YYY')
name='foo', public_key='XXX', fingerprint='YYY',
type='ssh')
self.stubs.Set(db, "key_pair_get",
_db_key_pair_get)
@ -365,15 +375,7 @@ class KeypairPolicyTestV21(test.TestCase):
rules = {self.policy_path + ':delete':
common_policy.parse_rule('')}
policy.set_rules(rules)
res = self.KeyPairController.delete(self.req, 'FAKE')
# NOTE: on v2.1, http status code is set as wsgi_code of API
# method instead of status_int in a response object.
if isinstance(self.KeyPairController, keypairs_v21.KeypairController):
status_int = self.KeyPairController.delete.wsgi_code
else:
status_int = res.status_int
self.assertEqual(202, status_int)
self.KeyPairController.delete(self.req, 'FAKE')
class KeypairsTestV2(KeypairsTestV21):
@ -384,6 +386,19 @@ class KeypairsTestV2(KeypairsTestV21):
self.controller = keypairs_v2.KeypairController()
class KeypairsTestV22(KeypairsTestV21):
wsgi_api_version = '2.2'
def test_keypair_list(self):
res_dict = self.controller.index(self.req)
expected = {'keypairs': [{'keypair': dict(keypair_data, name='FAKE',
type='ssh')}]}
self.assertEqual(expected, res_dict)
def _assert_keypair_type(self, res_dict):
self.assertEqual('ssh', res_dict['keypair']['type'])
class KeypairPolicyTestV2(KeypairPolicyTestV21):
KeyPairController = keypairs_v2.KeypairController()
policy_path = 'compute_extension:keypairs'

View File

@ -21,6 +21,7 @@ from nova.compute import api as compute_api
from nova import context
from nova import db
from nova import exception
from nova.objects import keypair as keypair_obj
from nova import quota
from nova.tests.unit.compute import test_compute
from nova.tests.unit import fake_notifier
@ -46,6 +47,7 @@ class KeypairAPITestCase(test_compute.BaseTestCase):
'HJAXVI+oCiyMMfffoTq16M1xfV58JstgtTqAXG+ZFpicGajREU'
'E/E3hO5MGgcHmyzIrWHKpe1n3oEGuz')
self.fingerprint = '4e:48:c6:a0:4a:f9:dd:b5:4c:85:54:5a:af:43:47:5a'
self.keypair_type = keypair_obj.KEYPAIR_TYPE_SSH
self.key_destroyed = False
def _keypair_db_call_stubs(self):
@ -108,12 +110,13 @@ class CreateImportSharedTestMixIn(object):
up by the test runner unless they are part of a 'concrete' test case.
"""
def assertKeyNameRaises(self, exc_class, expected_message, name):
def assertKeypairRaises(self, exc_class, expected_message, name):
func = getattr(self.keypair_api, self.func_name)
args = []
if self.func_name == 'import_key_pair':
args.append(self.pub_key)
args.append(self.keypair_type)
exc = self.assertRaises(exc_class, func, self.ctxt, self.ctxt.user_id,
name, *args)
@ -121,7 +124,7 @@ class CreateImportSharedTestMixIn(object):
def assertInvalidKeypair(self, expected_message, name):
msg = 'Keypair data is invalid: %s' % expected_message
self.assertKeyNameRaises(exception.InvalidKeypair, msg, name)
self.assertKeypairRaises(exception.InvalidKeypair, msg, name)
def test_name_too_short(self):
msg = ('Keypair name must be string and between 1 '
@ -137,6 +140,11 @@ class CreateImportSharedTestMixIn(object):
msg = "Keypair name contains unsafe characters"
self.assertInvalidKeypair(msg, '* BAD CHARACTERS! *')
def test_invalid_keypair_type(self):
self.keypair_type = 'fakey_type'
msg = 'Specified Keypair type "fakey_type" is invalid'
self.assertInvalidKeypair(msg, 'test')
def test_already_exists(self):
def db_key_pair_create_duplicate(context, keypair):
raise exception.KeyPairExists(key_name=keypair.get('name', ''))
@ -145,7 +153,7 @@ class CreateImportSharedTestMixIn(object):
msg = ("Key pair '%(key_name)s' already exists." %
{'key_name': self.existing_key_name})
self.assertKeyNameRaises(exception.KeyPairExists, msg,
self.assertKeypairRaises(exception.KeyPairExists, msg,
self.existing_key_name)
def test_quota_limit(self):
@ -155,7 +163,7 @@ class CreateImportSharedTestMixIn(object):
self.stubs.Set(QUOTAS, "count", fake_quotas_count)
msg = "Maximum number of key pairs exceeded"
self.assertKeyNameRaises(exception.KeypairLimitExceeded, msg, 'foo')
self.assertKeypairRaises(exception.KeypairLimitExceeded, msg, 'foo')
class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
@ -165,6 +173,7 @@ class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
keypair, private_key = self.keypair_api.create_key_pair(
self.ctxt, self.ctxt.user_id, 'foo')
self.assertEqual('foo', keypair['name'])
self.assertEqual(self.keypair_type, keypair['type'])
self._check_notifications()
@ -180,6 +189,7 @@ class ImportKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
self.assertEqual('foo', keypair['name'])
self.assertEqual(self.fingerprint, keypair['fingerprint'])
self.assertEqual(self.pub_key, keypair['public_key'])
self.assertEqual(self.keypair_type, keypair['type'])
self._check_notifications(action='import')
def test_bad_key_data(self):