diff options
Diffstat (limited to 'troveclient')
| -rw-r--r-- | troveclient/tests/test_backups.py | 123 | ||||
| -rw-r--r-- | troveclient/v1/backups.py | 189 | ||||
| -rw-r--r-- | troveclient/v1/shell.py | 74 |
3 files changed, 386 insertions, 0 deletions
diff --git a/troveclient/tests/test_backups.py b/troveclient/tests/test_backups.py index 85fb043..d3c44ec 100644 --- a/troveclient/tests/test_backups.py +++ b/troveclient/tests/test_backups.py @@ -14,6 +14,7 @@ # under the License. import mock +from mock import patch import testtools import uuid @@ -147,3 +148,125 @@ class BackupManagerTest(testtools.TestCase): resp.status_code = 422 self.backups.api.client.delete = mock.Mock(return_value=(resp, None)) self.assertRaises(Exception, self.backups.delete, 'backup1') + + @patch('troveclient.v1.backups.mistral_client') + def test_auth_mistral_client(self, mistral_client): + with patch.object(self.backups.api.client, 'auth') as auth: + self.backups._get_mistral_client() + mistral_client.assert_called_with( + auth_url=auth.auth_url, username=auth._username, + api_key=auth._password, + project_name=auth._project_name) + + def test_build_schedule(self): + cron_trigger = mock.Mock() + wf_input = {'name': 'foo', 'instance': 'myinst', 'parent_id': None} + sched = self.backups._build_schedule(cron_trigger, wf_input) + self.assertEqual(cron_trigger.name, sched.id) + self.assertEqual(wf_input['name'], sched.name) + self.assertEqual(wf_input['instance'], sched.instance) + self.assertEqual(cron_trigger.workflow_input, sched.input) + + def test_schedule_create(self): + instance = mock.Mock() + pattern = mock.Mock() + name = 'myback' + + def make_cron_trigger(name, wf, workflow_input=None, pattern=None): + return mock.Mock(name=name, pattern=pattern, + workflow_input=workflow_input) + cron_triggers = mock.Mock() + cron_triggers.create = mock.Mock(side_effect=make_cron_trigger) + mistral_client = mock.Mock(cron_triggers=cron_triggers) + + sched = self.backups.schedule_create(instance, pattern, name, + mistral_client=mistral_client) + self.assertEqual(pattern, sched.pattern) + self.assertEqual(name, sched.name) + self.assertEqual(instance.id, sched.instance) + + def test_schedule_list(self): + instance = mock.Mock(id='the_uuid') + backup_name = "wf2" + + test_input = [('wf1', 'foo'), (backup_name, instance.id)] + cron_triggers = mock.Mock() + cron_triggers.list = mock.Mock( + return_value=[ + mock.Mock(workflow_input='{"name": "%s", "instance": "%s"}' + % (name, inst), name=name) + for name, inst in test_input + ]) + mistral_client = mock.Mock(cron_triggers=cron_triggers) + + sched_list = self.backups.schedule_list(instance, mistral_client) + self.assertEqual(1, len(sched_list)) + the_sched = sched_list.pop() + self.assertEqual(backup_name, the_sched.name) + self.assertEqual(instance.id, the_sched.instance) + + def test_schedule_show(self): + instance = mock.Mock(id='the_uuid') + backup_name = "myback" + + cron_triggers = mock.Mock() + cron_triggers.get = mock.Mock( + return_value=mock.Mock( + name=backup_name, + workflow_input='{"name": "%s", "instance": "%s"}' + % (backup_name, instance.id))) + mistral_client = mock.Mock(cron_triggers=cron_triggers) + + sched = self.backups.schedule_show("dummy", mistral_client) + self.assertEqual(backup_name, sched.name) + self.assertEqual(instance.id, sched.instance) + + def test_schedule_delete(self): + cron_triggers = mock.Mock() + cron_triggers.delete = mock.Mock() + mistral_client = mock.Mock(cron_triggers=cron_triggers) + self.backups.schedule_delete("dummy", mistral_client) + cron_triggers.delete.assert_called() + + def test_execution_list(self): + instance = mock.Mock(id='the_uuid') + wf_input = '{"name": "wf2", "instance": "%s"}' % instance.id + wf_name = self.backups.backup_create_workflow + + execution_list_result = [ + [mock.Mock(id=1, input=wf_input, workflow_name=wf_name, + to_dict=mock.Mock(return_value={'id': 1})), + mock.Mock(id=2, input="{}", workflow_name=wf_name)], + [mock.Mock(id=3, input=wf_input, workflow_name=wf_name, + to_dict=mock.Mock(return_value={'id': 3})), + mock.Mock(id=4, input="{}", workflow_name=wf_name)], + [mock.Mock(id=5, input=wf_input, workflow_name=wf_name, + to_dict=mock.Mock(return_value={'id': 5})), + mock.Mock(id=6, input="{}", workflow_name=wf_name)], + [mock.Mock(id=7, input=wf_input, workflow_name="bar"), + mock.Mock(id=8, input="{}", workflow_name=wf_name)] + ] + + cron_triggers = mock.Mock() + cron_triggers.get = mock.Mock( + return_value=mock.Mock(workflow_name=wf_name, + workflow_input=wf_input)) + + mistral_executions = mock.Mock() + mistral_executions.list = mock.Mock(side_effect=execution_list_result) + mistral_client = mock.Mock(cron_triggers=cron_triggers, + executions=mistral_executions) + + el = self.backups.execution_list("dummy", mistral_client, limit=2) + self.assertEqual(2, len(el)) + el = self.backups.execution_list("dummy", mistral_client, limit=2) + self.assertEqual(1, len(el)) + the_exec = el.pop() + self.assertEqual(5, the_exec.id) + + def test_execution_delete(self): + mistral_executions = mock.Mock() + mistral_executions.delete = mock.Mock() + mistral_client = mock.Mock(executions=mistral_executions) + self.backups.execution_delete("dummy", mistral_client) + mistral_executions.delete.assert_called() diff --git a/troveclient/v1/backups.py b/troveclient/v1/backups.py index d0ac673..f7621be 100644 --- a/troveclient/v1/backups.py +++ b/troveclient/v1/backups.py @@ -15,6 +15,12 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import six +import uuid + +from mistralclient.api.client import client as mistral_client + from troveclient import base from troveclient import common @@ -25,6 +31,21 @@ class Backup(base.Resource): return "<Backup: %s>" % self.name +class Schedule(base.Resource): + """Schedule is a resource used to hold information about scheduled backups. + """ + def __repr__(self): + return "<Schedule: %s>" % self.name + + +class ScheduleExecution(base.Resource): + """ScheduleExecution is a resource used to hold information about + the execution of a scheduled backup. + """ + def __repr__(self): + return "<Execution: %s>" % self.name + + class Backups(base.ManagerWithFind): """Manage :class:`Backups` information.""" @@ -87,3 +108,171 @@ class Backups(base.ManagerWithFind): url = "/backups/%s" % base.getid(backup) resp, body = self.api.client.delete(url) common.check_for_exceptions(resp, body, url) + + backup_create_workflow = "trove.backup_create" + + def _get_mistral_client(self): + if hasattr(self.api.client, 'auth'): + auth_url = self.api.client.auth.auth_url + user = self.api.client.auth._username + key = self.api.client.auth._password + tenant_name = self.api.client.auth._project_name + else: + auth_url = self.api.client.auth_url + user = self.api.client.username + key = self.api.client.password + tenant_name = self.api.client.tenant + + return mistral_client(auth_url=auth_url, username=user, api_key=key, + project_name=tenant_name) + + def _build_schedule(self, cron_trigger, wf_input): + if isinstance(wf_input, six.string_types): + wf_input = json.loads(wf_input) + sched_info = {"id": cron_trigger.name, + "name": wf_input["name"], + "instance": wf_input['instance'], + "parent_id": wf_input.get('parent_id', None), + "created_at": cron_trigger.created_at, + "next_execution_time": cron_trigger.next_execution_time, + "pattern": cron_trigger.pattern, + "input": cron_trigger.workflow_input + } + if hasattr(cron_trigger, 'updated_at'): + sched_info["updated_at"] = cron_trigger.updated_at + return Schedule(self, sched_info, loaded=True) + + def schedule_create(self, instance, pattern, name, + description=None, incremental=None, + mistral_client=None): + """Create a new schedule to backup the given instance. + + :param instance: instance to backup. + :param: pattern: cron pattern for schedule. + :param name: name for backup. + :param description: (optional). + :param incremental: flag for incremental backup (optional). + :returns: :class:`Backups` + """ + + if not mistral_client: + mistral_client = self._get_mistral_client() + + inst_id = base.getid(instance) + cron_name = str(uuid.uuid4()) + wf_input = {"instance": inst_id, + "name": name, + "description": description, + "incremental": incremental + } + + cron_trigger = mistral_client.cron_triggers.create( + cron_name, self.backup_create_workflow, pattern=pattern, + workflow_input=wf_input) + + return self._build_schedule(cron_trigger, wf_input) + + def schedule_list(self, instance, mistral_client=None): + """Get a list of all backup schedules for an instance. + + :param: instance for which to list schedules. + :rtype: list of :class:`Schedule`. + """ + inst_id = base.getid(instance) + if not mistral_client: + mistral_client = self._get_mistral_client() + + return [self._build_schedule(cron_trig, cron_trig.workflow_input) + for cron_trig in mistral_client.cron_triggers.list() + if inst_id in cron_trig.workflow_input] + + def schedule_show(self, schedule, mistral_client=None): + """Get details of a backup schedule. + + :param: schedule to show. + :rtype: :class:`Schedule`. + """ + if isinstance(schedule, Schedule): + schedule = schedule.id + + if not mistral_client: + mistral_client = self._get_mistral_client() + + schedule = mistral_client.cron_triggers.get(schedule) + return self._build_schedule(schedule, schedule.workflow_input) + + def schedule_delete(self, schedule, mistral_client=None): + """Remove a given backup schedule. + + :param schedule: schedule to delete. + """ + + if isinstance(schedule, Schedule): + schedule = schedule.id + + if not mistral_client: + mistral_client = self._get_mistral_client() + + mistral_client.cron_triggers.delete(schedule) + + def execution_list(self, schedule, mistral_client=None, + marker='', limit=None): + """Get a list of all executions of a scheduled backup. + + :param: schedule for which to list executions. + :rtype: list of :class:`ScheduleExecution`. + """ + + if isinstance(schedule, Schedule): + schedule = schedule.id + + if isinstance(marker, ScheduleExecution): + marker = getattr(marker, 'id') + + if not mistral_client: + mistral_client = self._get_mistral_client() + + cron_trigger = mistral_client.cron_triggers.get(schedule) + ct_input = json.loads(cron_trigger.workflow_input) + + def mistral_execution_generator(): + m = marker + while True: + the_list = mistral_client.executions.list(marker=m, limit=50, + sort_dirs='desc') + if the_list: + for the_item in the_list: + yield the_item + m = the_list[-1].id + else: + raise StopIteration() + + def execution_list_generator(): + yielded = 0 + for sexec in mistral_execution_generator(): + if (sexec.workflow_name == cron_trigger.workflow_name + and ct_input == json.loads(sexec.input)): + yield ScheduleExecution(self, sexec.to_dict(), + loaded=True) + yielded += 1 + if limit and yielded == limit: + raise StopIteration() + + return list(execution_list_generator()) + + def execution_delete(self, execution, mistral_client=None): + """Remove a given schedule execution. + + :param id: id of execution to remove. + """ + + exec_id = (execution.id if isinstance(execution, ScheduleExecution) + else execution) + + if isinstance(execution, ScheduleExecution): + execution = execution.name + + if not mistral_client: + mistral_client = self._get_mistral_client() + + mistral_client.executions.delete(exec_id) diff --git a/troveclient/v1/shell.py b/troveclient/v1/shell.py index 14ab7e5..5451b09 100644 --- a/troveclient/v1/shell.py +++ b/troveclient/v1/shell.py @@ -982,6 +982,80 @@ def do_backup_copy(cs, args): _print_object(backup) +@utils.arg('instance', metavar='<instance>', + help='ID or name of the instance.') +@utils.arg('pattern', metavar='<pattern>', + help='Cron style pattern describing schedule occurrence.') +@utils.arg('name', metavar='<name>', help='Name of the backup.') +@utils.arg('--description', metavar='<description>', + default=None, + help='An optional description for the backup.') +@utils.arg('--incremental', action="store_true", default=False, + help='Flag to select incremental backup based on most recent' + ' backup.') +@utils.service_type('database') +def do_schedule_create(cs, args): + """Schedules backups for an instance.""" + instance = _find_instance(cs, args.instance) + backup = cs.backups.schedule_create(instance, args.pattern, args.name, + description=args.description, + incremental=args.incremental) + _print_object(backup) + + +@utils.arg('instance', metavar='<instance>', + help='ID or name of the instance.') +@utils.service_type('database') +def do_schedule_list(cs, args): + """Lists scheduled backups for an instance.""" + instance = _find_instance(cs, args.instance) + schedules = cs.backups.schedule_list(instance) + utils.print_list(schedules, ['id', 'name', 'pattern', + 'next_execution_time'], + order_by='next_execution_time') + + +@utils.arg('id', metavar='<schedule id>', help='Id of the schedule.') +@utils.service_type('database') +def do_schedule_show(cs, args): + """Shows details of a schedule.""" + _print_object(cs.backups.schedule_show(args.id)) + + +@utils.arg('id', metavar='<schedule id>', help='Id of the schedule.') +@utils.service_type('database') +def do_schedule_delete(cs, args): + """Deletes a schedule.""" + cs.backups.schedule_delete(args.id) + + +@utils.arg('id', metavar='<schedule id>', help='Id of the schedule.') +@utils.arg('--limit', metavar='<limit>', + default=None, type=int, + help='Return up to N number of the most recent executions.') +@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_execution_list(cs, args): + """Lists executions of a scheduled backup of an instance.""" + executions = cs.backups.execution_list(args.id, marker=args.marker, + limit=args.limit) + + utils.print_list(executions, ['id', 'created_at', 'state', 'output'], + labels={'created_at': 'Execution Time'}, + order_by='created_at') + + +@utils.arg('execution', metavar='<execution>', + help='Id of the execution to delete.') +@utils.service_type('database') +def do_execution_delete(cs, args): + """Deletes an execution.""" + cs.backups.execution_delete(args.execution) + + # Database related actions @utils.arg('instance', metavar='<instance>', |
