From 358dbf3c80cd444fcd3667d2d7a43ef725a9c748 Mon Sep 17 00:00:00 2001 From: Peter Stachowski Date: Fri, 1 Apr 2016 21:01:24 +0000 Subject: [PATCH] Locality support for replication In order to allow replication sets to be all on the same hypervisor (affinity) or all on different hypervisors (anti-affinity) a new argument (locality) needed to be added to the Trove create API. This changeset addresses the Trove client part of this feature. A --locality flag is now available on the 'create' command and is passed to the server for processing. The --replica_count argument was also cleaned up in that it's not passed on unless it is set. If --replica_of is specified, the default is set to '1.' DocImpact: New functionality Partially implements: blueprint replication-cluster-locality Change-Id: I18f242983775526a7f1e2644302ebdc0dac025cf --- ...port-for-replication-5834f1a2dcaf6883.yaml | 6 ++ troveclient/tests/fakes.py | 3 + troveclient/tests/test_instances.py | 6 +- troveclient/tests/test_v1_shell.py | 65 +++++++++++++++++-- troveclient/v1/instances.py | 4 +- troveclient/v1/shell.py | 33 +++++++--- 6 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/locality-support-for-replication-5834f1a2dcaf6883.yaml diff --git a/releasenotes/notes/locality-support-for-replication-5834f1a2dcaf6883.yaml b/releasenotes/notes/locality-support-for-replication-5834f1a2dcaf6883.yaml new file mode 100644 index 00000000..b90c6366 --- /dev/null +++ b/releasenotes/notes/locality-support-for-replication-5834f1a2dcaf6883.yaml @@ -0,0 +1,6 @@ +--- +features: + - A --locality flag was added to the trove create command + to allow a user to specify whether new replicas should + be on the same hypervisor (affinity) or on different + hypervisors (anti-affinity). diff --git a/troveclient/tests/fakes.py b/troveclient/tests/fakes.py index 0feed291..0fa053cd 100644 --- a/troveclient/tests/fakes.py +++ b/troveclient/tests/fakes.py @@ -595,6 +595,9 @@ class FakeHTTPClient(base_client.HTTPClient): def get_instances_1234_root(self, **kw): return (200, {}, {"rootEnabled": 'True'}) + def get_instances_master_1(self, **kw): + return (200, {}, {"instance": {"id": 'myid'}}) + def get_clusters_cls_1234_root(self, **kw): return (200, {}, {"rootEnabled": 'True'}) diff --git a/troveclient/tests/test_instances.py b/troveclient/tests/test_instances.py index 1e8e77d2..32876072 100644 --- a/troveclient/tests/test_instances.py +++ b/troveclient/tests/test_instances.py @@ -100,7 +100,9 @@ class InstancesTest(testtools.TestCase): datastore="datastore", datastore_version="datastore-version", nics=nics, slave_of='test', - modules=['mod_id']) + replica_count=4, + modules=['mod_id'], + locality='affinity') self.assertEqual("/instances", p) self.assertEqual("instance", i) self.assertEqual(['db1', 'db2'], b["instance"]["databases"]) @@ -118,6 +120,8 @@ class InstancesTest(testtools.TestCase): self.assertNotIn('slave_of', b['instance']) self.assertTrue(mock_warn.called) self.assertEqual([{'id': 'mod_id'}], b["instance"]["modules"]) + self.assertEqual(4, b["instance"]["replica_count"]) + self.assertEqual('affinity', b["instance"]["locality"]) def test_list(self): page_mock = mock.Mock() diff --git a/troveclient/tests/test_v1_shell.py b/troveclient/tests/test_v1_shell.py index 316cdc5d..faf1548d 100644 --- a/troveclient/tests/test_v1_shell.py +++ b/troveclient/tests/test_v1_shell.py @@ -261,8 +261,7 @@ class ShellTest(utils.TestCase): {'instance': { 'volume': {'size': 1, 'type': 'lvm'}, 'flavorRef': 1, - 'name': 'test-member-1', - 'replica_count': 1 + 'name': 'test-member-1' }}) def test_boot_with_modules(self): @@ -274,7 +273,6 @@ class ShellTest(utils.TestCase): 'volume': {'size': 1, 'type': 'lvm'}, 'flavorRef': 1, 'name': 'test-member-1', - 'replica_count': 1, 'modules': [{'id': '4321'}, {'id': '8765'}] }}) @@ -286,10 +284,67 @@ class ShellTest(utils.TestCase): {'instance': { 'volume': {'size': 1, 'type': 'lvm'}, 'flavorRef': 1, - 'name': 'test-member-1', + 'name': 'test-member-1' + }}) + + def test_boot_repl_set(self): + self.run_command('create repl-1 1 --size 1 --locality=anti-affinity ' + '--replica_count=4') + self.assert_called_anytime( + 'POST', '/instances', + {'instance': { + 'volume': {'size': 1, 'type': None}, + 'flavorRef': 1, + 'name': 'repl-1', + 'replica_count': 4, + 'locality': 'anti-affinity' + }}) + + def test_boot_replica(self): + self.run_command('create slave-1 1 --size 1 --replica_of=master_1') + self.assert_called_anytime( + 'POST', '/instances', + {'instance': { + 'volume': {'size': 1, 'type': None}, + 'flavorRef': 1, + 'name': 'slave-1', + 'replica_of': 'myid', 'replica_count': 1 }}) + def test_boot_replica_count(self): + self.run_command('create slave-1 1 --size 1 --replica_of=master_1 ' + '--replica_count=3') + self.assert_called_anytime( + 'POST', '/instances', + {'instance': { + 'volume': {'size': 1, 'type': None}, + 'flavorRef': 1, + 'name': 'slave-1', + 'replica_of': 'myid', + 'replica_count': 3 + }}) + + def test_boot_locality(self): + self.run_command('create master-1 1 --size 1 --locality=affinity') + self.assert_called_anytime( + 'POST', '/instances', + {'instance': { + 'volume': {'size': 1, 'type': None}, + 'flavorRef': 1, + 'name': 'master-1', + 'locality': 'affinity' + }}) + + def test_boot_locality_error(self): + cmd = ('create slave-1 1 --size 1 --locality=affinity ' + '--replica_of=master_1') + self.assertRaisesRegexp( + exceptions.ValidationError, + 'Cannot specify locality when adding replicas to existing ' + 'master.', + self.run_command, cmd) + def test_boot_nic_error(self): cmd = ('create test-member-1 1 --size 1 --volume_type lvm ' '--nic net-id=some-id,port-id=some-id') @@ -307,7 +362,6 @@ class ShellTest(utils.TestCase): 'flavorRef': 1, 'name': 'test-restore-1', 'restorePoint': {'backupRef': 'bk-1234'}, - 'replica_count': 1 }}) def test_boot_restore_by_name(self): @@ -319,7 +373,6 @@ class ShellTest(utils.TestCase): 'flavorRef': 1, 'name': 'test-restore-1', 'restorePoint': {'backupRef': 'bk-1234'}, - 'replica_count': 1 }}) def test_cluster_create(self): diff --git a/troveclient/v1/instances.py b/troveclient/v1/instances.py index 9fc51c25..73d93f02 100644 --- a/troveclient/v1/instances.py +++ b/troveclient/v1/instances.py @@ -89,7 +89,7 @@ class Instances(base.ManagerWithFind): restorePoint=None, availability_zone=None, datastore=None, datastore_version=None, nics=None, configuration=None, replica_of=None, slave_of=None, replica_count=None, - modules=None): + modules=None, locality=None): """Create (boot) a new instance.""" body = {"instance": { @@ -129,6 +129,8 @@ class Instances(base.ManagerWithFind): body["instance"]["replica_count"] = replica_count if modules: body["instance"]["modules"] = self._get_module_list(modules) + if locality: + body["instance"]["locality"] = locality return self._create("/instances", body, "instance") diff --git a/troveclient/v1/shell.py b/troveclient/v1/shell.py index 666ba3cb..edff25c8 100644 --- a/troveclient/v1/shell.py +++ b/troveclient/v1/shell.py @@ -26,6 +26,7 @@ INSTANCE_ERROR = ("Instance argument(s) must be of the form --instance " NIC_ERROR = ("Invalid NIC argument: %s. Must specify either net-id or port-id " "but not both. Please refer to help.") NO_LOG_FOUND_ERROR = "ERROR: No published '%s' log was found for %s." +LOCALITY_DOMAIN = ['affinity', 'anti-affinity'] try: import simplejson as json @@ -470,26 +471,42 @@ def do_update(cs, args): @utils.arg('--replica_count', metavar='', type=int, - default=1, - help='Number of replicas to create (defaults to %(default)s).') + default=None, + help='Number of replicas to create (defaults to 1 if replica_of ' + 'specified).') @utils.arg('--module', metavar='', type=str, dest='modules', action='append', default=[], help='ID or name of the module to apply. Specify multiple ' 'times to apply multiple modules.') +@utils.arg('--locality', + metavar='', + default=None, + choices=LOCALITY_DOMAIN, + help='Locality policy to use when creating replicas. Choose ' + 'one of %(choices)s.') @utils.service_type('database') def do_create(cs, args): """Creates a new instance.""" - volume = None - replica_of_instance = None flavor_id = _find_flavor(cs, args.flavor).id + volume = None if args.size: volume = {"size": args.size, "type": args.volume_type} restore_point = None if args.backup: restore_point = {"backupRef": _find_backup(cs, args.backup).id} + replica_of = None + replica_count = args.replica_count if args.replica_of: - replica_of_instance = _find_instance(cs, args.replica_of) + replica_of = _find_instance(cs, args.replica_of) + replica_count = replica_count or 1 + locality = None + if args.locality: + locality = args.locality + if replica_of: + raise exceptions.ValidationError( + 'Cannot specify locality when adding replicas to existing ' + 'master.') databases = [{'name': value} for value in args.databases] users = [{'name': n, 'password': p, 'databases': databases} for (n, p) in [z.split(':')[:2] for z in args.users]] @@ -514,9 +531,9 @@ def do_create(cs, args): datastore_version=args.datastore_version, nics=nics, configuration=args.configuration, - replica_of=replica_of_instance, - replica_count=args.replica_count, - modules=modules) + replica_of=replica_of, + replica_count=replica_count, + modules=modules, locality=locality) _print_instance(instance)