diff options
author | amcrn <amcreynolds@ebaysf.com> | 2014-08-12 16:52:19 -0700 |
---|---|---|
committer | amcrn <amcreynolds@ebaysf.com> | 2014-09-02 16:38:56 -0700 |
commit | 6852bdcefc17712b4b57a530adfb406e059e52e2 (patch) | |
tree | 6cce0ad46cb5e07d54c8152315485bea50cf481b | |
parent | 33c76fab16b80f32a04f83e1653d4f809c22d3bd (diff) | |
download | python-troveclient-6852bdcefc17712b4b57a530adfb406e059e52e2.tar.gz |
Clusters troveclient Implementation
adds clusters support to the troveclient.
Co-Authored-By: Ranjitha Vemula <rvemula@ebaysf.com>
Co-Authored-By: Michael Yu <michayu@ebaysf.com>
Co-Authored-By: Mat Lowery <mlowery@ebaysf.com>
Partially implements: blueprint clustering
Change-Id: I6ed2c4c79a17fcf8f14c587cab6a8ec3acaf319f
-rw-r--r-- | troveclient/compat/client.py | 3 | ||||
-rw-r--r-- | troveclient/tests/test_clusters.py | 120 | ||||
-rw-r--r-- | troveclient/tests/test_instances.py | 4 | ||||
-rw-r--r-- | troveclient/v1/client.py | 3 | ||||
-rw-r--r-- | troveclient/v1/clusters.py | 92 | ||||
-rw-r--r-- | troveclient/v1/instances.py | 5 | ||||
-rw-r--r-- | troveclient/v1/management.py | 42 | ||||
-rw-r--r-- | troveclient/v1/shell.py | 122 |
8 files changed, 387 insertions, 4 deletions
diff --git a/troveclient/compat/client.py b/troveclient/compat/client.py index 03802a0..c373802 100644 --- a/troveclient/compat/client.py +++ b/troveclient/compat/client.py @@ -302,6 +302,7 @@ class Dbaas(object): from troveclient.compat import versions from troveclient.v1 import accounts from troveclient.v1 import backups + from troveclient.v1 import clusters from troveclient.v1 import configurations from troveclient.v1 import databases from troveclient.v1 import datastores @@ -335,6 +336,7 @@ class Dbaas(object): self.hosts = hosts.Hosts(self) self.quota = quota.Quotas(self) self.backups = backups.Backups(self) + self.clusters = clusters.Clusters(self) self.security_groups = security_groups.SecurityGroups(self) self.security_group_rules = security_groups.SecurityGroupRules(self) self.datastores = datastores.Datastores(self) @@ -343,6 +345,7 @@ class Dbaas(object): DatastoreVersionMembers(self)) self.storage = storage.StorageInfo(self) self.management = management.Management(self) + self.mgmt_cluster = management.MgmtClusters(self) self.mgmt_flavor = management.MgmtFlavors(self) self.accounts = accounts.Accounts(self) self.diagnostics = diagnostics.DiagnosticsInterrogator(self) diff --git a/troveclient/tests/test_clusters.py b/troveclient/tests/test_clusters.py new file mode 100644 index 0000000..bf3061b --- /dev/null +++ b/troveclient/tests/test_clusters.py @@ -0,0 +1,120 @@ +# Copyright (c) 2014 eBay Software Foundation +# 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 mock +import testtools + +from troveclient import base +from troveclient.v1 import clusters + +""" +Unit tests for clusters.py +""" + + +class ClusterTest(testtools.TestCase): + + def setUp(self): + super(ClusterTest, self).setUp() + + def tearDown(self): + super(ClusterTest, self).tearDown() + + @mock.patch.object(clusters.Cluster, '__init__', return_value=None) + def test___repr__(self, mock_init): + cluster = clusters.Cluster() + cluster.name = "cluster-1" + self.assertEqual('<Cluster: cluster-1>', cluster.__repr__()) + + @mock.patch.object(clusters.Cluster, '__init__', return_value=None) + def test_delete(self, mock_init): + cluster = clusters.Cluster() + cluster.manager = mock.Mock() + db_delete_mock = mock.Mock(return_value=None) + cluster.manager.delete = db_delete_mock + cluster.delete() + self.assertEqual(1, db_delete_mock.call_count) + + +class ClustersTest(testtools.TestCase): + + def setUp(self): + super(ClustersTest, self).setUp() + + def tearDown(self): + super(ClustersTest, self).tearDown() + + @mock.patch.object(clusters.Clusters, '__init__', return_value=None) + def get_clusters(self, mock_init): + clusters_test = clusters.Clusters() + clusters_test.api = mock.Mock() + clusters_test.api.client = mock.Mock() + clusters_test.resource_class = mock.Mock(return_value="cluster-1") + return clusters_test + + def test_create(self): + def side_effect_func(path, body, resp_key): + return path, body, resp_key + + clusters_test = self.get_clusters() + clusters_test._create = mock.Mock(side_effect=side_effect_func) + instance = [{'flavor-id': 11, 'volume': 2}] + path, body, resp_key = clusters_test.create("test-name", "datastore", + "datastore-version", + instance) + self.assertEqual("/clusters", path) + self.assertEqual("cluster", resp_key) + self.assertEqual("test-name", body["cluster"]["name"]) + self.assertEqual("datastore", body["cluster"]["datastore"]["type"]) + self.assertEqual("datastore-version", + body["cluster"]["datastore"]["version"]) + self.assertEqual(instance, body["cluster"]["instances"]) + + def test_list(self): + page_mock = mock.Mock() + clusters_test = self.get_clusters() + clusters_test._paginated = page_mock + limit = "test-limit" + marker = "test-marker" + clusters_test.list(limit, marker) + page_mock.assert_called_with("/clusters", "clusters", limit, marker) + + @mock.patch.object(base, 'getid', return_value="cluster1") + def test_get(self, mock_id): + def side_effect_func(path, inst): + return path, inst + clusters_test = self.get_clusters() + clusters_test._get = mock.Mock(side_effect=side_effect_func) + self.assertEqual(('/clusters/cluster1', 'cluster'), + clusters_test.get(1)) + + def test_delete(self): + resp = mock.Mock() + resp.status_code = 200 + body = None + clusters_test = self.get_clusters() + clusters_test.api.client.delete = mock.Mock(return_value=(resp, body)) + clusters_test.delete('cluster1') + resp.status_code = 500 + self.assertRaises(Exception, clusters_test.delete, 'cluster1') + + +class ClusterStatusTest(testtools.TestCase): + + def test_constants(self): + self.assertEqual("ACTIVE", clusters.ClusterStatus.ACTIVE) + self.assertEqual("BUILD", clusters.ClusterStatus.BUILD) + self.assertEqual("FAILED", clusters.ClusterStatus.FAILED) + self.assertEqual("SHUTDOWN", clusters.ClusterStatus.SHUTDOWN) diff --git a/troveclient/tests/test_instances.py b/troveclient/tests/test_instances.py index 15c2912..2754b5c 100644 --- a/troveclient/tests/test_instances.py +++ b/troveclient/tests/test_instances.py @@ -117,8 +117,10 @@ class InstancesTest(testtools.TestCase): self.instances._paginated = page_mock limit = "test-limit" marker = "test-marker" + include_clustered = {'include_clustered': False} self.instances.list(limit, marker) - page_mock.assert_called_with("/instances", "instances", limit, marker) + page_mock.assert_called_with("/instances", "instances", limit, marker, + include_clustered) def test_get(self): def side_effect_func(path, inst): diff --git a/troveclient/v1/client.py b/troveclient/v1/client.py index 1b42cb5..8a34a8e 100644 --- a/troveclient/v1/client.py +++ b/troveclient/v1/client.py @@ -16,6 +16,7 @@ from troveclient import client as trove_client from troveclient.v1 import backups +from troveclient.v1 import clusters from troveclient.v1 import configurations from troveclient.v1 import databases from troveclient.v1 import datastores @@ -57,6 +58,7 @@ class Client(object): self.users = users.Users(self) self.databases = databases.Databases(self) self.backups = backups.Backups(self) + self.clusters = clusters.Clusters(self) self.instances = instances.Instances(self) self.limits = limits.Limits(self) self.root = root.Root(self) @@ -73,6 +75,7 @@ class Client(object): # self.quota = Quotas(self) # self.storage = StorageInfo(self) # self.management = Management(self) + # self.management = MgmtClusters(self) # self.mgmt_flavor = MgmtFlavors(self) # self.accounts = Accounts(self) # self.diagnostics = DiagnosticsInterrogator(self) diff --git a/troveclient/v1/clusters.py b/troveclient/v1/clusters.py new file mode 100644 index 0000000..dc9c2f0 --- /dev/null +++ b/troveclient/v1/clusters.py @@ -0,0 +1,92 @@ +# Copyright (c) 2014 eBay Software Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from troveclient import base +from troveclient import common + + +class Cluster(base.Resource): + """A Cluster is an opaque cluster used to store Database clusters.""" + def __repr__(self): + return "<Cluster: %s>" % self.name + + def delete(self): + """Delete the cluster.""" + self.manager.delete(self) + + +class Clusters(base.ManagerWithFind): + """Manage :class:`Cluster` resources.""" + resource_class = Cluster + + def create(self, name, datastore, datastore_version, instances=None): + """Create (boot) a new cluster.""" + body = {"cluster": { + "name": name + }} + datastore_obj = { + "type": datastore, + "version": datastore_version + } + body["cluster"]["datastore"] = datastore_obj + if instances: + body["cluster"]["instances"] = instances + + return self._create("/clusters", body, "cluster") + + def list(self, limit=None, marker=None): + """Get a list of all clusters. + + :rtype: list of :class:`Cluster`. + """ + return self._paginated("/clusters", "clusters", limit, marker) + + def get(self, cluster): + """Get a specific cluster. + + :rtype: :class:`Cluster` + """ + return self._get("/clusters/%s" % base.getid(cluster), + "cluster") + + def delete(self, cluster): + """Delete the specified cluster. + + :param cluster: The cluster to delete + """ + url = "/clusters/%s" % base.getid(cluster) + resp, body = self.api.client.delete(url) + common.check_for_exceptions(resp, body, url) + + def add_shard(self, cluster): + """Adds a shard to the specified cluster. + + :param cluster: The cluster to add a shard to + """ + url = "/clusters/%s" % base.getid(cluster) + body = {"add_shard": {}} + resp, body = self.api.client.post(url, body=body) + common.check_for_exceptions(resp, body, url) + if body: + return self.resource_class(self, body, loaded=True) + return body + + +class ClusterStatus(object): + + ACTIVE = "ACTIVE" + BUILD = "BUILD" + FAILED = "FAILED" + SHUTDOWN = "SHUTDOWN" diff --git a/troveclient/v1/instances.py b/troveclient/v1/instances.py index f667654..2171333 100644 --- a/troveclient/v1/instances.py +++ b/troveclient/v1/instances.py @@ -115,12 +115,13 @@ class Instances(base.ManagerWithFind): resp, body = self.api.client.patch(url, body=body) common.check_for_exceptions(resp, body, url) - def list(self, limit=None, marker=None): + def list(self, limit=None, marker=None, include_clustered=False): """Get a list of all instances. :rtype: list of :class:`Instance`. """ - return self._paginated("/instances", "instances", limit, marker) + return self._paginated("/instances", "instances", limit, marker, + {"include_clustered": include_clustered}) def get(self, instance): """Get a specific instances. diff --git a/troveclient/v1/management.py b/troveclient/v1/management.py index 782b3c1..3dc57ac 100644 --- a/troveclient/v1/management.py +++ b/troveclient/v1/management.py @@ -16,6 +16,7 @@ from troveclient import base from troveclient import common +from troveclient.v1 import clusters from troveclient.v1 import flavors from troveclient.v1 import instances @@ -108,6 +109,47 @@ class Management(base.ManagerWithFind): self._action(instance_id, body) +class MgmtClusters(base.ManagerWithFind): + """Manage :class:`Cluster` resources.""" + resource_class = clusters.Cluster + + # Appease the abc gods + def list(self): + pass + + def show(self, cluster): + """Get details of one cluster.""" + return self._get("/mgmt/clusters/%s" % base.getid(cluster), 'cluster') + + def index(self, deleted=None, limit=None, marker=None): + """Show an overview of all local clusters. + + Optionally, filter by deleted status. + + :rtype: list of :class:`Cluster`. + """ + form = '' + if deleted is not None: + if deleted: + form = "?deleted=true" + else: + form = "?deleted=false" + + url = "/mgmt/clusters%s" % form + return self._paginated(url, "clusters", limit, marker) + + def _action(self, cluster_id, body): + """Perform a cluster action, e.g. reset-task.""" + url = "/mgmt/clusters/%s/action" % cluster_id + resp, body = self.api.client.post(url, body=body) + common.check_for_exceptions(resp, body, url) + + def reset_task(self, cluster_id): + """Reset the current cluster task to NONE.""" + body = {'reset-task': {}} + self._action(cluster_id, body) + + class MgmtFlavors(base.ManagerWithFind): """Manage :class:`Flavor` resources.""" resource_class = flavors.Flavor diff --git a/troveclient/v1/shell.py b/troveclient/v1/shell.py index a33b145..83be5be 100644 --- a/troveclient/v1/shell.py +++ b/troveclient/v1/shell.py @@ -95,6 +95,11 @@ def _find_instance(cs, instance): return utils.find_resource(cs.instances, instance) +def _find_cluster(cs, cluster): + """Get a cluster by ID.""" + return utils.find_resource(cs.clusters, cluster) + + def _find_flavor(cs, flavor): """Get a flavor by ID.""" return utils.find_resource(cs.flavors, flavor) @@ -131,10 +136,15 @@ def do_flavor_show(cs, args): 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('--include-clustered', dest='include_clustered', + action="store_true", default=False, + help="Include instances that are part of a cluster " + "(default false).") @utils.service_type('database') def do_list(cs, args): """Lists all the instances.""" - instances = cs.instances.list(limit=args.limit, marker=args.marker) + instances = cs.instances.list(limit=args.limit, marker=args.marker, + include_clustered=args.include_clustered) for instance in instances: setattr(instance, 'flavor_id', instance.flavor['id']) @@ -150,6 +160,26 @@ def do_list(cs, args): 'flavor_id', 'size']) +@utils.arg('--limit', metavar='<limit>', type=int, default=None, + help='Limit the number of results displayed.') +@utils.arg('--marker', metavar='<ID>', 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_cluster_list(cs, args): + """Lists all the clusters.""" + clusters = cs.clusters.list(limit=args.limit, marker=args.marker) + + for cluster in clusters: + setattr(cluster, 'datastore_version', + cluster.datastore['version']) + setattr(cluster, 'datastore', cluster.datastore['type']) + setattr(cluster, 'task_name', cluster.task['name']) + utils.print_list(clusters, ['id', 'name', 'datastore', + 'datastore_version', 'task_name']) + + @utils.arg('instance', metavar='<instance>', help='ID or name of the instance.') @utils.service_type('database') @@ -159,6 +189,39 @@ def do_show(cs, args): _print_instance(instance) +@utils.arg('cluster', metavar='<cluster>', help='ID or name of the cluster.') +@utils.service_type('database') +def do_cluster_show(cs, args): + """Shows details of a cluster.""" + cluster = _find_cluster(cs, args.cluster) + info = cluster._info.copy() + info['datastore'] = cluster.datastore['type'] + info['datastore_version'] = cluster.datastore['version'] + info['task_name'] = cluster.task['name'] + info['task_description'] = cluster.task['description'] + del info['task'] + if hasattr(cluster, 'ip'): + info['ip'] = ', '.join(cluster.ip) + del info['instances'] + cluster._info = info + _print_instance(cluster) + + +@utils.arg('cluster', metavar='<cluster>', help='ID or name of the cluster.') +@utils.service_type('database') +def do_cluster_instances(cs, args): + """Lists all instances of a cluster.""" + cluster = _find_cluster(cs, args.cluster) + instances = cluster._info['instances'] + for instance in instances: + instance['flavor_id'] = instance['flavor']['id'] + if instance.get('volume'): + instance['size'] = instance['volume']['size'] + utils.print_list( + instances, ['id', 'name', 'flavor_id', 'size', 'status'], + obj_is_dict=True) + + @utils.arg('instance', metavar='<instance>', help='ID of the instance.') @utils.service_type('database') def do_delete(cs, args): @@ -166,6 +229,13 @@ def do_delete(cs, args): cs.instances.delete(args.instance) +@utils.arg('cluster', metavar='<cluster>', help='ID of the cluster.') +@utils.service_type('database') +def do_cluster_delete(cs, args): + """Deletes a cluster.""" + cs.clusters.delete(args.cluster) + + @utils.arg('instance', metavar='<instance>', type=str, @@ -290,6 +360,56 @@ def do_create(cs, args): _print_instance(instance) +@utils.arg('name', + metavar='<name>', + type=str, + help='Name of the cluster.') +@utils.arg('datastore', + metavar='<datastore>', + help='A datastore name or UUID.') +@utils.arg('datastore_version', + metavar='<datastore_version>', + help='A datastore version name or UUID.') +@utils.arg('--instance', + metavar="<flavor_id=flavor_id,volume=volume>", + action='append', + dest='instances', + default=[], + help="Create an instance for the cluster. Specify multiple " + "times to create multiple instances.") +@utils.service_type('database') +def do_cluster_create(cs, args): + """Creates a new 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 == "flavor_id": + instance_info["flavorRef"] = v + 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 <flavor_id=flavor_id," + "volume=volume>.") + raise exceptions.CommandError(err_msg) + instances.append(instance_info) + cluster = cs.clusters.create(args.name, + args.datastore, + args.datastore_version, + instances=instances) + cluster._info['task_name'] = cluster.task['name'] + cluster._info['task_description'] = cluster.task['description'] + del cluster._info['task'] + cluster._info['datastore'] = cluster.datastore['type'] + cluster._info['datastore_version'] = cluster.datastore['version'] + del cluster._info['instances'] + _print_instance(cluster) + + @utils.arg('instance', metavar='<instance>', type=str, |