Microversion 2.35 adds keypairs pagination support

After this microversion Nova API allows to get several keypairs
with the help of new optional parameters 'limit' and 'marker'
which were added to GET /os-keypairs request.

Partial-Bug: #1599904

Implements blueprint: keypairs-pagination

Change-Id: Idd3757f5be90ec4af1bd1a7ca3f9c20319dbfd33
This commit is contained in:
Pavel Kholkin 2016-07-06 15:02:11 +03:00
parent 777386b4bf
commit 47358449d3
22 changed files with 350 additions and 13 deletions

View File

@ -23,6 +23,8 @@ Request
.. rest_parameters:: parameters.yaml
- user_id: keypair_user
- limit: keypair_limit
- marker: keypair_marker
Response
--------

View File

@ -477,6 +477,25 @@ ip_query:
in: query
required: false
type: string
keypair_limit:
description: |
Requests a page size of items. Returns a number of items up to a limit value.
Use the ``limit`` parameter to make an initial limited request and use the
last-seen item from the response as the ``marker`` parameter value in a
subsequent limited request.
in: query
required: false
type: integer
min_version: 2.35
keypair_marker:
description: |
The last-seen item. Use the ``limit`` parameter to make an initial limited
request and use the last-seen item from the response as the ``marker``
parameter value in a subsequent limited request.
in: query
required: false
type: string
min_version: 2.35
keypair_type_in:
in: query
required: false

View File

@ -0,0 +1,18 @@
{
"keypairs": [
{
"keypair": {
"fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd",
"name": "keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3",
"type": "ssh",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n"
}
}
],
"keypairs_links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/keypairs?limit=1&marker=keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3",
"rel": "next"
}
]
}

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-5d935425-31d5-48a7-a0f1-e76e9813f2c3",
"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,18 @@
{
"keypairs": [
{
"keypair": {
"fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd",
"name": "keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3",
"type": "ssh",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n"
}
}
],
"keypairs_links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/keypairs?user_id=user2&limit=1&marker=keypair-5d935425-31d5-48a7-a0f1-e76e9813f2c3",
"rel": "next"
}
]
}

View File

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

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-ab9ff2e6-a6d7-4915-a241-044c369c07f9",
"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

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.34",
"version": "2.35",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.34",
"version": "2.35",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -87,6 +87,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
os-Migratelive Action does not throw badRequest in case of
pre-checks failure. Verification result is available over
instance-actions.
* 2.35 - Adds keypairs pagination support.
"""
# The minimum and maximum versions of the API supported
@ -95,7 +96,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.34"
_MAX_API_VERSION = "2.35"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -18,8 +18,10 @@
import webob
import webob.exc
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute.schemas import keypairs
from nova.api.openstack.compute.views import keypairs as keypairs_view
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova.api import validation
@ -36,8 +38,12 @@ ALIAS = 'os-keypairs'
class KeypairController(wsgi.Controller):
"""Keypair API controller for the OpenStack API."""
_view_builder_class = keypairs_view.ViewBuilder
def __init__(self):
self.api = compute_api.KeypairAPI()
super(KeypairController, self).__init__()
def _filter_keypair(self, keypair, **attrs):
# TODO(claudiub): After v2 and v2.1 is no longer supported,
@ -221,7 +227,13 @@ class KeypairController(wsgi.Controller):
# behaviors in this keypair resource.
return {'keypair': keypair}
@wsgi.Controller.api_version("2.10")
@wsgi.Controller.api_version("2.35")
@extensions.expected_errors(400)
def index(self, req):
user_id = self._get_user_id(req)
return self._index(req, links=True, type=True, user_id=user_id)
@wsgi.Controller.api_version("2.10", "2.34") # noqa
@extensions.expected_errors(())
def index(self, req):
# handle optional user-id for admin only
@ -238,20 +250,38 @@ class KeypairController(wsgi.Controller):
def index(self, req):
return self._index(req)
def _index(self, req, user_id=None, **keypair_filters):
def _index(self, req, user_id=None, links=False, **keypair_filters):
"""List of keypairs for a user."""
context = req.environ['nova.context']
user_id = user_id or context.user_id
context.can(kp_policies.POLICY_ROOT % 'index',
target={'user_id': user_id,
'project_id': context.project_id})
key_pairs = self.api.get_key_pairs(context, user_id)
rval = []
for key_pair in key_pairs:
rval.append({'keypair': self._filter_keypair(key_pair,
**keypair_filters)})
return {'keypairs': rval}
if api_version_request.is_supported(req, min_version='2.35'):
limit, marker = common.get_limit_and_marker(req)
else:
limit = marker = None
try:
key_pairs = self.api.get_key_pairs(
context, user_id, limit=limit, marker=marker)
except exception.MarkerNotFound as e:
raise webob.exc.HTTPBadRequest(explanation=e.format_message())
key_pairs = [self._filter_keypair(key_pair, **keypair_filters)
for key_pair in key_pairs]
keypairs_list = [{'keypair': key_pair} for key_pair in key_pairs]
keypairs_dict = {'keypairs': keypairs_list}
if links:
keypairs_links = self._view_builder.get_links(req, key_pairs)
if keypairs_links:
keypairs_dict['keypairs_links'] = keypairs_links
return keypairs_dict
class Controller(wsgi.Controller):

View File

@ -0,0 +1,25 @@
# Copyright 2016 Mirantis Inc
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from nova.api.openstack import common
class ViewBuilder(common.ViewBuilder):
_collection_name = "keypairs"
def get_links(self, request, keypairs):
return self._get_collection_links(request, keypairs,
self._collection_name, 'name')

View File

@ -350,3 +350,14 @@ user documentation.
Checks in ``os-migrateLive`` before live-migration actually starts are now
made in background. ``os-migrateLive`` is not throwing `400 Bad Request` if
pre-live-migration checks fail.
2.35
----
Added pagination support for keypairs.
Optional parameters 'limit' and 'marker' were added to GET /os-keypairs
request, the default sort_key was changed to 'name' field as ASC order,
the generic request format is::
GET /os-keypairs?limit={limit}&marker={kp_name}

View File

@ -3981,9 +3981,10 @@ class KeypairAPI(base.Base):
objects.KeyPair.destroy_by_name(context, user_id, key_name)
self._notify(context, 'delete.end', key_name)
def get_key_pairs(self, context, user_id):
def get_key_pairs(self, context, user_id, limit=None, marker=None):
"""List key pairs."""
return objects.KeyPairList.get_by_user(context, user_id)
return objects.KeyPairList.get_by_user(
context, user_id, limit=limit, marker=marker)
def get_key_pair(self, context, user_id, key_name):
"""Get a keypair by name."""

View File

@ -0,0 +1,18 @@
{
"keypairs": [
{
"keypair": {
"fingerprint": "%(fingerprint)s",
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"public_key": "%(public_key)s"
}
}
],
"keypairs_links": [
{
"href": "%(versioned_compute_endpoint)s/keypairs?limit=1&marker=%(keypair_name)s",
"rel": "next"
}
]
}

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,18 @@
{
"keypairs": [
{
"keypair": {
"fingerprint": "%(fingerprint)s",
"name": "%(keypair_name)s",
"type": "%(keypair_type)s",
"public_key": "%(public_key)s"
}
}
],
"keypairs_links": [
{
"href": "%(versioned_compute_endpoint)s/keypairs?user_id=user2&limit=1&marker=%(keypair_name)s",
"rel": "next"
}
]
}

View File

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

View File

@ -210,3 +210,76 @@ class KeyPairsV210SampleJsonTestNotAdmin(KeyPairsV210SampleJsonTest):
response = self._do_post('os-keypairs', 'keypairs-post-req', subs)
self.assertEqual(403, response.status_code)
class KeyPairsV235SampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
ADMIN_API = True
sample_dir = "keypairs"
microversion = '2.35'
expected_post_status_code = 201
scenarios = [('v2_35', {'api_major_version': 'v2.1'})]
def setUp(self):
super(KeyPairsV235SampleJsonTest, self).setUp()
self.api.microversion = self.microversion
# TODO(pkholkin): this is only needed because we randomly choose the
# uuid each time.
def generalize_subs(self, subs, vanilla_regexes):
subs['keypair_name'] = 'keypair-[0-9a-f-]+'
return subs
def test_keypairs_post(self, user="admin", kp_name=None):
return self._check_keypairs_post(
keypair_type=keypair_obj.KEYPAIR_TYPE_SSH,
user_id=user, kp_name=kp_name)
def _check_keypairs_post(self, **kwargs):
"""Get api sample of key pairs post request."""
key_name = kwargs.pop('kp_name')
if not key_name:
key_name = 'keypair-' + str(uuid.uuid4())
subs = dict(keypair_name=key_name, **kwargs)
response = self._do_post('os-keypairs', 'keypairs-post-req', subs)
subs = {'keypair_name': key_name}
self._verify_response('keypairs-post-resp', subs, response,
self.expected_post_status_code)
return key_name
def test_keypairs_list(self):
# Get api sample of key pairs list request.
# sort key_pairs by name before paging
keypairs = sorted([self.test_keypairs_post() for i in range(3)])
response = self._do_get('os-keypairs?marker=%s&limit=1' % keypairs[1])
subs = {'keypair_name': keypairs[2]}
self._verify_response('keypairs-list-resp', subs, response, 200)
def test_keypairs_list_for_different_users(self):
# Get api sample of key pairs list request.
# create common kp_names for two users
kp_names = ['keypair-' + str(uuid.uuid4()) for i in range(3)]
# sort key_pairs by name before paging
keypairs_user1 = sorted([self.test_keypairs_post(
user="user1", kp_name=kp_name) for kp_name in kp_names])
keypairs_user2 = sorted([self.test_keypairs_post(
user="user2", kp_name=kp_name) for kp_name in kp_names])
# get all keypairs after the second for user1
response = self._do_get('os-keypairs?user_id=user1&marker=%s'
% keypairs_user1[1])
subs = {'keypair_name': keypairs_user1[2]}
self._verify_response(
'keypairs-list-user1-resp', subs, response, 200)
# get only one keypair after the second for user2
response = self._do_get('os-keypairs?user_id=user2&marker=%s&limit=1'
% keypairs_user2[1])
subs = {'keypair_name': keypairs_user2[2]}
self._verify_response(
'keypairs-list-user2-resp', subs, response, 200)

View File

@ -562,3 +562,43 @@ class KeypairsTestV210(KeypairsTestV22):
self.assertRaises(exception.PolicyNotAuthorized,
self.controller.create,
req, body=body)
class KeypairsTestV235(test.TestCase):
base_url = '/v2/fake'
wsgi_api_version = '2.35'
def _setup_app_and_controller(self):
self.app_server = fakes.wsgi_app_v21(init_only=('os-keypairs'))
self.controller = keypairs_v21.KeypairController()
def setUp(self):
super(KeypairsTestV235, self).setUp()
self._setup_app_and_controller()
@mock.patch("nova.db.key_pair_get_all_by_user")
def test_keypair_list_limit_and_marker(self, mock_kp_get):
mock_kp_get.side_effect = db_key_pair_get_all_by_user
req = fakes.HTTPRequest.blank(
self.base_url + '/os-keypairs?limit=3&marker=fake_marker',
version=self.wsgi_api_version, use_admin_context=True)
res_dict = self.controller.index(req)
mock_kp_get.assert_called_once_with(
req.environ['nova.context'], 'fake_user',
limit=3, marker='fake_marker')
response = {'keypairs': [{'keypair': dict(keypair_data, name='FAKE',
type='ssh')}]}
self.assertEqual(res_dict, response)
@mock.patch('nova.compute.api.KeypairAPI.get_key_pairs')
def test_keypair_list_limit_and_marker_invalid_marker(self, mock_kp_get):
mock_kp_get.side_effect = exception.MarkerNotFound(marker='unknown_kp')
req = fakes.HTTPRequest.blank(
self.base_url + '/os-keypairs?limit=3&marker=unknown_kp',
version=self.wsgi_api_version, use_admin_context=True)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req)

View File

@ -0,0 +1,5 @@
---
features:
- Added microversion v2.35 that adds pagination support for keypairs with
the help of new optional parameters 'limit' and 'marker' which were added
to GET /os-keypairs request.