From cb5736433b6fefa67f58f1cdd2e118f5d6c04e7c Mon Sep 17 00:00:00 2001 From: Daniel Salinas Date: Sat, 15 Feb 2014 00:36:07 -0600 Subject: [PATCH] Add instance_metadata functionality to the trove python library Implements: blueprint trove-metadata Change-Id: I4e498844afd2d2730fad2176dccaf1d61d8798c1 --- .gitignore | 1 + troveclient/base.py | 4 + troveclient/compat/cli.py | 15 +++ troveclient/compat/client.py | 2 + troveclient/compat/tests/test_common.py | 2 + troveclient/tests/test_metadata.py | 169 ++++++++++++++++++++++++ troveclient/v1/client.py | 2 + troveclient/v1/metadata.py | 93 +++++++++++++ troveclient/v1/shell.py | 55 ++++++++ 9 files changed, 343 insertions(+) create mode 100644 troveclient/tests/test_metadata.py create mode 100644 troveclient/v1/metadata.py diff --git a/.gitignore b/.gitignore index c9cfb12e..da3e8715 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/* build/* html/* *.egg* +.coverage rdserver.txt python-troveclient.iml AUTHORS diff --git a/troveclient/base.py b/troveclient/base.py index cc005d45..a82ccb98 100644 --- a/troveclient/base.py +++ b/troveclient/base.py @@ -186,6 +186,10 @@ class Manager(utils.HookableMixin): resp, body = self.api.client.put(url, body=body) return body + def _edit(self, url, body): + resp, body = self.api.client.patch(url, body=body) + return body + class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)): """Like a `Manager`, but with additional `find()`/`findall()` methods.""" diff --git a/troveclient/compat/cli.py b/troveclient/compat/cli.py index 32440279..c5d07180 100644 --- a/troveclient/compat/cli.py +++ b/troveclient/compat/cli.py @@ -415,6 +415,20 @@ class SecurityGroupCommands(common.AuthedCommandsBase): self.dbaas.security_group_rules.delete(self.id) +class MetadataCommands(common.AuthedCommandsBase): + """Commands to create/update/replace/delete/show metadata for an instance + """ + params = [ + 'instance_id', + 'metadata' + ] + + def show(self): + """Show instance metadata.""" + self._require('instance_id') + self._pretty_print(self.dbaas.metadata.show(self.instance_id)) + + COMMANDS = { 'auth': common.Auth, 'instance': InstanceCommands, @@ -427,6 +441,7 @@ COMMANDS = { 'root': RootCommands, 'version': VersionCommands, 'secgroup': SecurityGroupCommands, + 'metadata': MetadataCommands, } diff --git a/troveclient/compat/client.py b/troveclient/compat/client.py index bc400743..5c5d5fe7 100644 --- a/troveclient/compat/client.py +++ b/troveclient/compat/client.py @@ -311,6 +311,7 @@ class Dbaas(object): from troveclient.v1 import instances from troveclient.v1 import limits from troveclient.v1 import management + from troveclient.v1 import metadata from troveclient.v1 import quota from troveclient.v1 import root from troveclient.v1 import security_groups @@ -347,6 +348,7 @@ class Dbaas(object): self.configurations = configurations.Configurations(self) config_parameters = configurations.ConfigurationParameters(self) self.configuration_parameters = config_parameters + self.metadata = metadata.Metadata(self) class Mgmt(object): def __init__(self, dbaas): diff --git a/troveclient/compat/tests/test_common.py b/troveclient/compat/tests/test_common.py index 53ed30fa..1a1ceb1a 100644 --- a/troveclient/compat/tests/test_common.py +++ b/troveclient/compat/tests/test_common.py @@ -230,8 +230,10 @@ class CommandsBaseTest(testtools.TestCase): self.assertIsNone(self.cmd_base._pretty_print(func)) def test__dumps(self): + orig_dumps = json.dumps json.dumps = mock.Mock(return_value="test-dump") self.assertEqual("test-dump", self.cmd_base._dumps("item")) + json.dumps = orig_dumps def test__pretty_list(self): func = mock.Mock(return_value=None) diff --git a/troveclient/tests/test_metadata.py b/troveclient/tests/test_metadata.py new file mode 100644 index 00000000..49e9d137 --- /dev/null +++ b/troveclient/tests/test_metadata.py @@ -0,0 +1,169 @@ +# 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 mock +import testtools +from troveclient.v1 import metadata + + +class TestMetadata(testtools.TestCase): + def setUp(self): + super(TestMetadata, self).setUp() + self.orig__init = metadata.Metadata.__init__ + metadata.Metadata.__init__ = mock.Mock(return_value=None) + self.metadata = metadata.Metadata() + self.metadata.manager = mock.Mock() + self.metadata.api = mock.Mock() + self.metadata.api.client = mock.Mock() + + self.instance_uuid = '3fbc8d6d-3f87-41d9-a4a1-060830dc6c4c' + self.metadata_key = 'metakey' + self.new_metadata_key = 'newmetakey' + self.metadata_value = {'metavalue': [1, 2, 3]} + + def tearDown(self): + super(TestMetadata, self).tearDown() + metadata.Metadata.__init__ = self.orig__init + + def test_list(self): + def side_effect_func(path, config): + return path, config + + self.metadata._get = mock.Mock(side_effect=side_effect_func) + path, config = self.metadata.list(self.instance_uuid) + self.assertEqual('/instances/%s/metadata' % self.instance_uuid, path) + self.assertEqual('metadata', config) + + def test_show(self): + def side_effect_func(path, config): + return path, config + + self.metadata._get = mock.Mock(side_effect=side_effect_func) + path, config = self.metadata.show(self.instance_uuid, + self.metadata_key) + self.assertEqual('/instances/%s/metadata/%s' % + (self.instance_uuid, self.metadata_key), path) + self.assertEqual('metadata', config) + + def test_create(self): + def side_effect_func(path, body, config): + return path, body, config + + create_body = { + 'metadata': { + 'value': self.metadata_value + } + } + + self.metadata._create = mock.Mock(side_effect=side_effect_func) + path, body, config = self.metadata.create(self.instance_uuid, + self.metadata_key, + self.metadata_value) + self.assertEqual('/instances/%s/metadata/%s' % + (self.instance_uuid, self.metadata_key), path) + self.assertEqual(create_body, body) + self.assertEqual('metadata', config) + + def test_edit(self): + def side_effect_func(path, body): + return path, body + + edit_body = { + 'metadata': { + 'value': self.metadata_value + } + } + + self.metadata._edit = mock.Mock(side_effect=side_effect_func) + path, body = self.metadata.edit(self.instance_uuid, + self.metadata_key, + self.metadata_value) + self.assertEqual('/instances/%s/metadata/%s' % + (self.instance_uuid, self.metadata_key), path) + self.assertEqual(edit_body, body) + + def test_update(self): + def side_effect_func(path, body): + return path, body + + update_body = { + 'metadata': { + 'key': self.new_metadata_key, + 'value': self.metadata_value + } + } + + self.metadata._update = mock.Mock(side_effect=side_effect_func) + path, body = self.metadata.update(self.instance_uuid, + self.metadata_key, + self.new_metadata_key, + self.metadata_value) + self.assertEqual('/instances/%s/metadata/%s' % + (self.instance_uuid, self.metadata_key), path) + self.assertEqual(update_body, body) + + def test_delete(self): + def side_effect_func(path): + return path + + self.metadata._delete = mock.Mock(side_effect=side_effect_func) + path = self.metadata.delete(self.instance_uuid, self.metadata_key) + self.assertEqual('/instances/%s/metadata/%s' % + (self.instance_uuid, self.metadata_key), path) + + def test_parse_value_valid_json_in(self): + value = {'one': [2, 3, 4]} + ser_value = json.dumps(value) + new_value = self.metadata._parse_value(ser_value) + self.assertEqual(value, new_value) + + def test_parse_value_string_in(self): + value = 'this is a string' + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) + + def test_parse_value_dict_in(self): + value = {'one': [2, 3, 4]} + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) + + def test_parse_value_list_in(self): + value = [2, 3, 4] + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) + + def test_parse_value_tuple_in(self): + value = (2, 3, 4) + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) + + def test_parse_value_float_in(self): + value = 1.32 + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) + + def test_parse_value_int_in(self): + value = 1 + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) + + def test_parse_value_invalid_json_in(self): + # NOTE(imsplitbit): it's worth mentioning here and in the code that + # if you give _parse_value invalid json you get the string passed back + # to you. + value = "{'one': [2, 3, 4]}" + new_value = self.metadata._parse_value(value) + self.assertEqual(value, new_value) diff --git a/troveclient/v1/client.py b/troveclient/v1/client.py index 75972bcf..8afa87e3 100644 --- a/troveclient/v1/client.py +++ b/troveclient/v1/client.py @@ -22,6 +22,7 @@ from troveclient.v1 import datastores from troveclient.v1 import flavors from troveclient.v1 import instances from troveclient.v1 import limits +from troveclient.v1 import metadata from troveclient.v1 import root from troveclient.v1 import security_groups from troveclient.v1 import users @@ -66,6 +67,7 @@ class Client(object): self.configurations = configurations.Configurations(self) config_parameters = configurations.ConfigurationParameters(self) self.configuration_parameters = config_parameters + self.metadata = metadata.Metadata(self) #self.hosts = Hosts(self) #self.quota = Quotas(self) diff --git a/troveclient/v1/metadata.py b/troveclient/v1/metadata.py new file mode 100644 index 00000000..8288a5b5 --- /dev/null +++ b/troveclient/v1/metadata.py @@ -0,0 +1,93 @@ +# 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 +from troveclient import base + + +class MetadataResource(base.Resource): + def __getitem__(self, item): + return self.__dict__[item] + + def __contains__(self, item): + if item in self.__dict__: + return True + else: + return False + + +class Metadata(base.Manager): + + resource_class = MetadataResource + + def list(self, instance_id): + return self._get('/instances/%s/metadata' % instance_id, 'metadata') + + def show(self, instance_id, key): + return self._get('/instances/%s/metadata/%s' % (instance_id, key), + 'metadata') + + def create(self, instance_id, key, value): + body = { + 'metadata': { + 'value': self._parse_value(value) + } + } + return self._create( + '/instances/%s/metadata/%s' % (instance_id, key), body, 'metadata') + + def update(self, instance_id, key, newkey, value): + body = { + 'metadata': { + 'key': newkey, + 'value': self._parse_value(value) + } + } + return self._update( + '/instances/%s/metadata/%s' % (instance_id, key), body) + + def edit(self, instance_id, key, value): + body = { + 'metadata': { + 'value': self._parse_value(value) + } + } + return self._edit( + '/instances/%s/metadata/%s' % (instance_id, key), body) + + def delete(self, instance_id, key): + return self._delete('/instances/%s/metadata/%s' % (instance_id, key)) + + @staticmethod + def _parse_value(value): + """This method is used to parse if a string was passed to any of the + methods we should first try to deserialize it using json.loads. This + is needed to facilitate users passing serialized structures from the + cli. + + :param value: A value of type dict, list, tuple, int, float, str + + :returns value: + """ + # NOTE(imsplitbit): if you give _parse_value invalid json you get + # the string passed back to you. + if isinstance(value, str): + try: + value = json.loads(value) + except ValueError: + # the value passed in was a string but not json + pass + + return value diff --git a/troveclient/v1/shell.py b/troveclient/v1/shell.py index 6c1b69de..8fbc1165 100644 --- a/troveclient/v1/shell.py +++ b/troveclient/v1/shell.py @@ -811,3 +811,58 @@ def do_configuration_update(cs, args): args.values, args.name, args.description) + + +@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.service_type('database') +def do_metadata_list(cs, args): + """Shows all metadata for instance .""" + result = cs.metadata.list(args.instance_id) + _print_instance(result) + + +@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.arg('key', metavar='', help='key to display') +@utils.service_type('database') +def do_metadata_show(cs, args): + """Shows metadata entry for key and instance .""" + result = cs.metadata.show(args.instance_id, args.key) + _print_instance(result) + + +@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.arg('key', metavar='', help='Key to replace') +@utils.arg('value', metavar='', + help='New value to assign to ') +@utils.service_type('database') +def do_metadata_edit(cs, args): + """Replaces metadata value with a new one, this is non-destructive.""" + cs.metadata.edit(args.instance_id, args.key, args.value) + + +@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.arg('key', metavar='', help='Key to update') +@utils.arg('newkey', metavar='', help='New key') +@utils.arg('value', metavar='', help='Value to assign to ') +@utils.service_type('database') +def do_metadata_update(cs, args): + """Updates metadata, this is destructive.""" + cs.metadata.update(args.instance_id, args.key, args.newkey, args.value) + + +@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.arg('key', metavar='', help='Key for assignment') +@utils.arg('value', metavar='', help='Value to assign to ') +@utils.service_type('database') +def do_metadata_create(cs, args): + """Creates metadata in the database for instance .""" + result = cs.metadata.create(args.instance_id, args.key, args.value) + _print_instance(result) + + +@utils.arg('instance_id', metavar='', help='UUID for instance') +@utils.arg('key', metavar='', help='Metadata key to delete') +@utils.service_type('database') +def do_metadata_delete(cs, args): + """Deletes metadata for instance .""" + cs.metadata.delete(args.instance_id, args.key)