From 533c9c5ba14581ac06b31f82531f9c749d489868 Mon Sep 17 00:00:00 2001
From: Fabien Boucher <fabien.boucher@enovance.com>
Date: Mon, 13 Jan 2014 22:39:28 +0100
Subject: [PATCH] Add capabilities option

This patch adds a capabilities option on swiftclient.
This option uses the new /info endpoint to request the
remote capabilities and nicely display it.

Change-Id: Ie34b454511d5527e402e66e1fdb72120f427f2fd
---
 bin/swift                 | 43 ++++++++++++++++++++++++++++++++++++++-
 doc/manpages/swift.1      | 10 +++++++--
 swiftclient/client.py     | 32 +++++++++++++++++++++++++++++
 tests/test_swiftclient.py | 28 +++++++++++++++++++++++++
 4 files changed, 110 insertions(+), 3 deletions(-)

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 = "[<proxy_url>]"
+st_capabilities_help = '''
+Retrieve capability of the proxy
+
+Optional positional arguments:
+  <proxy_url>           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)