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:
parent
21a422e426
commit
c836f425bc
14
doc/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json
Normal file
14
doc/v3/api_samples/keypairs/v2.2/keypairs-get-resp.json
Normal 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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
12
doc/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json
Normal file
12
doc/v3/api_samples/keypairs/v2.2/keypairs-list-resp.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
6
doc/v3/api_samples/keypairs/v2.2/keypairs-post-req.json
Normal file
6
doc/v3/api_samples/keypairs/v2.2/keypairs-post-req.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"keypair": {
|
||||
"name": "keypair-ab9ff2e6-a6d7-4915-a241-044c369c07f9",
|
||||
"type": "ssh"
|
||||
}
|
||||
}
|
10
doc/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json
Normal file
10
doc/v3/api_samples/keypairs/v2.2/keypairs-post-resp.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"keypair": {
|
||||
"name": "%(keypair_name)s",
|
||||
"type": "%(keypair_type)s",
|
||||
"public_key": "%(public_key)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"keypair": {
|
||||
"fingerprint": "%(fingerprint)s",
|
||||
"name": "%(keypair_name)s",
|
||||
"type": "%(keypair_type)s",
|
||||
"public_key": "%(public_key)s",
|
||||
"user_id": "fake"
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"keypairs": [
|
||||
{
|
||||
"keypair": {
|
||||
"fingerprint": "%(fingerprint)s",
|
||||
"name": "%(keypair_name)s",
|
||||
"type": "%(keypair_type)s",
|
||||
"public_key": "%(public_key)s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"keypair": {
|
||||
"name": "%(keypair_name)s",
|
||||
"type": "%(keypair_type)s"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user