diff --git a/README.rst b/README.rst index 1347703..74a9582 100644 --- a/README.rst +++ b/README.rst @@ -80,13 +80,51 @@ You can see the list of available commands typing:: $ kingbird --help +About sync template command +============================ +Provide the input file in .yaml/.yml/.json to sync multiple resource-types to multiple regions +Sample input file for .yaml/.yml +-------------------------------- +Eg:: + + Sync: + - resource_type: fake_resource_type + resources: + - fake_resource_1 + - fake_resource_2 + source_region: + - fake_source_region + target_region: + - fake_target_region_1 + - fake_target_region_2 + + +Sample input file for .json +-------------------------- +Eg:: + + { + "Sync": [ + { + "resource_type": "fake_resource_type", + "resources": [ + "fake_resource_1", + "fake_resource_2" + ], + "source_region":["fake_source_region"], + "target_region":["fake_target_region_1","fake_target_region_2"] + } + ] + } + + Useful Links ============ * Free software: Apache license * `PyPi`_ - package installation * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications -* `Bugs`_ - issue tracking +* `Bugs` - issue tracking * `Source`_ * `How to Contribute`_ * `Documentation`_ diff --git a/kingbirdclient/commands/v1/sync_manager.py b/kingbirdclient/commands/v1/sync_manager.py index b997695..e5c7d7f 100644 --- a/kingbirdclient/commands/v1/sync_manager.py +++ b/kingbirdclient/commands/v1/sync_manager.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import yaml + from osc_lib.command import command from kingbirdclient.commands.v1 import base @@ -142,6 +145,72 @@ class ResourceSync(base.KingbirdLister): return kingbird_client.sync_manager.sync_resources(**kwargs) +class TemplateResourceSync(base.KingbirdLister): + """Sync multiple resource-types to multiple regions.""" + + def _get_format_function(self): + return sync_format + + def get_parser(self, parsed_args): + parser = super(TemplateResourceSync, self).get_parser(parsed_args) + + parser.add_argument( + '--template', + required=True, + help='Specify the name of an input file in .yaml/.yml/.json.' + ) + + return parser + + def _get_resources(self, parsed_args): + kingbird_client = self.app.client_manager.sync_engine + kwargs = dict() + sync_template = parsed_args.template + if sync_template.endswith('.yaml') or sync_template.endswith('.yml') \ + or sync_template.endswith('.json'): + try: + if sync_template.endswith('.json'): + with open(sync_template) as json_data: + data = json.load(json_data) + else: + data = yaml.load(open(sync_template)) + except Exception: + raise exceptions.TemplateError( + 'Syntactical errors in the template') + else: + raise exceptions.TemplateError( + 'Provide a template with a valid extension(.yaml/.yml/.json)') + for iteration in data['Sync']: + if 'source_region' not in iteration: + raise exceptions.TemplateError( + 'source_region parameter is missing in template') + if not iteration['source_region']: + raise exceptions.TemplateError( + 'source_region parameter value is missing') + if 'target_region' not in iteration: + raise exceptions.TemplateError( + 'target_region parameter is missing in template') + if not iteration['target_region']: + raise exceptions.TemplateError( + 'target_region parameter value is missing') + if 'resource_type' not in iteration: + raise exceptions.TemplateError( + 'resource_type parameter is missing in template') + if not iteration['resource_type']: + raise exceptions.TemplateError( + 'resource_type parameter value is missing') + if 'resources' not in iteration: + raise exceptions.TemplateError( + 'resources parameter is missing in template') + if not iteration['resources']: + raise exceptions.TemplateError( + 'resources parameter value is missing') + + kwargs.update(data) + + return kingbird_client.sync_manager.sync_resources(**kwargs) + + class SyncList(base.KingbirdLister): """List Sync Jobs.""" diff --git a/kingbirdclient/exceptions.py b/kingbirdclient/exceptions.py index ffc1732..4235d99 100644 --- a/kingbirdclient/exceptions.py +++ b/kingbirdclient/exceptions.py @@ -54,3 +54,12 @@ class APIException(Exception): super(APIException, self).__init__(error_message) self.error_code = error_code self.error_message = error_message + + +class TemplateError(KingbirdClientException): + message = "Insufficient parameters in the template" + code = "TEMPLATE_ERROR_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message diff --git a/kingbirdclient/shell.py b/kingbirdclient/shell.py index a3e678c..4662a46 100644 --- a/kingbirdclient/shell.py +++ b/kingbirdclient/shell.py @@ -471,6 +471,7 @@ class KingbirdShell(app.App): 'sync create': sm.ResourceSync, 'sync list': sm.SyncList, 'sync show': sm.SyncShow, + 'sync template': sm.TemplateResourceSync, 'sync delete': sm.SyncDelete, } diff --git a/kingbirdclient/tests/v1/test_sync_manager.py b/kingbirdclient/tests/v1/test_sync_manager.py index cc675f7..1f69f80 100644 --- a/kingbirdclient/tests/v1/test_sync_manager.py +++ b/kingbirdclient/tests/v1/test_sync_manager.py @@ -13,12 +13,13 @@ # limitations under the License. import mock - +import os from oslo_utils import timeutils from oslo_utils import uuidutils from kingbirdclient.api.v1 import sync_manager as sm from kingbirdclient.commands.v1 import sync_manager as sync_cmd +from kingbirdclient import exceptions from kingbirdclient.tests import base TIME_NOW = timeutils.utcnow().isoformat() @@ -31,6 +32,41 @@ FAKE_SOURCE_REGION = 'fake_region_1' FAKE_TARGET_REGION = 'fake_region_2' FAKE_RESOURCE_TYPE = 'fake_resource' +tempdef = """Sync: +- resource_type: fake_resource_type + resources: + - fake_resource_1 + - fake_resource_2 + source_region: + - fake_source_region + target_region: + - fake_target_region_1 + - fake_target_region_2 +""" +RESOURCE_TYPE_INDEX = tempdef.index('resource_type:') +RESOURCE_INDEX = tempdef.index('resources:') +SOURCE_INDEX = tempdef.index('source_region:') +TARGET_INDEX = tempdef.index('target_region:') + +tempdefjson = """{ + "Sync": [ + { + "resource_type": "fake_resource_type", + "resources": [ + "fake_resource_1", + "fake_resource_2" + ], + "source_region":["fake_source_region"], + "target_region":["fake_target_region_1","fake_target_region_2"] + } + ] +} + """ +RESOURCE_TYPE_INDEX_JSON = tempdefjson.index('"resource_type"') +RESOURCE_INDEX_JSON = tempdefjson.index('"resources"') +SOURCE_INDEX_JSON = tempdefjson.index('"source_region"') +TARGET_INDEX_JSON = tempdefjson.index(",", SOURCE_INDEX_JSON) + RESOURCE_DICT = { 'ID': ID, 'STATUS': FAKE_STATUS, @@ -80,6 +116,7 @@ SYNC_RESOURCEMANAGER = sm.Resource(mock, id=RESOURCE_DICT['ID'], class TestCLISyncManagerV1(base.BaseCommandTest): + """Testcases for sync command.""" def test_sync_jobs_list(self): self.client.sync_manager.list_sync_jobs.return_value = [SYNCMANAGER] @@ -196,3 +233,146 @@ class TestCLISyncManagerV1(base.BaseCommandTest): '--target', FAKE_TARGET_REGION, '--force']) self.assertEqual([(ID, FAKE_STATUS, TIME_NOW)], actual_call[1]) + + def test_template_resource_sync_with_template_yaml(self): + with open('test_template.yaml', 'w') as f: + f.write(tempdef) + f.close() + self.client.sync_manager.sync_resources.\ + return_value = [SYNC_RESOURCEMANAGER] + actual_call = self.call( + sync_cmd.TemplateResourceSync, app_args=[ + '--template', 'test_template.yaml']) + self.assertEqual([(ID, FAKE_STATUS, TIME_NOW)], actual_call[1]) + os.remove("test_template.yaml") + + def test_template_resource_sync_with_template_yml(self): + with open('test_template.yml', 'w') as f: + f.write(tempdef) + f.close() + self.client.sync_manager.sync_resources.\ + return_value = [SYNC_RESOURCEMANAGER] + actual_call = self.call( + sync_cmd.TemplateResourceSync, app_args=[ + '--template', 'test_template.yml']) + self.assertEqual([(ID, FAKE_STATUS, TIME_NOW)], actual_call[1]) + os.remove("test_template.yml") + + def test_template_resource_sync_with_template_json(self): + with open('test_template.json', 'w') as f: + f.write(tempdefjson) + f.close() + self.client.sync_manager.sync_resources.\ + return_value = [SYNC_RESOURCEMANAGER] + actual_call = self.call( + sync_cmd.TemplateResourceSync, app_args=[ + '--template', 'test_template.json']) + self.assertEqual([(ID, FAKE_STATUS, TIME_NOW)], actual_call[1]) + os.remove("test_template.json") + + def test_template_resource_sync_without_template(self): + self.client.sync_manager.sync_resources.\ + return_value = [SYNC_RESOURCEMANAGER] + self.assertRaises( + SystemExit, self.call, sync_cmd.TemplateResourceSync, app_args=[]) + + def test_template_resource_sync_invalid_extension(self): + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', 'test_template.yzx']) + + def test_template_resource_sync_source_missing_yaml(self): + temp = tempdef.replace(tempdef[SOURCE_INDEX:TARGET_INDEX], "") + with open('test_source_missing_template.yaml', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', 'test_source_missing_template.yaml']) + os.remove("test_source_missing_template.yaml") + + def test_template_resource_sync_target_missing_yaml(self): + temp = tempdef.replace(tempdef[TARGET_INDEX:], "") + with open('test_target_missing_template.yaml', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', 'test_target_missing_template.yaml']) + os.remove("test_target_missing_template.yaml") + + def test_template_resource_sync_resource_type_missing_yaml(self): + temp = tempdef.replace(tempdef[RESOURCE_TYPE_INDEX:RESOURCE_INDEX], "") + with open('test_resource_type_missing_template.yaml', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', + 'test_resource_type_missing_template.yaml']) + os.remove("test_resource_type_missing_template.yaml") + + def test_template_resource_sync_resources_missing_yaml(self): + temp = tempdef.replace(tempdef[RESOURCE_INDEX:SOURCE_INDEX], "") + with open('test_resource_missing_template.yaml', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', 'test_resource_missing_template.yaml']) + os.remove("test_resource_missing_template.yaml") + + def test_template_resource_sync_source_missing_json(self): + temp = tempdefjson.replace( + tempdefjson[SOURCE_INDEX_JSON:TARGET_INDEX_JSON + 1], "") + with open('test_source_missing_template.json', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', + 'test_source_missing_template.json']) + os.remove("test_source_missing_template.json") + + def test_template_resource_sync_target_missing_json(self): + temp = tempdefjson.replace( + tempdefjson[TARGET_INDEX_JSON:tempdefjson.index("}")], "") + with open('test_target_missing_template.json', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', 'test_target_missing_template.json']) + os.remove("test_target_missing_template.json") + + def test_template_resource_sync_resource_type_missing_json(self): + temp = tempdefjson.replace( + tempdefjson[RESOURCE_TYPE_INDEX_JSON:RESOURCE_INDEX_JSON], "") + with open('test_resource_type_missing_template.json', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', + 'test_resource_type_missing_template.json']) + os.remove("test_resource_type_missing_template.json") + + def test_template_resource_sync_resources_missing_json(self): + temp = tempdefjson.replace( + tempdefjson[RESOURCE_INDEX_JSON:SOURCE_INDEX_JSON], "") + with open('test_resource_missing_template.json', 'w') as f: + f.write(temp) + f.close() + self.assertRaises( + exceptions.TemplateError, self.call, + sync_cmd.TemplateResourceSync, + app_args=['--template', 'test_resource_missing_template.json']) + os.remove("test_resource_missing_template.json")