From 995fe11dbe3b82769f2d5ffda67fd184489296d9 Mon Sep 17 00:00:00 2001 From: chenying Date: Thu, 22 Jun 2017 14:53:38 +0800 Subject: [PATCH] Add plan commands for OpenStackClinet plugin in karbor Change-Id: I8e10fc1775cd07271ff5651e104b6b8b26743be3 Partially-Implements: karbor-support-python-openstackclient --- karborclient/osc/v1/plans.py | 174 ++++++++++++++++++- karborclient/tests/unit/osc/v1/test_plans.py | 84 ++++++++- karborclient/utils.py | 146 ++++++++++++++++ karborclient/v1/shell.py | 154 ++-------------- setup.cfg | 6 +- 5 files changed, 418 insertions(+), 146 deletions(-) create mode 100644 karborclient/utils.py diff --git a/karborclient/osc/v1/plans.py b/karborclient/osc/v1/plans.py index afd3c85..678a983 100644 --- a/karborclient/osc/v1/plans.py +++ b/karborclient/osc/v1/plans.py @@ -12,11 +12,17 @@ """Data protection V1 plan action implementations""" +import six + +from oslo_utils import uuidutils + from osc_lib.command import command -from osc_lib import utils +from osc_lib import utils as osc_utils from oslo_log import log as logging +from karborclient.common.apiclient import exceptions from karborclient.i18n import _ +from karborclient import utils class ListPlans(command.Lister): @@ -93,6 +99,170 @@ class ListPlans(command.Lister): column_headers = ['Id', 'Name', 'Description', 'Provider id', 'Status'] return (column_headers, - (utils.get_item_properties( + (osc_utils.get_item_properties( s, column_headers ) for s in data)) + + +class ShowPlan(command.ShowOne): + _description = "Shows plan details" + + def get_parser(self, prog_name): + parser = super(ShowPlan, self).get_parser(prog_name) + parser.add_argument( + 'plan', + metavar="", + help="ID of plan." + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.data_protection + plan = osc_utils.find_resource(client.plans, parsed_args.plan) + + plan._info.pop("links", None) + return zip(*sorted(six.iteritems(plan._info))) + + +class CreatePlan(command.ShowOne): + _description = "Creates a plan" + + def get_parser(self, prog_name): + parser = super(CreatePlan, self).get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help='Plan name.' + ) + parser.add_argument( + 'provider_id', + metavar='', + help='ID of provider.' + ) + parser.add_argument( + 'resources', + metavar='', + help='Resource in list must be a dict when creating' + ' a plan. The keys of resource are id ,type, name and ' + 'extra_info. The extra_info field is optional.' + ) + parser.add_argument( + '--parameters-json', + type=str, + dest='parameters_json', + metavar='', + default=None, + help='Plan parameters in json format.' + ) + parser.add_argument( + '--parameters', + action='append', + metavar='resource_type=[,resource_id=,key=val,...]', + default=[], + help='Plan parameters, may be specified multiple times. ' + 'resource_type: type of resource to apply parameters. ' + 'resource_id: limit the parameters to a specific resource. ' + 'Other keys and values: according to provider\'s protect ' + 'schema.' + ) + parser.add_argument( + '--description', + metavar='', + help='The description of a plan.' + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.data_protection + if not uuidutils.is_uuid_like(parsed_args.provider_id): + raise exceptions.CommandError( + "Invalid provider id provided.") + plan_resources = utils.extract_resources(parsed_args) + utils.check_resources(client, plan_resources) + plan_parameters = utils.extract_parameters(parsed_args) + plan = client.plans.create(parsed_args.name, parsed_args.provider_id, + plan_resources, plan_parameters, + description=parsed_args.description) + + plan._info.pop("links", None) + return zip(*sorted(six.iteritems(plan._info))) + + +class UpdatePlan(command.ShowOne): + _description = "Update a plan" + + def get_parser(self, prog_name): + parser = super(UpdatePlan, self).get_parser(prog_name) + parser.add_argument( + "plan_id", + metavar="", + help="Id of plan to update." + ) + parser.add_argument( + "--name", + metavar="", + help="A name to which the plan will be renamed." + ) + parser.add_argument( + "--resources", + metavar="", + help="Resources to which the plan will be updated." + ) + parser.add_argument( + "--status", + metavar="", + help="status to which the plan will be updated." + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.data_protection + data = {} + if parsed_args.name is not None: + data['name'] = parsed_args.name + if parsed_args.resources is not None: + plan_resources = utils.extract_resources(parsed_args) + data['resources'] = plan_resources + if parsed_args.status is not None: + data['status'] = parsed_args.status + try: + plan = osc_utils.find_resource(client.plans, + parsed_args.plan_id) + plan = client.plans.update(plan.id, data) + except exceptions.NotFound: + raise exceptions.CommandError( + "Plan %s not found" % parsed_args.plan_id) + else: + plan._info.pop("links", None) + return zip(*sorted(six.iteritems(plan._info))) + + +class DeletePlan(command.Command): + _description = "Delete plan" + + def get_parser(self, prog_name): + parser = super(DeletePlan, self).get_parser(prog_name) + parser.add_argument( + 'plan', + metavar='', + nargs="+", + help='ID of plan.' + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.data_protection + failure_count = 0 + for plan_id in parsed_args.plan: + try: + plan = osc_utils.find_resource(client.plans, plan_id) + client.plans.delete(plan.id) + except exceptions.NotFound: + failure_count += 1 + raise exceptions.CommandError( + "Failed to delete '{0}'; plan not found". + format(plan_id)) + if failure_count == len(parsed_args.plan): + raise exceptions.CommandError( + "Unable to find and delete any of the " + "specified plan.") diff --git a/karborclient/tests/unit/osc/v1/test_plans.py b/karborclient/tests/unit/osc/v1/test_plans.py index 4e7cdf2..ce2ed28 100644 --- a/karborclient/tests/unit/osc/v1/test_plans.py +++ b/karborclient/tests/unit/osc/v1/test_plans.py @@ -22,7 +22,10 @@ PLAN_INFO = { "description": "", "parameters": {}, "id": "204c825e-eb2f-4609-95ab-70b3caa43ac8", - "resources": [], + "resources": [{ + 'type': 'OS::Cinder::Volume', + 'id': '71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14', + 'name': 'testsinglevolume'}], "name": "OS Volume protection plan." } @@ -63,3 +66,82 @@ class TestListPlans(TestPlans): "cf56bd3e-97a7-4078-b6d5-f36246333fd9", "suspended")] self.assertEqual(expected_data, list(data)) + + +class TestCreatePlan(TestPlans): + def setUp(self): + super(TestCreatePlan, self).setUp() + self.plans_mock.create.return_value = plans.Plan( + None, PLAN_INFO) + # Command to test + self.cmd = osc_plans.CreatePlan(self.app, None) + + def test_plan_create(self): + arglist = ['OS Volume protection plan.', + 'cf56bd3e-97a7-4078-b6d5-f36246333fd9', + "'71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14'=" + "'OS::Cinder::Volume'='testsinglevolume'"] + verifylist = [('name', 'OS Volume protection plan.'), + ('provider_id', 'cf56bd3e-97a7-4078-b6d5-f36246333fd9'), + ('resources', "'71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14'=" + "'OS::Cinder::Volume'='testsinglevolume'")] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.plans_mock.create.assert_called_once_with( + 'OS Volume protection plan.', + 'cf56bd3e-97a7-4078-b6d5-f36246333fd9', + [{'id': "'71bfe64a-e0b9-4a91-9e15-a7fc9ab31b14'", + 'type': "'OS::Cinder::Volume'", + 'name': "'testsinglevolume'"}], + {}, description=None) + + +class TestUpdatePlan(TestPlans): + def setUp(self): + super(TestUpdatePlan, self).setUp() + self.plans_mock.get.return_value = plans.Plan( + None, PLAN_INFO) + self.plans_mock.update.return_value = plans.Plan( + None, PLAN_INFO) + # Command to test + self.cmd = osc_plans.UpdatePlan(self.app, None) + + def test_plan_update(self): + arglist = ['204c825e-eb2f-4609-95ab-70b3caa43ac8', + '--status', 'started'] + verifylist = [('plan_id', '204c825e-eb2f-4609-95ab-70b3caa43ac8'), + ('status', 'started')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.plans_mock.update.assert_called_once_with( + '204c825e-eb2f-4609-95ab-70b3caa43ac8', + {'status': 'started'}) + + +class TestDeletePlan(TestPlans): + def setUp(self): + super(TestDeletePlan, self).setUp() + self.plans_mock.get.return_value = plans.Plan( + None, PLAN_INFO) + # Command to test + self.cmd = osc_plans.DeletePlan(self.app, None) + + def test_plan_create(self): + arglist = ['204c825e-eb2f-4609-95ab-70b3caa43ac8'] + verifylist = [('plan', ['204c825e-eb2f-4609-95ab-70b3caa43ac8'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.plans_mock.delete.assert_called_once_with( + '204c825e-eb2f-4609-95ab-70b3caa43ac8') diff --git a/karborclient/utils.py b/karborclient/utils.py new file mode 100644 index 0000000..60240d0 --- /dev/null +++ b/karborclient/utils.py @@ -0,0 +1,146 @@ +# 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 oslo_serialization import jsonutils +from oslo_utils import uuidutils + +from karborclient.common.apiclient import exceptions + + +def extract_resources(args): + resources = [] + for data in args.resources.split(','): + if '=' in data and len(data.split('=')) in [3, 4]: + resource = dict(zip(['id', 'type', 'name', 'extra_info'], + data.split('='))) + if resource.get('extra_info'): + resource['extra_info'] = jsonutils.loads( + resource.get('extra_info')) + else: + raise exceptions.CommandError( + "Unable to parse parameter resources. " + "The keys of resource are id , type, name and " + "extra_info. The extra_info field is optional.") + resources.append(resource) + return resources + + +def check_resources(cs, resources): + # check the resource whether it is available + for resource in resources: + try: + instance = cs.protectables.get_instance( + resource["type"], resource["id"]) + except exceptions.NotFound: + raise exceptions.CommandError( + "The resource: %s can not be found." % resource["id"]) + else: + if instance is None: + raise exceptions.CommandError( + "The resource: %s is invalid." % resource["id"]) + + +def extract_parameters(args): + if all((args.parameters, args.parameters_json)): + raise exceptions.CommandError( + "Must provide parameters or parameters-json, not both") + if not any((args.parameters, args.parameters_json)): + return {} + + if args.parameters_json: + return jsonutils.loads(args.parameters_json) + parameters = {} + for resource_params in args.parameters: + resource_type = None + resource_id = None + parameter = {} + for param_kv in resource_params.split(','): + try: + key, value = param_kv.split('=') + except Exception: + raise exceptions.CommandError( + 'parameters must be in the form: key1=val1,key2=val2,...' + ) + if key == "resource_type": + resource_type = value + elif key == "resource_id": + if not uuidutils.is_uuid_like(value): + raise exceptions.CommandError('resource_id must be a uuid') + resource_id = value + else: + parameter[key] = value + if resource_type is None: + raise exceptions.CommandError( + 'Must specify resource_type for parameters' + ) + if resource_id is None: + resource_key = resource_type + else: + resource_key = "%s#%s" % (resource_type, resource_id) + parameters[resource_key] = parameter + + return parameters + + +def extract_instances_parameters(args): + parameters = {} + for parameter in args.parameters: + if '=' in parameter: + (key, value) = parameter.split('=', 1) + else: + key = parameter + value = None + + parameters[key] = value + return parameters + + +def extract_extra_info(args): + checkpoint_extra_info = {} + for data in args.extra_info: + # unset doesn't require a val, so we have the if/else + if '=' in data: + (key, value) = data.split('=', 1) + else: + key = data + value = None + + checkpoint_extra_info[key] = value + return checkpoint_extra_info + + +def extract_properties(args): + properties = {} + if args.properties is None: + return properties + for data in args.properties.split(','): + if '=' in data: + (resource_key, resource_value) = data.split('=', 1) + else: + raise exceptions.CommandError( + "Unable to parse parameter properties.") + + properties[resource_key] = resource_value + return properties + + +def extract_operation_definition(args): + operation_definition = {} + for data in args.operation_definition.split(','): + if '=' in data: + (resource_key, resource_value) = data.split('=', 1) + else: + raise exceptions.CommandError( + "Unable to parse parameter operation_definition.") + + operation_definition[resource_key] = resource_value + return operation_definition diff --git a/karborclient/v1/shell.py b/karborclient/v1/shell.py index c942f2b..24cfda7 100644 --- a/karborclient/v1/shell.py +++ b/karborclient/v1/shell.py @@ -15,12 +15,12 @@ import json import os from datetime import datetime -from oslo_serialization import jsonutils from oslo_utils import uuidutils from karborclient.common.apiclient import exceptions from karborclient.common import base from karborclient.common import utils +from karborclient import utils as arg_utils @utils.arg('--all-tenants', @@ -145,9 +145,9 @@ def do_plan_create(cs, args): if not uuidutils.is_uuid_like(args.provider_id): raise exceptions.CommandError( "Invalid provider id provided.") - plan_resources = _extract_resources(args) - _check_resources(cs, plan_resources) - plan_parameters = _extract_parameters(args) + plan_resources = arg_utils.extract_resources(args) + arg_utils.check_resources(cs, plan_resources) + plan_parameters = arg_utils.extract_parameters(args) plan = cs.plans.create(args.name, args.provider_id, plan_resources, plan_parameters, description=args.description) dict_format_list = {"resources", "parameters"} @@ -198,7 +198,7 @@ def do_plan_update(cs, args): if args.name is not None: data['name'] = args.name if args.resources is not None: - plan_resources = _extract_resources(args) + plan_resources = arg_utils.extract_resources(args) data['resources'] = plan_resources if args.status is not None: data['status'] = args.status @@ -211,39 +211,6 @@ def do_plan_update(cs, args): utils.print_dict(plan.to_dict()) -def _extract_resources(args): - resources = [] - for data in args.resources.split(','): - if '=' in data and len(data.split('=')) in [3, 4]: - resource = dict(zip(['id', 'type', 'name', 'extra_info'], - data.split('='))) - if resource.get('extra_info'): - resource['extra_info'] = jsonutils.loads( - resource.get('extra_info')) - else: - raise exceptions.CommandError( - "Unable to parse parameter resources. " - "The keys of resource are id , type, name and " - "extra_info. The extra_info field is optional.") - resources.append(resource) - return resources - - -def _check_resources(cs, resources): - # check the resource whether it is available - for resource in resources: - try: - instance = cs.protectables.get_instance( - resource["type"], resource["id"]) - except exceptions.NotFound: - raise exceptions.CommandError( - "The resource: %s can not be found." % resource["id"]) - else: - if instance is None: - raise exceptions.CommandError( - "The resource: %s is invalid." % resource["id"]) - - @utils.arg('provider_id', metavar='', help='Provider id.') @@ -286,7 +253,7 @@ def do_restore_create(cs, args): raise exceptions.CommandError( "Invalid checkpoint id provided.") - restore_parameters = _extract_parameters(args) + restore_parameters = arg_utils.extract_parameters(args) restore_auth = None if args.restore_target is not None: if args.restore_username is None: @@ -307,48 +274,6 @@ def do_restore_create(cs, args): utils.print_dict(restore.to_dict(), dict_format_list=dict_format_list) -def _extract_parameters(args): - if all((args.parameters, args.parameters_json)): - raise exceptions.CommandError( - "Must provide parameters or parameters-json, not both") - if not any((args.parameters, args.parameters_json)): - return {} - - if args.parameters_json: - return jsonutils.loads(args.parameters_json) - parameters = {} - for resource_params in args.parameters: - resource_type = None - resource_id = None - parameter = {} - for param_kv in resource_params.split(','): - try: - key, value = param_kv.split('=') - except Exception: - raise exceptions.CommandError( - 'parameters must be in the form: key1=val1,key2=val2,...' - ) - if key == "resource_type": - resource_type = value - elif key == "resource_id": - if not uuidutils.is_uuid_like(value): - raise exceptions.CommandError('resource_id must be a uuid') - resource_id = value - else: - parameter[key] = value - if resource_type is None: - raise exceptions.CommandError( - 'Must specify resource_type for parameters' - ) - if resource_id is None: - resource_key = resource_type - else: - resource_key = "%s#%s" % (resource_type, resource_id) - parameters[resource_key] = parameter - - return parameters - - @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', @@ -475,7 +400,7 @@ def do_protectable_show(cs, args): def do_protectable_show_instance(cs, args): """Shows instance details.""" search_opts = { - 'parameters': (_extract_instances_parameters(args) + 'parameters': (arg_utils.extract_instances_parameters(args) if args.parameters else None), } instance = cs.protectables.get_instance(args.protectable_type, @@ -529,7 +454,7 @@ def do_protectable_list_instances(cs, args): search_opts = { 'type': args.type, - 'parameters': (_extract_instances_parameters(args) + 'parameters': (arg_utils.extract_instances_parameters(args) if args.parameters else None), } @@ -557,19 +482,6 @@ def do_protectable_list_instances(cs, args): sortby_index=sortby_index, formatters=formatters) -def _extract_instances_parameters(args): - parameters = {} - for parameter in args.parameters: - if '=' in parameter: - (key, value) = parameter.split('=', 1) - else: - key = parameter - value = None - - parameters[key] = value - return parameters - - @utils.arg('provider_id', metavar='', help='Id of provider.') @@ -657,7 +569,7 @@ def do_checkpoint_create(cs, args): checkpoint_extra_info = None if args.extra_info is not None: - checkpoint_extra_info = _extract_extra_info(args) + checkpoint_extra_info = arg_utils.extract_extra_info(args) checkpoint = cs.checkpoints.create(args.provider_id, args.plan_id, checkpoint_extra_info) dict_format_list = {"protection_plan"} @@ -666,20 +578,6 @@ def do_checkpoint_create(cs, args): json_format_list=json_format_list) -def _extract_extra_info(args): - checkpoint_extra_info = {} - for data in args.extra_info: - # unset doesn't require a val, so we have the if/else - if '=' in data: - (key, value) = data.split('=', 1) - else: - key = data - value = None - - checkpoint_extra_info[key] = value - return checkpoint_extra_info - - @utils.arg('provider_id', metavar='', help='ID of provider.') @@ -919,27 +817,12 @@ def do_trigger_list(cs, args): help='Properties of trigger.') def do_trigger_create(cs, args): """Creates a trigger.""" - trigger_properties = _extract_properties(args) + trigger_properties = arg_utils.extract_properties(args) trigger = cs.triggers.create(args.name, args.type, trigger_properties) dict_format_list = {"properties"} utils.print_dict(trigger.to_dict(), dict_format_list=dict_format_list) -def _extract_properties(args): - properties = {} - if args.properties is None: - return properties - for data in args.properties.split(','): - if '=' in data: - (resource_key, resource_value) = data.split('=', 1) - else: - raise exceptions.CommandError( - "Unable to parse parameter properties.") - - properties[resource_key] = resource_value - return properties - - @utils.arg("trigger_id", metavar="", help="Id of trigger to update.") @utils.arg("--name", metavar="", @@ -949,7 +832,7 @@ def _extract_properties(args): def do_trigger_update(cs, args): """Update a trigger.""" trigger_info = {} - trigger_properties = _extract_properties(args) + trigger_properties = arg_utils.extract_properties(args) trigger_info['name'] = args.name trigger_info['properties'] = trigger_properties trigger = cs.triggers.update(args.trigger_id, trigger_info) @@ -1095,7 +978,7 @@ def do_scheduledoperation_list(cs, args): help='Operation definition of scheduled operation.') def do_scheduledoperation_create(cs, args): """Creates a scheduled operation.""" - operation_definition = _extract_operation_definition(args) + operation_definition = arg_utils.extract_operation_definition(args) scheduledoperation = cs.scheduled_operations.create(args.name, args.operation_type, args.trigger_id, @@ -1105,19 +988,6 @@ def do_scheduledoperation_create(cs, args): dict_format_list=dict_format_list) -def _extract_operation_definition(args): - operation_definition = {} - for data in args.operation_definition.split(','): - if '=' in data: - (resource_key, resource_value) = data.split('=', 1) - else: - raise exceptions.CommandError( - "Unable to parse parameter operation_definition.") - - operation_definition[resource_key] = resource_value - return operation_definition - - @utils.arg('scheduledoperation', metavar='', help='ID of scheduled operation.') diff --git a/setup.cfg b/setup.cfg index 4012942..57deead 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,11 @@ openstack.cli.extension = data_protection = karborclient.osc.plugin openstack.data_protection.v1 = - dataprotection_plan_list = karborclient.osc.v1.plans:ListPlans + data_protection_plan_list = karborclient.osc.v1.plans:ListPlans + data_protection_plan_show = karborclient.osc.v1.plans:ShowPlan + data_protection_plan_create = karborclient.osc.v1.plans:CreatePlan + data_protection_plan_update = karborclient.osc.v1.plans:UpdatePlan + data_protection_plan_delete = karborclient.osc.v1.plans:DeletePlan [build_sphinx] source-dir = doc/source