diff --git a/bin/swift b/bin/swift index 03e84705..6459a436 100755 --- a/bin/swift +++ b/bin/swift @@ -1125,6 +1125,41 @@ def st_upload(parser, args, thread_manager): thread_manager.error('Account not found') +st_capabilities_options = "[]" +st_capabilities_help = ''' +Retrieve capability of the proxy + +Optional positional arguments: + proxy URL of the cluster to retreive capabilities +''' + + +def st_capabilities(parser, args, thread_manager): + def _print_compo_cap(name, capabilities): + for feature, options in sorted(capabilities.items(), + key=lambda x: x[0]): + thread_manager.print_msg("%s: %s" % (name, feature)) + if options: + thread_manager.print_msg(" Options:") + for key, value in sorted(options.items(), + key=lambda x: x[0]): + thread_manager.print_msg(" %s: %s" % (key, value)) + (options, args) = parse_args(parser, args) + if (args and len(args) > 2): + thread_manager.error('Usage: %s capabilities %s\n%s', + basename(argv[0]), + st_capabilities_options, st_capabilities_help) + return + conn = get_conn(options) + url = None + if len(args) == 2: + url = args[1] + capabilities = conn.get_capabilities(url) + _print_compo_cap('Core', {'swift': capabilities['swift']}) + del capabilities['swift'] + _print_compo_cap('Additional middleware', capabilities) + + def split_headers(options, prefix='', thread_manager=None): """ Splits 'Key: Value' strings and returns them as a dictionary. @@ -1177,6 +1212,9 @@ def parse_args(parser, args, enforce_requires=True): 'region_name': options.os_region_name, } + if len(args) > 1 and args[0] == "capabilities": + return options, args + if (options.os_options.get('object_storage_url') and options.os_options.get('auth_token') and options.auth_version == '2.0'): @@ -1227,6 +1265,8 @@ Positional arguments: stat Displays information for the account, container, or object upload Uploads files or directories to the given container + capabilities List cluster capabilities + Examples: %%prog -A https://auth.api.rackspacecloud.com/v1.0 -U user -K api_key stat -v @@ -1362,7 +1402,8 @@ Examples: (options, args) = parse_args(parser, argv[1:], enforce_requires=False) parser.enable_interspersed_args() - commands = ('delete', 'download', 'list', 'post', 'stat', 'upload') + commands = ('delete', 'download', 'list', 'post', + 'stat', 'upload', 'capabilities') if not args or args[0] not in commands: parser.print_usage() if args: diff --git a/doc/manpages/swift.1 b/doc/manpages/swift.1 index a6292fc9..48a105f4 100644 --- a/doc/manpages/swift.1 +++ b/doc/manpages/swift.1 @@ -89,14 +89,20 @@ You can specify optional headers with the repeatable cURL-like option .RE \fBdelete\fR [\fIcommand-options\fR] [\fIcontainer\fR] [\fIobject\fR] [\fIobject\fR] [...] - .RS 4 Deletes everything in the account (with --all), or everything in a container, or a list of objects depending on the args given. Segments of manifest objects will be deleted as well, unless you specify the --leave-segments option. - .RE +\fBcapabilities\fR [\fIproxy-url\fR] +.RS 4 +Displays cluster capabilities. The output includes the list of the activated +Swift middlewares as well as relevant options for each ones. Addtionaly the +command displays relevant options for the Swift core. If the proxy-url option +is not provided the storage-url retreived after authentication is used as +proxy-url. +.RE .SH OPTIONS .PD 0 diff --git a/swiftclient/client.py b/swiftclient/client.py index dc457c6b..c6a11517 100644 --- a/swiftclient/client.py +++ b/swiftclient/client.py @@ -1028,6 +1028,29 @@ def delete_object(url, token=None, container=None, name=None, http_conn=None, http_response_content=body) +def get_capabilities(http_conn): + """ + Get cluster capability infos. + + :param http_conn: HTTP connection + :returns: a dict containing the cluster capabilities + :raises ClientException: HTTP Capabilities GET failed + """ + parsed, conn = http_conn + conn.request('GET', parsed.path, '') + resp = conn.getresponse() + body = resp.read() + http_log((parsed.geturl(), 'GET',), {'headers': {}}, resp, body) + if resp.status < 200 or resp.status >= 300: + raise ClientException('Capabilities GET failed', + http_scheme=parsed.scheme, + http_host=conn.host, http_port=conn.port, + http_path=parsed.path, http_status=resp.status, + http_reason=resp.reason, + http_response_content=body) + return json_loads(body) + + class Connection(object): """Convenience class to make requests that will also retry the request""" @@ -1263,3 +1286,12 @@ class Connection(object): return self._retry(None, delete_object, container, obj, query_string=query_string, response_dict=response_dict) + + def get_capabilities(self, url=None): + if not url: + url, _ = self.get_auth() + scheme = urlparse(url).scheme + netloc = urlparse(url).netloc + url = scheme + '://' + netloc + '/info' + http_conn = http_connection(url, ssl_compression=self.ssl_compression) + return get_capabilities(http_conn) diff --git a/tests/test_swiftclient.py b/tests/test_swiftclient.py index 390a40eb..f8211c47 100644 --- a/tests/test_swiftclient.py +++ b/tests/test_swiftclient.py @@ -656,6 +656,20 @@ class TestDeleteObject(MockHttpTest): query_string="hello=20") +class TestGetCapabilities(MockHttpTest): + + def test_ok(self): + conn = self.fake_http_connection(200, body='{}') + http_conn = conn('http://www.test.com/info') + self.assertEqual(type(c.get_capabilities(http_conn)), dict) + self.assertTrue(http_conn[1].has_been_read) + + def test_server_error(self): + conn = self.fake_http_connection(500) + http_conn = conn('http://www.test.com/info') + self.assertRaises(c.ClientException, c.get_capabilities, http_conn) + + class TestConnection(MockHttpTest): def test_instance(self): @@ -703,6 +717,20 @@ class TestConnection(MockHttpTest): for method, args in method_signatures: method(*args) + def test_get_capabilities(self): + conn = c.Connection() + with mock.patch('swiftclient.client.get_capabilities') as get_cap: + conn.get_capabilities('http://storage2.test.com') + parsed = get_cap.call_args[0][0][0] + self.assertEqual(parsed.path, '/info') + self.assertEqual(parsed.netloc, 'storage2.test.com') + conn.get_auth = lambda: ('http://storage.test.com/v1/AUTH_test', + 'token') + conn.get_capabilities() + parsed = get_cap.call_args[0][0][0] + self.assertEqual(parsed.path, '/info') + self.assertEqual(parsed.netloc, 'storage.test.com') + def test_retry(self): c.http_connection = self.fake_http_connection(500)