diff options
author | Peter Stachowski <peter@tesora.com> | 2016-02-23 15:11:33 -0500 |
---|---|---|
committer | Peter Stachowski <peter@tesora.com> | 2016-02-24 20:05:39 -0500 |
commit | cf8fee5fa67e3b62e1891e24558d04fdfa4e9f95 (patch) | |
tree | d01df5c66bc6d82ecb8ff62781aae95895b87fa3 | |
parent | 634dd615e65a9c77afe8cba8c68f5217cc756f27 (diff) | |
download | python-troveclient-cf8fee5fa67e3b62e1891e24558d04fdfa4e9f95.tar.gz |
Add suport for module maintenance commands
This adds support in the python API and Trove CLI
for module maintenance commands. These commands include:
- module-list
- module-show
- module-create
- module-update
- module-delete
Partially Implements: blueprint module-management
Change-Id: I54d37025275dee4731ad49ebbd21612c4464e4c4
-rw-r--r-- | troveclient/compat/client.py | 2 | ||||
-rw-r--r-- | troveclient/tests/test_modules.py | 113 | ||||
-rw-r--r-- | troveclient/v1/client.py | 2 | ||||
-rw-r--r-- | troveclient/v1/modules.py | 132 | ||||
-rw-r--r-- | troveclient/v1/shell.py | 165 |
5 files changed, 414 insertions, 0 deletions
diff --git a/troveclient/compat/client.py b/troveclient/compat/client.py index d59cf81..7885c9b 100644 --- a/troveclient/compat/client.py +++ b/troveclient/compat/client.py @@ -313,6 +313,7 @@ class Dbaas(object): from troveclient.v1 import limits from troveclient.v1 import management from troveclient.v1 import metadata + from troveclient.v1 import modules from troveclient.v1 import quota from troveclient.v1 import root from troveclient.v1 import security_groups @@ -354,6 +355,7 @@ class Dbaas(object): config_parameters = configurations.ConfigurationParameters(self) self.configuration_parameters = config_parameters self.metadata = metadata.Metadata(self) + self.modules = modules.Modules(self) self.mgmt_configs = management.MgmtConfigurationParameters(self) self.mgmt_datastore_versions = management.MgmtDatastoreVersions(self) diff --git a/troveclient/tests/test_modules.py b/troveclient/tests/test_modules.py new file mode 100644 index 0000000..01ee548 --- /dev/null +++ b/troveclient/tests/test_modules.py @@ -0,0 +1,113 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# 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. +# + +import mock +import testtools +from troveclient.v1 import modules + + +class TestModules(testtools.TestCase): + def setUp(self): + super(TestModules, self).setUp() + + self.mod_init_patcher = mock.patch( + 'troveclient.v1.modules.Module.__init__', + mock.Mock(return_value=None)) + self.addCleanup(self.mod_init_patcher.stop) + self.mod_init_patcher.start() + self.mods_init_patcher = mock.patch( + 'troveclient.v1.modules.Modules.__init__', + mock.Mock(return_value=None)) + self.addCleanup(self.mods_init_patcher.stop) + self.mods_init_patcher.start() + + self.module_name = 'mod_1' + self.module_id = 'mod-id' + self.module = mock.Mock() + self.module.id = self.module_id + self.module.name = self.module_name + + self.modules = modules.Modules() + self.modules.api = mock.Mock() + self.modules.api.client = mock.Mock() + self.modules.resource_class = mock.Mock(return_value=self.module_name) + + def tearDown(self): + super(TestModules, self).tearDown() + + def test_create(self): + def side_effect_func(path, body, mod): + return path, body, mod + + self.modules._create = mock.Mock(side_effect=side_effect_func) + path, body, mod = self.modules.create( + self.module_name, "test", "my_contents", + description="my desc", + all_tenants=False, + datastore="ds", + datastore_version="ds-version", + auto_apply=True, + visible=True, + live_update=False) + self.assertEqual("/modules", path) + self.assertEqual("module", mod) + self.assertEqual(self.module_name, body["module"]["name"]) + self.assertEqual("ds", body["module"]["datastore"]["type"]) + self.assertEqual("ds-version", body["module"]["datastore"]["version"]) + self.assertFalse(body["module"]["all_tenants"]) + self.assertTrue(body["module"]["auto_apply"]) + self.assertTrue(body["module"]["visible"]) + self.assertFalse(body["module"]["live_update"]) + + def test_update(self): + resp = mock.Mock() + resp.status_code = 200 + body = {'module': None} + self.modules.api.client.put = mock.Mock(return_value=(resp, body)) + self.modules.update(self.module_id) + self.modules.update(self.module_id, name='new_name') + self.modules.update(self.module) + self.modules.update(self.module, name='new_name') + resp.status_code = 500 + self.assertRaises(Exception, self.modules.update, self.module_name) + + def test_list(self): + page_mock = mock.Mock() + self.modules._paginated = page_mock + limit = "test-limit" + marker = "test-marker" + self.modules.list(limit, marker) + page_mock.assert_called_with( + "/modules", "modules", limit, marker, query_strings=None) + + def test_get(self): + def side_effect_func(path, inst): + return path, inst + + self.modules._get = mock.Mock(side_effect=side_effect_func) + self.assertEqual( + ('/modules/%s' % self.module_name, 'module'), + self.modules.get(self.module_name)) + + def test_delete(self): + resp = mock.Mock() + resp.status_code = 200 + body = None + self.modules.api.client.delete = mock.Mock(return_value=(resp, body)) + self.modules.delete(self.module_name) + self.modules.delete(self.module) + resp.status_code = 500 + self.assertRaises(Exception, self.modules.delete, self.module_name) diff --git a/troveclient/v1/client.py b/troveclient/v1/client.py index 90c9c17..af50b87 100644 --- a/troveclient/v1/client.py +++ b/troveclient/v1/client.py @@ -25,6 +25,7 @@ from troveclient.v1 import instances from troveclient.v1 import limits # from troveclient.v1 import management from troveclient.v1 import metadata +from troveclient.v1 import modules from troveclient.v1 import root from troveclient.v1 import security_groups from troveclient.v1 import users @@ -76,6 +77,7 @@ class Client(object): config_parameters = configurations.ConfigurationParameters(self) self.configuration_parameters = config_parameters self.metadata = metadata.Metadata(self) + self.modules = modules.Modules(self) # self.hosts = Hosts(self) # self.quota = Quotas(self) diff --git a/troveclient/v1/modules.py b/troveclient/v1/modules.py new file mode 100644 index 0000000..ec66b20 --- /dev/null +++ b/troveclient/v1/modules.py @@ -0,0 +1,132 @@ +# Copyright 2016 Tesora, Inc. +# All Rights Reserved. +# +# 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. +# + +import base64 + +from troveclient import base +from troveclient import common + + +class Module(base.Resource): + + NO_CHANGE_TO_ARG = 'no_change_to_argument' + + def __repr__(self): + return "<Module: %s>" % self.name + + +class Modules(base.ManagerWithFind): + """Manage :class:`Module` resources.""" + resource_class = Module + + def _encode_string(self, data_str): + byte_array = bytearray(data_str, 'utf-8') + return base64.b64encode(byte_array) + + def create(self, name, module_type, contents, description=None, + all_tenants=None, datastore=None, + datastore_version=None, auto_apply=None, + visible=None, live_update=None): + """Create a new module.""" + + contents = self._encode_string(contents) + body = {"module": { + "name": name, + "module_type": module_type, + "contents": contents, + }} + if description is not None: + body["module"]["description"] = description + datastore_obj = {} + if datastore: + datastore_obj["type"] = datastore + if datastore_version: + datastore_obj["version"] = datastore_version + if datastore_obj: + body["module"]["datastore"] = datastore_obj + if all_tenants is not None: + body["module"]["all_tenants"] = int(all_tenants) + if auto_apply is not None: + body["module"]["auto_apply"] = int(auto_apply) + if visible is not None: + body["module"]["visible"] = int(visible) + if live_update is not None: + body["module"]["live_update"] = int(live_update) + + return self._create("/modules", body, "module") + + def update(self, module, name=None, module_type=None, + contents=None, description=None, + all_tenants=None, datastore=Module.NO_CHANGE_TO_ARG, + datastore_version=Module.NO_CHANGE_TO_ARG, auto_apply=None, + visible=None, live_update=None): + """Update an existing module. Passing in + datastore=None or datastore_version=None has the effect of + making it available for all datastores/versions. + """ + body = { + "module": { + } + } + if name is not None: + body["module"]["name"] = name + if module_type is not None: + body["module"]["type"] = module_type + if contents is not None: + contents = self._encode_string(contents) + body["module"]["contents"] = contents + if description is not None: + body["module"]["description"] = description + datastore_obj = {} + if datastore is None or datastore != Module.NO_CHANGE_TO_ARG: + datastore_obj["type"] = datastore + if (datastore_version is None or + datastore_version != Module.NO_CHANGE_TO_ARG): + datastore_obj["version"] = datastore_version + if datastore_obj: + body["module"]["datastore"] = datastore_obj + if all_tenants is not None: + body["module"]["all_tenants"] = int(all_tenants) + if auto_apply is not None: + body["module"]["auto_apply"] = int(auto_apply) + if visible is not None: + body["module"]["visible"] = int(visible) + if live_update is not None: + body["module"]["live_update"] = int(live_update) + + url = "/modules/%s" % base.getid(module) + resp, body = self.api.client.put(url, body=body) + common.check_for_exceptions(resp, body, url) + return Module(self, body['module'], loaded=True) + + def list(self, limit=None, marker=None, datastore=None): + """Get a list of all modules.""" + query_strings = None + if datastore: + query_strings = {"datastore": datastore} + return self._paginated( + "/modules", "modules", limit, marker, query_strings=query_strings) + + def get(self, module): + """Get a specific module.""" + return self._get( + "/modules/%s" % base.getid(module), "module") + + def delete(self, module): + """Delete the specified module.""" + url = "/modules/%s" % base.getid(module) + resp, body = self.api.client.delete(url) + common.check_for_exceptions(resp, body, url) diff --git a/troveclient/v1/shell.py b/troveclient/v1/shell.py index fd4e50a..17bf110 100644 --- a/troveclient/v1/shell.py +++ b/troveclient/v1/shell.py @@ -16,6 +16,7 @@ from __future__ import print_function +import argparse import sys import time @@ -138,6 +139,21 @@ def _find_backup(cs, backup): return utils.find_resource(cs.backups, backup) +def _find_module(cs, module): + """Get a module by ID.""" + return utils.find_resource(cs.modules, module) + + +def _find_datastore(cs, datastore): + """Get a datastore by ID.""" + return utils.find_resource(cs.datastores, datastore) + + +def _find_datastore_version(cs, datastore_version): + """Get a datastore version by ID.""" + return utils.find_resource(cs.datastores, datastore_version) + + # Flavor related calls @utils.arg('--datastore_type', metavar='<datastore_type>', default=None, @@ -1395,6 +1411,155 @@ def do_metadata_delete(cs, args): cs.metadata.delete(args.instance_id, args.key) +@utils.arg('--datastore', metavar='<datastore>', + help='Name or ID of datastore to list modules for.') +@utils.service_type('database') +def do_module_list(cs, args): + """Lists the modules available.""" + datastore = None + if args.datastore: + datastore = _find_datastore(cs, args.datastore) + module_list = cs.modules.list(datastore=datastore) + utils.print_list( + module_list, + ['id', 'tenant', 'name', 'type', 'datastore', + 'datastore_version', 'auto_apply', 'visible'], + labels={'datastore_version': 'Version'}) + + +@utils.arg('module', metavar='<module>', + help='ID or name of the module.') +@utils.service_type('database') +def do_module_show(cs, args): + """Shows details of a module.""" + module = _find_module(cs, args.module) + _print_object(module) + + +@utils.arg('name', metavar='<name>', type=str, help='Name of the module.') +@utils.arg('type', metavar='<type>', type=str, + help='Type of the module. The type must be supported by a ' + 'corresponding module plugin on the datastore it is ' + 'applied to.') +@utils.arg('file', metavar='<filename>', type=argparse.FileType('rb', 0), + help='File containing data contents for the module.') +@utils.arg('--description', metavar='<description>', type=str, + help='Description of the module.') +@utils.arg('--datastore', metavar='<datastore>', + help='Name or ID of datastore this module can be applied to. ' + 'If not specified, module can be applied to all datastores.') +@utils.arg('--datastore_version', metavar='<version>', + help='Name or ID of datastore version this module can be applied ' + 'to. If not specified, module can be applied to all versions.') +@utils.arg('--auto_apply', action='store_true', default=False, + help='Automatically apply this module when creating an instance ' + 'or cluster.') +@utils.arg('--all_tenants', action='store_true', default=False, + help='Module is valid for all tenants (Admin only).') +# This option is to suppress the module from module-list for non-admin +@utils.arg('--hidden', action='store_true', default=False, + help=argparse.SUPPRESS) +@utils.arg('--live_update', action='store_true', default=False, + help='Allow module to be updated even if it is already applied ' + 'to a current instance or cluster. Automatically attempt to ' + 'reapply this module if the contents change.') +@utils.service_type('database') +def do_module_create(cs, args): + """Create a module.""" + + contents = args.file.read() + if not contents: + raise exceptions.ValidationError( + "The file '%s' must contain some data" % args.file) + + module = cs.modules.create( + args.name, args.type, contents, description=args.description, + all_tenants=args.all_tenants, datastore=args.datastore, + datastore_version=args.datastore_version, + auto_apply=args.auto_apply, visible=not args.hidden, + live_update=args.live_update) + _print_object(module) + + +@utils.arg('module', metavar='<module>', type=str, + help='Name or ID of the module.') +@utils.arg('--name', metavar='<name>', type=str, default=None, + help='Name of the module.') +@utils.arg('--type', metavar='<type>', type=str, default=None, + help='Type of the module. The type must be supported by a ' + 'corresponding module plugin on the datastore it is ' + 'applied to.') +@utils.arg('--file', metavar='<filename>', type=argparse.FileType('rb', 0), + default=None, + help='File containing data contents for the module.') +@utils.arg('--description', metavar='<description>', type=str, default=None, + help='Description of the module.') +@utils.arg('--datastore', metavar='<datastore>', + help='Name or ID of datastore this module can be applied to. ' + 'If not specified, module can be applied to all datastores.') +@utils.arg('--all_datastores', dest='datastore', action='store_const', + const=None, + help='Module is valid for all datastores.') +@utils.arg('--datastore_version', metavar='<version>', + help='Name or ID of datastore version this module can be applied ' + 'to. If not specified, module can be applied to all versions.') +@utils.arg('--all_datastore_versions', dest='datastore_version', + action='store_const', const=None, + help='Module is valid for all datastore version.') +@utils.arg('--auto_apply', action='store_true', default=None, + help='Automatically apply this module when creating an instance ' + 'or cluster.') +@utils.arg('--no_auto_apply', dest='auto_apply', action='store_false', + default=None, + help='Do not automatically apply this module when creating an ' + 'instance or cluster.') +@utils.arg('--all_tenants', action='store_true', default=None, + help='Module is valid for all tenants (Admin only).') +@utils.arg('--no_all_tenants', dest='all_tenants', action='store_false', + default=None, + help='Module is valid for current tenant only (Admin only).') +# This option is to suppress the module from module-list for non-admin +@utils.arg('--hidden', action='store_true', default=None, + help=argparse.SUPPRESS) +# This option is to allow the module to be seen from module-list for non-admin +@utils.arg('--no_hidden', dest='hidden', action='store_false', default=None, + help=argparse.SUPPRESS) +@utils.arg('--live_update', action='store_true', default=None, + help='Allow module to be updated or deleted even if it is already ' + 'applied to a current instance or cluster. Automatically ' + 'attempt to reapply this module if the contents change.') +@utils.arg('--no_live_update', dest='live_update', action='store_false', + default=None, + help='Restricts a module from being updated or deleted if it is ' + 'already applied to a current instance or cluster.') +@utils.service_type('database') +def do_module_update(cs, args): + """Create a module.""" + module = _find_module(cs, args.module) + contents = args.file.read() if args.file else None + visible = not args.hidden if args.hidden is not None else None + datastore_args = {} + if args.datastore: + datastore_args['datastore'] = args.datastore + if args.datastore_version: + datastore_args['datastore_version'] = args.datastore_version + updated_module = cs.modules.update( + module, name=args.name, module_type=args.type, contents=contents, + description=args.description, all_tenants=args.all_tenants, + auto_apply=args.auto_apply, visible=visible, + live_update=args.live_update, **datastore_args) + _print_object(updated_module) + + +@utils.arg('module', metavar='<module>', + help='ID or name of the module.') +@utils.service_type('database') +def do_module_delete(cs, args): + """Delete a module.""" + module = _find_module(cs, args.module) + cs.modules.delete(module) + + @utils.arg('instance', metavar='<instance>', help='Id or Name of the instance.') @utils.service_type('database') |