summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stachowski <peter@tesora.com>2016-02-23 15:11:33 -0500
committerPeter Stachowski <peter@tesora.com>2016-02-24 20:05:39 -0500
commitcf8fee5fa67e3b62e1891e24558d04fdfa4e9f95 (patch)
treed01df5c66bc6d82ecb8ff62781aae95895b87fa3
parent634dd615e65a9c77afe8cba8c68f5217cc756f27 (diff)
downloadpython-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.py2
-rw-r--r--troveclient/tests/test_modules.py113
-rw-r--r--troveclient/v1/client.py2
-rw-r--r--troveclient/v1/modules.py132
-rw-r--r--troveclient/v1/shell.py165
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')