Merge "Adding pagination support for backups"

This commit is contained in:
Jenkins
2014-01-09 16:06:28 +00:00
committed by Gerrit Code Review
16 changed files with 258 additions and 216 deletions

View File

@@ -27,9 +27,10 @@ import os
import six
from troveclient.openstack.common.apiclient import exceptions
from troveclient import utils
from troveclient import common
from troveclient.openstack.common.apiclient import exceptions
from troveclient.openstack.common.py3kcompat import urlutils
# Python 2.4 compat
try:
@@ -60,6 +61,21 @@ class Manager(utils.HookableMixin):
def __init__(self, api):
self.api = api
def _paginated(self, url, response_key, limit=None, marker=None):
resp, body = self.api.client.get(common.limit_url(url, limit, marker))
if not body:
raise Exception("Call to " + url + " did not return a body.")
links = body.get('links', [])
next_links = [link['href'] for link in links if link['rel'] == 'next']
next_marker = None
for link in next_links:
# Extract the marker from the url.
parsed_url = urlutils.urlparse(link)
query_dict = dict(urlutils.parse_qsl(parsed_url.query))
next_marker = query_dict.get('marker')
data = [self.resource_class(self, res) for res in body[response_key]]
return common.Paginated(data, next_marker=next_marker, links=links)
def _list(self, url, response_key, obj_class=None, body=None):
resp = None
if body:

View File

@@ -0,0 +1,101 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Rackspace Hosting
#
# 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.
import testtools
import mock
import uuid
from troveclient.v1 import backups
"""
Unit tests for backups.py
"""
class BackupTest(testtools.TestCase):
def setUp(self):
super(BackupTest, self).setUp()
self.backup_id = str(uuid.uuid4())
self.info = {'name': 'my backup', 'id': self.backup_id}
self.api = mock.Mock()
self.manager = backups.Backups(self.api)
self.backup = backups.Backup(self.manager, self.info)
def tearDown(self):
super(BackupTest, self).tearDown()
def test___repr__(self):
self.assertEqual(repr(self.backup), '<Backup: my backup>')
class BackupManagerTest(testtools.TestCase):
def setUp(self):
super(BackupManagerTest, self).setUp()
self.backups = backups.Backups(mock.Mock())
def tearDown(self):
super(BackupManagerTest, self).tearDown()
def test_create(self):
create_mock = mock.Mock()
self.backups._create = create_mock
args = {'name': 'test_backup', 'instance': '1'}
body = {'backup': args}
self.backups.create(**args)
create_mock.assert_called_with('/backups', body, 'backup')
def test_create_description(self):
create_mock = mock.Mock()
self.backups._create = create_mock
args = {'name': 'test_backup', 'instance': '1', 'description': 'foo'}
body = {'backup': args}
self.backups.create(**args)
create_mock.assert_called_with('/backups', body, 'backup')
def test_list(self):
page_mock = mock.Mock()
self.backups._paginated = page_mock
limit = "test-limit"
marker = "test-marker"
self.backups.list(limit, marker)
page_mock.assert_called_with("/backups", "backups", limit, marker)
def test_get(self):
get_mock = mock.Mock()
self.backups._get = get_mock
self.backups.get(1)
get_mock.assert_called_with('/backups/1', 'backup')
def test_delete(self):
resp = mock.Mock()
resp.status_code = 200
delete_mock = mock.Mock(return_value=(resp, None))
self.backups.api.client.delete = delete_mock
self.backups.delete('backup1')
delete_mock.assert_called_with('/backups/backup1')
def test_delete_500(self):
resp = mock.Mock()
resp.status_code = 500
self.backups.api.client.delete = mock.Mock(return_value=(resp, None))
self.assertRaises(Exception, self.backups.delete, 'backup1')
def test_delete_422(self):
resp = mock.Mock()
resp.status_code = 422
self.backups.api.client.delete = mock.Mock(return_value=(resp, None))
self.assertRaises(Exception, self.backups.delete, 'backup1')

View File

@@ -25,6 +25,7 @@ import mock
from troveclient import base
from troveclient.openstack.common.apiclient import exceptions
from troveclient import common
from troveclient import utils
"""
@@ -254,6 +255,66 @@ class ManagerListTest(ManagerTest):
self.assertEqual(len(data_), len(l))
class MangerPaginationTests(ManagerTest):
def setUp(self):
super(MangerPaginationTests, self).setUp()
self.manager = base.Manager()
self.manager.api = mock.Mock()
self.manager.api.client = mock.Mock()
self.manager.resource_class = base.Resource
self.response_key = "response_key"
self.data = [{"foo": "p1"}, {"foo": "p2"}]
self.next_data = [{"foo": "p3"}, {"foo": "p4"}]
self.marker = 'test-marker'
self.limit = '20'
self.url = "http://test_url"
self.next_url = '%s?marker=%s&limit=%s' % (self.url, self.marker,
self.limit)
self.links = [{'href': self.next_url, 'rel': 'next'}]
self.body = {
self.response_key: self.data,
'links': self.links
}
self.next_body = {self.response_key: self.next_data}
def side_effect(url):
if url == self.url:
return (None, self.body)
if url == self.next_url:
return (None, self.next_body)
self.manager.api.client.get = mock.Mock(side_effect=side_effect)
def tearDown(self):
super(MangerPaginationTests, self).tearDown()
def test_pagination(self):
resp = self.manager._paginated(self.url, self.response_key)
self.manager.api.client.get.assert_called_with(self.url)
self.assertEqual(resp.items[0].foo, 'p1')
self.assertEqual(resp.items[1].foo, 'p2')
self.assertEqual(resp.next, self.marker)
self.assertEqual(resp.links, self.links)
self.assertTrue(isinstance(resp, common.Paginated))
def test_pagination_next(self):
resp = self.manager._paginated(self.url, self.response_key,
limit=self.limit, marker=self.marker)
self.manager.api.client.get.assert_called_with(self.next_url)
self.assertEqual(resp.items[0].foo, 'p3')
self.assertEqual(resp.items[1].foo, 'p4')
self.assertEqual(resp.next, None)
self.assertEqual(resp.links, [])
self.assertTrue(isinstance(resp, common.Paginated))
def test_pagination_error(self):
self.manager.api.client.get = mock.Mock(return_value=(None, None))
self.assertRaises(Exception, self.manager._paginated,
self.url, self.response_key)
class FakeResource(object):
def __init__(self, _id, properties):
self.id = _id

View File

@@ -35,9 +35,7 @@ class CommonTest(testtools.TestCase):
def test_limit_url(self):
url = "test-url"
limit = None
marker = None
self.assertEqual(url, common.limit_url(url))
self.assertEqual(url, common.limit_url(url, limit=None, marker=None))
limit = "test-limit"
marker = "test-marker"

View File

@@ -68,14 +68,15 @@ class DatastoresTest(testtools.TestCase):
base.getid = self.orig_base_getid
def test_list(self):
def side_effect_func(path, inst, limit, marker):
return path, inst, limit, marker
self.datastores._list = mock.Mock(side_effect=side_effect_func)
page_mock = mock.Mock()
self.datastores._paginated = page_mock
limit = "test-limit"
marker = "test-marker"
expected = ("/datastores", "datastores", limit, marker)
self.assertEqual(expected, self.datastores.list(limit, marker))
self.datastores.list(limit, marker)
page_mock.assert_called_with("/datastores", "datastores",
limit, marker)
self.datastores.list()
page_mock.assert_called_with("/datastores", "datastores", None, None)
def test_get(self):
def side_effect_func(path, inst):
@@ -108,16 +109,13 @@ class DatastoreVersionsTest(testtools.TestCase):
base.getid = self.orig_base_getid
def test_list(self):
def side_effect_func(path, inst, limit, marker):
return path, inst, limit, marker
self.datastore_versions._list = mock.Mock(side_effect=side_effect_func)
page_mock = mock.Mock()
self.datastore_versions._paginated = page_mock
limit = "test-limit"
marker = "test-marker"
expected = ("/datastores/datastore1/versions",
"versions", limit, marker)
self.assertEqual(expected, self.datastore_versions.list(
"datastore1", limit, marker))
self.datastore_versions.list("datastore1", limit, marker)
page_mock.assert_called_with("/datastores/datastore1/versions",
"versions", limit, marker)
def test_get(self):
def side_effect_func(path, inst):

View File

@@ -104,30 +104,13 @@ class InstancesTest(testtools.TestCase):
b["instance"]["datastore"]["version"])
self.assertEqual(103, b["instance"]["flavorRef"])
def test__list(self):
self.instances.api.client.get = mock.Mock(return_value=('resp', None))
self.assertRaises(Exception, self.instances._list, "url", None)
body = mock.Mock()
body.get = mock.Mock(
return_value=[{'href': 'http://test.net/test_file',
'rel': 'next'}]
)
body.__getitem__ = mock.Mock(return_value='instance1')
#self.instances.resource_class = mock.Mock(return_value="instance-1")
self.instances.api.client.get = mock.Mock(return_value=('resp', body))
_expected = [{'href': 'http://test.net/test_file', 'rel': 'next'}]
self.assertEqual(_expected, self.instances._list("url", None).links)
def test_list(self):
def side_effect_func(path, inst, limit, marker):
return path, inst, limit, marker
self.instances._list = mock.Mock(side_effect=side_effect_func)
page_mock = mock.Mock()
self.instances._paginated = page_mock
limit = "test-limit"
marker = "test-marker"
expected = ("/instances", "instances", limit, marker)
self.assertEqual(expected, self.instances.list(limit, marker))
self.instances.list(limit, marker)
page_mock.assert_called_with("/instances", "instances", limit, marker)
def test_get(self):
def side_effect_func(path, inst):

View File

@@ -68,21 +68,6 @@ class ManagementTest(testtools.TestCase):
management.RootHistory.__init__ = self.orig_hist__init
base.getid = self.orig_base_getid
def test__list(self):
self.management.api.client.get = mock.Mock(return_value=('resp', None))
self.assertRaises(Exception, self.management._list, "url", None)
body = mock.Mock()
body.get = mock.Mock(
return_value=[{'href': 'http://test.net/test_file',
'rel': 'next'}]
)
body.__getitem__ = mock.Mock(return_value='instance1')
self.management.resource_class = mock.Mock(return_value="instance-1")
self.management.api.client.get = mock.Mock(return_value=('resp', body))
_expected = [{'href': 'http://test.net/test_file', 'rel': 'next'}]
self.assertEqual(_expected, self.management._list("url", None).links)
def test_show(self):
def side_effect_func(path, instance):
return path, instance
@@ -91,14 +76,17 @@ class ManagementTest(testtools.TestCase):
self.assertEqual(('/mgmt/instances/instance1', 'instance'), (p, i))
def test_index(self):
def side_effect_func(url, name, limit, marker):
return url
self.management._list = mock.Mock(side_effect=side_effect_func)
self.assertEqual('/mgmt/instances?deleted=true',
self.management.index(deleted=True))
self.assertEqual('/mgmt/instances?deleted=false',
self.management.index(deleted=False))
page_mock = mock.Mock()
self.management._paginated = page_mock
self.management.index(deleted=True)
page_mock.assert_called_with('/mgmt/instances?deleted=true',
'instances', None, None)
self.management.index(deleted=False)
page_mock.assert_called_with('/mgmt/instances?deleted=false',
'instances', None, None)
self.management.index(deleted=True, limit=10, marker="foo")
page_mock.assert_called_with('/mgmt/instances?deleted=true',
'instances', 10, "foo")
def test_root_enabled_history(self):
self.management.api.client.get = mock.Mock(return_value=('resp', None))

View File

@@ -117,30 +117,14 @@ class UsersTest(testtools.TestCase):
self._resp.status_code = 400
self.assertRaises(Exception, self.users.delete, 34, 'user1')
def test__list(self):
def side_effect_func(self, val):
return val
key = 'key'
body = mock.Mock()
body.get = mock.Mock(
return_value=[{'href': 'http://test.net/test_file',
'rel': 'next'}]
)
body.__getitem__ = mock.Mock(return_value=["test-value"])
resp = mock.Mock()
resp.status_code = 200
self.users.resource_class = mock.Mock(side_effect=side_effect_func)
self.users.api.client.get = mock.Mock(return_value=(resp, body))
self.assertEqual(["test-value"], self.users._list('url', key).items)
self.users.api.client.get = mock.Mock(return_value=(resp, None))
self.assertRaises(Exception, self.users._list, 'url', None)
def test_list(self):
def side_effect_func(path, user, limit, marker):
return path
self.users._list = mock.Mock(side_effect=side_effect_func)
self.assertEqual('/instances/instance1/users', self.users.list(1))
page_mock = mock.Mock()
self.users._paginated = page_mock
self.users.list(1)
page_mock.assert_called_with('/instances/instance1/users',
'users', None, None)
limit = 'test-limit'
marker = 'test-marker'
self.users.list(1, limit, marker)
page_mock.assert_called_with('/instances/instance1/users',
'users', limit, marker)

View File

@@ -51,7 +51,7 @@ class Backups(base.ManagerWithFind):
:rtype: list of :class:`Backups`.
"""
return self._list("/backups", "backups", limit, marker)
return self._paginated("/backups", "backups", limit, marker)
def create(self, name, instance, description=None):
"""

View File

@@ -18,7 +18,6 @@
from troveclient import base
from troveclient import common
from troveclient.openstack.common.py3kcompat import urlutils
class Database(base.Resource):
@@ -52,34 +51,14 @@ class Databases(base.ManagerWithFind):
resp, body = self.api.client.delete(url)
common.check_for_exceptions(resp, body)
def _list(self, url, response_key, limit=None, marker=None):
resp, body = self.api.client.get(common.limit_url(url, limit, marker))
common.check_for_exceptions(resp, body)
if not body:
raise Exception("Call to " + url +
" did not return a body.")
links = body.get('links', [])
next_links = [link['href'] for link in links if link['rel'] == 'next']
next_marker = None
for link in next_links:
# Extract the marker from the url.
parsed_url = urlutils.urlparse(link)
query_dict = dict(urlutils.parse_qsl(parsed_url.query))
next_marker = query_dict.get('marker', None)
databases = body[response_key]
databases = [self.resource_class(self, res) for res in databases]
return common.Paginated(
databases, next_marker=next_marker, links=links
)
def list(self, instance, limit=None, marker=None):
"""
Get a list of all Databases from the instance.
:rtype: list of :class:`Database`.
"""
return self._list("/instances/%s/databases" % base.getid(instance),
"databases", limit, marker)
url = "/instances/%s/databases" % base.getid(instance)
return self._paginated(url, "databases", limit, marker)
# def get(self, instance, database):
# """

View File

@@ -46,7 +46,7 @@ class Datastores(base.ManagerWithFind):
:rtype: list of :class:`Datastore`.
"""
return self._list("/datastores", "datastores", limit, marker)
return self._paginated("/datastores", "datastores", limit, marker)
def get(self, datastore):
"""
@@ -73,8 +73,8 @@ class DatastoreVersions(base.ManagerWithFind):
:rtype: list of :class:`DatastoreVersion`.
"""
return self._list("/datastores/%s/versions" % datastore,
"versions", limit, marker)
return self._paginated("/datastores/%s/versions" % datastore,
"versions", limit, marker)
def get(self, datastore, datastore_version):
"""

View File

@@ -19,11 +19,9 @@
from troveclient import base
from troveclient import common
from troveclient.openstack.common.apiclient import exceptions
from troveclient.openstack.common.py3kcompat import urlutils
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
REBOOT_SOFT = 'SOFT'
REBOOT_HARD = 'HARD'
class Instance(base.Resource):
@@ -85,31 +83,13 @@ class Instances(base.ManagerWithFind):
return self._create("/instances", body, "instance")
def _list(self, url, response_key, limit=None, marker=None):
resp, body = self.api.client.get(common.limit_url(url, limit, marker))
if not body:
raise Exception("Call to " + url + " did not return a body.")
links = body.get('links', [])
next_links = [link['href'] for link in links if link['rel'] == 'next']
next_marker = None
for link in next_links:
# Extract the marker from the url.
parsed_url = urlutils.urlparse(link)
query_dict = dict(urlutils.parse_qsl(parsed_url.query))
next_marker = query_dict.get('marker', None)
instances = body[response_key]
instances = [self.resource_class(self, res) for res in instances]
return common.Paginated(
instances, next_marker=next_marker, links=links
)
def list(self, limit=None, marker=None):
"""
Get a list of all instances.
:rtype: list of :class:`Instance`.
"""
return self._list("/instances", "instances", limit, marker)
return self._paginated("/instances", "instances", limit, marker)
def get(self, instance):
"""
@@ -120,14 +100,14 @@ class Instances(base.ManagerWithFind):
return self._get("/instances/%s" % base.getid(instance),
"instance")
def backups(self, instance):
def backups(self, instance, limit=None, marker=None):
"""
Get the list of backups for a specific instance.
:rtype: list of :class:`Backups`.
"""
return self._list("/instances/%s/backups" % base.getid(instance),
"backups")
url = "/instances/%s/backups" % base.getid(instance)
return self._paginated(url, "backups", limit, marker)
def delete(self, instance):
"""
@@ -137,8 +117,7 @@ class Instances(base.ManagerWithFind):
"""
resp, body = self.api.client.delete("/instances/%s" %
base.getid(instance))
if resp.status_code in (422, 500):
raise exceptions.from_response(resp, body)
common.check_for_exceptions(resp, body)
def _action(self, instance_id, body):
"""

View File

@@ -18,7 +18,6 @@
from troveclient import base
from troveclient import common
from troveclient.openstack.common.py3kcompat import urlutils
from troveclient.v1 import instances
from troveclient.v1 import flavors
@@ -39,24 +38,6 @@ class Management(base.ManagerWithFind):
def list(self):
pass
def _list(self, url, response_key, limit=None, marker=None):
resp, body = self.api.client.get(common.limit_url(url, limit, marker))
if not body:
raise Exception("Call to " + url + " did not return a body.")
links = body.get('links', [])
next_links = [link['href'] for link in links if link['rel'] == 'next']
next_marker = None
for link in next_links:
# Extract the marker from the url.
parsed_url = urlutils.urlparse(link)
query_dict = dict(urlutils.parse_qsl(parsed_url.query))
next_marker = query_dict.get('marker', None)
instances = body[response_key]
instances = [self.resource_class(self, res) for res in instances]
return common.Paginated(
instances, next_marker=next_marker, links=links
)
def show(self, instance):
"""
Get details of one instance.
@@ -82,7 +63,7 @@ class Management(base.ManagerWithFind):
form = "?deleted=false"
url = "/mgmt/instances%s" % form
return self._list(url, "instances", limit, marker)
return self._paginated(url, "instances", limit, marker)
def root_enabled_history(self, instance):
"""

View File

@@ -19,8 +19,6 @@
from troveclient import base
from troveclient import common
from troveclient.openstack.common.apiclient import exceptions
from troveclient.openstack.common.py3kcompat import urlutils
class SecurityGroup(base.Resource):
@@ -37,32 +35,14 @@ class SecurityGroups(base.ManagerWithFind):
"""
resource_class = SecurityGroup
def _list(self, url, response_key, limit=None, marker=None):
resp, body = self.api.client.get(common.limit_url(url, limit, marker))
if not body:
raise Exception("Call to " + url + " did not return a body.")
links = body.get('links', [])
next_links = [link['href'] for link in links if link['rel'] == 'next']
next_marker = None
for link in next_links:
# Extract the marker from the url.
parsed_url = urlutils.urlparse(link)
query_dict = dict(urlutils.parse_qsl(parsed_url.query))
next_marker = query_dict.get('marker', None)
instances = body[response_key]
instances = [self.resource_class(self, res) for res in instances]
return common.Paginated(
instances, next_marker=next_marker, links=links
)
def list(self, limit=None, marker=None):
"""
Get a list of all security groups.
:rtype: list of :class:`SecurityGroup`.
"""
return self._list("/security-groups", "security_groups", limit,
marker)
return self._paginated("/security-groups", "security_groups",
limit, marker)
def get(self, security_group):
"""
@@ -118,8 +98,7 @@ class SecurityGroupRules(base.ManagerWithFind):
"""
resp, body = self.api.client.delete("/security-group-rules/%s" %
base.getid(security_group_rule))
if resp.status_code in (422, 500):
raise exceptions.from_response(resp, body)
common.check_for_exceptions(resp, body)
# Appease the abc gods
def list(self):

View File

@@ -240,21 +240,36 @@ def do_backup_show(cs, args):
_print_instance(backup)
@utils.arg('--limit', metavar='<limit>',
default=None,
help='Return up to N number of the most recent backups.')
@utils.arg('instance', metavar='<instance>', help='ID of the instance.')
@utils.service_type('database')
def do_backup_list_instance(cs, args):
"""List available backups for an instance."""
backups = cs.instances.backups(args.instance)
utils.print_list(backups, ['id', 'instance_id',
'name', 'description', 'status'])
wrapper = cs.instances.backups(args.instance, limit=args.limit)
backups = wrapper.items
while wrapper.next and not args.limit:
wrapper = cs.instances.backups(args.instance, marker=wrapper.next)
backups += wrapper.items
utils.print_list(backups, ['id', 'name', 'status', 'updated'],
order_by='updated')
@utils.arg('--limit', metavar='<limit>',
default=None,
help='Return up to N number of the most recent backups.')
@utils.service_type('database')
def do_backup_list(cs, args):
"""List available backups."""
backups = cs.backups.list()
utils.print_list(backups, ['id', 'instance_id',
'name', 'description', 'status'])
wrapper = cs.backups.list(limit=args.limit)
backups = wrapper.items
while wrapper.next and not args.limit:
wrapper = cs.backups.list(marker=wrapper.next)
backups += wrapper.items
utils.print_list(backups, ['id', 'instance_id', 'name',
'status', 'updated'],
order_by='updated')
@utils.arg('backup', metavar='<backup>', help='ID of the backup.')

View File

@@ -19,7 +19,6 @@
from troveclient import base
from troveclient.v1 import databases
from troveclient import common
from troveclient.openstack.common.py3kcompat import urlutils
class User(base.Resource):
@@ -52,33 +51,14 @@ class Users(base.ManagerWithFind):
resp, body = self.api.client.delete(url)
common.check_for_exceptions(resp, body)
def _list(self, url, response_key, limit=None, marker=None):
resp, body = self.api.client.get(common.limit_url(url, limit, marker))
common.check_for_exceptions(resp, body)
if not body:
raise Exception("Call to " + url +
" did not return a body.")
links = body.get('links', [])
next_links = [link['href'] for link in links if link['rel'] == 'next']
next_marker = None
for link in next_links:
# Extract the marker from the url.
parsed_url = urlutils.urlparse(link)
query_dict = dict(urlutils.parse_qsl(parsed_url.query))
next_marker = query_dict.get('marker', None)
users = [self.resource_class(self, res) for res in body[response_key]]
return common.Paginated(
users, next_marker=next_marker, links=links
)
def list(self, instance, limit=None, marker=None):
"""
Get a list of all Users from the instance's Database.
:rtype: list of :class:`User`.
"""
return self._list("/instances/%s/users" % base.getid(instance),
"users", limit, marker)
url = "/instances/%s/users" % base.getid(instance)
return self._paginated(url, "users", limit, marker)
def get(self, instance_id, username, hostname=None):
"""