From b8d375d79d1779136784ced351297872f036eca7 Mon Sep 17 00:00:00 2001
From: Daira Hopwood <daira@jacaranda.org>
Date: Sun, 24 Feb 2013 04:08:23 +0000
Subject: [PATCH] OpenStack: support HP Cloud Object Storage. Also make the
 choice of auth protocol for Rackspace configurable via openstack.provider,
 and change the reauth period to 11 hours.

Signed-off-by: David-Sarah Hopwood <david-sarah@jacaranda.org>
---
 docs/backends/cloud.rst                       | 68 +++++++++++++------
 .../cloud/openstack/openstack_container.py    | 68 +++++++++++--------
 src/allmydata/test/test_client.py             |  4 +-
 src/allmydata/test/test_storage.py            |  2 +-
 4 files changed, 88 insertions(+), 54 deletions(-)

diff --git a/docs/backends/cloud.rst b/docs/backends/cloud.rst
index 7c5f0e6d..c74370f1 100644
--- a/docs/backends/cloud.rst
+++ b/docs/backends/cloud.rst
@@ -60,22 +60,25 @@ user and product token pair is needed by a given storage server.
 .. _DevPay: http://docs.amazonwebservices.com/AmazonDevPay/latest/DevPayGettingStartedGuide/
 
 
-OpenStack and Rackspace Cloud Files
-===================================
+OpenStack
+=========
 
 `OpenStack`_ is an open standard for cloud services, including cloud storage.
-Rackspace ( `<https://www.rackspace.com>`__ and `<https://www.rackspace.co.uk>`__ )
-provides a storage service called `Cloud Files`_ based on OpenStack.
+The cloud backend currently supports two OpenStack storage providers:
+
+* Rackspace ( `<https://www.rackspace.com>`__ and `<https://www.rackspace.co.uk>`__ )
+  provides a service called `Cloud Files`_.
+* HP ( `<https://www.hpcloud.com/>`__ ) provides a service called
+  `HP Cloud Object Storage`_.
+
+Other OpenStack storage providers may be supported in future.
 
 .. _OpenStack: https://www.openstack.org/
 .. _Cloud Files: http://www.rackspace.com/cloud/files/
+.. _HP Cloud Object Storage: https://www.hpcloud.com/products/object-storage
 
-The authentication service is less precisely specified than other parts of
-the OpenStack standards, and so our implementation of it is currently specific
-to Rackspace. Other OpenStack storage providers may be supported in future.
-
-To enable storing shares on Rackspace Cloud Files, add the following keys
-to the server's ``tahoe.cfg`` file:
+To enable storing shares on one of these services, add the following keys to
+the server's ``tahoe.cfg`` file:
 
 ``[storage]``
 
@@ -84,11 +87,32 @@ to the server's ``tahoe.cfg`` file:
     This turns off the local filesystem backend and enables use of the cloud
     backend with OpenStack.
 
-``openstack.provider = (string, optional)``
+``openstack.provider = (string, optional, case-insensitive)``
+
+    The supported providers are ``rackspace.com``, ``rackspace.co.uk``,
+    ``hpcloud.com west``, and ``hpcloud.com east``. For Rackspace, use the
+    site on which the Rackspace user account was created. For HP, "west"
+    and "east" refer to the two storage regions in the United States.
 
-    The supported providers are ``rackspace.com`` and ``rackspace.co.uk``.
-    Use the one corresponding to the site on which the Rackspace user account
-    was created. The default is ``rackspace.com``.
+    The default is ``rackspace.com``.
+
+``openstack.container = (string, required)``
+
+    This controls which container will be used to hold shares. The Tahoe-LAFS
+    storage server will only modify and access objects in the configured
+    container. Multiple storage servers cannot share the same container.
+
+``openstack.url = (URL string, optional)``
+
+    This overrides the URL used to access the authentication service. It
+    does not need to be set when using Rackspace or HP accounts, because the
+    correct service is chosen based on ``openstack.provider`` by default.
+
+Authentication is less precisely specified than other parts of the OpenStack
+standards, and so the two supported providers require slightly different user
+credentials, described below.
+
+*If using Rackspace:*
 
 ``openstack.username = (string, required)``
 
@@ -99,14 +123,14 @@ to the server's ``tahoe.cfg`` file:
     The API key should be stored in a separate file named
     ``private/openstack_api_key``.
 
-``openstack.container = (string, required)``
+*If using HP:*
 
-    This controls which container will be used to hold shares. The Tahoe-LAFS
-    storage server will only modify and access objects in the configured
-    container. Multiple storage servers cannot share the same container.
+``openstack.access_key_id = (string, required)``
 
-``openstack.url = (URL string, optional)``
+``openstack.tenant_id = (string, required)``
 
-    This overrides the URL used to access the authentication service. It
-    does not need to be set when using a Rackspace account, because the
-    correct service is chosen based on ``openstack.provider`` by default.
+    These are the Access Key ID and Tenant ID (not the tenant name) obtained
+    by logging in at `<https://console.hpcloud.com/account/api_keys>`__.
+
+    The secret key, obtained from the same page by clicking SHOW, should
+    be stored in a separate file named ``private/openstack_secret_key``.
diff --git a/src/allmydata/storage/backends/cloud/openstack/openstack_container.py b/src/allmydata/storage/backends/cloud/openstack/openstack_container.py
index 617ed163..26dc0061 100644
--- a/src/allmydata/storage/backends/cloud/openstack/openstack_container.py
+++ b/src/allmydata/storage/backends/cloud/openstack/openstack_container.py
@@ -18,30 +18,51 @@ from allmydata.storage.backends.cloud.cloud_common import IContainer, \
 # Enabling this will cause secrets to be logged.
 UNSAFE_DEBUG = False
 
-#AUTH_PATH = "v1.0"
-AUTH_PATH = "v2.0/tokens"
 
 DEFAULT_AUTH_URLS = {
-    "rackspace.com": "https://identity.api.rackspacecloud.com/" + AUTH_PATH,
-    "rackspace.co.uk": "https://lon.identity.api.rackspacecloud.com/" + AUTH_PATH,
+    "rackspace.com v1": "https://identity.api.rackspacecloud.com/v1.0",
+    "rackspace.co.uk v1": "https://lon.identity.api.rackspacecloud.com/v1.0",
+    "rackspace.com": "https://identity.api.rackspacecloud.com/v2.0/tokens",
+    "rackspace.co.uk": "https://lon.identity.api.rackspacecloud.com/v2.0/tokens",
+    "hpcloud.com west": "https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/tokens",
+    "hpcloud.com east": "https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/tokens",
 }
 
-USER_AGENT = "Tahoe-LAFS OpenStack client"
 
 def configure_openstack_container(storedir, config):
-    api_key = config.get_or_create_private_config("openstack_api_key")
     provider = config.get_config("storage", "openstack.provider", "rackspace.com").lower()
     if provider not in DEFAULT_AUTH_URLS:
         raise InvalidValueError("[storage]openstack.provider %r is not recognized\n"
-                                "Valid providers are: %s" % (provider, ", ".join(DEFAULT_AUTH_URLS.keys())))
+                                "Valid providers are: %s" % (provider, ", ".join(sorted(DEFAULT_AUTH_URLS.keys()))))
 
     auth_service_url = config.get_config("storage", "openstack.url", DEFAULT_AUTH_URLS[provider])
-    username = config.get_config("storage", "openstack.username")
     container_name = config.get_config("storage", "openstack.container")
-    reauth_period = 23*60*60 #seconds
+    reauth_period = 11*60*60 #seconds
+
+    access_key_id = config.get_config("storage", "openstack.access_key_id", None)
+    if access_key_id is None:
+        username = config.get_config("storage", "openstack.username")
+        api_key = config.get_private_config("openstack_api_key")
+        if auth_service_url.endswith("/v1.0"):
+            authenticator = AuthenticatorV1(auth_service_url, username, api_key)
+        else:
+            authenticator = AuthenticatorV2(auth_service_url, {
+              'RAX-KSKEY:apiKeyCredentials': {
+                'username': username,
+                'apiKey': api_key,
+              }
+            })
+    else:
+        tenant_id = config.get_config("storage", "openstack.tenant_id")
+        secret_key = config.get_private_config("openstack_secret_key")
+        authenticator = AuthenticatorV2(auth_service_url, {
+          'apiAccessKeyCredentials': {
+            'accessKey': access_key_id,
+            'secretKey': secret_key,
+          },
+          'tenantId': tenant_id,
+        })
 
-    AuthenticatorClass = {"v1.0": AuthenticatorV1, "v2.0/tokens": AuthenticatorV2}[AUTH_PATH]
-    authenticator = AuthenticatorClass(auth_service_url, username, api_key)
     auth_client = AuthenticationClient(authenticator, reauth_period)
     return OpenStackContainer(auth_client, container_name)
 
@@ -92,28 +113,17 @@ class AuthenticatorV2(object):
     """
     Authenticates according to V2 protocol as documented by Rackspace:
     <http://docs.rackspace.com/auth/api/v2.0/auth-client-devguide/content/POST_authenticate_v2.0_tokens_.html>.
+
+    This is also compatible with HP's protocol (using different credentials):
+    <https://docs.hpcloud.com/api/identity#authenticate-jumplink-span>.
     """
 
-    def __init__(self, auth_service_url, username, api_key):
+    def __init__(self, auth_service_url, credentials):
         self._auth_service_url = auth_service_url
-        self._username = username
-        self._api_key = api_key
-        #self._password = password
+        self._credentials = credentials
 
     def make_auth_request(self):
-        # I suspect that 'RAX-KSKEY:apiKeyCredentials' is Rackspace-specific.
-        request = {
-          'auth': {
-        #    'passwordCredentials': {
-        #      'username': self._username,
-        #      'password': self._password,
-        #    }
-            'RAX-KSKEY:apiKeyCredentials': {
-              'username': self._username,
-              'apiKey': self._api_key,
-            }
-          }
-        }
+        request = {'auth': self._credentials}
         json = simplejson.dumps(request)
         request_headers = {
             'Content-Type': ['application/json'],
@@ -143,7 +153,7 @@ class AuthenticatorV2(object):
                     for endpoint in endpoints:
                         if not default_region or endpoint['region'] == default_region:
                             public_storage_url = endpoint['publicURL']
-                            internal_storage_url = endpoint['internalURL']
+                            internal_storage_url = endpoint.get('internalURL', None)
                             return AuthenticationInfo(auth_token, public_storage_url, internal_storage_url)
         except KeyError, e:
             raise CloudServiceError(None, response.code,
diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py
index da5b946b..2c0eed7a 100644
--- a/src/allmydata/test/test_client.py
+++ b/src/allmydata/test/test_client.py
@@ -371,10 +371,10 @@ class Basic(testutil.ReallyEqualMixin, unittest.TestCase):
 
         c = client.Client(basedir)
         mock_Authenticator.assert_called_with("https://identity.api.rackspacecloud.com/v2.0/tokens",
-                                              "alex", "dummy")
+                                              {'RAX-KSKEY:apiKeyCredentials': {'username': 'alex', 'apiKey': 'dummy'}})
         authclient_call_args = mock_AuthenticationClient.call_args_list
         self.failUnlessEqual(len(authclient_call_args), 1)
-        self.failUnlessEqual(authclient_call_args[0][0][1:], (23*60*60,))
+        self.failUnlessEqual(authclient_call_args[0][0][1:], (11*60*60,))
         container_call_args = mock_OpenStackContainer.call_args_list
         self.failUnlessEqual(len(container_call_args), 1)
         self.failUnlessEqual(container_call_args[0][0][1:], ("test",))
diff --git a/src/allmydata/test/test_storage.py b/src/allmydata/test/test_storage.py
index afbf0d7b..81ac6741 100644
--- a/src/allmydata/test/test_storage.py
+++ b/src/allmydata/test/test_storage.py
@@ -517,7 +517,7 @@ class OpenStackCloudBackend(ServiceParentMixin, WorkdirMixin, ShouldFailMixin, u
                 if default is _None:
                     self.failUnlessIn(option, storage_config)
                 return storage_config.get(option, default)
-            def get_or_create_private_config(mock_self, filename):
+            def get_private_config(mock_self, filename):
                 return fileutil.read(os.path.join(privatedir, filename))
 
         self.workdir = self.workdir(name)
-- 
2.45.2