Add Sync template feature to KB-client.
Existing feature "sync create" is to sync single resource-type to single target region i.e one resource off one resource-type to one region. Issue with "sync create": Kingbird cannot sync more than one resource-type to multiple target regions in a single request. This commit from kingbird client is to enhance the existing "sync create" job. Working of sync template feature: User have to provide a input file with .yaml or .yml or .json extension. This input file should consists of ->resource-type ->resources ->source_region ->target_region. Added testcases for the same. Change-Id: I5fb0e4619b20201585e5398013ba25219e24dc80
This commit is contained in:
parent
78c3169c4a
commit
90d71c0749
40
README.rst
40
README.rst
@ -80,13 +80,51 @@ You can see the list of available commands typing::
|
|||||||
|
|
||||||
$ kingbird --help
|
$ 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
|
Useful Links
|
||||||
============
|
============
|
||||||
* Free software: Apache license
|
* Free software: Apache license
|
||||||
* `PyPi`_ - package installation
|
* `PyPi`_ - package installation
|
||||||
* `Launchpad project`_ - release management
|
* `Launchpad project`_ - release management
|
||||||
* `Blueprints`_ - feature specifications
|
* `Blueprints`_ - feature specifications
|
||||||
* `Bugs`_ - issue tracking
|
* `Bugs` - issue tracking
|
||||||
* `Source`_
|
* `Source`_
|
||||||
* `How to Contribute`_
|
* `How to Contribute`_
|
||||||
* `Documentation`_
|
* `Documentation`_
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
from osc_lib.command import command
|
from osc_lib.command import command
|
||||||
|
|
||||||
from kingbirdclient.commands.v1 import base
|
from kingbirdclient.commands.v1 import base
|
||||||
@ -142,6 +145,72 @@ class ResourceSync(base.KingbirdLister):
|
|||||||
return kingbird_client.sync_manager.sync_resources(**kwargs)
|
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):
|
class SyncList(base.KingbirdLister):
|
||||||
"""List Sync Jobs."""
|
"""List Sync Jobs."""
|
||||||
|
|
||||||
|
@ -54,3 +54,12 @@ class APIException(Exception):
|
|||||||
super(APIException, self).__init__(error_message)
|
super(APIException, self).__init__(error_message)
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
self.error_message = error_message
|
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
|
||||||
|
@ -471,6 +471,7 @@ class KingbirdShell(app.App):
|
|||||||
'sync create': sm.ResourceSync,
|
'sync create': sm.ResourceSync,
|
||||||
'sync list': sm.SyncList,
|
'sync list': sm.SyncList,
|
||||||
'sync show': sm.SyncShow,
|
'sync show': sm.SyncShow,
|
||||||
|
'sync template': sm.TemplateResourceSync,
|
||||||
'sync delete': sm.SyncDelete,
|
'sync delete': sm.SyncDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +13,13 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
import os
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
from kingbirdclient.api.v1 import sync_manager as sm
|
from kingbirdclient.api.v1 import sync_manager as sm
|
||||||
from kingbirdclient.commands.v1 import sync_manager as sync_cmd
|
from kingbirdclient.commands.v1 import sync_manager as sync_cmd
|
||||||
|
from kingbirdclient import exceptions
|
||||||
from kingbirdclient.tests import base
|
from kingbirdclient.tests import base
|
||||||
|
|
||||||
TIME_NOW = timeutils.utcnow().isoformat()
|
TIME_NOW = timeutils.utcnow().isoformat()
|
||||||
@ -31,6 +32,41 @@ FAKE_SOURCE_REGION = 'fake_region_1'
|
|||||||
FAKE_TARGET_REGION = 'fake_region_2'
|
FAKE_TARGET_REGION = 'fake_region_2'
|
||||||
FAKE_RESOURCE_TYPE = 'fake_resource'
|
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 = {
|
RESOURCE_DICT = {
|
||||||
'ID': ID,
|
'ID': ID,
|
||||||
'STATUS': FAKE_STATUS,
|
'STATUS': FAKE_STATUS,
|
||||||
@ -80,6 +116,7 @@ SYNC_RESOURCEMANAGER = sm.Resource(mock, id=RESOURCE_DICT['ID'],
|
|||||||
|
|
||||||
|
|
||||||
class TestCLISyncManagerV1(base.BaseCommandTest):
|
class TestCLISyncManagerV1(base.BaseCommandTest):
|
||||||
|
"""Testcases for sync command."""
|
||||||
|
|
||||||
def test_sync_jobs_list(self):
|
def test_sync_jobs_list(self):
|
||||||
self.client.sync_manager.list_sync_jobs.return_value = [SYNCMANAGER]
|
self.client.sync_manager.list_sync_jobs.return_value = [SYNCMANAGER]
|
||||||
@ -196,3 +233,146 @@ class TestCLISyncManagerV1(base.BaseCommandTest):
|
|||||||
'--target', FAKE_TARGET_REGION,
|
'--target', FAKE_TARGET_REGION,
|
||||||
'--force'])
|
'--force'])
|
||||||
self.assertEqual([(ID, FAKE_STATUS, TIME_NOW)], actual_call[1])
|
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")
|
||||||
|
Loading…
Reference in New Issue
Block a user