adds support for configurations management

Reason:
Adding configuration group support for the python-troveclient

Changes:
adding configuration api calls
adding the compat client for tests and xml
added the shell cmds for the configurations in the client
made the path to datastore/version/parameters
added unit tests

partially implements blueprint configuration-management

Change-Id: Ifc0b4077c93a805898b4cd157e33172f64e85b55
This commit is contained in:
Craig Vyvial 2013-09-09 10:21:49 -05:00
parent c803faa538
commit 8bc981aba5
10 changed files with 657 additions and 6 deletions

@ -207,6 +207,9 @@ class HTTPClient(object):
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)
def patch(self, url, **kwargs):
return self._cs_request(url, 'PATCH', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)

@ -45,9 +45,24 @@ class InstanceCommands(common.AuthedCommandsBase):
'name',
'size',
'backup',
'availability_zone'
'availability_zone',
'configuration_id',
]
def _get_configuration_ref(self):
configuration_ref = None
if self.configuration_id is not None:
if self.configuration_id == "":
configuration_ref = self.configuration_id
else:
configuration_ref = "/".join(
[
self.dbaas.client.service_url,
self.configuration_id,
]
)
return configuration_ref
def create(self):
"""Create a new instance"""
self._require('name', 'flavor')
@ -59,7 +74,15 @@ class InstanceCommands(common.AuthedCommandsBase):
restorePoint = {"backupRef": self.backup}
self._pretty_print(self.dbaas.instances.create, self.name,
self.flavor, volume, restorePoint=restorePoint,
availability_zone=self.availability_zone)
availability_zone=self.availability_zone,
configuration_ref=self._get_configuration_ref())
# TODO(pdmars): is this actually what this should be named?
def modify(self):
"""Modify an instance"""
self._require('id')
self._pretty_print(self.dbaas.instances.modify, self.id,
configuration_ref=self._get_configuration_ref())
def delete(self):
"""Delete the specified instance"""
@ -101,6 +124,11 @@ class InstanceCommands(common.AuthedCommandsBase):
self._require('id')
self._pretty_print(self.dbaas.instances.restart, self.id)
def configuration(self):
"""Get configuration for the specified instance"""
self._require('id')
self._pretty_print(self.dbaas.instances.configuration, self.id)
class FlavorsCommands(common.AuthedCommandsBase):
"""Commands for listing Flavors"""
@ -292,6 +320,65 @@ class BackupsCommands(common.AuthedCommandsBase):
self._pretty_print(self.dbaas.backups.delete, self.id)
class DatastoreConfigurationParameters(common.AuthedCommandsBase):
"""Command to show configuration parameters for a datastore"""
params = ['datastore', 'parameter']
def parameters(self):
"""List parameters that can be set"""
self._require('datastore')
self._pretty_print(self.dbaas.configuration_parameters.parameters,
self.datastore)
def get_parameter(self):
"""List parameters that can be set"""
self._require('datastore', 'parameter')
self._pretty_print(self.dbaas.configuration_parameters.get_parameter,
self.datastore, self.parameter)
class ConfigurationsCommands(common.AuthedCommandsBase):
"""Command to manage and show configurations"""
params = ['name', 'instances', 'values', 'description', 'parameter']
def get(self):
"""Get details for the specified configuration"""
self._require('id')
self._pretty_print(self.dbaas.configurations.get, self.id)
def list_instances(self):
"""Get details for the specified configuration"""
self._require('id')
self._pretty_list(self.dbaas.configurations.instances, self.id)
def list(self):
"""List configurations"""
self._pretty_list(self.dbaas.configurations.list)
def create(self):
"""Create a new configuration"""
self._require('name', 'values')
self._pretty_print(self.dbaas.configurations.create, self.name,
self.values, self.description)
def update(self):
"""Update an existing configuration"""
self._require('id', 'values')
self._pretty_print(self.dbaas.configurations.update, self.id,
self.values, self.name, self.description)
def edit(self):
"""Edit an existing configuration values"""
self._require('id', 'values')
self._pretty_print(self.dbaas.configurations.edit, self.id,
self.values)
def delete(self):
"""Delete a configuration"""
self._require('id')
self._pretty_print(self.dbaas.configurations.delete, self.id)
class SecurityGroupCommands(common.AuthedCommandsBase):
"""Commands to list and show Security Groups For an Instance and """
"""create and delete security group rules for them. """
@ -334,6 +421,7 @@ COMMANDS = {
'database': DatabaseCommands,
'limit': LimitsCommands,
'backup': BackupsCommands,
'configuration': ConfigurationsCommands,
'user': UserCommands,
'root': RootCommands,
'version': VersionCommands,

@ -236,6 +236,9 @@ class TroveHTTPClient(httplib2.Http):
def get(self, url, **kwargs):
return self._cs_request(url, 'GET', **kwargs)
def patch(self, url, **kwargs):
return self._cs_request(url, 'PATCH', **kwargs)
def post(self, url, **kwargs):
return self._cs_request(url, 'POST', **kwargs)
@ -300,6 +303,7 @@ class Dbaas(object):
from troveclient.compat import versions
from troveclient.v1 import accounts
from troveclient.v1 import backups
from troveclient.v1 import configurations
from troveclient.v1 import databases
from troveclient.v1 import datastores
from troveclient.v1 import diagnostics
@ -341,6 +345,9 @@ class Dbaas(object):
self.accounts = accounts.Accounts(self)
self.diagnostics = diagnostics.DiagnosticsInterrogator(self)
self.hwinfo = diagnostics.HwInfoInterrogator(self)
self.configurations = configurations.Configurations(self)
config_parameters = configurations.ConfigurationParameters(self)
self.configuration_parameters = config_parameters
class Mgmt(object):
def __init__(self, dbaas):

@ -37,7 +37,8 @@ LISTIFY = {
"security_groups": [[]],
"backups": [[]],
"datastores": [[]],
"datastore_versions": [[]]
"datastore_versions": [[]],
"configuration_parameters": [[]],
}
@ -78,6 +79,10 @@ TYPE_MAP = {
"to_port": int,
},
"quotas": IntDict,
"configuration_parameter": {
"max": int,
"min": int,
},
}
TYPE_MAP["flavors"] = TYPE_MAP["flavor"]

@ -0,0 +1,146 @@
# Copyright 2011 OpenStack Foundation
# Copyright 2014 Rackspace Hosting
# 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 json
import testtools
import mock
from troveclient.v1 import configurations
from troveclient import base
"""
Unit tests for configurations.py
"""
class ConfigurationTest(testtools.TestCase):
def setUp(self):
super(ConfigurationTest, self).setUp()
self.orig__init = configurations.Configuration.__init__
configurations.Configuration.__init__ = mock.Mock(return_value=None)
self.configuration = configurations.Configuration()
def tearDown(self):
super(ConfigurationTest, self).tearDown()
configurations.Configuration.__init__ = self.orig__init
def test___repr__(self):
self.configuration.name = "config-1"
self.assertEqual('<Configuration: config-1>',
self.configuration.__repr__())
class ConfigurationsTest(testtools.TestCase):
def setUp(self):
super(ConfigurationsTest, self).setUp()
self.orig__init = configurations.Configurations.__init__
configurations.Configurations.__init__ = mock.Mock(return_value=None)
self.configurations = configurations.Configurations()
self.configurations.api = mock.Mock()
self.configurations.api.client = mock.Mock()
self.orig_base_getid = base.getid
base.getid = mock.Mock(return_value="configuration1")
def tearDown(self):
super(ConfigurationsTest, self).tearDown()
configurations.Configurations.__init__ = self.orig__init
base.getid = self.orig_base_getid
def _get_mock_method(self):
self._resp = mock.Mock()
self._body = None
self._url = None
def side_effect_func(url, body=None):
self._body = body
self._url = url
return (self._resp, body)
return mock.Mock(side_effect=side_effect_func)
def _build_fake_configuration(self, name, values, description=None):
return {
'name': name,
'values': values,
'description': description,
}
def test_create(self):
self.configurations.api.client.post = self._get_mock_method()
self._resp.status_code = 200
config = '{"test":12}'
self.configurations.create('config1', config, 'test description')
self.assertEqual('/configurations', self._url)
expected = {
'description': 'test description',
'name': 'config1',
'values': json.loads(config)
}
self.assertEqual({"configuration": expected}, self._body)
def test_delete(self):
self.configurations.api.client.delete = self._get_mock_method()
self._resp.status_code = 200
self.configurations.delete(27)
self.assertEqual('/configurations/27', self._url)
self._resp.status_code = 500
self.assertRaises(Exception, self.configurations.delete, 34)
def test_list(self):
def side_effect_func(path, user, limit, marker):
return path
self.configurations._list = mock.Mock(side_effect=side_effect_func)
self.assertEqual('/configurations', self.configurations.list(1))
def test_get(self):
def side_effect_func(path, config):
return path, config
self.configurations._get = mock.Mock(side_effect=side_effect_func)
self.assertEqual(('/configurations/configuration1', 'configuration'),
self.configurations.get(123))
def test_instances(self):
def side_effect_func(path, config, limit, marker):
return path
self.configurations._list = mock.Mock(side_effect=side_effect_func)
self.assertEqual('/configurations/configuration1/instances',
self.configurations.instances(123))
def test_update(self):
def side_effect_func(path, config):
return path
self.configurations.api.client.put = self._get_mock_method()
self._resp.status_code = 200
config = '{"test":12}'
self.configurations.update(27, config)
self.assertEqual('/configurations/27', self._url)
self._resp.status_code = 500
self.assertRaises(Exception, self.configurations.update, 34)
def test_edit(self):
def side_effect_func(path, config):
return path
self.configurations.api.client.patch = self._get_mock_method()
self._resp.status_code = 200
config = '{"test":12}'
self.configurations.edit(27, config)
self.assertEqual('/configurations/27', self._url)
self._resp.status_code = 500
self.assertRaises(Exception, self.configurations.edit, 34)

@ -169,6 +169,24 @@ class InstancesTest(testtools.TestCase):
self.assertEqual(253, self._instance_id)
self.assertEqual({'restart': {}}, self._body)
def test_modify(self):
resp = mock.Mock()
resp.status_code = 200
body = None
self.instances.api.client.put = mock.Mock(return_value=(resp, body))
self.instances.modify(123)
self.instances.modify(123, 321)
resp.status_code = 500
self.assertRaises(Exception, self.instances.modify, 'instance1')
def test_configuration(self):
def side_effect_func(path, inst):
return path, inst
self.instances._get = mock.Mock(side_effect=side_effect_func)
self.assertEqual(('/instances/instance1/configuration', 'instance'),
self.instances.configuration(1))
class InstanceStatusTest(testtools.TestCase):
@ -180,3 +198,5 @@ class InstanceStatusTest(testtools.TestCase):
self.assertEqual("REBOOT", instances.InstanceStatus.REBOOT)
self.assertEqual("RESIZE", instances.InstanceStatus.RESIZE)
self.assertEqual("SHUTDOWN", instances.InstanceStatus.SHUTDOWN)
self.assertEqual("RESTART_REQUIRED",
instances.InstanceStatus.RESTART_REQUIRED)

@ -18,6 +18,7 @@
from troveclient import client as trove_client
from troveclient.v1 import backups
from troveclient.v1 import configurations
from troveclient.v1 import databases
from troveclient.v1 import datastores
from troveclient.v1 import flavors
@ -65,6 +66,9 @@ class Client(object):
self.security_groups = security_groups.SecurityGroups(self)
self.datastores = datastores.Datastores(self)
self.datastore_versions = datastores.DatastoreVersions(self)
self.configurations = configurations.Configurations(self)
config_parameters = configurations.ConfigurationParameters(self)
self.configuration_parameters = config_parameters
#self.hosts = Hosts(self)
#self.quota = Quotas(self)

@ -0,0 +1,172 @@
# Copyright (c) 2014 OpenStack, LLC.
# 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 json
from troveclient import base
from troveclient import common
class Configuration(base.Resource):
"""
Configuration is a resource used to hold configuration information.
"""
def __repr__(self):
return "<Configuration: %s>" % self.name
class Configurations(base.ManagerWithFind):
"""
Manage :class:`Configurations` information.
"""
resource_class = Configuration
def get(self, configuration):
"""
Get a specific configuration.
:rtype: :class:`Configurations`
"""
return self._get("/configurations/%s" % base.getid(configuration),
"configuration")
def instances(self, configuration, limit=None, marker=None):
"""
Get a list of instances on a configuration.
:rtype: :class:`Configurations`
"""
return self._list("/configurations/%s/instances" %
base.getid(configuration),
"instances", limit, marker)
def list(self, limit=None, marker=None):
"""
Get a list of all configurations.
:rtype: list of :class:`Configurations`.
"""
return self._list("/configurations", "configurations", limit, marker)
def create(self, name, values, description=None, datastore=None,
datastore_version=None):
"""
Create a new configuration.
"""
body = {
"configuration": {
"name": name,
"values": json.loads(values)
}
}
datastore_obj = {}
if datastore:
datastore_obj["type"] = datastore
if datastore_version:
datastore_obj["version"] = datastore_version
if datastore_obj:
body["configuration"]["datastore"] = datastore_obj
if description:
body['configuration']['description'] = description
return self._create("/configurations", body, "configuration")
def update(self, configuration_id, values, name=None, description=None):
"""
Update an existing configuration.
"""
body = {
"configuration": {
"values": json.loads(values)
}
}
if name:
body['configuration']['name'] = name
if description:
body['configuration']['description'] = description
url = "/configurations/%s" % configuration_id
resp, body = self.api.client.put(url, body=body)
common.check_for_exceptions(resp, body, url)
def edit(self, configuration_id, values):
"""
Update an existing configuration.
"""
body = {
"configuration": {
"values": json.loads(values)
}
}
url = "/configurations/%s" % configuration_id
resp, body = self.api.client.patch(url, body=body)
common.check_for_exceptions(resp, body, url)
def delete(self, configuration_id):
"""
Delete the specified configuration.
:param configuration_id: The configuration id to delete
"""
url = "/configurations/%s" % configuration_id
resp, body = self.api.client.delete(url)
common.check_for_exceptions(resp, body, url)
class ConfigurationParameter(base.Resource):
"""
Configuration Parameter.
"""
def __repr__(self):
return "<ConfigurationParameter: %s>" % self.__dict__
class ConfigurationParameters(base.ManagerWithFind):
"""
Manage :class:`ConfigurationParameters` information.
"""
resource_class = ConfigurationParameter
def parameters(self, datastore, version):
"""
Get a list of valid parameters that can be changed.
"""
return self._list("/datastores/%s/versions/%s/parameters" %
(datastore, version), "configuration-parameters")
def get_parameter(self, datastore, version, key):
"""
Get a list of valid parameters that can be changed.
"""
return self._get("/datastores/%s/versions/%s/parameters/%s" %
(datastore, version, key))
def parameters_by_version(self, version):
"""
Get a list of valid parameters that can be changed.
"""
return self._list("/datastores/versions/%s/parameters" % version,
"configuration-parameters")
def get_parameter_by_version(self, version, key):
"""
Get a list of valid parameters that can be changed.
"""
return self._get("/datastores/versions/%s/parameters/%s" %
(version, key))
# Appease the abc gods
def list(self):
pass

@ -55,7 +55,7 @@ class Instances(base.ManagerWithFind):
def create(self, name, flavor_id, volume=None, databases=None, users=None,
restorePoint=None, availability_zone=None, datastore=None,
datastore_version=None, nics=None):
datastore_version=None, nics=None, configuration=None):
"""
Create (boot) a new instance.
"""
@ -82,9 +82,22 @@ class Instances(base.ManagerWithFind):
body["instance"]["datastore"] = datastore_obj
if nics:
body["instance"]["nics"] = nics
if configuration:
body["instance"]["configuration"] = configuration
return self._create("/instances", body, "instance")
def modify(self, instance_id, configuration=None):
body = {
"instance": {
}
}
if configuration is not None:
body["instance"]["configuration"] = configuration
url = "/instances/%s" % instance_id
resp, body = self.api.client.put(url, body=body)
common.check_for_exceptions(resp, body, url)
def list(self, limit=None, marker=None):
"""
Get a list of all instances.
@ -155,6 +168,15 @@ class Instances(base.ManagerWithFind):
body = {'restart': {}}
self._action(instance_id, body)
def configuration(self, instance):
"""
Get a configuration on instances.
:rtype: :class:`Instance`
"""
return self._get("/instances/%s/configuration" % base.getid(instance),
"instance")
Instances.resize_flavor = Instances.resize_instance
@ -168,3 +190,4 @@ class InstanceStatus(object):
REBOOT = "REBOOT"
RESIZE = "RESIZE"
SHUTDOWN = "SHUTDOWN"
RESTART_REQUIRED = "RESTART_REQUIRED"

@ -128,6 +128,8 @@ def do_show(cs, args):
if hasattr(instance, 'datastore'):
instance._info['datastore'] = instance.datastore['type']
instance._info['datastore_version'] = instance.datastore['version']
if hasattr(instance, 'configuration'):
instance._info['configuration'] = instance.configuration['id']
_print_instance(instance)
@ -184,6 +186,10 @@ def do_delete(cs, args):
"v4-fixed-ip: IPv4 fixed address for NIC (optional), "
"port-id: attach NIC to port with this UUID "
"(required if no net-id)")
@utils.arg('--configuration',
metavar='<configuration>',
default=None,
help='UUID of the configuration group to attach to the instance')
@utils.service_type('database')
def do_create(cs, args):
"""Creates a new instance."""
@ -216,7 +222,8 @@ def do_create(cs, args):
availability_zone=args.availability_zone,
datastore=args.datastore,
datastore_version=args.datastore_version,
nics=nics)
nics=nics,
configuration=args.configuration)
instance._info['flavor'] = instance.flavor['id']
if hasattr(instance, 'volume'):
instance._info['volume'] = instance.volume['size']
@ -224,7 +231,8 @@ def do_create(cs, args):
instance._info['datastore'] = instance.datastore['type']
instance._info['datastore_version'] = instance.datastore['version']
del(instance._info['links'])
if hasattr(instance, 'configuration'):
instance._info['configuration'] = instance.configuration['id']
_print_instance(instance)
@ -626,3 +634,178 @@ def do_datastore_version_show(cs, args):
' to retrieve a datastore version'
' by name.')
_print_instance(datastore_version)
# configuration group related functions
@utils.arg('configuration',
metavar='<configuration>',
type=str,
help='UUID of the configuration group to attach to the instance')
@utils.arg('instance',
metavar='<instance>',
type=str,
help='UUID of the instance')
@utils.service_type('database')
def do_configuration_attach(cs, args):
"""Attaches a configuration group to an instance."""
cs.instances.modify(args.instance, args.configuration)
@utils.arg('name', metavar='<name>', help='Name of the configuration group.')
@utils.arg('values', metavar='<values>',
help='Dictionary of the values to set.')
@utils.arg('--datastore', metavar='<datastore>',
help='Datastore assigned to the configuration group.')
@utils.arg('--datastore_version', metavar='<datastore_version>',
help='Datastore version UUID assigned to the configuration group.')
@utils.arg('--description', metavar='<description>',
default=None,
help='An optional description for the configuration group.')
@utils.service_type('database')
def do_configuration_create(cs, args):
"""Creates a configuration group."""
config_grp = cs.configurations.create(
args.name,
args.values,
description=args.description,
datastore=args.datastore,
datastore_version=args.datastore_version)
_print_instance(config_grp)
@utils.arg('instance',
metavar='<instance>',
type=str,
help='UUID of the instance')
@utils.service_type('database')
def do_configuration_default(cs, args):
"""Shows the default configuration of an instance."""
configs = cs.instances.configuration(args.instance)
utils.print_dict(configs._info['configuration'])
@utils.arg('configuration_group', metavar='<configuration_group>',
help='ID of the configuration group.')
@utils.service_type('database')
def do_configuration_delete(cs, args):
"""Deletes a configuration group."""
cs.configurations.delete(args.configuration_group)
@utils.arg('instance',
metavar='<instance>',
type=str,
help='UUID of the instance')
@utils.service_type('database')
def do_configuration_detach(cs, args):
"""Detaches a configuration group from an instance."""
cs.instances.modify(args.instance)
@utils.arg('--datastore', metavar='<datastore>',
default=None,
help='UUID or name of the datastore to list configuration '
'parameters for. Optional if UUID of the'
' datastore_version is provided.')
@utils.arg('datastore_version',
metavar='<datastore_version>',
help='Datastore version name or UUID assigned to the '
'configuration group.')
@utils.arg('parameter', metavar='<parameter>',
help='Name of the configuration parameter.')
@utils.service_type('database')
def do_configuration_parameter_show(cs, args):
"""Shows details of a configuration parameter."""
if args.datastore:
param = cs.configuration_parameters.get_parameter(
args.datastore,
args.datastore_version,
args.parameter)
elif utils.is_uuid_like(args.datastore_version):
param = cs.configuration_parameters.get_parameter_by_version(
args.datastore_version,
args.parameter)
_print_instance(param)
@utils.arg('--datastore', metavar='<datastore>',
default=None,
help='UUID or name of the datastore to list configuration '
'parameters for. Optional if UUID of the'
' datastore_version is provided.')
@utils.arg('datastore_version',
metavar='<datastore_version>',
help='Datastore version name or UUID assigned to the '
'configuration group.')
@utils.service_type('database')
def do_configuration_parameter_list(cs, args):
"""Lists available parameters for a configuration group."""
if args.datastore:
params = cs.configuration_parameters.parameters(
args.datastore,
args.datastore_version)
elif utils.is_uuid_like(args.datastore_version):
params = cs.configuration_parameters.parameters_by_version(
args.datastore_version)
else:
raise exceptions.NoUniqueMatch('The datastore name or id is required'
' to retrieve the parameters for the'
' configuration group by name.')
utils.print_list(params, ['name', 'type', 'min', 'max',
'restart_required'])
@utils.arg('configuration_group', metavar='<configuration_group>',
help='ID of the configuration group.')
@utils.arg('values', metavar='<values>',
help='Dictionary of the values to set.')
@utils.service_type('database')
def do_configuration_patch(cs, args):
"""Patches a configuration group."""
cs.configurations.edit(args.configuration_group,
args.values)
@utils.arg('configuration_group', metavar='<configuration_group>',
help='ID of the configuration group.')
@utils.service_type('database')
def do_configuration_instances(cs, args):
"""Lists all instances associated with a configuration group."""
params = cs.configurations.instances(args.configuration_group)
utils.print_list(params, ['id', 'name'])
@utils.service_type('database')
def do_configuration_list(cs, args):
"""Lists all configuration groups."""
config_grps = cs.configurations.list()
utils.print_list(config_grps, ['id', 'name', 'description',
'datastore_version_id'])
@utils.arg('configuration_group', metavar='<configuration_group>',
help='ID of the configuration group.')
@utils.service_type('database')
def do_configuration_show(cs, args):
"""Shows details of a configuration group."""
config_grp = cs.configurations.get(args.configuration_group)
_print_instance(config_grp)
@utils.arg('configuration_group', metavar='<configuration_group>',
help='ID of the configuration group.')
@utils.arg('values', metavar='<values>',
help='Dictionary of the values to set.')
@utils.arg('--name', metavar='<name>', default=None,
help='Name of the configuration group.')
@utils.arg('--description', metavar='<description>',
default=None,
help='An optional description for the configuration group.')
@utils.service_type('database')
def do_configuration_update(cs, args):
"""Updates a configuration group."""
cs.configurations.update(args.configuration_group,
args.values,
args.name,
args.description)