Skip to content

Commit

Permalink
Add detach-replica support
Browse files Browse the repository at this point in the history
Add a PATCH route to unset the slave_of property on an instance, i.e. to
detach a replica from its replication source.

Rename existing 'detach_replication_slave' functions to 'detach_replica'
to be consistent with the python-troveclient.

Add test case to tests/api/replication.py.

Partially Implements: blueprint replication-v1

Change-Id: I3596a3b76cc484b42db38f02e51c028d5ff2ed6c
  • Loading branch information
glucas1 committed Sep 2, 2014
1 parent c22f658 commit 661dbd4
Show file tree
Hide file tree
Showing 16 changed files with 113 additions and 30 deletions.
4 changes: 4 additions & 0 deletions trove/common/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def _instance_router(self, mapper):
controller=instance_resource,
action="update",
conditions={'method': ['PUT']})
mapper.connect("/{tenant_id}/instances/{id}",
controller=instance_resource,
action="edit",
conditions={'method': ['PATCH']})
mapper.connect("/{tenant_id}/instances/{id}",
controller=instance_resource,
action="delete",
Expand Down
14 changes: 14 additions & 0 deletions trove/common/apischema.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@
}
}
},
"edit": {
"name": "instance:edit",
"type": "object",
"required": ["instance"],
"properties": {
"instance": {
"type": "object",
"required": [],
"properties": {
"slave_of": {},
}
}
}
},
"action": {
"resize": {
"volume": {
Expand Down
6 changes: 3 additions & 3 deletions trove/guestagent/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,9 @@ def attach_replication_slave(self, snapshot, slave_config=None):
self._cast("attach_replication_slave", snapshot=snapshot,
slave_config=slave_config)

def detach_replication_slave(self):
LOG.debug("Detaching slave %s from its master.", self.id)
self._call("detach_replication_slave", AGENT_HIGH_TIMEOUT)
def detach_replica(self):
LOG.debug("Detaching replica %s from its replication source.", self.id)
self._call("detach_replica", AGENT_HIGH_TIMEOUT)

def demote_replication_master(self):
LOG.debug("Demoting instance %s to non-master.", self.id)
Expand Down
5 changes: 2 additions & 3 deletions trove/guestagent/datastore/cassandra/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,9 @@ def attach_replication_slave(self, context, snapshot, slave_config):
raise exception.DatastoreOperationNotSupported(
operation='attach_replication_slave', datastore=MANAGER)

def detach_replication_slave(self, context):
LOG.debug("Detaching replication slave.")
def detach_replica(self, context):
raise exception.DatastoreOperationNotSupported(
operation='detach_replication_slave', datastore=MANAGER)
operation='detach_replica', datastore=MANAGER)

def demote_replication_master(self, context):
LOG.debug("Demoting replication master.")
Expand Down
5 changes: 2 additions & 3 deletions trove/guestagent/datastore/couchbase/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,9 @@ def attach_replication_slave(self, context, snapshot, slave_config):
raise exception.DatastoreOperationNotSupported(
operation='attach_replication_slave', datastore=MANAGER)

def detach_replication_slave(self, context):
LOG.debug("Detaching replication slave.")
def detach_replica(self, context):
raise exception.DatastoreOperationNotSupported(
operation='detach_replication_slave', datastore=MANAGER)
operation='detach_replica', datastore=MANAGER)

def demote_replication_master(self, context):
LOG.debug("Demoting replication slave.")
Expand Down
4 changes: 2 additions & 2 deletions trove/guestagent/datastore/mongodb/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ def attach_replication_slave(self, context, snapshot, slave_config):
raise exception.DatastoreOperationNotSupported(
operation='attach_replication_slave', datastore=MANAGER)

def detach_replication_slave(self, context):
def detach_replica(self, context):
raise exception.DatastoreOperationNotSupported(
operation='detach_replication_slave', datastore=MANAGER)
operation='detach_replica', datastore=MANAGER)

def demote_replication_master(self, context):
raise exception.DatastoreOperationNotSupported(
Expand Down
4 changes: 2 additions & 2 deletions trove/guestagent/datastore/mysql/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ def attach_replication_slave(self, context, snapshot, slave_config):
app.status.set_status(rd_instance.ServiceStatuses.FAILED)
raise

def detach_replication_slave(self, context):
LOG.debug("Detaching replication snapshot.")
def detach_replica(self, context):
LOG.debug("Detaching replica.")
app = MySqlApp(MySqlAppStatus.get())
replication = REPLICATION_STRATEGY_CLASS(context)
replication.detach_slave(app)
Expand Down
4 changes: 2 additions & 2 deletions trove/guestagent/datastore/redis/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ def attach_replication_slave(self, context, snapshot, slave_config):
raise exception.DatastoreOperationNotSupported(
operation='attach_replication_slave', datastore=MANAGER)

def detach_replication_slave(self, context):
def detach_replica(self, context):
raise exception.DatastoreOperationNotSupported(
operation='detach_replication_slave', datastore=MANAGER)
operation='detach_replica', datastore=MANAGER)

def demote_replication_master(self, context):
raise exception.DatastoreOperationNotSupported(
Expand Down
9 changes: 9 additions & 0 deletions trove/instance/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,15 @@ def restart(self):
self.update_db(task_status=InstanceTasks.REBOOTING)
task_api.API(self.context).restart(self.id)

def detach_replica(self):
self.validate_can_perform_action()
LOG.info(_("Detaching instance %s from its replication source.")
% self.id)
if not self.slave_of_id:
raise exception.BadRequest(_("Instance %s is not a replica.")
% self.id)
task_api.API(self.context).detach_replica(self.id)

def migrate(self, host=None):
self.validate_can_perform_action()
LOG.info(_("Migrating instance id = %(instance_id)s "
Expand Down
19 changes: 18 additions & 1 deletion trove/instance/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def action(self, req, body, tenant_id, id):
_actions = {
'restart': self._action_restart,
'resize': self._action_resize,
'reset_password': self._action_reset_password
'reset_password': self._action_reset_password,
}
selected_action = None
action_name = None
Expand Down Expand Up @@ -261,6 +261,23 @@ def update(self, req, id, body, tenant_id):
instance.unassign_configuration()
return wsgi.Result(None, 202)

def edit(self, req, id, body, tenant_id):
"""
Updates the instance to set or unset one or more attributes.
"""
LOG.info(_("Editing instance for tenant id %s.") % tenant_id)
LOG.debug(logging.mask_password("req: %s"), req)
LOG.debug(logging.mask_password("body: %s"), body)
context = req.environ[wsgi.CONTEXT_KEY]

instance = models.Instance.load(context, id)

if 'slave_of' in body['instance']:
LOG.debug("Detaching replica from source.")
instance.detach_replica()

return wsgi.Result(None, 202)

def configuration(self, req, tenant_id, id):
"""
Returns the default configuration template applied to the instance.
Expand Down
5 changes: 5 additions & 0 deletions trove/taskmanager/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ def restart(self, instance_id):
self.cast(self.context,
self.make_msg("restart", instance_id=instance_id))

def detach_replica(self, instance_id):
LOG.debug("Making async call to detach replica: %s" % instance_id)
self.cast(self.context,
self.make_msg("detach_replica", instance_id=instance_id))

def migrate(self, instance_id, host):
LOG.debug("Making async call to migrate instance: %s" % instance_id)
self.cast(self.context,
Expand Down
4 changes: 4 additions & 0 deletions trove/taskmanager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ def restart(self, context, instance_id):
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
instance_tasks.restart()

def detach_replica(self, context, instance_id):
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
instance_tasks.detach_replica()

def migrate(self, context, instance_id, host):
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
instance_tasks.migrate(host)
Expand Down
8 changes: 8 additions & 0 deletions trove/taskmanager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,14 @@ def _get_replication_snapshot():
return run_with_quotas(self.context.tenant, {'backups': 1},
_get_replication_snapshot)

def detach_replica(self):
LOG.debug("Calling detach_replica on %s" % self.id)
try:
self.guest.detach_replica()
self.update_db(slave_of_id=None)
except (GuestError, GuestTimeout):
LOG.exception(_("Failed to detach replica %s.") % self.id)

def reboot(self):
try:
LOG.debug("Stopping datastore on instance %s." % self.id)
Expand Down
42 changes: 33 additions & 9 deletions trove/tests/api/replication.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from proboscis.asserts import assert_raises
from proboscis.asserts import assert_true
from proboscis.decorators import time_out
from proboscis import SkipTest
from trove.common.utils import generate_uuid
from trove.common.utils import poll_until
from trove.tests.api.instances import CheckInstance
Expand All @@ -41,6 +42,19 @@ def __init__(self):
existing_db_on_master = generate_uuid()


def slave_is_running(running=True):

def check_slave_is_running():
server = create_server_connection(slave_instance.id)
cmd = ("mysqladmin extended-status "
"| awk '/Slave_running/{print $4}'")
stdout, stderr = server.execute(cmd)
expected = "ON" if running else "OFF"
return stdout.rstrip() == expected

return check_slave_is_running


@test(depends_on_classes=[WaitForGuestInstallationToFinish],
groups=[GROUP])
class CreateReplicationSlave(object):
Expand Down Expand Up @@ -99,15 +113,7 @@ def find_database():
@test
@time_out(5 * 60)
def test_correctly_started_replication(self):

def slave_is_running():
server = create_server_connection(slave_instance.id)
cmd = ("mysqladmin extended-status "
"| awk '/Slave_running/{print $4}'")
stdout, stderr = server.execute(cmd)
return stdout == "ON\n"

poll_until(slave_is_running)
poll_until(slave_is_running())

@test(depends_on=[test_correctly_started_replication])
def test_create_db_on_master(self):
Expand Down Expand Up @@ -154,6 +160,24 @@ def test_get_master_instance(self):
@test(groups=[GROUP],
depends_on=[WaitForCreateSlaveToFinish],
runs_after=[VerifySlave])
class DetachReplica(object):

@test
@time_out(5 * 60)
def test_detach_replica(self):
if CONFIG.fake_mode:
raise SkipTest("Detach replica not supported in fake mode")

instance_info.dbaas.instances.edit(slave_instance.id,
detach_replica_source=True)
assert_equal(202, instance_info.dbaas.last_http_code)

poll_until(slave_is_running(False))


@test(groups=[GROUP],
depends_on=[WaitForCreateSlaveToFinish],
runs_after=[DetachReplica])
class DeleteSlaveInstance(object):

@test
Expand Down
6 changes: 3 additions & 3 deletions trove/tests/unittests/guestagent/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,11 @@ def test_attach_replication_slave(self):
# verify
self._verify_rpc_cast(exp_msg, rpc.cast)

def test_detach_replication_slave(self):
def test_detach_replica(self):
rpc.call = mock.Mock()
exp_msg = RpcMsgMatcher('detach_replication_slave')
exp_msg = RpcMsgMatcher('detach_replica')
# execute
self.api.detach_replication_slave()
self.api.detach_replica()
# verify
self._verify_rpc_call(exp_msg, rpc.call)

Expand Down
4 changes: 2 additions & 2 deletions trove/tests/unittests/guestagent/test_mysql_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def test_attach_replication_slave_invalid(self):
# assertions
self.assertEqual(mock_replication.enable_as_slave.call_count, 0)

def test_detach_replication_slave(self):
def test_detach_replica(self):
mock_status = MagicMock()
dbaas.MySqlAppStatus.get = MagicMock(return_value=mock_status)

Expand All @@ -343,7 +343,7 @@ def test_detach_replication_slave(self):
self.mock_rs_class.return_value = mock_replication

# entry point
self.manager.detach_replication_slave(self.context)
self.manager.detach_replica(self.context)
# assertions
self.assertEqual(mock_replication.detach_slave.call_count, 1)

Expand Down

0 comments on commit 661dbd4

Please sign in to comment.