summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoramcrn <amcreynolds@ebaysf.com>2014-08-12 16:52:19 -0700
committeramcrn <amcreynolds@ebaysf.com>2014-09-02 16:38:56 -0700
commit6852bdcefc17712b4b57a530adfb406e059e52e2 (patch)
tree6cce0ad46cb5e07d54c8152315485bea50cf481b
parent33c76fab16b80f32a04f83e1653d4f809c22d3bd (diff)
downloadpython-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.py3
-rw-r--r--troveclient/tests/test_clusters.py120
-rw-r--r--troveclient/tests/test_instances.py4
-rw-r--r--troveclient/v1/client.py3
-rw-r--r--troveclient/v1/clusters.py92
-rw-r--r--troveclient/v1/instances.py5
-rw-r--r--troveclient/v1/management.py42
-rw-r--r--troveclient/v1/shell.py122
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,