diff options
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/api/object_store_v1.py | 166 | ||||
| -rw-r--r-- | openstackclient/common/utils.py | 34 | ||||
| -rw-r--r-- | openstackclient/compute/client.py | 10 | ||||
| -rw-r--r-- | openstackclient/image/v1/image.py | 26 | ||||
| -rw-r--r-- | openstackclient/object/v1/account.py | 83 | ||||
| -rw-r--r-- | openstackclient/object/v1/container.py | 62 | ||||
| -rw-r--r-- | openstackclient/object/v1/object.py | 74 | ||||
| -rw-r--r-- | openstackclient/shell.py | 4 | ||||
| -rw-r--r-- | openstackclient/tests/api/test_object_store_v1.py | 2 | ||||
| -rw-r--r-- | openstackclient/tests/common/test_utils.py | 54 | ||||
| -rw-r--r-- | openstackclient/tests/object/v1/test_container_all.py | 2 | ||||
| -rw-r--r-- | openstackclient/tests/object/v1/test_object_all.py | 2 | ||||
| -rw-r--r-- | openstackclient/volume/v2/volume.py | 2 |
13 files changed, 488 insertions, 33 deletions
diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index c52eeb3a..c870332a 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -44,9 +44,8 @@ class APIv1(api.BaseAPI): """ response = self.create(container, method='PUT') - url_parts = urlparse(self.endpoint) data = { - 'account': url_parts.path.split('/')[-1], + 'account': self._find_account_id(), 'container': container, 'x-trans-id': response.headers.get('x-trans-id', None), } @@ -140,6 +139,23 @@ class APIv1(api.BaseAPI): for object in objects: self.object_save(container=container, object=object['name']) + def container_set( + self, + container, + properties, + ): + """Set container properties + + :param string container: + name of container to modify + :param dict properties: + properties to add or update for the container + """ + + headers = self._set_properties(properties, 'X-Container-Meta-%s') + if headers: + self.create(container, headers=headers) + def container_show( self, container=None, @@ -154,12 +170,13 @@ class APIv1(api.BaseAPI): response = self._request('HEAD', container) data = { - 'account': response.headers.get('x-container-meta-owner', None), + 'account': self._find_account_id(), 'container': container, 'object_count': response.headers.get( 'x-container-object-count', None, ), + 'meta-owner': response.headers.get('x-container-meta-owner', None), 'bytes_used': response.headers.get('x-container-bytes-used', None), 'read_acl': response.headers.get('x-container-read', None), 'write_acl': response.headers.get('x-container-write', None), @@ -168,6 +185,24 @@ class APIv1(api.BaseAPI): } return data + def container_unset( + self, + container, + properties, + ): + """Unset container properties + + :param string container: + name of container to modify + :param dict properties: + properties to remove from the container + """ + + headers = self._unset_properties(properties, + 'X-Remove-Container-Meta-%s') + if headers: + self.create(container, headers=headers) + def object_create( self, container=None, @@ -194,9 +229,8 @@ class APIv1(api.BaseAPI): method='PUT', data=f, ) - url_parts = urlparse(self.endpoint) data = { - 'account': url_parts.path.split('/')[-1], + 'account': self._find_account_id(), 'container': container, 'object': object, 'x-trans-id': response.headers.get('X-Trans-Id', None), @@ -332,6 +366,46 @@ class APIv1(api.BaseAPI): for chunk in response.iter_content(): f.write(chunk) + def object_set( + self, + container, + object, + properties, + ): + """Set object properties + + :param string container: + container name for object to modify + :param string object: + name of object to modify + :param dict properties: + properties to add or update for the container + """ + + headers = self._set_properties(properties, 'X-Object-Meta-%s') + if headers: + self.create("%s/%s" % (container, object), headers=headers) + + def object_unset( + self, + container, + object, + properties, + ): + """Unset object properties + + :param string container: + container name for object to modify + :param string object: + name of object to modify + :param dict properties: + properties to remove from the object + """ + + headers = self._unset_properties(properties, 'X-Remove-Object-Meta-%s') + if headers: + self.create("%s/%s" % (container, object), headers=headers) + def object_show( self, container=None, @@ -352,10 +426,11 @@ class APIv1(api.BaseAPI): response = self._request('HEAD', "%s/%s" % (container, object)) data = { - 'account': response.headers.get('x-container-meta-owner', None), + 'account': self._find_account_id(), 'container': container, 'object': object, 'content-type': response.headers.get('content-type', None), + 'meta-owner': response.headers.get('x-container-meta-owner', None), } if 'content-length' in response.headers: data['content-length'] = response.headers.get( @@ -386,3 +461,82 @@ class APIv1(api.BaseAPI): data[key.lower()] = value return data + + def account_set( + self, + properties, + ): + """Set account properties + + :param dict properties: + properties to add or update for the account + """ + + headers = self._set_properties(properties, 'X-Account-Meta-%s') + if headers: + # NOTE(stevemar): The URL (first argument) in this case is already + # set to the swift account endpoint, because that's how it's + # registered in the catalog + self.create("", headers=headers) + + def account_show(self): + """Show account details""" + + # NOTE(stevemar): Just a HEAD request to the endpoint already in the + # catalog should be enough. + response = self._request("HEAD", "") + data = {} + for k, v in response.headers.iteritems(): + data[k] = v + # Map containers, bytes and objects a bit nicer + data['Containers'] = data.pop('x-account-container-count', None) + data['Objects'] = data.pop('x-account-object-count', None) + data['Bytes'] = data.pop('x-account-bytes-used', None) + # Add in Account info too + data['Account'] = self._find_account_id() + return data + + def account_unset( + self, + properties, + ): + """Unset account properties + + :param dict properties: + properties to remove from the account + """ + + headers = self._unset_properties(properties, + 'X-Remove-Account-Meta-%s') + if headers: + self.create("", headers=headers) + + def _find_account_id(self): + url_parts = urlparse(self.endpoint) + return url_parts.path.split('/')[-1] + + def _unset_properties(self, properties, header_tag): + # NOTE(stevemar): As per the API, the headers have to be in the form + # of "X-Remove-Account-Meta-Book: x". In the case where metadata is + # removed, we can set the value of the header to anything, so it's + # set to 'x'. In the case of a Container property we use: + # "X-Remove-Container-Meta-Book: x", and the same logic applies for + # Object properties + + headers = {} + for k in properties: + header_name = header_tag % k + headers[header_name] = 'x' + return headers + + def _set_properties(self, properties, header_tag): + # NOTE(stevemar): As per the API, the headers have to be in the form + # of "X-Account-Meta-Book: MobyDick". In the case of a Container + # property we use: "X-Add-Container-Meta-Book: MobyDick", and the same + # logic applies for Object properties + + headers = {} + for k, v in properties.iteritems(): + header_name = header_tag % k + headers[header_name] = v + return headers diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index b6726bfa..51e2a2f9 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -94,12 +94,15 @@ def find_resource(manager, name_or_id, **kwargs): if len(kwargs) == 0: kwargs = {} - # Prepare the kwargs for calling find - if 'NAME_ATTR' in manager.resource_class.__dict__: - # novaclient does this for oddball resources - kwargs[manager.resource_class.NAME_ATTR] = name_or_id - else: - kwargs['name'] = name_or_id + try: + # Prepare the kwargs for calling find + if 'NAME_ATTR' in manager.resource_class.__dict__: + # novaclient does this for oddball resources + kwargs[manager.resource_class.NAME_ATTR] = name_or_id + else: + kwargs['name'] = name_or_id + except Exception: + pass # finally try to find entity by name try: @@ -118,7 +121,24 @@ def find_resource(manager, name_or_id, **kwargs): (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) else: - raise + pass + + try: + for resource in manager.list(): + # short circuit and return the first match + if (resource.get('id') == name_or_id or + resource.get('name') == name_or_id): + return resource + else: + # we found no match, keep going to bomb out + pass + except Exception: + # in case the list fails for some reason + pass + + # if we hit here, we've failed, report back this error: + msg = "Could not find resource %s" % name_or_id + raise exceptions.CommandError(msg) def format_dict(data): diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 8ac5f324..23a4deca 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -34,13 +34,8 @@ _compute_api_version = None def make_client(instance): """Returns a compute service client.""" - # Defer client imports until we actually need them + # Defer client import until we actually need them from novaclient import client as nova_client - from novaclient import extension - try: - from novaclient.v2.contrib import list_extensions - except ImportError: - from novaclient.v1_1.contrib import list_extensions if _compute_api_version is not None: version = _compute_api_version @@ -52,7 +47,8 @@ def make_client(instance): # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG - extensions = [extension.Extension('list_extensions', list_extensions)] + extensions = [ext for ext in nova_client.discover_extensions(version) + if ext.name == "list_extensions"] # Remember interface only if it is set kwargs = utils.build_kwargs_dict('endpoint_type', instance._interface) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 68c81cd5..81d384f7 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -213,6 +213,8 @@ class CreateImage(show.ShowOne): if parsed_args.private: kwargs['is_public'] = False + info = {} + if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: volume_client = self.app.client_manager.volume @@ -241,18 +243,18 @@ class CreateImage(show.ShowOne): # do a chunked transfer kwargs["data"] = sys.stdin - # Wrap the call to catch exceptions in order to close files - try: - image = image_client.images.create(**kwargs) - finally: - # Clean up open files - make sure data isn't a string - if ('data' in kwargs and hasattr(kwargs['data'], 'close') and - kwargs['data'] != sys.stdin): - kwargs['data'].close() - - info = {} - info.update(image._info) - info['properties'] = utils.format_dict(info.get('properties', {})) + if not parsed_args.volume: + # Wrap the call to catch exceptions in order to close files + try: + image = image_client.images.create(**kwargs) + finally: + # Clean up open files - make sure data isn't a string + if ('data' in kwargs and hasattr(kwargs['data'], 'close') and + kwargs['data'] != sys.stdin): + kwargs['data'].close() + + info.update(image._info) + info['properties'] = utils.format_dict(info.get('properties', {})) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py new file mode 100644 index 00000000..4ff890ce --- /dev/null +++ b/openstackclient/object/v1/account.py @@ -0,0 +1,83 @@ +# 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. +# + +"""Account v1 action implementations""" + +import logging + +from cliff import command +from cliff import show +import six + +from openstackclient.common import parseractions +from openstackclient.common import utils + + +class SetAccount(command.Command): + """Set account properties""" + + log = logging.getLogger(__name__ + '.SetAccount') + + def get_parser(self, prog_name): + parser = super(SetAccount, self).get_parser(prog_name) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this account " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.account_set( + properties=parsed_args.property, + ) + + +class ShowAccount(show.ShowOne): + """Display account details""" + + log = logging.getLogger(__name__ + '.ShowAccount') + + @utils.log_method(log) + def take_action(self, parsed_args): + data = self.app.client_manager.object_store.account_show() + return zip(*sorted(six.iteritems(data))) + + +class UnsetAccount(command.Command): + """Unset account properties""" + + log = logging.getLogger(__name__ + '.UnsetAccount') + + def get_parser(self, prog_name): + parser = super(UnsetAccount, self).get_parser(prog_name) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from account ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.account_unset( + properties=parsed_args.property, + ) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 49173deb..b8eb4c25 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -23,6 +23,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -178,6 +179,36 @@ class SaveContainer(command.Command): ) +class SetContainer(command.Command): + """Set container properties""" + + log = logging.getLogger(__name__ + '.SetContainer') + + def get_parser(self, prog_name): + parser = super(SetContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Container to modify', + ) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this container " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.container_set( + parsed_args.container, + properties=parsed_args.property, + ) + + class ShowContainer(show.ShowOne): """Display container details""" @@ -200,3 +231,34 @@ class ShowContainer(show.ShowOne): ) return zip(*sorted(six.iteritems(data))) + + +class UnsetContainer(command.Command): + """Unset container properties""" + + log = logging.getLogger(__name__ + '.UnsetContainer') + + def get_parser(self, prog_name): + parser = super(UnsetContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Container to modify', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from container ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.container_unset( + parsed_args.container, + properties=parsed_args.property, + ) diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index c90f0319..a023e3a0 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -23,6 +23,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -221,6 +222,42 @@ class SaveObject(command.Command): ) +class SetObject(command.Command): + """Set object properties""" + + log = logging.getLogger(__name__ + '.SetObject') + + def get_parser(self, prog_name): + parser = super(SetObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Modify <object> from <container>', + ) + parser.add_argument( + 'object', + metavar='<object>', + help='Object to modify', + ) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this object " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.object_set( + parsed_args.container, + parsed_args.object, + properties=parsed_args.property, + ) + + class ShowObject(show.ShowOne): """Display object details""" @@ -249,3 +286,40 @@ class ShowObject(show.ShowOne): ) return zip(*sorted(six.iteritems(data))) + + +class UnsetObject(command.Command): + """Unset object properties""" + + log = logging.getLogger(__name__ + '.UnsetObject') + + def get_parser(self, prog_name): + parser = super(UnsetObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Modify <object> from <container>', + ) + parser.add_argument( + 'object', + metavar='<object>', + help='Object to modify', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from object ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.object_unset( + parsed_args.container, + parsed_args.object, + properties=parsed_args.property, + ) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index d3a7d8a5..5b36b8b2 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -79,6 +79,10 @@ class OpenStackShell(app.App): help.HelpCommand.auth_required = False complete.CompleteCommand.auth_required = False + # Slight change to the meaning of --debug + self.DEFAULT_DEBUG_VALUE = None + self.DEFAULT_DEBUG_HELP = 'Set debug logging and traceback on errors.' + super(OpenStackShell, self).__init__( description=__doc__.strip(), version=openstackclient.__version__, diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py index b18a003d..323bb8e0 100644 --- a/openstackclient/tests/api/test_object_store_v1.py +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -157,6 +157,7 @@ class TestContainer(TestObjectAPIv1): 'container': 'qaz', 'object_count': '1', 'bytes_used': '577', + 'meta-owner': FAKE_ACCOUNT, 'read_acl': None, 'write_acl': None, 'sync_to': None, @@ -322,6 +323,7 @@ class TestObject(TestObjectAPIv1): 'content-type': 'text/alpha', 'content-length': '577', 'last-modified': '20130101', + 'meta-owner': FAKE_ACCOUNT, 'etag': 'qaz', 'wife': 'Wilma', 'x-tra-header': 'yabba-dabba-do', diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index a25a5ba5..373c0de4 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -20,6 +20,7 @@ import mock from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.tests import fakes from openstackclient.tests import utils as test_utils PASSWORD = "Pa$$w0rd" @@ -27,6 +28,18 @@ WASSPORD = "Wa$$p0rd" DROWSSAP = "dr0w$$aP" +class FakeOddballResource(fakes.FakeResource): + + def get(self, attr): + """get() is needed for utils.find_resource()""" + if attr == 'id': + return self.id + elif attr == 'name': + return self.name + else: + return None + + class TestUtils(test_utils.TestCase): def test_get_password_good(self): @@ -242,6 +255,47 @@ class TestFindResource(test_utils.TestCase): self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) + def test_find_resource_silly_resource(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + silly_resource = FakeOddballResource( + None, + {'id': '12345', 'name': self.name}, + loaded=True, + ) + self.manager.list = mock.Mock( + return_value=[silly_resource, ], + ) + result = utils.find_resource(self.manager, self.name) + self.assertEqual(silly_resource, result) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + + def test_find_resource_silly_resource_not_found(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + self.manager.list = mock.Mock(return_value=[]) + result = self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + self.name) + self.assertEqual("Could not find resource legos", + str(result)) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + def test_format_dict(self): expected = "a='b', c='d', e='f'" self.assertEqual(expected, diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 8b200e09..4477f2e0 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -316,6 +316,7 @@ class TestContainerShow(TestContainerAll): 'account', 'bytes_used', 'container', + 'meta-owner', 'object_count', 'read_acl', 'sync_key', @@ -327,6 +328,7 @@ class TestContainerShow(TestContainerAll): object_fakes.ACCOUNT_ID, '123', 'ernie', + object_fakes.ACCOUNT_ID, '42', 'qaz', 'rfv', diff --git a/openstackclient/tests/object/v1/test_object_all.py b/openstackclient/tests/object/v1/test_object_all.py index 7a76ab76..41fe6324 100644 --- a/openstackclient/tests/object/v1/test_object_all.py +++ b/openstackclient/tests/object/v1/test_object_all.py @@ -160,6 +160,7 @@ class TestObjectShow(TestObjectAll): 'content-type', 'etag', 'last-modified', + 'meta-owner', 'object', 'x-object-manifest', ) @@ -171,6 +172,7 @@ class TestObjectShow(TestObjectAll): 'text/plain', '4c4e39a763d58392724bccf76a58783a', 'yesterday', + object_fakes.ACCOUNT_ID, object_fakes.object_name_1, 'manifest', ) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index ad6215e4..758f312b 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -175,7 +175,7 @@ class DeleteVolume(command.Command): action="store_true", default=False, help="Attempt forced removal of volume(s), regardless of state " - "(defaults to False" + "(defaults to False)" ) return parser |
