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: REST_API_VERSION_HISTORY = """REST API Version History:
* 2.1 - Initial version. Equivalent to v2.0 code * 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 # 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 # 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.1" _MAX_API_VERSION = "2.2"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@@ -107,11 +107,8 @@ class KeypairController(object):
authorize(context, action='show') authorize(context, action='show')
try: try:
# Since this method returns the whole object, functional test # The return object needs to be a dict in order to pop the 'type'
# test_keypairs_get is failing, receiving an unexpected field # field, since it is incompatible with API version <= 2.1.
# '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.
keypair = self.api.get_key_pair(context, context.user_id, id) keypair = self.api.get_key_pair(context, context.user_id, id)
keypair = self._filter_keypair(keypair, created_at=True, keypair = self._filter_keypair(keypair, created_at=True,
deleted=True, deleted_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.compute import api as compute_api
from nova import exception from nova import exception
from nova.i18n import _ from nova.i18n import _
from nova.objects import keypair as keypair_obj
ALIAS = 'os-keypairs' ALIAS = 'os-keypairs'
@@ -39,6 +40,8 @@ class KeypairController(wsgi.Controller):
self.api = compute_api.KeypairAPI() self.api = compute_api.KeypairAPI()
def _filter_keypair(self, keypair, **attrs): 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 = { clean = {
'name': keypair.name, 'name': keypair.name,
'public_key': keypair.public_key, 'public_key': keypair.public_key,
@@ -48,9 +51,28 @@ class KeypairController(wsgi.Controller):
clean[attr] = keypair[attr] clean[attr] = keypair[attr]
return clean return clean
# TODO(oomichi): Here should be 201(Created) instead of 200 by v2.1 @wsgi.Controller.api_version("2.2")
# +microversions because the keypair creation finishes when returning @wsgi.response(201)
# a response. @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)) @extensions.expected_errors((400, 403, 409))
@validation.schema(keypairs.create) @validation.schema(keypairs.create)
def create(self, req, body): def create(self, req, body):
@@ -59,29 +81,34 @@ class KeypairController(wsgi.Controller):
Sending name will generate a key and return private_key Sending name will generate a key and return private_key
and fingerprint. 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: params: keypair object with:
name (required) - string name (required) - string
public_key (optional) - string public_key (optional) - string
""" """
return self._create(req, body)
def _create(self, req, body, **keypair_filters):
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context, action='create') authorize(context, action='create')
params = body['keypair'] params = body['keypair']
name = params['name'] name = params['name']
key_type = params.get('key_type', keypair_obj.KEYPAIR_TYPE_SSH)
try: try:
if 'public_key' in params: if 'public_key' in params:
keypair = self.api.import_key_pair(context, keypair = self.api.import_key_pair(context,
context.user_id, name, context.user_id, name,
params['public_key']) params['public_key'], key_type)
keypair = self._filter_keypair(keypair, user_id=True) keypair = self._filter_keypair(keypair, user_id=True,
**keypair_filters)
else: else:
keypair, private_key = self.api.create_key_pair( keypair, private_key = self.api.create_key_pair(
context, context.user_id, name) context, context.user_id, name, key_type)
keypair = self._filter_keypair(keypair, user_id=True) keypair = self._filter_keypair(keypair, user_id=True,
**keypair_filters)
keypair['private_key'] = private_key keypair['private_key'] = private_key
return {'keypair': keypair} return {'keypair': keypair}
@@ -94,12 +121,19 @@ class KeypairController(wsgi.Controller):
except exception.KeyPairExists as exc: except exception.KeyPairExists as exc:
raise webob.exc.HTTPConflict(explanation=exc.format_message()) raise webob.exc.HTTPConflict(explanation=exc.format_message())
# TODO(oomichi): Here should be 204(No Content) instead of 202 by v2.1 @wsgi.Controller.api_version("2.1", "2.1")
# +microversions because the resource keypair has been deleted completely
# when returning a response.
@wsgi.response(202) @wsgi.response(202)
@extensions.expected_errors(404) @extensions.expected_errors(404)
def delete(self, req, id): 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.""" """Delete a keypair with a given name."""
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context, action='delete') authorize(context, action='delete')
@@ -108,23 +142,29 @@ class KeypairController(wsgi.Controller):
except exception.KeypairNotFound as exc: except exception.KeypairNotFound as exc:
raise webob.exc.HTTPNotFound(explanation=exc.format_message()) raise webob.exc.HTTPNotFound(explanation=exc.format_message())
@wsgi.Controller.api_version("2.2")
@extensions.expected_errors(404) @extensions.expected_errors(404)
def show(self, req, id): 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.""" """Return data for the given key name."""
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context, action='show') authorize(context, action='show')
try: try:
# Since this method returns the whole object, functional test # The return object needs to be a dict in order to pop the 'type'
# test_keypairs_get is failing, receiving an unexpected field # field, if the api_version < 2.2.
# '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.
keypair = self.api.get_key_pair(context, context.user_id, id) keypair = self.api.get_key_pair(context, context.user_id, id)
keypair = self._filter_keypair(keypair, created_at=True, keypair = self._filter_keypair(keypair, created_at=True,
deleted=True, deleted_at=True, deleted=True, deleted_at=True,
id=True, user_id=True, id=True, user_id=True,
updated_at=True) updated_at=True, **keypair_filters)
except exception.KeypairNotFound as exc: except exception.KeypairNotFound as exc:
raise webob.exc.HTTPNotFound(explanation=exc.format_message()) raise webob.exc.HTTPNotFound(explanation=exc.format_message())
# TODO(oomichi): It is necessary to filter a response of keypair with # 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. # behaviors in this keypair resource.
return {'keypair': keypair} return {'keypair': keypair}
@wsgi.Controller.api_version("2.2")
@extensions.expected_errors(()) @extensions.expected_errors(())
def index(self, req): 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.""" """List of keypairs for a user."""
context = req.environ['nova.context'] context = req.environ['nova.context']
authorize(context, action='index') authorize(context, action='index')
key_pairs = self.api.get_key_pairs(context, context.user_id) key_pairs = self.api.get_key_pairs(context, context.user_id)
rval = [] rval = []
for key_pair in key_pairs: 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} return {'keypairs': rval}

View File

@@ -32,6 +32,24 @@ create = {
'additionalProperties': False, '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 = { server_create = {
'key_name': parameter_types.name, '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 If no version is specified then the API will behave as if a version
request of v2.1 was requested. 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 = self.get_notifier()
notify.info(context, 'keypair.%s' % event_suffix, payload) 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 safe_chars = "_- " + string.digits + string.ascii_letters
clean_value = "".join(x for x in key_name if x in safe_chars) clean_value = "".join(x for x in key_name if x in safe_chars)
if clean_value != key_name: if clean_value != key_name:
@@ -3752,9 +3756,10 @@ class KeypairAPI(base.Base):
raise exception.KeypairLimitExceeded() raise exception.KeypairLimitExceeded()
@wrap_exception() @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.""" """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) self._notify(context, 'import.start', key_name)
@@ -3763,7 +3768,7 @@ class KeypairAPI(base.Base):
keypair = objects.KeyPair(context) keypair = objects.KeyPair(context)
keypair.user_id = user_id keypair.user_id = user_id
keypair.name = key_name keypair.name = key_name
keypair.type = keypair_obj.KEYPAIR_TYPE_SSH keypair.type = key_type
keypair.fingerprint = fingerprint keypair.fingerprint = fingerprint
keypair.public_key = public_key keypair.public_key = public_key
keypair.create() keypair.create()
@@ -3773,9 +3778,10 @@ class KeypairAPI(base.Base):
return keypair return keypair
@wrap_exception() @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.""" """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) self._notify(context, 'create.start', key_name)
@@ -3784,7 +3790,7 @@ class KeypairAPI(base.Base):
keypair = objects.KeyPair(context) keypair = objects.KeyPair(context)
keypair.user_id = user_id keypair.user_id = user_id
keypair.name = key_name keypair.name = key_name
keypair.type = keypair_obj.KEYPAIR_TYPE_SSH keypair.type = key_type
keypair.fingerprint = fingerprint keypair.fingerprint = fingerprint
keypair.public_key = public_key keypair.public_key = public_key
keypair.create() keypair.create()

View File

@@ -270,6 +270,7 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
'public_key': 'ssh-rsa[ a-zA-Z0-9/+=]*' 'public_key': 'ssh-rsa[ a-zA-Z0-9/+=]*'
'Generated-by-Nova', 'Generated-by-Nova',
'fingerprint': '([0-9a-f]{2}:){15}[0-9a-f]{2}', 'fingerprint': '([0-9a-f]{2}:){15}[0-9a-f]{2}',
'keypair_type': 'ssh|x509',
'host': self._get_host(), 'host': self._get_host(),
'host_name': '[0-9a-z]{32}', 'host_name': '[0-9a-z]{32}',
'glance_host': self._get_glance_host(), '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 import uuid
from nova.objects import keypair as keypair_obj
from nova.tests.functional.v3 import api_sample_base from nova.tests.functional.v3 import api_sample_base
class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3): class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3):
request_api_version = None
sample_dir = "keypairs" sample_dir = "keypairs"
expected_delete_status_code = 202
expected_post_status_code = 200
def generalize_subs(self, subs, vanilla_regexes): def generalize_subs(self, subs, vanilla_regexes):
subs['keypair_name'] = 'keypair-[0-9a-f-]+' subs['keypair_name'] = 'keypair-[0-9a-f-]+'
return subs return subs
def test_keypairs_post(self, public_key=None): 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.""" """Get api sample of key pairs post request."""
key_name = 'keypair-' + str(uuid.uuid4()) key_name = 'keypair-' + str(uuid.uuid4())
response = self._do_post('os-keypairs', 'keypairs-post-req', subs = dict(keypair_name=key_name, **kwargs)
{'keypair_name': key_name}) response = self._do_post('os-keypairs', 'keypairs-post-req', subs,
api_version=self.request_api_version)
subs = self._get_regexes() subs = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name 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 # NOTE(maurosr): return the key_name is necessary cause the
# verification returns the label of the last compared information in # verification returns the label of the last compared information in
# the response, not necessarily the key name. # the response, not necessarily the key name.
return key_name return key_name
def test_keypairs_import_key_post(self): 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. # Get api sample of key pairs post to import user's key.
key_name = 'keypair-' + str(uuid.uuid4()) key_name = 'keypair-' + str(uuid.uuid4())
subs = { subs = {
@@ -49,16 +63,19 @@ class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3):
"9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc" "9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc"
"pSxsIbECHw== Generated-by-Nova" "pSxsIbECHw== Generated-by-Nova"
} }
subs.update(**kwargs)
response = self._do_post('os-keypairs', 'keypairs-import-post-req', response = self._do_post('os-keypairs', 'keypairs-import-post-req',
subs) subs, api_version=self.request_api_version)
subs = self._get_regexes() subs = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name 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): def test_keypairs_list(self):
# Get api sample of key pairs list request. # Get api sample of key pairs list request.
key_name = self.test_keypairs_post() 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 = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name subs['keypair_name'] = '(%s)' % key_name
self._verify_response('keypairs-list-resp', subs, response, 200) self._verify_response('keypairs-list-resp', subs, response, 200)
@@ -66,7 +83,30 @@ class KeyPairsSampleJsonTest(api_sample_base.ApiSampleTestBaseV3):
def test_keypairs_get(self): def test_keypairs_get(self):
# Get api sample of key pairs get request. # Get api sample of key pairs get request.
key_name = self.test_keypairs_post() 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 = self._get_regexes()
subs['keypair_name'] = '(%s)' % key_name subs['keypair_name'] = '(%s)' % key_name
self._verify_response('keypairs-get-resp', subs, response, 200) 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.contrib import keypairs as keypairs_v2
from nova.api.openstack.compute.plugins.v3 import keypairs as keypairs_v21 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 db
from nova import exception from nova import exception
from nova.openstack.common import policy as common_policy 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): class KeypairsTestV21(test.TestCase):
base_url = '/v2/fake' base_url = '/v2/fake'
validation_error = exception.ValidationError validation_error = exception.ValidationError
wsgi_api_version = os_wsgi.DEFAULT_API_VERSION
def _setup_app_and_controller(self): def _setup_app_and_controller(self):
self.app_server = fakes.wsgi_app_v21(init_only=('os-keypairs', self.app_server = fakes.wsgi_app_v21(init_only=('os-keypairs',
@@ -85,7 +87,7 @@ class KeypairsTestV21(test.TestCase):
osapi_compute_ext_list=['Keypairs']) osapi_compute_ext_list=['Keypairs'])
self._setup_app_and_controller() 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): def test_keypair_list(self):
res_dict = self.controller.index(self.req) res_dict = self.controller.index(self.req)
@@ -97,6 +99,7 @@ class KeypairsTestV21(test.TestCase):
res_dict = self.controller.create(self.req, body=body) res_dict = self.controller.create(self.req, body=body)
self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
self.assertTrue(len(res_dict['keypair']['private_key']) > 0) self.assertTrue(len(res_dict['keypair']['private_key']) > 0)
self._assert_keypair_type(res_dict)
def _test_keypair_create_bad_request_case(self, def _test_keypair_create_bad_request_case(self,
body, body,
@@ -161,6 +164,7 @@ class KeypairsTestV21(test.TestCase):
# FIXME(ja): sholud we check that public_key was sent to create? # FIXME(ja): sholud we check that public_key was sent to create?
self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0) self.assertTrue(len(res_dict['keypair']['fingerprint']) > 0)
self.assertNotIn('private_key', res_dict['keypair']) self.assertNotIn('private_key', res_dict['keypair'])
self._assert_keypair_type(res_dict)
def test_keypair_import_quota_limit(self): def test_keypair_import_quota_limit(self):
@@ -234,7 +238,8 @@ class KeypairsTestV21(test.TestCase):
def _db_key_pair_get(context, user_id, name): def _db_key_pair_get(context, user_id, name):
return dict(test_keypair.fake_keypair, 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) 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('foo', res_dict['keypair']['name'])
self.assertEqual('XXX', res_dict['keypair']['public_key']) self.assertEqual('XXX', res_dict['keypair']['public_key'])
self.assertEqual('YYY', res_dict['keypair']['fingerprint']) self.assertEqual('YYY', res_dict['keypair']['fingerprint'])
self._assert_keypair_type(res_dict)
def test_keypair_show_not_found(self): def test_keypair_show_not_found(self):
@@ -283,6 +289,9 @@ class KeypairsTestV21(test.TestCase):
self.assertIn('key_name', server_dict) self.assertIn('key_name', server_dict)
self.assertEqual(server_dict['key_name'], '') self.assertEqual(server_dict['key_name'], '')
def _assert_keypair_type(self, res_dict):
self.assertNotIn('type', res_dict['keypair'])
class KeypairPolicyTestV21(test.TestCase): class KeypairPolicyTestV21(test.TestCase):
KeyPairController = keypairs_v21.KeypairController() KeyPairController = keypairs_v21.KeypairController()
@@ -293,7 +302,8 @@ class KeypairPolicyTestV21(test.TestCase):
def _db_key_pair_get(context, user_id, name): def _db_key_pair_get(context, user_id, name):
return dict(test_keypair.fake_keypair, 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", self.stubs.Set(db, "key_pair_get",
_db_key_pair_get) _db_key_pair_get)
@@ -365,15 +375,7 @@ class KeypairPolicyTestV21(test.TestCase):
rules = {self.policy_path + ':delete': rules = {self.policy_path + ':delete':
common_policy.parse_rule('')} common_policy.parse_rule('')}
policy.set_rules(rules) policy.set_rules(rules)
res = self.KeyPairController.delete(self.req, 'FAKE') 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)
class KeypairsTestV2(KeypairsTestV21): class KeypairsTestV2(KeypairsTestV21):
@@ -384,6 +386,19 @@ class KeypairsTestV2(KeypairsTestV21):
self.controller = keypairs_v2.KeypairController() 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): class KeypairPolicyTestV2(KeypairPolicyTestV21):
KeyPairController = keypairs_v2.KeypairController() KeyPairController = keypairs_v2.KeypairController()
policy_path = 'compute_extension:keypairs' 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 context
from nova import db from nova import db
from nova import exception from nova import exception
from nova.objects import keypair as keypair_obj
from nova import quota from nova import quota
from nova.tests.unit.compute import test_compute from nova.tests.unit.compute import test_compute
from nova.tests.unit import fake_notifier from nova.tests.unit import fake_notifier
@@ -46,6 +47,7 @@ class KeypairAPITestCase(test_compute.BaseTestCase):
'HJAXVI+oCiyMMfffoTq16M1xfV58JstgtTqAXG+ZFpicGajREU' 'HJAXVI+oCiyMMfffoTq16M1xfV58JstgtTqAXG+ZFpicGajREU'
'E/E3hO5MGgcHmyzIrWHKpe1n3oEGuz') 'E/E3hO5MGgcHmyzIrWHKpe1n3oEGuz')
self.fingerprint = '4e:48:c6:a0:4a:f9:dd:b5:4c:85:54:5a:af:43:47:5a' 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 self.key_destroyed = False
def _keypair_db_call_stubs(self): 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. 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) func = getattr(self.keypair_api, self.func_name)
args = [] args = []
if self.func_name == 'import_key_pair': if self.func_name == 'import_key_pair':
args.append(self.pub_key) args.append(self.pub_key)
args.append(self.keypair_type)
exc = self.assertRaises(exc_class, func, self.ctxt, self.ctxt.user_id, exc = self.assertRaises(exc_class, func, self.ctxt, self.ctxt.user_id,
name, *args) name, *args)
@@ -121,7 +124,7 @@ class CreateImportSharedTestMixIn(object):
def assertInvalidKeypair(self, expected_message, name): def assertInvalidKeypair(self, expected_message, name):
msg = 'Keypair data is invalid: %s' % expected_message 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): def test_name_too_short(self):
msg = ('Keypair name must be string and between 1 ' msg = ('Keypair name must be string and between 1 '
@@ -137,6 +140,11 @@ class CreateImportSharedTestMixIn(object):
msg = "Keypair name contains unsafe characters" msg = "Keypair name contains unsafe characters"
self.assertInvalidKeypair(msg, '* BAD 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 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', ''))
@@ -145,7 +153,7 @@ class CreateImportSharedTestMixIn(object):
msg = ("Key pair '%(key_name)s' already exists." % msg = ("Key pair '%(key_name)s' already exists." %
{'key_name': self.existing_key_name}) {'key_name': self.existing_key_name})
self.assertKeyNameRaises(exception.KeyPairExists, msg, self.assertKeypairRaises(exception.KeyPairExists, msg,
self.existing_key_name) self.existing_key_name)
def test_quota_limit(self): def test_quota_limit(self):
@@ -155,7 +163,7 @@ class CreateImportSharedTestMixIn(object):
self.stubs.Set(QUOTAS, "count", fake_quotas_count) self.stubs.Set(QUOTAS, "count", fake_quotas_count)
msg = "Maximum number of key pairs exceeded" msg = "Maximum number of key pairs exceeded"
self.assertKeyNameRaises(exception.KeypairLimitExceeded, msg, 'foo') self.assertKeypairRaises(exception.KeypairLimitExceeded, msg, 'foo')
class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn): class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
@@ -165,6 +173,7 @@ class CreateKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
keypair, private_key = self.keypair_api.create_key_pair( keypair, private_key = self.keypair_api.create_key_pair(
self.ctxt, self.ctxt.user_id, 'foo') self.ctxt, self.ctxt.user_id, 'foo')
self.assertEqual('foo', keypair['name']) self.assertEqual('foo', keypair['name'])
self.assertEqual(self.keypair_type, keypair['type'])
self._check_notifications() self._check_notifications()
@@ -180,6 +189,7 @@ class ImportKeypairTestCase(KeypairAPITestCase, CreateImportSharedTestMixIn):
self.assertEqual('foo', keypair['name']) self.assertEqual('foo', keypair['name'])
self.assertEqual(self.fingerprint, keypair['fingerprint']) self.assertEqual(self.fingerprint, keypair['fingerprint'])
self.assertEqual(self.pub_key, keypair['public_key']) self.assertEqual(self.pub_key, keypair['public_key'])
self.assertEqual(self.keypair_type, keypair['type'])
self._check_notifications(action='import') self._check_notifications(action='import')
def test_bad_key_data(self): def test_bad_key_data(self):