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
This commit is contained in:
Peter Stachowski
2016-02-23 15:11:33 -05:00
parent 634dd615e6
commit cf8fee5fa6
5 changed files with 414 additions and 0 deletions

View File

@@ -313,6 +313,7 @@ class Dbaas(object):
from troveclient.v1 import limits from troveclient.v1 import limits
from troveclient.v1 import management from troveclient.v1 import management
from troveclient.v1 import metadata from troveclient.v1 import metadata
from troveclient.v1 import modules
from troveclient.v1 import quota from troveclient.v1 import quota
from troveclient.v1 import root from troveclient.v1 import root
from troveclient.v1 import security_groups from troveclient.v1 import security_groups
@@ -354,6 +355,7 @@ class Dbaas(object):
config_parameters = configurations.ConfigurationParameters(self) config_parameters = configurations.ConfigurationParameters(self)
self.configuration_parameters = config_parameters self.configuration_parameters = config_parameters
self.metadata = metadata.Metadata(self) self.metadata = metadata.Metadata(self)
self.modules = modules.Modules(self)
self.mgmt_configs = management.MgmtConfigurationParameters(self) self.mgmt_configs = management.MgmtConfigurationParameters(self)
self.mgmt_datastore_versions = management.MgmtDatastoreVersions(self) self.mgmt_datastore_versions = management.MgmtDatastoreVersions(self)

View File

@@ -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)

View File

@@ -25,6 +25,7 @@ from troveclient.v1 import instances
from troveclient.v1 import limits from troveclient.v1 import limits
# from troveclient.v1 import management # from troveclient.v1 import management
from troveclient.v1 import metadata from troveclient.v1 import metadata
from troveclient.v1 import modules
from troveclient.v1 import root from troveclient.v1 import root
from troveclient.v1 import security_groups from troveclient.v1 import security_groups
from troveclient.v1 import users from troveclient.v1 import users
@@ -76,6 +77,7 @@ class Client(object):
config_parameters = configurations.ConfigurationParameters(self) config_parameters = configurations.ConfigurationParameters(self)
self.configuration_parameters = config_parameters self.configuration_parameters = config_parameters
self.metadata = metadata.Metadata(self) self.metadata = metadata.Metadata(self)
self.modules = modules.Modules(self)
# self.hosts = Hosts(self) # self.hosts = Hosts(self)
# self.quota = Quotas(self) # self.quota = Quotas(self)

132
troveclient/v1/modules.py Normal file
View File

@@ -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)

View File

@@ -16,6 +16,7 @@
from __future__ import print_function from __future__ import print_function
import argparse
import sys import sys
import time import time
@@ -138,6 +139,21 @@ def _find_backup(cs, backup):
return utils.find_resource(cs.backups, 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 # Flavor related calls
@utils.arg('--datastore_type', metavar='<datastore_type>', @utils.arg('--datastore_type', metavar='<datastore_type>',
default=None, default=None,
@@ -1395,6 +1411,155 @@ def do_metadata_delete(cs, args):
cs.metadata.delete(args.instance_id, args.key) 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>', @utils.arg('instance', metavar='<instance>',
help='Id or Name of the instance.') help='Id or Name of the instance.')
@utils.service_type('database') @utils.service_type('database')