diff -Nru python-troveclient-1.2.0/debian/changelog python-troveclient-1.3.0/debian/changelog --- python-troveclient-1.2.0/debian/changelog 2015-08-28 08:02:49.000000000 +0000 +++ python-troveclient-1.3.0/debian/changelog 2015-09-24 19:54:15.000000000 +0000 @@ -1,12 +1,21 @@ -python-troveclient (1:1.2.0-4) experimental; urgency=medium +python-troveclient (1:1.3.0-1~ubuntu15.10.1~ppa201509241554) wily; urgency=medium - [ Corey Bryant ] - * Add epoch to version to align with Ubuntu. + * No-change backport to wily + + -- Corey Bryant Thu, 24 Sep 2015 15:54:15 -0400 + +python-troveclient (1:1.3.0-1) UNRELEASED; urgency=medium + + * New upstream release. + * d/control: Align dependencies and versions with upstream. - [ Thomas Goirand ] - * Do not test with Python 3.5: it's broken. + -- Corey Bryant Thu, 24 Sep 2015 15:41:28 -0400 + +python-troveclient (1:1.2.0-3) experimental; urgency=medium + + * Add epoch to version to align with Ubuntu. - -- Thomas Goirand Fri, 28 Aug 2015 08:00:56 +0000 + -- Corey Bryant Wed, 26 Aug 2015 15:56:07 -0400 python-troveclient (1.2.0-3) experimental; urgency=medium diff -Nru python-troveclient-1.2.0/debian/control python-troveclient-1.3.0/debian/control --- python-troveclient-1.2.0/debian/control 2015-08-28 08:02:49.000000000 +0000 +++ python-troveclient-1.3.0/debian/control 2015-09-24 19:52:25.000000000 +0000 @@ -8,37 +8,39 @@ dh-python, openstack-pkg-tools, python-all, - python-pbr (>= 0.11), + python-pbr (>= 1.6), python-setuptools, python-sphinx, python3-all, - python3-pbr (>= 0.11), + python3-pbr (>= 1.6), python3-setuptools, Build-Depends-Indep: python-babel, python-coverage, python-hacking, python-httplib2, - python-keystoneclient (>= 1:1.3.0), - python-mock, - python-oslo.utils (>= 1.4.0), + python-keystoneclient (>= 1:1.6.0), + python-mock (>= 1.2), + python-oslo.utils (>= 2.0.0), python-oslosphinx (>= 2.5.0), python-prettytable, python-requests (>= 2.5.2), python-requests-mock (>= 0.6.0), + python-testscenarios, python-simplejson, python-six (>= 1.9.0), - python-testtools, + python-testtools (>= 1.4.0), python3-babel, python3-httplib2, python3-keystoneclient (>= 1:1.3.0), - python3-mock, - python3-oslo.utils (>= 1.4.0), + python3-mock (>= 1.2), + python3-oslo.utils (>= 2.0.0), python3-prettytable, python3-requests (>= 2.5.2), python3-requests-mock (>= 0.6.0), + python3-testscenarios, python3-simplejson, python3-six (>= 1.9.0), - python3-testtools, + python3-testtools (>= 1.4.0), subunit (>= 1.1.0), testrepository, Standards-Version: 3.9.6 @@ -50,9 +52,9 @@ Package: python-troveclient Architecture: all Depends: python-babel, - python-keystoneclient (>= 1:1.3.0), - python-oslo.utils (>= 1.4.0), - python-pbr (>= 0.11), + python-keystoneclient (>= 1:1.6.0), + python-oslo.utils (>= 2.0.0), + python-pbr (>= 1.6), python-prettytable, python-requests (>= 2.5.2), python-simplejson, @@ -72,8 +74,8 @@ Architecture: all Depends: python-babel, python3-keystoneclient (>= 1:1.3.0), - python3-oslo.utils (>= 1.4.0), - python3-pbr (>= 0.11), + python3-oslo.utils (>= 2.0.0), + python3-pbr (>= 1.6), python3-prettytable, python3-requests (>= 2.5.2), python3-simplejson, diff -Nru python-troveclient-1.2.0/debian/rules python-troveclient-1.3.0/debian/rules --- python-troveclient-1.2.0/debian/rules 2015-08-28 08:02:49.000000000 +0000 +++ python-troveclient-1.3.0/debian/rules 2015-09-24 19:08:54.000000000 +0000 @@ -29,7 +29,7 @@ override_dh_auto_test: ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS))) @echo "===> Running tests" - set -e ; set -x ; for i in 2.7 3.4 ; do \ + set -e ; set -x ; for i in 2.7 $(PYTHON3S) ; do \ PYMAJOR=`echo $$i | cut -d'.' -f1` ; \ echo "===> Testing with python$$i (python$$PYMAJOR)" ; \ rm -rf .testrepository ; \ diff -Nru python-troveclient-1.2.0/requirements.txt python-troveclient-1.3.0/requirements.txt --- python-troveclient-1.2.0/requirements.txt 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/requirements.txt 2015-09-04 20:59:26.000000000 +0000 @@ -1,12 +1,12 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -pbr>=0.11,<2.0 +pbr<2.0,>=1.6 argparse -PrettyTable>=0.7,<0.8 +PrettyTable<0.8,>=0.7 requests>=2.5.2 simplejson>=2.2.0 -oslo.utils>=1.4.0 # Apache-2.0 -python-keystoneclient>=1.3.0 +oslo.utils>=2.0.0 # Apache-2.0 +python-keystoneclient>=1.6.0 Babel>=1.3 six>=1.9.0 diff -Nru python-troveclient-1.2.0/setup.py python-troveclient-1.3.0/setup.py --- python-troveclient-1.2.0/setup.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/setup.py 2015-09-04 20:59:26.000000000 +0000 @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,5 +25,5 @@ pass setuptools.setup( - setup_requires=['pbr'], + setup_requires=['pbr>=1.3'], pbr=True) diff -Nru python-troveclient-1.2.0/test-requirements.txt python-troveclient-1.3.0/test-requirements.txt --- python-troveclient-1.2.0/test-requirements.txt 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/test-requirements.txt 2015-09-04 20:59:26.000000000 +0000 @@ -1,13 +1,14 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=0.10.0,<0.11 +hacking<0.11,>=0.10.0 coverage>=3.6 discover -oslosphinx>=2.5.0 # Apache-2.0 -requests-mock>=0.6.0 # Apache-2.0 -sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 +oslosphinx>=2.5.0 # Apache-2.0 +requests-mock>=0.6.0 # Apache-2.0 +sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 testrepository>=0.0.18 -testtools>=0.9.36,!=1.2.0 -mock>=1.0 +testscenarios>=0.4 +testtools>=1.4.0 +mock>=1.2 httplib2>=0.7.5 diff -Nru python-troveclient-1.2.0/troveclient/base.py python-troveclient-1.3.0/troveclient/base.py --- python-troveclient-1.2.0/troveclient/base.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/base.py 2015-09-04 20:59:26.000000000 +0000 @@ -175,12 +175,15 @@ def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) resp, body = self.api.client.post(url, body=body) - if return_raw: - return body[response_key] + if body: + if return_raw: + return body[response_key] - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key]) + with self.completion_cache('human_id', self.resource_class, + mode="a"): + with self.completion_cache('uuid', self.resource_class, + mode="a"): + return self.resource_class(self, body[response_key]) def _delete(self, url): resp, body = self.api.client.delete(url) diff -Nru python-troveclient-1.2.0/troveclient/client.py python-troveclient-1.3.0/troveclient/client.py --- python-troveclient-1.2.0/troveclient/client.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/client.py 2015-09-04 20:59:26.000000000 +0000 @@ -26,8 +26,8 @@ from keystoneclient import adapter from oslo_utils import importutils +from troveclient import exceptions from troveclient.openstack.common.apiclient import client -from troveclient.openstack.common.apiclient import exceptions from troveclient import service_catalog try: diff -Nru python-troveclient-1.2.0/troveclient/compat/client.py python-troveclient-1.3.0/troveclient/compat/client.py --- python-troveclient-1.2.0/troveclient/compat/client.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/compat/client.py 2015-09-04 20:59:26.000000000 +0000 @@ -355,6 +355,7 @@ self.configuration_parameters = config_parameters self.metadata = metadata.Metadata(self) self.mgmt_configs = management.MgmtConfigurationParameters(self) + self.mgmt_datastore_versions = management.MgmtDatastoreVersions(self) class Mgmt(object): def __init__(self, dbaas): @@ -362,6 +363,7 @@ self.hosts = dbaas.hosts self.accounts = dbaas.accounts self.storage = dbaas.storage + self.datastore_version = dbaas.mgmt_datastore_versions self.mgmt = Mgmt(self) diff -Nru python-troveclient-1.2.0/troveclient/shell.py python-troveclient-1.3.0/troveclient/shell.py --- python-troveclient-1.2.0/troveclient/shell.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/shell.py 2015-09-04 20:59:26.000000000 +0000 @@ -210,6 +210,9 @@ ks_session.Session.register_cli_options(parser) identity.Password.register_argparse_arguments(parser) + parser.set_defaults(insecure=utils.env('TROVECLIENT_INSECURE', + default=False)) + parser.add_argument('--os-tenant-name', metavar='', default=utils.env('OS_TENANT_NAME'), diff -Nru python-troveclient-1.2.0/troveclient/tests/fakes.py python-troveclient-1.3.0/troveclient/tests/fakes.py --- python-troveclient-1.2.0/troveclient/tests/fakes.py 1970-01-01 00:00:00.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/tests/fakes.py 2015-09-04 20:59:26.000000000 +0000 @@ -0,0 +1,547 @@ +# Copyright [2015] Hewlett-Packard Development Company, L.P. +# +# 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 six.moves.urllib import parse +from troveclient import client as base_client +from troveclient.tests import utils +from troveclient.v1 import client + + +def get_version_map(): + return { + '1.0': 'troveclient.tests.fakes.FakeClient', + } + + +def assert_has_keys(dict, required=[], optional=[]): + keys = dict.keys() + for k in required: + try: + assert k in keys + except AssertionError: + raise AssertionError("key: %s not found." % k) + + +class FakeClient(client.Client): + + def __init__(self, *args, **kwargs): + client.Client.__init__(self, 'username', 'password', + 'project_id', 'auth_url', + extensions=kwargs.get('extensions')) + self.client = FakeHTTPClient(**kwargs) + + def assert_called(self, method, url, body=None, pos=-1): + """Assert than an API method was just called.""" + expected = (method, url) + called = self.client.callstack[pos][0:2] + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + assert expected == called, \ + 'Expected %s %s; got %s %s' % (expected + called) + + if body is not None: + if self.client.callstack[pos][2] != body: + raise AssertionError('%r != %r' % + (self.client.callstack[pos][2], body)) + + def assert_called_anytime(self, method, url, body=None): + """Assert than an API method was called anytime in the test.""" + expected = (method, url) + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + for entry in self.client.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s; got %s' % (expected, self.client.callstack) + if body is not None: + try: + assert entry[2] == body + except AssertionError: + print(entry[2]) + print("!=") + print(body) + raise + + self.client.callstack = [] + + +class FakeHTTPClient(base_client.HTTPClient): + + def __init__(self, **kwargs): + self.username = 'username' + self.password = 'password' + self.auth_url = 'auth_url' + self.management_url = ( + 'http://trove-api:8779/v1.0/14630bc0e9ef4e248c9753eaf57b0f6e') + self.tenant_id = 'tenant_id' + self.callstack = [] + self.projectid = 'projectid' + self.user = 'user' + self.region_name = 'region_name' + self.endpoint_type = 'endpoint_type' + self.service_type = 'service_type' + self.service_name = 'service_name' + self.volume_service_name = 'volume_service_name' + self.timings = 'timings' + self.bypass_url = 'bypass_url' + self.os_cache = 'os_cache' + self.http_log_debug = 'http_log_debug' + + def _cs_request(self, url, method, **kwargs): + # Check that certain things are called correctly + if method in ['GET', 'DELETE']: + assert 'body' not in kwargs + elif method == 'PUT': + assert 'body' in kwargs + + if url is not None: + # Call the method + args = parse.parse_qsl(parse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_') + munged_url = munged_url.replace('.', '_') + munged_url = munged_url.replace('-', '_') + munged_url = munged_url.replace(' ', '_') + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.callstack.append((method, url, kwargs.get('body'))) + + status, headers, body = getattr(self, callback)(**kwargs) + r = utils.TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) + return r, body + + def get_instances(self, **kw): + return (200, {}, {"instances": [ + { + "id": "1234", + "name": "test-member-1", + "status": "ACTIVE", + "ip": ["10.0.0.13"], + "volume": {"size": 2}, + "flavor": {"id": "2"}, + "datastore": {"version": "5.6", "type": "mysql"}}, + { + "id": "5678", + "name": "test-member-2", + "status": "ACTIVE", + "ip": ["10.0.0.14"], + "volume": {"size": 2}, + "flavor": {"id": "2"}, + "datastore": {"version": "5.6", "type": "mysql"}}]}) + + def get_instances_1234(self, **kw): + r = {'instance': self.get_instances()[2]['instances'][0]} + return (200, {}, r) + + def post_instances(self, body, **kw): + assert_has_keys( + body['instance'], + required=['name', 'flavorRef'], + optional=['volume']) + if 'volume' in body['instance']: + assert_has_keys(body['instance']['volume'], required=['size']) + return (202, {}, self.get_instances_1234()[2]) + + def get_flavors(self, **kw): + return (200, {}, {"flavors": [ + { + "str_id": "1", + "ram": 512, + "id": 1, + "name": "m1.tiny"}, + { + "str_id": "10", + "ram": 768, + "id": 10, + "name": "eph.rd-smaller"}, + { + "str_id": "2", + "ram": 2048, + "id": 2, + "name": "m1.small"}, + { + "str_id": "3", + "ram": 4096, + "id": 3, + "name": "m1.medium"}, + { + "str_id": "7d0d16e5-875f-4198-b6da-90ab2d3e899e", + "ram": 8192, + "id": None, + "name": "m1.uuid"}]}) + + def get_datastores_mysql_versions_some_version_id_flavors(self, **kw): + return self.get_flavors() + + def get_flavors_1(self, **kw): + r = {'flavor': self.get_flavors()[2]['flavors'][0]} + return (200, {}, r) + + def get_flavors_2(self, **kw): + r = {'flavor': self.get_flavors()[2]['flavors'][2]} + return (200, {}, r) + + def get_flavors_m1_tiny(self, **kw): + r = {'flavor': self.get_flavors()[2]['flavors'][0]} + return (200, {}, r) + + def get_flavors_m1_small(self, **kw): + r = {'flavor': self.get_flavors()[2]['flavors'][2]} + return (200, {}, r) + + def get_flavors_m1_uuid(self, **kw): + r = {'flavor': self.get_flavors()[2]['flavors'][4]} + return (200, {}, r) + + def get_clusters(self, **kw): + return (200, {}, {"clusters": [ + { + "instances": [ + { + "type": "member", + "id": "member-1", + "ip": ["10.0.0.3"], + "flavor": {"id": "2"}, + "name": "test-clstr-member-1" + }, + { + "type": "member", + "id": "member-2", + "ip": ["10.0.0.4"], + "flavor": {"id": "2"}, + "name": "test-clstr-member-2" + }], + "updated": "2015-05-02T11:06:19", + "task": {"description": "No tasks for the cluster.", "id": 1, + "name": "NONE"}, + "name": "test-clstr", + "created": "2015-05-02T10:37:04", + "datastore": {"version": "7.1", "type": "vertica"}, + "id": "cls-1234"}]}) + + def get_clusters_cls_1234(self, **kw): + r = {'cluster': self.get_clusters()[2]['clusters'][0]} + return (200, {}, r) + + def delete_instances_1234(self, **kw): + return (202, {}, None) + + def delete_clusters_cls_1234(self, **kw): + return (202, {}, None) + + def patch_instances_1234(self, **kw): + return (202, {}, None) + + def post_clusters(self, body, **kw): + assert_has_keys( + body['cluster'], + required=['instances', 'datastore', 'name']) + if 'instances' in body['cluster']: + for instance in body['cluster']['instances']: + assert_has_keys(instance, required=['volume', 'flavorRef']) + return (202, {}, self.get_clusters_cls_1234()[2]) + + def post_clusters_cls_1234(self, body, **kw): + return (202, {}, None) + + def post_instances_1234_action(self, **kw): + return (202, {}, None) + + def get_datastores(self, **kw): + return (200, {}, {"datastores": [ + { + "default_version": "v-56", + "versions": [{"id": "v-56", "name": "5.6"}], + "id": "d-123", + "name": "mysql"}, + { + "default_version": "v-71", + "versions": [{"id": "v-71", "name": "7.1"}], + "id": "d-456", + "name": "vertica" + }]}) + + def get_datastores_d_123(self, **kw): + r = {'datastore': self.get_datastores()[2]['datastores'][0]} + return (200, {}, r) + + def get_datastores_d_123_versions(self, **kw): + return (200, {}, {"versions": [ + { + "datastore": "d-123", + "id": "v-56", + "name": "5.6"}]}) + + def get_datastores_d_123_versions_v_56(self, **kw): + r = {'version': self.get_datastores_d_123_versions()[2]['versions'][0]} + return (200, {}, r) + + def get_configurations(self, **kw): + return (200, {}, {"configurations": [ + { + "datastore_name": "mysql", + "updated": "2015-05-16T10:24:29", + "name": "test_config", + "created": "2015-05-16T10:24:28", + "datastore_version_name": "5.6", + "id": "c-123", + "values": {"max_connections": 5}, + "datastore_version_id": "d-123", "description": ''}]}) + + def get_configurations_c_123(self, **kw): + r = {'configuration': self.get_configurations()[2]['configurations'][0] + } + return (200, {}, r) + + def get_datastores_d_123_versions_v_156_parameters(self, **kw): + return (200, {}, {"configuration-parameters": [ + { + "type": "string", + "name": "character_set_results", + "datastore_version_id": "d-123", + "restart_required": "false"}, + { + "name": "connect_timeout", + "min": 2, + "max": 31536000, + "restart_required": "false", + "type": "integer", + "datastore_version_id": "d-123"}, + { + "type": "string", + "name": "character_set_client", + "datastore_version_id": "d-123", + "restart_required": "false"}, + { + "name": "max_connections", + "min": 1, + "max": 100000, + "restart_required": "false", + "type": "integer", + "datastore_version_id": "d-123"}]}) + + def get_datastores_d_123_versions_v_56_parameters_max_connections(self, + **kw): + r = self.get_datastores_d_123_versions_v_156_parameters()[ + 2]['configuration-parameters'][3] + return (200, {}, r) + + def get_configurations_c_123_instances(self, **kw): + return (200, {}, {"instances": []}) + + def delete_configurations_c_123(self, **kw): + return (202, {}, None) + + def get_instances_1234_configuration(self, **kw): + return (200, {}, {"instance": {"configuration": { + "tmp_table_size": "15M", + "innodb_log_files_in_group": "2", + "skip-external-locking": "1", + "max_user_connections": "98"}}}) + + def put_instances_1234(self, **kw): + return (202, {}, None) + + def patch_instances_1234_metadata_key_123(self, **kw): + return (202, {}, None) + + def put_instances_1234_metadata_key_123(self, **kw): + return (202, {}, None) + + def delete_instances_1234_metadata_key_123(self, **kw): + return (202, {}, None) + + def post_instances_1234_metadata_key123(self, body, **kw): + return (202, {}, {'metadata': {}}) + + def get_instances_1234_metadata(self, **kw): + return (200, {}, {"metadata": {}}) + + def get_instances_1234_metadata_key123(self, **kw): + return (200, {}, {"metadata": {}}) + + def get_limits(self, **kw): + return (200, {}, {"limits": [ + { + "max_backups": 50, + "verb": "ABSOLUTE", + "max_volumes": 20, + "max_instances": 5}]}) + + def get_backups(self, **kw): + return (200, {}, {"backups": [ + { + "status": "COMPLETED", + "updated": "2015-05-16T14:23:08", + "description": None, + "datastore": {"version": "5.6", "type": "mysql", + "version_id": "v-56"}, + "id": "bk-1234", + "size": 0.11, + "name": "bkp_1", + "created": "2015-05-16T14:22:28", + "instance_id": "1234", + "parent_id": None, + "locationRef": ("http://backup_srvr/database_backups/" + "bk-1234.xbstream.gz.enc")}, + { + "status": "COMPLETED", + "updated": "2015-05-16T14:22:12", + "description": None, + "datastore": {"version": "5.6", "type": "mysql", + "version_id": "v-56"}, + "id": "bk-5678", + "size": 0.11, + "name": "test_bkp", + "created": "2015-05-16T14:21:27", + "instance_id": "5678", + "parent_id": None, + "locationRef": ("http://backup_srvr/database_backups/" + "bk-5678.xbstream.gz.enc")}]}) + + def get_backups_bk_1234(self, **kw): + r = {'backup': self.get_backups()[2]['backups'][0]} + return (200, {}, r) + + def get_instances_1234_backups(self, **kw): + r = {'backups': [self.get_backups()[2]['backups'][0]]} + return (200, {}, r) + + def delete_backups_bk_1234(self, **kw): + return (202, {}, None) + + def post_backups(self, body, **kw): + assert_has_keys( + body['backup'], + required=['name'], + optional=['description', 'parent']) + return (202, {}, self.get_backups_bk_1234()[2]) + + def get_instances_1234_databases(self, **kw): + return (200, {}, {"databases": [ + {"name": "db_1"}, + {"name": "db_2"}, + {"name": "performance_schema"}]}) + + def delete_instances_1234_databases_db_1(self, **kw): + return (202, {}, None) + + def post_instances_1234_databases(self, body, **kw): + assert_has_keys( + body, + required=['databases']) + for database in body['databases']: + assert_has_keys(database, required=['name'], + optional=['character_set', 'collate']) + return (202, {}, + self.get_instances_1234_databases()[2]['databases'][0]) + + def get_instances_1234_users(self, **kw): + return (200, {}, {"users": [ + {"host": "%", "name": "jacob", "databases": []}, + {"host": "%", "name": "rocky", "databases": []}, + {"host": "%", "name": "harry", "databases": [{"name": "db1"}]}]}) + + def get_instances_1234_users_jacob(self, **kw): + r = {'user': self.get_instances_1234_users()[2]['users'][0]} + return (200, {}, r) + + def delete_instances_1234_users_jacob(self, **kw): + return (202, {}, None) + + def post_instances_1234_users(self, body, **kw): + assert_has_keys( + body, + required=['users']) + for database in body['users']: + assert_has_keys(database, required=['name', 'password'], + optional=['databases']) + return (202, {}, self.get_instances_1234_users()[2]['users'][0]) + + def get_instances_1234_users_jacob_databases(self, **kw): + r = {'databases': [ + self.get_instances_1234_databases()[2]['databases'][0], + self.get_instances_1234_databases()[2]['databases'][1]]} + return (200, {}, r) + + def put_instances_1234_users_jacob(self, **kw): + return (202, {}, None) + + def put_instances_1234_users_jacob_databases(self, **kw): + return (202, {}, None) + + def delete_instances_1234_users_jacob_databases_db1(self, **kw): + return (202, {}, None) + + def post_instances_1234_root(self, **kw): + return (202, {}, {"user": {"password": "password", "name": "root"}}) + + def post_clusters_cls_1234_root(self, **kw): + return (202, {}, {"user": {"password": "password", "name": "root"}}) + + def get_instances_1234_root(self, **kw): + return (200, {}, {"rootEnabled": 'True'}) + + def get_clusters_cls_1234_root(self, **kw): + return (200, {}, {"rootEnabled": 'True'}) + + def get_security_groups(self, **kw): + return (200, {}, {"security_groups": [ + { + "instance_id": "1234", + "updated": "2015-05-16T17:29:45", + "name": "SecGroup_1234", + "created": "2015-05-16T17:29:45", + "rules": [{"to_port": 3306, "cidr": "0.0.0.0/0", + "from_port": 3306, + "protocol": "tcp", "id": "1"}], + "id": "2", + "description": "Security Group for 1234"}]}) + + def get_security_groups_2(self, **kw): + r = {'security_group': self.get_security_groups()[ + 2]['security_groups'][0]} + return (200, {}, r) + + def delete_security_group_rules_2(self, **kw): + return (202, {}, None) + + def post_security_group_rules(self, body, **kw): + assert_has_keys(body['security_group_rule'], required=['cidr', 'cidr']) + return (202, {}, {"security_group_rule": [ + { + "from_port": 3306, + "protocol": "tcp", + "created": "2015-05-16T17:55:05", + "to_port": 3306, + "security_group_id": "2", + "cidr": "15.0.0.0/24", "id": 3}]}) diff -Nru python-troveclient-1.2.0/troveclient/tests/test_client.py python-troveclient-1.3.0/troveclient/tests/test_client.py --- python-troveclient-1.2.0/troveclient/tests/test_client.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/tests/test_client.py 2015-09-04 20:59:26.000000000 +0000 @@ -15,13 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. +import fixtures +from keystoneclient import adapter +import logging +import mock +import requests import testtools -import troveclient.v1.client - from troveclient import client as other_client +from troveclient import exceptions from troveclient.openstack.common.apiclient import client -from troveclient.openstack.common.apiclient import exceptions +from troveclient import service_catalog +import troveclient.v1.client class ClientTest(testtools.TestCase): @@ -37,3 +42,446 @@ self.assertRaises(exceptions.UnsupportedVersion, client.BaseClient.get_class, 'database', '0', version_map) + + def test_client_with_auth_system_without_auth_plugin(self): + self.assertRaisesRegexp( + exceptions.AuthSystemNotFound, "AuthSystemNotFound: 'something'", + other_client.HTTPClient, user='user', password='password', + projectid='project', timeout=2, auth_url="http://www.blah.com", + auth_system='something') + + def test_client_with_auth_system_without_endpoint(self): + auth_plugin = mock.Mock() + auth_plugin.get_auth_url = mock.Mock(return_value=None) + self.assertRaises( + exceptions.EndpointNotFound, + other_client.HTTPClient, user='user', password='password', + projectid='project', timeout=2, auth_plugin=auth_plugin, + auth_url=None, auth_system='something') + + def test_client_with_timeout(self): + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com", + insecure=True) + self.assertEqual(2, instance.timeout) + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + with mock.patch('requests.request', mock_request): + instance.authenticate() + requests.request.assert_called_with( + mock.ANY, mock.ANY, timeout=2, headers=mock.ANY, + verify=mock.ANY) + + def test_client_unauthorized(self): + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com", + cacert=mock.Mock()) + instance.auth_token = 'foobar' + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.side_effect = other_client.exceptions.Unauthorized(401) + with mock.patch('requests.request', mock_request): + self.assertRaises( + exceptions.Unauthorized, instance.get, '/instances') + + def test_client_bad_request(self): + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com") + instance.auth_token = 'foobar' + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.side_effect = other_client.exceptions.BadRequest() + with mock.patch('requests.request', mock_request): + self.assertRaises( + exceptions.BadRequest, instance.get, '/instances') + + def test_client_with_client_exception(self): + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com", + retries=2) + instance.auth_token = 'foobar' + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.side_effect = other_client.exceptions.ClientException() + type(mock_request.side_effect).code = mock.PropertyMock( + side_effect=[501, 111]) + with mock.patch('requests.request', mock_request): + self.assertRaises( + exceptions.ClientException, instance.get, '/instances') + + def test_client_connection_error(self): + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project', + timeout=2, + auth_url="http://www.blah.com", + retries=2) + instance.auth_token = 'foobar' + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.side_effect = requests.exceptions.ConnectionError( + 'connection refused') + with mock.patch('requests.request', mock_request): + self.assertRaisesRegexp( + exceptions.ClientException, + 'Unable to establish connection: connection refused', + instance.get, '/instances') + + @mock.patch.object(other_client.HTTPClient, 'request', + return_value=(200, "{'versions':[]}")) + def _check_version_url(self, management_url, version_url, mock_request): + projectid = '25e469aa1848471b875e68cde6531bc5' + instance = other_client.HTTPClient(user='user', + password='password', + projectid=projectid, + auth_url="http://www.blah.com") + instance.auth_token = 'foobar' + instance.management_url = management_url % projectid + mock_get_service_url = mock.Mock(return_value=instance.management_url) + instance.get_service_url = mock_get_service_url + instance.version = 'v2.0' + + # If passing None as the part of url, a client accesses the url which + # doesn't include "v2/" for getting API version info. + instance.get('') + mock_request.assert_called_once_with(instance.management_url, 'GET', + headers=mock.ANY) + mock_request.reset_mock() + + # Otherwise, a client accesses the url which includes "v2/". + instance.get('/instances') + url = instance.management_url + '/instances' + mock_request.assert_called_once_with(url, 'GET', headers=mock.ANY) + + def test_client_version_url(self): + self._check_version_url('http://foo.com/v1/%s', 'http://foo.com/') + + def test_client_version_url_with_tenant_name(self): + self._check_version_url('http://foo.com/trove/v1/%s', + 'http://foo.com/trove/') + + def test_log_req(self): + self.logger = self.useFixture( + fixtures.FakeLogger( + format="%(message)s", + level=logging.DEBUG, + nuke_handlers=True + ) + ) + cs = other_client.HTTPClient(user='user', + password='password', + projectid=None, + auth_url="http://www.blah.com", + http_log_debug=True) + cs.http_log_req(('/foo', 'GET'), {'headers': {}}) + cs.http_log_req(('/foo', 'GET'), + {'headers': {'X-Auth-Token': 'totally_bogus'}}) + cs.http_log_req( + ('/foo', 'GET'), + {'headers': {}, + 'data': '{"auth": {"passwordCredentials": ' + '{"password": "password"}}}'}) + + output = self.logger.output.split('\n') + + self.assertIn("REQ: curl -i /foo -X GET", output) + self.assertIn( + "REQ: curl -i /foo -X GET -H " + '"X-Auth-Token: totally_bogus"', + output) + self.assertIn( + "REQ: curl -i /foo -X GET -d " + '\'{"auth": {"passwordCredentials": {"password":' + ' "password"}}}\'', + output) + + @mock.patch.object(service_catalog, 'ServiceCatalog') + def test_client_auth_token(self, mock_service_catalog): + auth_url = 'http://www.blah.com' + proxy_token = 'foobar' + proxy_tenant_id = 'user' + mock_service_catalog.return_value.get_token = mock.Mock( + return_value=proxy_token) + instance = other_client.HTTPClient(proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + user=None, + password=None, + tenant_id=proxy_tenant_id, + projectid=None, + timeout=2, + auth_url=auth_url) + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + + with mock.patch('requests.request', mock_request): + instance.authenticate() + mock_request.assert_called_with( + 'GET', auth_url + '/tokens/foobar?belongsTo=user', + headers={'User-Agent': 'python-troveclient', + 'Accept': 'application/json', + 'X-Auth-Token': proxy_token}, + timeout=2, verify=True) + + @mock.patch.object(service_catalog, 'ServiceCatalog', side_effect=KeyError) + def test_client_auth_token_authorization_failure(self, + mock_service_catalog): + auth_url = 'http://www.blah.com' + proxy_token = 'foobar' + proxy_tenant_id = 'user' + mock_service_catalog.return_value.get_token = mock.Mock( + return_value=proxy_token) + instance = other_client.HTTPClient(proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + user=None, + password=None, + tenant_id=proxy_tenant_id, + projectid=None, + timeout=2, + auth_url=auth_url) + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + + with mock.patch('requests.request', mock_request): + self.assertRaises(exceptions.AuthorizationFailure, + instance.authenticate) + + @mock.patch.object(service_catalog, 'ServiceCatalog', + side_effect=other_client.exceptions.EndpointNotFound) + def test_client_auth_token_endpoint_not_found(self, mock_service_catalog): + auth_url = 'http://www.blah.com' + proxy_token = 'foobar' + proxy_tenant_id = 'user' + mock_service_catalog.return_value.get_token = mock.Mock( + return_value=proxy_token) + instance = other_client.HTTPClient(proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + user=None, + password=None, + tenant_id=proxy_tenant_id, + projectid=None, + timeout=2, + auth_url=auth_url) + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v2.0' + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + + with mock.patch('requests.request', mock_request): + self.assertRaises(exceptions.EndpointNotFound, + instance.authenticate) + + @mock.patch.object(service_catalog, 'ServiceCatalog') + def test_client_auth_token_v1_auth_failure(self, mock_service_catalog): + auth_url = 'http://www.blah.com' + proxy_token = 'foobar' + proxy_tenant_id = 'user' + mock_service_catalog.return_value.get_token = mock.Mock( + return_value=proxy_token) + instance = other_client.HTTPClient(proxy_token=proxy_token, + proxy_tenant_id=proxy_tenant_id, + user=None, + password=None, + tenant_id=proxy_tenant_id, + projectid=None, + timeout=2, + auth_url=auth_url) + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v1.0' + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + + with mock.patch('requests.request', mock_request): + self.assertRaises(exceptions.NoTokenLookupException, + instance.authenticate) + + @mock.patch.object(service_catalog, 'ServiceCatalog') + def test_client_auth_token_v1_auth(self, mock_service_catalog): + auth_url = 'http://www.blah.com' + proxy_token = 'foobar' + mock_service_catalog.return_value.get_token = mock.Mock( + return_value=proxy_token) + instance = other_client.HTTPClient(user='user', + password='password', + projectid='projectid', + timeout=2, + auth_url=auth_url) + instance.management_url = 'http://example.com' + instance.get_service_url = mock.Mock(return_value='http://example.com') + instance.version = 'v1.0' + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + } + headers = {'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'python-troveclient'} + with mock.patch('requests.request', mock_request): + instance.authenticate() + called_args, called_kwargs = mock_request.call_args + self.assertEqual(('POST', 'http://www.blah.com/v2.0/tokens'), + called_args) + self.assertDictEqual(headers, called_kwargs['headers']) + + def test_client_get(self): + auth_url = 'http://www.blah.com' + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project_id', + timeout=2, + auth_url=auth_url) + instance._cs_request = mock.Mock() + + instance.get('clusters') + instance._cs_request.assert_called_with('clusters', 'GET') + + def test_client_patch(self): + auth_url = 'http://www.blah.com' + body = mock.Mock() + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project_id', + timeout=2, + auth_url=auth_url) + instance._cs_request = mock.Mock() + + instance.patch('instances/dummy-instance-id', body=body) + instance._cs_request.assert_called_with( + 'instances/dummy-instance-id', 'PATCH', body=body) + + def test_client_post(self): + auth_url = 'http://www.blah.com' + body = {"add_shard": {}} + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project_id', + timeout=2, + auth_url=auth_url) + instance._cs_request = mock.Mock() + + instance.post('clusters/dummy-cluster-id', body=body) + instance._cs_request.assert_called_with( + 'clusters/dummy-cluster-id', 'POST', body=body) + + def test_client_put(self): + auth_url = 'http://www.blah.com' + body = {"user": {"password": "new_password"}} + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project_id', + timeout=2, + auth_url=auth_url) + instance._cs_request = mock.Mock() + + instance.put('instances/dummy-instance-id/user/dummy-user', body=body) + instance._cs_request.assert_called_with( + 'instances/dummy-instance-id/user/dummy-user', 'PUT', body=body) + + def test_client_delete(self): + auth_url = 'http://www.blah.com' + instance = other_client.HTTPClient(user='user', + password='password', + projectid='project_id', + timeout=2, + auth_url=auth_url) + instance._cs_request = mock.Mock() + + instance.delete('/backups/dummy-backup-id') + instance._cs_request.assert_called_with('/backups/dummy-backup-id', + 'DELETE') + + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + def test_database_service_name(self, m_request): + m_request.return_value = (mock.MagicMock(status_code=200), None) + + client = other_client.SessionClient(session=mock.MagicMock(), + auth=mock.MagicMock()) + client.request("http://no.where", 'GET') + self.assertIsNone(client.database_service_name) + + client = other_client.SessionClient(session=mock.MagicMock(), + auth=mock.MagicMock(), + database_service_name='myservice') + client.request("http://no.where", 'GET') + self.assertEqual('myservice', client.database_service_name) + + @mock.patch.object(adapter.LegacyJsonAdapter, 'request') + @mock.patch.object(adapter.LegacyJsonAdapter, 'get_endpoint', + return_value=None) + def test_error_sessionclient(self, m_end_point, m_request): + m_request.return_value = (mock.MagicMock(status_code=200), None) + + self.assertRaises(exceptions.EndpointNotFound, + other_client.SessionClient, + session=mock.MagicMock(), + auth=mock.MagicMock()) + + def test_construct_http_client(self): + mock_request = mock.Mock() + mock_request.return_value = requests.Response() + mock_request.return_value.status_code = 200 + mock_request.return_value.headers = { + 'x-server-management-url': 'blah.com', + 'x-auth-token': 'blah', + } + with mock.patch('requests.request', mock_request): + self.assertIsInstance(other_client._construct_http_client(), + other_client.HTTPClient) + self.assertIsInstance( + other_client._construct_http_client(session=mock.Mock(), + auth=mock.Mock()), + other_client.SessionClient) diff -Nru python-troveclient-1.2.0/troveclient/tests/test_management.py python-troveclient-1.3.0/troveclient/tests/test_management.py --- python-troveclient-1.2.0/troveclient/tests/test_management.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/tests/test_management.py 2015-09-04 20:59:26.000000000 +0000 @@ -183,3 +183,98 @@ self.assertEqual(1024, b["flavor"]["ram"]) self.assertEqual(2, b["flavor"]["vcpu"]) self.assertEqual(1, b["flavor"]["flavor_id"]) + + +class MgmtDatastoreVersionsTest(testtools.TestCase): + + def setUp(self): + super(MgmtDatastoreVersionsTest, self).setUp() + self.orig__init = management.MgmtDatastoreVersions.__init__ + management.MgmtDatastoreVersions.__init__ = mock.Mock( + return_value=None) + self.ds_version = management.MgmtDatastoreVersions() + self.ds_version.api = mock.Mock() + self.ds_version.api.client = mock.Mock() + self.ds_version.resource_class = mock.Mock(return_value="ds-version-1") + self.orig_base_getid = base.getid + base.getid = mock.Mock(return_value="ds-version1") + + def tearDown(self): + super(MgmtDatastoreVersionsTest, self).tearDown() + management.MgmtDatastoreVersions.__init__ = self.orig__init + base.getid = self.orig_base_getid + + def _get_mock_method(self): + self._resp = mock.Mock() + self._body = None + self._url = None + + def side_effect_func(url, body=None): + self._body = body + self._url = url + return (self._resp, body) + + return mock.Mock(side_effect=side_effect_func) + + def test_create(self): + def side_effect_func(path, body, *kw): + return path, body + + self.ds_version._create = mock.Mock(side_effect=side_effect_func) + p, b, = self.ds_version.create( + "ds-version1", "mysql", "mysql", "image-id", + ["mysql-server-5.5"], "true", "true") + self.assertEqual("/mgmt/datastore-versions", p) + self.assertEqual("ds-version1", b["version"]["name"]) + self.assertEqual("mysql", b["version"]["datastore_name"]) + self.assertEqual("mysql", b["version"]["datastore_manager"]) + self.assertEqual("image-id", b["version"]["image"]) + self.assertEqual(["mysql-server-5.5"], b["version"]["packages"]) + self.assertTrue(b["version"]["active"]) + self.assertTrue(b["version"]["default"]) + + def test_get(self): + def side_effect_func(path, ins): + return path, ins + self.ds_version._get = mock.Mock(side_effect=side_effect_func) + p, i = self.ds_version.get('ds-version-1') + self.assertEqual(('/mgmt/datastore-versions/ds-version-1', 'version'), + (p, i)) + + def test_list(self): + page_mock = mock.Mock() + self.ds_version._paginated = page_mock + self.ds_version.list() + page_mock.assert_called_with('/mgmt/datastore-versions', + 'versions', None, None) + self.ds_version.list(limit=10, marker="foo") + page_mock.assert_called_with('/mgmt/datastore-versions', + 'versions', 10, "foo") + + def test_delete(self): + resp = mock.Mock() + resp.status_code = 202 + self.ds_version.api.client.delete = mock.Mock( + return_value=(resp, None) + ) + self.ds_version.delete('ds-version-1') + self.assertEqual(1, self.ds_version.api.client.delete.call_count) + self.ds_version.api.client.delete.assert_called_with( + '/mgmt/datastore-versions/ds-version-1') + + resp.status_code = 400 + self.assertRaises(Exception, self.ds_version.delete, 'ds-version-2') + self.assertEqual(2, self.ds_version.api.client.delete.call_count) + self.ds_version.api.client.delete.assert_called_with( + '/mgmt/datastore-versions/ds-version-2') + + def test_edit(self): + self.ds_version.api.client.patch = self._get_mock_method() + self._resp.status_code = 202 + self.ds_version.edit('ds-version-1', image="new-image-id") + self.assertEqual('/mgmt/datastore-versions/ds-version-1', self._url) + self.assertDictEqual({"image": "new-image-id"}, self._body) + + self._resp.status_code = 400 + self.assertRaises(Exception, self.ds_version.edit, 'ds-version-1', + "new-mgr", "non-existent-image") diff -Nru python-troveclient-1.2.0/troveclient/tests/test_secgroups.py python-troveclient-1.3.0/troveclient/tests/test_secgroups.py --- python-troveclient-1.2.0/troveclient/tests/test_secgroups.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/tests/test_secgroups.py 2015-09-04 20:59:26.000000000 +0000 @@ -60,21 +60,17 @@ class SecGroupRuleTest(testtools.TestCase): - def setUp(self): + @mock.patch.object(security_groups.SecurityGroupRules, '__init__', + mock.Mock(return_value=None)) + @mock.patch.object(security_groups.SecurityGroupRule, '__init__', + mock.Mock(return_value=None)) + def setUp(self, *args): super(SecGroupRuleTest, self).setUp() - self.orig__init = security_groups.SecurityGroupRule.__init__ - security_groups.SecurityGroupRule.__init__ = mock.Mock( - return_value=None - ) - security_groups.SecurityGroupRules.__init__ = mock.Mock( - return_value=None - ) self.security_group_rule = security_groups.SecurityGroupRule() self.security_group_rules = security_groups.SecurityGroupRules() def tearDown(self): super(SecGroupRuleTest, self).tearDown() - security_groups.SecurityGroupRule.__init__ = self.orig__init def test___repr__(self): self.security_group_rule.group_id = 1 diff -Nru python-troveclient-1.2.0/troveclient/tests/test_v1_shell.py python-troveclient-1.3.0/troveclient/tests/test_v1_shell.py --- python-troveclient-1.2.0/troveclient/tests/test_v1_shell.py 1970-01-01 00:00:00.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/tests/test_v1_shell.py 2015-09-04 20:59:26.000000000 +0000 @@ -0,0 +1,545 @@ +# Copyright [2015] Hewlett-Packard Development Company, L.P. +# 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. + +import six + +import fixtures +import mock +import troveclient.client +from troveclient import exceptions +import troveclient.shell +from troveclient.tests import fakes +from troveclient.tests import utils +import troveclient.v1.shell + + +class ShellFixture(fixtures.Fixture): + + def setUp(self): + super(ShellFixture, self).setUp() + self.shell = troveclient.shell.OpenStackTroveShell() + + def tearDown(self): + if hasattr(self.shell, 'cs'): + self.shell.cs.clear_callstack() + super(ShellFixture, self).tearDown() + + +class ShellTest(utils.TestCase): + FAKE_ENV = { + 'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_PROJECT_ID': 'project_id', + 'OS_AUTH_URL': 'http://no.where/v2.0', + } + + def setUp(self, *args): + """Run before each test.""" + super(ShellTest, self).setUp() + + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + self.shell = self.useFixture(ShellFixture()).shell + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('troveclient.client.get_version_map', + return_value=fakes.get_version_map()) + @mock.patch('troveclient.v1.shell._find_instance_or_cluster', + return_value=('1234', 'instance')) + def run_command(self, cmd, mock_find_instance_or_cluster, + mock_get_version_map, mock_stdout): + if isinstance(cmd, list): + self.shell.main(cmd) + else: + self.shell.main(cmd.split()) + return mock_stdout.getvalue() + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('troveclient.client.get_version_map', + return_value=fakes.get_version_map()) + @mock.patch('troveclient.v1.shell._find_instance_or_cluster', + return_value=('cls-1234', 'cluster')) + def run_command_clusters(self, cmd, mock_find_instance_or_cluster, + mock_get_version_map, mock_stdout): + if isinstance(cmd, list): + self.shell.main(cmd) + else: + self.shell.main(cmd.split()) + return mock_stdout.getvalue() + + def assert_called(self, method, url, body=None, **kwargs): + return self.shell.cs.assert_called(method, url, body, **kwargs) + + def assert_called_anytime(self, method, url, body=None): + return self.shell.cs.assert_called_anytime(method, url, body) + + def test_instance_list(self): + self.run_command('list') + self.assert_called('GET', '/instances') + + def test_instance_show(self): + self.run_command('show 1234') + self.assert_called('GET', '/instances/1234') + + def test_instance_delete(self): + self.run_command('delete 1234') + self.assert_called('DELETE', '/instances/1234') + + def test_instance_update(self): + self.run_command('update 1234') + self.assert_called('PATCH', '/instances/1234') + + def test_resize_instance(self): + self.run_command('resize-instance 1234 1') + self.assert_called('POST', '/instances/1234/action') + + def test_resize_volume(self): + self.run_command('resize-volume 1234 3') + self.assert_called('POST', '/instances/1234/action') + + def test_restart(self): + self.run_command('restart 1234') + self.assert_called('POST', '/instances/1234/action') + + def test_detach_replica(self): + self.run_command('detach-replica 1234') + self.assert_called('PATCH', '/instances/1234') + + def test_promote_to_replica_source(self): + self.run_command('promote-to-replica-source 1234') + self.assert_called('POST', '/instances/1234/action') + + def test_eject_replica_source(self): + self.run_command('eject-replica-source 1234') + self.assert_called('POST', '/instances/1234/action') + + def test_flavor_list(self): + self.run_command('flavor-list') + self.assert_called('GET', '/flavors') + + def test_flavor_list_with_datastore(self): + cmd = ('flavor-list --datastore_type mysql ' + '--datastore_version_id some-version-id') + self.run_command(cmd) + self.assert_called( + 'GET', '/datastores/mysql/versions/some-version-id/flavors') + + def test_flavor_list_error(self): + cmd = 'flavor-list --datastore_type mysql' + exepcted_error_msg = ('Missing argument\(s\): ' + 'datastore_type, datastore_version_id') + self.assertRaisesRegexp( + exceptions.MissingArgs, exepcted_error_msg, self.run_command, + cmd) + + def test_flavor_show(self): + self.run_command('flavor-show 1') + self.assert_called('GET', '/flavors/1') + + def test_flavor_show_by_name(self): + self.run_command('flavor-show m1.tiny') # defined in fakes.py + self.assert_called('GET', '/flavors/m1.tiny') + + def test_flavor_show_uuid(self): + self.run_command('flavor-show m1.uuid') + self.assert_called('GET', '/flavors/m1.uuid') + + def test_cluster_list(self): + self.run_command('cluster-list') + self.assert_called('GET', '/clusters') + + def test_cluster_show(self): + self.run_command('cluster-show cls-1234') + self.assert_called('GET', '/clusters/cls-1234') + + def test_cluster_instances(self): + self.run_command('cluster-instances cls-1234') + self.assert_called('GET', '/clusters/cls-1234') + + def test_cluster_delete(self): + self.run_command('cluster-delete cls-1234') + self.assert_called('DELETE', '/clusters/cls-1234') + + def test_boot(self): + self.run_command('create test-member-1 1 --size 1') + self.assert_called_anytime( + 'POST', '/instances', + {'instance': { + 'volume': {'size': 1}, + 'flavorRef': 1, + 'name': 'test-member-1', + 'replica_count': 1 + }}) + + def test_boot_by_flavor_name(self): + self.run_command('create test-member-1 m1.tiny --size 1') + self.assert_called_anytime( + 'POST', '/instances', + {'instance': { + 'volume': {'size': 1}, + 'flavorRef': 1, + 'name': 'test-member-1', + 'replica_count': 1 + }}) + + def test_boot_nic_error(self): + cmd = ('create test-member-1 1 --size 1 ' + '--nic net-id=some-id,port-id=some-id') + self.assertRaisesRegexp( + exceptions.ValidationError, 'Invalid nic argument', + self.run_command, cmd) + + def test_cluster_create(self): + cmd = ('cluster-create test-clstr vertica 7.1 ' + '--instance flavor=2,volume=2 ' + '--instance flavor=2,volume=1') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/clusters', + {'cluster': { + 'instances': [ + { + 'volume': {'size': '2'}, + 'flavorRef': '2' + }, + { + 'volume': {'size': '1'}, + 'flavorRef': '2' + }], + 'datastore': {'version': '7.1', 'type': 'vertica'}, + 'name': 'test-clstr'}}) + + def test_cluster_create_by_flavor_name(self): + cmd = ('cluster-create test-clstr vertica 7.1 ' + '--instance flavor=m1.small,volume=2 ' + '--instance flavor=m1.small,volume=1') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/clusters', + {'cluster': { + 'instances': [ + { + 'volume': {'size': '2'}, + 'flavorRef': '2' + }, + { + 'volume': {'size': '1'}, + 'flavorRef': '2' + }], + 'datastore': {'version': '7.1', 'type': 'vertica'}, + 'name': 'test-clstr'}}) + + def test_cluster_create_error(self): + cmd = ('cluster-create test-clstr vertica 7.1 --instance volume=2 ' + '--instance flavor=2,volume=1') + self.assertRaisesRegexp( + exceptions.ValidationError, 'flavor is required', + self.run_command, cmd) + + def test_cluster_grow(self): + cmd = ('cluster-grow cls-1234 ' + '--instance flavor=2,volume=2 ' + '--instance flavor=2,volume=1') + self.run_command(cmd) + self.assert_called('POST', '/clusters/cls-1234') + + def test_cluster_shrink(self): + cmd = ('cluster-shrink cls-1234 1234') + self.run_command(cmd) + self.assert_called('POST', '/clusters/cls-1234') + + def test_datastore_list(self): + self.run_command('datastore-list') + self.assert_called('GET', '/datastores') + + def test_datastore_show(self): + self.run_command('datastore-show d-123') + self.assert_called('GET', '/datastores/d-123') + + def test_datastore_version_list(self): + self.run_command('datastore-version-list d-123') + self.assert_called('GET', '/datastores/d-123/versions') + + def test_datastore_version_show(self): + self.run_command('datastore-version-show v-56 --datastore d-123') + self.assert_called('GET', '/datastores/d-123/versions/v-56') + + def test_datastore_version_show_error(self): + expected_error_msg = ('The datastore name or id is required to ' + 'retrieve a datastore version by name.') + self.assertRaisesRegexp(exceptions.NoUniqueMatch, expected_error_msg, + self.run_command, + 'datastore-version-show v-56') + + def test_configuration_list(self): + self.run_command('configuration-list') + self.assert_called('GET', '/configurations') + + def test_configuration_show(self): + self.run_command('configuration-show c-123') + self.assert_called('GET', '/configurations/c-123') + + def test_configuration_create(self): + cmd = "configuration-create c-123 some-thing" + self.assertRaises(ValueError, self.run_command, cmd) + + def test_configuration_update(self): + cmd = "configuration-update c-123 some-thing" + self.assertRaises(ValueError, self.run_command, cmd) + + def test_configuration_patch(self): + cmd = "configuration-patch c-123 some-thing" + self.assertRaises(ValueError, self.run_command, cmd) + + def test_configuration_parameter_list(self): + cmd = 'configuration-parameter-list v-156 --datastore d-123' + self.run_command(cmd) + self.assert_called('GET', + '/datastores/d-123/versions/v-156/parameters') + + def test_configuration_parameter_list_error(self): + expected_error_msg = ('The datastore name or id is required to ' + 'retrieve the parameters for the configuration ' + 'group by name') + self.assertRaisesRegexp( + exceptions.NoUniqueMatch, expected_error_msg, + self.run_command, 'configuration-parameter-list v-156') + + def test_configuration_parameter_show(self): + cmd = ('configuration-parameter-show v_56 ' + 'max_connections --datastore d_123') + self.run_command(cmd) + self.assert_called( + 'GET', + '/datastores/d_123/versions/v_56/parameters/max_connections') + + def test_configuration_instances(self): + cmd = 'configuration-instances c-123' + self.run_command(cmd) + self.assert_called('GET', '/configurations/c-123/instances') + + def test_configuration_delete(self): + self.run_command('configuration-delete c-123') + self.assert_called('DELETE', '/configurations/c-123') + + def test_configuration_default(self): + self.run_command('configuration-default 1234') + self.assert_called('GET', '/instances/1234/configuration') + + def test_configuration_attach(self): + self.run_command('configuration-attach 1234 c-123') + self.assert_called('PUT', '/instances/1234') + + def test_configuration_detach(self): + self.run_command('configuration-detach 1234') + self.assert_called('PUT', '/instances/1234') + + def test_metadata_edit(self): + self.run_command('metadata-edit 1234 key-123 value-123') + self.assert_called('PATCH', '/instances/1234/metadata/key-123') + + def test_metadata_update(self): + self.run_command('metadata-update 1234 key-123 key-456 value-123') + self.assert_called('PUT', '/instances/1234/metadata/key-123') + + def test_metadata_delete(self): + self.run_command('metadata-delete 1234 key-123') + self.assert_called('DELETE', '/instances/1234/metadata/key-123') + + def test_metadata_create(self): + self.run_command('metadata-create 1234 key123 value123') + self.assert_called_anytime( + 'POST', '/instances/1234/metadata/key123', + {'metadata': {'value': 'value123'}}) + + def test_metadata_list(self): + self.run_command('metadata-list 1234') + self.assert_called('GET', '/instances/1234/metadata') + + def test_metadata_show(self): + self.run_command('metadata-show 1234 key123') + self.assert_called('GET', '/instances/1234/metadata/key123') + + def test_limit_list(self): + self.run_command('limit-list') + self.assert_called('GET', '/limits') + + def test_backup_list(self): + self.run_command('backup-list') + self.assert_called('GET', '/backups') + + def test_backup_show(self): + self.run_command('backup-show bk-1234') + self.assert_called('GET', '/backups/bk-1234') + + def test_backup_list_instance(self): + self.run_command('backup-list-instance 1234') + self.assert_called('GET', '/instances/1234/backups') + + def test_backup_delete(self): + self.run_command('backup-delete bk-1234') + self.assert_called('DELETE', '/backups/bk-1234') + + def test_backup_create(self): + self.run_command('backup-create 1234 bkp_1') + self.assert_called_anytime( + 'POST', '/backups', + {'backup': { + 'instance': '1234', + 'name': 'bkp_1' + }}) + + def test_backup_copy(self): + self.run_command('backup-copy new_bkp bk-1234') + self.assert_called_anytime( + 'POST', '/backups', + {'backup': { + 'name': 'new_bkp', + 'backup': {'region': None, 'id': 'bk-1234'} + }}) + + def test_database_list(self): + self.run_command('database-list 1234') + self.assert_called('GET', '/instances/1234/databases') + + def test_database_delete(self): + self.run_command('database-delete 1234 db_1') + self.assert_called('DELETE', '/instances/1234/databases/db_1') + + def test_database_create(self): + cmd = ('database-create 1234 db_1 --character_set utf8 ' + '--collate utf8_general_ci') + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/instances/1234/databases', + {'databases': [{'character_set': 'utf8', + 'name': 'db_1', + 'collate': 'utf8_general_ci'}]}) + + def test_user_list(self): + self.run_command('user-list 1234') + self.assert_called('GET', '/instances/1234/users') + + def test_user_show(self): + self.run_command('user-show 1234 jacob') + self.assert_called('GET', '/instances/1234/users/jacob') + + def test_user_delete(self): + self.run_command('user-delete 1234 jacob') + self.assert_called('DELETE', '/instances/1234/users/jacob') + + def test_user_create(self): + self.run_command('user-create 1234 jacob password') + self.assert_called_anytime( + 'POST', '/instances/1234/users', + {'users': [{ + 'password': 'password', + 'name': 'jacob', + 'databases': []}]}) + + def test_user_show_access(self): + self.run_command('user-show-access 1234 jacob') + self.assert_called('GET', '/instances/1234/users/jacob/databases') + + def test_user_update_host(self): + cmd = 'user-update-attributes 1234 jacob --new_host 10.0.0.1' + self.run_command(cmd) + self.assert_called('PUT', '/instances/1234/users/jacob') + + def test_user_update_name(self): + self.run_command('user-update-attributes 1234 jacob --new_name sam') + self.assert_called('PUT', '/instances/1234/users/jacob') + + def test_user_update_password(self): + cmd = 'user-update-attributes 1234 jacob --new_password new_pwd' + self.run_command(cmd) + self.assert_called('PUT', '/instances/1234/users/jacob') + + def test_user_grant_access(self): + self.run_command('user-grant-access 1234 jacob db1 db2') + self.assert_called('PUT', '/instances/1234/users/jacob/databases') + + def test_user_revoke_access(self): + self.run_command('user-revoke-access 1234 jacob db1') + self.assert_called('DELETE', + '/instances/1234/users/jacob/databases/db1') + + def test_root_enable_instance(self): + self.run_command('root-enable 1234') + self.assert_called_anytime('POST', '/instances/1234/root') + + def test_root_enable_cluster(self): + self.run_command_clusters('root-enable cls-1234') + self.assert_called_anytime('POST', '/clusters/cls-1234/root') + + def test_root_show_instance(self): + self.run_command('root-show 1234') + self.assert_called('GET', '/instances/1234/root') + + def test_root_show_cluster(self): + self.run_command_clusters('root-show cls-1234') + self.assert_called('GET', '/clusters/cls-1234/root') + + def test_secgroup_list(self): + self.run_command('secgroup-list') + self.assert_called('GET', '/security-groups') + + def test_secgroup_show(self): + self.run_command('secgroup-show 2') + self.assert_called('GET', '/security-groups/2') + + def test_secgroup_list_rules(self): + self.run_command('secgroup-list-rules 2') + self.assert_called('GET', '/security-groups/2') + + def test_secgroup_delete_rule(self): + self.run_command('secgroup-delete-rule 2') + self.assert_called('DELETE', '/security-group-rules/2') + + def test_secgroup_add_rule(self): + self.run_command('secgroup-add-rule 2 15.0.0.0/24') + self.assert_called_anytime( + 'POST', '/security-group-rules', + {'security_group_rule': { + 'cidr': '15.0.0.0/24', + 'group_id': '2', + }}) + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('troveclient.client.get_version_map', + return_value=fakes.get_version_map()) + @mock.patch('troveclient.v1.shell._find_instance', + side_effect=exceptions.CommandError) + @mock.patch('troveclient.v1.shell._find_cluster', + return_value='cls-1234') + def test_find_instance_or_cluster_find_cluster(self, mock_find_cluster, + mock_find_instance, + mock_get_version_map, + mock_stdout): + cmd = 'root-show cls-1234' + self.shell.main(cmd.split()) + self.assert_called('GET', '/clusters/cls-1234/root') + + @mock.patch('sys.stdout', new_callable=six.StringIO) + @mock.patch('troveclient.client.get_version_map', + return_value=fakes.get_version_map()) + @mock.patch('troveclient.v1.shell._find_instance', + return_value='1234') + def test_find_instance_or_cluster(self, mock_find_instance, + mock_get_version_map, mock_stdout): + cmd = 'root-show 1234' + self.shell.main(cmd.split()) + self.assert_called('GET', '/instances/1234/root') diff -Nru python-troveclient-1.2.0/troveclient/tests/utils.py python-troveclient-1.3.0/troveclient/tests/utils.py --- python-troveclient-1.2.0/troveclient/tests/utils.py 1970-01-01 00:00:00.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/tests/utils.py 2015-09-04 20:59:26.000000000 +0000 @@ -0,0 +1,81 @@ +# Copyright [2015] Hewlett-Packard Development Company, L.P. +# +# 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 os + +import fixtures +import mock +import requests +import testtools + +AUTH_URL = "http://localhost:5002/auth_url" +AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" +AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" + + +def _patch_mock_to_raise_for_invalid_assert_calls(): + def raise_for_invalid_assert_calls(wrapped): + def wrapper(_self, name): + valid_asserts = [ + 'assert_called_with', + 'assert_called_once_with', + 'assert_has_calls', + 'assert_any_calls'] + + if name.startswith('assert') and name not in valid_asserts: + raise AttributeError('%s is not a valid mock assert method' + % name) + + return wrapped(_self, name) + return wrapper + mock.Mock.__getattr__ = raise_for_invalid_assert_calls( + mock.Mock.__getattr__) + +# NOTE(gibi): needs to be called only once at import time +# to patch the mock lib +_patch_mock_to_raise_for_invalid_assert_calls() + + +class TestCase(testtools.TestCase): + TEST_REQUEST_BASE = { + 'verify': True, + } + + def setUp(self): + super(TestCase, self).setUp() + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + +class TestResponse(requests.Response): + """Class used to wrap requests.Response and provide some + convenience to initialize with a dict + """ + + def __init__(self, data): + super(TestResponse, self).__init__() + self._text = None + if isinstance(data, dict): + self.status_code = data.get('status_code') + self.headers = data.get('headers') + # Fake the text attribute to streamline Response creation + self._text = data.get('text') + else: + self.status_code = data diff -Nru python-troveclient-1.2.0/troveclient/utils.py python-troveclient-1.3.0/troveclient/utils.py --- python-troveclient-1.2.0/troveclient/utils.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/utils.py 2015-09-04 20:59:26.000000000 +0000 @@ -178,7 +178,7 @@ data = obj.get(field, '') else: data = getattr(obj, field, '') - row.append(data) + row.append(str(data)) # set the alignment to right-aligned if it's a numeric if set_align and hasattr(data, '__int__'): align[labels[field]] = 'r' diff -Nru python-troveclient-1.2.0/troveclient/v1/clusters.py python-troveclient-1.3.0/troveclient/v1/clusters.py --- python-troveclient-1.2.0/troveclient/v1/clusters.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/v1/clusters.py 2015-09-04 20:59:26.000000000 +0000 @@ -70,6 +70,15 @@ resp, body = self.api.client.delete(url) common.check_for_exceptions(resp, body, url) + def _action(self, cluster, body): + """Perform a cluster "action" -- grow/shrink/etc.""" + url = "/clusters/%s" % base.getid(cluster) + resp, body = self.api.client.post(url, body=body) + common.check_for_exceptions(resp, body, url) + if body: + return self.resource_class(self, body['cluster'], loaded=True) + return body + def add_shard(self, cluster): """Adds a shard to the specified cluster. @@ -83,6 +92,24 @@ return self.resource_class(self, body, loaded=True) return body + def grow(self, cluster, instances=None): + """Grow a cluster. + + :param cluster: The cluster to grow + :param instances: List of instances to add + """ + body = {"grow": instances} + return self._action(cluster, body) + + def shrink(self, cluster, instances=None): + """Shrink a cluster. + + :param cluster: The cluster to shrink + :param instances: List of instances to drop + """ + body = {"shrink": instances} + return self._action(cluster, body) + class ClusterStatus(object): diff -Nru python-troveclient-1.2.0/troveclient/v1/flavors.py python-troveclient-1.3.0/troveclient/v1/flavors.py --- python-troveclient-1.2.0/troveclient/v1/flavors.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/v1/flavors.py 2015-09-04 20:59:26.000000000 +0000 @@ -20,6 +20,11 @@ class Flavor(base.Resource): """A Flavor is an Instance type, specifying other things, like RAM size.""" + def __init__(self, manager, info, loaded=False): + super(Flavor, self).__init__(manager, info, loaded) + if self.id is None and self.str_id is not None: + self.id = self.str_id + def __repr__(self): return "" % self.name diff -Nru python-troveclient-1.2.0/troveclient/v1/management.py python-troveclient-1.3.0/troveclient/v1/management.py --- python-troveclient-1.2.0/troveclient/v1/management.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/v1/management.py 2015-09-04 20:59:26.000000000 +0000 @@ -14,10 +14,13 @@ # License for the specific language governing permissions and limitations # under the License. +import json + from troveclient import base from troveclient import common from troveclient.v1 import clusters from troveclient.v1 import configurations +from troveclient.v1 import datastores from troveclient.v1 import flavors from troveclient.v1 import instances @@ -252,3 +255,57 @@ "parameters/%(parameter_name)s" % output) resp, body = self.api.client.delete(url) common.check_for_exceptions(resp, body, url) + + +class MgmtDatastoreVersions(base.ManagerWithFind): + """Manage :class:`DatastoreVersion` resources.""" + resource_class = datastores.DatastoreVersion + + def list(self, limit=None, marker=None): + """List all datastore versions.""" + return self._paginated("/mgmt/datastore-versions", "versions", + limit, marker) + + def get(self, datastore_version_id): + """Get details of a datastore version.""" + return self._get("/mgmt/datastore-versions/%s" % datastore_version_id, + "version") + + def create(self, name, datastore_name, datastore_manager, image, + packages=[], active='true', default='false'): + """Create a new datastore version.""" + body = {"version": { + "name": name, + "datastore_name": datastore_name, + "datastore_manager": datastore_manager, + "image": image, + "packages": packages, + "active": json.loads(active), + "default": json.loads(default) + }} + + return self._create("/mgmt/datastore-versions", body, None, True) + + def edit(self, datastore_version_id, datastore_manager=None, image=None, + packages=[], active=None, default=None): + """Update a datastore-version.""" + body = {} + if datastore_manager is not None: + body['datastore_manager'] = datastore_manager + if image: + body['image'] = image + if packages: + body['packages'] = packages + if active is not None: + body['active'] = json.loads(active) + if default is not None: + body['default'] = json.loads(default) + url = ("/mgmt/datastore-versions/%s" % datastore_version_id) + resp, body = self.api.client.patch(url, body=body) + common.check_for_exceptions(resp, body, url) + + def delete(self, datastore_version_id): + """Delete a datastore version.""" + url = ("/mgmt/datastore-versions/%s" % datastore_version_id) + resp, body = self.api.client.delete(url) + common.check_for_exceptions(resp, body, url) diff -Nru python-troveclient-1.2.0/troveclient/v1/root.py python-troveclient-1.3.0/troveclient/v1/root.py --- python-troveclient-1.2.0/troveclient/v1/root.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/v1/root.py 2015-09-04 20:59:26.000000000 +0000 @@ -22,22 +22,55 @@ class Root(base.ManagerWithFind): """Manager class for Root resource.""" resource_class = users.User - url = "/instances/%s/root" + instances_url = "/instances/%s/root" + clusters_url = "/clusters/%s/root" def create(self, instance): """Implements root-enable API. - Enable the root user and return the root password for the specified db instance. """ - resp, body = self.api.client.post(self.url % base.getid(instance)) - common.check_for_exceptions(resp, body, self.url) + return self.create_instance_root(instance) + + def create_instance_root(self, instance, root_password=None): + """Implements root-enable for instances.""" + return self._enable_root(self.instances_url % base.getid(instance), + root_password) + + def create_cluster_root(self, cluster, root_password=None): + """Implements root-enable for clusters.""" + return self._enable_root(self.clusters_url % base.getid(cluster), + root_password) + + def _enable_root(self, uri, root_password=None): + """Implements root-enable API. + Enable the root user and return the root password for the + specified db instance or cluster. + """ + if root_password: + resp, body = self.api.client.post(uri, + body={"password": root_password}) + else: + resp, body = self.api.client.post(uri) + common.check_for_exceptions(resp, body, uri) return body['user']['name'], body['user']['password'] def is_root_enabled(self, instance): """Return whether root is enabled for the instance.""" - resp, body = self.api.client.get(self.url % base.getid(instance)) - common.check_for_exceptions(resp, body, self.url) + return self.is_instance_root_enabled(instance) + + def is_instance_root_enabled(self, instance): + """Returns whether root is enabled for the instance.""" + return self._is_root_enabled(self.instances_url % base.getid(instance)) + + def is_cluster_root_enabled(self, cluster): + """Returns whether root is enabled for the cluster.""" + return self._is_root_enabled(self.clusters_url % base.getid(cluster)) + + def _is_root_enabled(self, uri): + """Return whether root is enabled for the instance or the cluster.""" + resp, body = self.api.client.get(uri) + common.check_for_exceptions(resp, body, uri) return self.resource_class(self, body, loaded=True) # Appease the abc gods diff -Nru python-troveclient-1.2.0/troveclient/v1/shell.py python-troveclient-1.3.0/troveclient/v1/shell.py --- python-troveclient-1.2.0/troveclient/v1/shell.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/v1/shell.py 2015-09-04 20:59:26.000000000 +0000 @@ -19,6 +19,9 @@ import sys import time +INSTANCE_ERROR = ("Instance argument(s) must be of the form --instance " + "") + try: import simplejson as json except ImportError: @@ -90,13 +93,28 @@ # Fallback to str_id for flavors, where necessary if hasattr(obj, 'str_id'): - if hasattr(obj, 'id') and not obj.id: - obj._info['id'] = obj.str_id + obj._info['id'] = obj.id del(obj._info['str_id']) utils.print_dict(obj._info) +def _find_instance_or_cluster(cs, instance_or_cluster): + """Returns an instance or cluster, found by id, along with the type of + resource, instance or cluster, that was found. + Raises CommandError if none is found. + """ + try: + return _find_instance(cs, instance_or_cluster), 'instance' + except exceptions.CommandError: + try: + return _find_cluster(cs, instance_or_cluster), 'cluster' + except Exception: + raise exceptions.CommandError( + "No instance or cluster with a name or ID of '%s' exists." + % instance_or_cluster) + + def _find_instance(cs, instance): """Get an instance by ID.""" return utils.find_resource(cs.instances, instance) @@ -132,9 +150,8 @@ elif not args.datastore_type and not args.datastore_version_id: flavors = cs.flavors.list() else: - err_msg = ("Specify both and " - " to list datastore version associated flavors.") - raise exceptions.CommandError(err_msg) + raise exceptions.MissingArgs(['datastore_type', + 'datastore_version_id']) # Fallback to str_id where necessary. _flavors = [] @@ -251,6 +268,58 @@ obj_is_dict=True) +@utils.arg('--instance', + metavar="", + action='append', + dest='instances', + default=[], + help="Add an instance to the cluster. Specify " + "multiple times to create multiple instances.") +@utils.arg('cluster', metavar='', help='ID or name of the cluster.') +@utils.service_type('database') +def do_cluster_grow(cs, args): + """Adds more instances to a cluster.""" + cluster = _find_cluster(cs, args.cluster) + instances = [] + for instance_str in args.instances: + instance_info = {} + for z in instance_str.split(","): + for (k, v) in [z.split("=", 1)[:2]]: + if k == "name": + instance_info[k] = v + elif k == "flavor": + flavor_id = _find_flavor(cs, v).id + instance_info["flavorRef"] = str(flavor_id) + elif k == "volume": + instance_info["volume"] = {"size": v} + else: + instance_info[k] = v + if not instance_info.get('flavorRef'): + raise exceptions.CommandError( + 'flavor is required. ' + 'Instance arguments must be of the form ' + '--instance ' + ) + instances.append(instance_info) + cs.clusters.grow(cluster, instances=instances) + + +@utils.arg('cluster', metavar='', help='ID or name of the cluster.') +@utils.arg('instances', + nargs='+', + metavar='', + default=[], + help="Drop instance(s) from the cluster. Specify " + "multiple ids to drop multiple instances.") +@utils.service_type('database') +def do_cluster_shrink(cs, args): + """Drops instances from a cluster.""" + cluster = _find_cluster(cs, args.cluster) + instances = [{'id': _find_instance(cs, instance).id} + for instance in args.instances] + cs.clusters.shrink(cluster, instances=instances) + + @utils.arg('instance', metavar='', help='ID or name of the instance.') @utils.service_type('database') @@ -260,11 +329,12 @@ cs.instances.delete(instance) -@utils.arg('cluster', metavar='', help='ID of the cluster.') +@utils.arg('cluster', metavar='', help='ID or name of the cluster.') @utils.service_type('database') def do_cluster_delete(cs, args): """Deletes a cluster.""" - cs.clusters.delete(args.cluster) + cluster = _find_cluster(cs, args.cluster) + cs.clusters.delete(cluster) @utils.arg('instance', @@ -309,9 +379,9 @@ default=None, help="Size of the instance disk volume in GB. " "Required when volume support is enabled.") -@utils.arg('flavor_id', - metavar='', - help='Flavor of the instance.') +@utils.arg('flavor', + metavar='', + help='Flavor ID or name of the instance.') @utils.arg('--databases', metavar='', help='Optional list of databases.', nargs="+", default=[]) @@ -364,6 +434,7 @@ """Creates a new instance.""" volume = None replica_of_instance = None + flavor_id = _find_flavor(cs, args.flavor).id if args.size: volume = {"size": args.size} restore_point = None @@ -383,11 +454,11 @@ "the form --nic , with at minimum net-id or port-id " "(but not both) specified." % nic_str) - raise exceptions.CommandError(err_msg) + raise exceptions.ValidationError(err_msg) nics.append(nic_info) instance = cs.instances.create(args.name, - args.flavor_id, + flavor_id, volume=volume, databases=databases, users=users, @@ -413,7 +484,7 @@ metavar='', help='A datastore version name or UUID.') @utils.arg('--instance', - metavar="", + metavar="", action='append', dest='instances', default=[], @@ -427,18 +498,21 @@ instance_info = {} for z in instance_str.split(","): for (k, v) in [z.split("=", 1)[:2]]: - if k == "flavor_id": - instance_info["flavorRef"] = v + if k == "flavor": + flavor_id = _find_flavor(cs, v).id + instance_info["flavorRef"] = str(flavor_id) elif k == "volume": instance_info["volume"] = {"size": v} else: instance_info[k] = v if not instance_info.get('flavorRef'): - err_msg = ("flavor_id is required. Instance arguments must be " - "of the form --instance .") - raise exceptions.CommandError(err_msg) + err_msg = ("flavor is required. %s." % INSTANCE_ERROR) + raise exceptions.ValidationError(err_msg) instances.append(instance_info) + + if len(instances) == 0: + raise exceptions.MissingArgs(['instance']) + cluster = cs.clusters.create(args.name, args.datastore, args.datastore_version, @@ -456,27 +530,15 @@ metavar='', type=str, help='ID or name of the instance.') -@utils.arg('flavor_id', - metavar='', - help='New flavor of the instance.') -@utils.service_type('database') -def do_resize_flavor(cs, args): - """[DEPRECATED] Please use resize-instance instead.""" - do_resize_instance(cs, args) - - -@utils.arg('instance', - metavar='', - type=str, - help='ID or name of the instance.') -@utils.arg('flavor_id', - metavar='', +@utils.arg('flavor', + metavar='', help='New flavor of the instance.') @utils.service_type('database') def do_resize_instance(cs, args): """Resizes an instance with a new flavor.""" instance = _find_instance(cs, args.instance) - cs.instances.resize_instance(instance, args.flavor_id) + flavor_id = _find_flavor(cs, args.flavor).id + cs.instances.resize_instance(instance, flavor_id) @utils.arg('instance', @@ -553,11 +615,16 @@ @utils.arg('--limit', metavar='', default=None, help='Return up to N number of the most recent backups.') +@utils.arg('--marker', metavar='', type=str, default=None, + help='Begin displaying the results for IDs greater than the ' + 'specified marker. When used with --limit, set this to ' + 'the last ID displayed in the previous run.') @utils.service_type('database') def do_backup_list_instance(cs, args): """Lists available backups for an instance.""" instance = _find_instance(cs, args.instance) - wrapper = cs.instances.backups(instance, limit=args.limit) + wrapper = cs.instances.backups(instance, limit=args.limit, + marker=args.marker) backups = wrapper.items while wrapper.next and not args.limit: wrapper = cs.instances.backups(instance, marker=wrapper.next) @@ -570,13 +637,18 @@ @utils.arg('--limit', metavar='', default=None, help='Return up to N number of the most recent backups.') +@utils.arg('--marker', metavar='', type=str, default=None, + help='Begin displaying the results for IDs greater than the ' + 'specified marker. When used with --limit, set this to ' + 'the last ID displayed in the previous run.') @utils.arg('--datastore', metavar='', default=None, help='Name or ID of the datastore to list backups for.') @utils.service_type('database') def do_backup_list(cs, args): """Lists available backups.""" - wrapper = cs.backups.list(limit=args.limit, datastore=args.datastore) + wrapper = cs.backups.list(limit=args.limit, datastore=args.datastore, + marker=args.marker) backups = wrapper.items while wrapper.next and not args.limit: wrapper = cs.backups.list(marker=wrapper.next) @@ -834,23 +906,37 @@ # Root related commands -@utils.arg('instance', metavar='', - help='ID or name of the instance.') +@utils.arg('instance_or_cluster', metavar='', + help='ID or name of the instance or cluster.') +@utils.arg('--root_password', + metavar='', + default=None, + help='Root password to set.') @utils.service_type('database') def do_root_enable(cs, args): """Enables root for an instance and resets if already exists.""" - instance = _find_instance(cs, args.instance) - root = cs.root.create(instance) + instance_or_cluster, resource_type = _find_instance_or_cluster( + cs, args.instance_or_cluster) + if resource_type == 'instance': + root = cs.root.create_instance_root(instance_or_cluster, + args.root_password) + else: + root = cs.root.create_cluster_root(instance_or_cluster, + args.root_password) utils.print_dict({'name': root[0], 'password': root[1]}) -@utils.arg('instance', metavar='', - help='ID or name of the instance.') +@utils.arg('instance_or_cluster', metavar='', + help='ID or name of the instance or cluster.') @utils.service_type('database') def do_root_show(cs, args): - """Gets status if root was ever enabled for an instance.""" - instance = _find_instance(cs, args.instance) - root = cs.root.is_root_enabled(instance) + """Gets status if root was ever enabled for an instance or cluster.""" + instance_or_cluster, resource_type = _find_instance_or_cluster( + cs, args.instance_or_cluster) + if resource_type == 'instance': + root = cs.root.is_instance_root_enabled(instance_or_cluster) + else: + root = cs.root.is_cluster_root_enabled(instance_or_cluster) utils.print_dict({'is_root_enabled': root.rootEnabled}) @@ -869,7 +955,7 @@ @utils.arg('security_group', metavar='', - help='Security group ID') + help='Security group ID.') @utils.service_type('database') def do_secgroup_show(cs, args): """Shows details of a security group.""" @@ -1146,7 +1232,7 @@ args.description) -@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.arg('instance_id', metavar='', help='UUID for instance.') @utils.service_type('database') def do_metadata_list(cs, args): """Shows all metadata for instance .""" @@ -1154,8 +1240,8 @@ _print_object(result) -@utils.arg('instance_id', metavar='', help='UUID for instance') -@utils.arg('key', metavar='', help='key to display') +@utils.arg('instance_id', metavar='', help='UUID for instance.') +@utils.arg('key', metavar='', help='Key to display.') @utils.service_type('database') def do_metadata_show(cs, args): """Shows metadata entry for key and instance .""" @@ -1163,29 +1249,29 @@ _print_object(result) -@utils.arg('instance_id', metavar='', help='UUID for instance') -@utils.arg('key', metavar='', help='Key to replace') +@utils.arg('instance_id', metavar='', help='UUID for instance.') +@utils.arg('key', metavar='', help='Key to replace.') @utils.arg('value', metavar='', - help='New value to assign to ') + help='New value to assign to .') @utils.service_type('database') def do_metadata_edit(cs, args): """Replaces metadata value with a new one, this is non-destructive.""" cs.metadata.edit(args.instance_id, args.key, args.value) -@utils.arg('instance_id', metavar='', help='UUID for instance') -@utils.arg('key', metavar='', help='Key to update') -@utils.arg('newkey', metavar='', help='New key') -@utils.arg('value', metavar='', help='Value to assign to ') +@utils.arg('instance_id', metavar='', help='UUID for instance.') +@utils.arg('key', metavar='', help='Key to update.') +@utils.arg('newkey', metavar='', help='New key.') +@utils.arg('value', metavar='', help='Value to assign to .') @utils.service_type('database') def do_metadata_update(cs, args): """Updates metadata, this is destructive.""" cs.metadata.update(args.instance_id, args.key, args.newkey, args.value) -@utils.arg('instance_id', metavar='', help='UUID for instance') -@utils.arg('key', metavar='', help='Key for assignment') -@utils.arg('value', metavar='', help='Value to assign to ') +@utils.arg('instance_id', metavar='', help='UUID for instance.') +@utils.arg('key', metavar='', help='Key for assignment.') +@utils.arg('value', metavar='', help='Value to assign to .') @utils.service_type('database') def do_metadata_create(cs, args): """Creates metadata in the database for instance .""" @@ -1193,8 +1279,8 @@ _print_object(result) -@utils.arg('instance_id', metavar='', help='UUID for instance') -@utils.arg('key', metavar='', help='Metadata key to delete') +@utils.arg('instance_id', metavar='', help='UUID for instance.') +@utils.arg('key', metavar='', help='Metadata key to delete.') @utils.service_type('database') def do_metadata_delete(cs, args): """Deletes metadata for instance .""" diff -Nru python-troveclient-1.2.0/troveclient/v1/users.py python-troveclient-1.3.0/troveclient/v1/users.py --- python-troveclient-1.2.0/troveclient/v1/users.py 2015-05-26 08:58:51.000000000 +0000 +++ python-troveclient-1.3.0/troveclient/v1/users.py 2015-09-04 20:59:26.000000000 +0000 @@ -16,11 +16,13 @@ from troveclient import base from troveclient import common +from troveclient import exceptions from troveclient.v1 import databases class User(base.Resource): """A database user.""" + def __repr__(self): return "" % self.name @@ -67,7 +69,8 @@ :rtype: :class:`User`. """ if not newuserattr: - raise Exception("No updates specified for user %s" % username) + raise exceptions.ValidationError("No updates specified for user %s" + % username) instance_id = base.getid(instance) user = common.quote_user_host(username, hostname) user_dict = {}