Jakub Ruzicka fcc2a65
From 9d2d572eaa0c4fbc48588b103a176a132e0476d9 Mon Sep 17 00:00:00 2001
Jakub Ruzicka fcc2a65
From: Boris Pavlovic <boris@pavlovic.me>
Jakub Ruzicka fcc2a65
Date: Wed, 26 Mar 2014 15:22:03 +0400
Jakub Ruzicka fcc2a65
Subject: [PATCH] Fix session handling in novaclient
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
Prior to this patch, novaclient was handling sessions in an inconsistent
Jakub Ruzicka fcc2a65
manner.
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
Every time we created a client instance, it would use a global
Jakub Ruzicka fcc2a65
connection pool, which made it difficult to use in a process that is
Jakub Ruzicka fcc2a65
meant to be forked.
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
Obviously sessions like the ones provided by the requests library that
Jakub Ruzicka fcc2a65
will automatically cause connections to be kept alive should not be
Jakub Ruzicka fcc2a65
implicit. This patch moves the novaclient back to the age of a single
Jakub Ruzicka fcc2a65
session-less request call by default, but also adds two more
Jakub Ruzicka fcc2a65
resource-reuse friendly options that a user needs to be explicit about.
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
The first one is that both v1_1 and v3 clients can now be used as
Jakub Ruzicka fcc2a65
context managers,. where the session will be kept open (and thus the
Jakub Ruzicka fcc2a65
connection kept-alive) for the duration of the with block. This is far
Jakub Ruzicka fcc2a65
more ideal for a web worker use-case as the session can be made
Jakub Ruzicka fcc2a65
request-long.
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
The second one is the per-instance session. This is very similar to what
Jakub Ruzicka fcc2a65
we had up until now, except it is not a global object so forking is
Jakub Ruzicka fcc2a65
possible as long as each child instantiates it's own client. The session
Jakub Ruzicka fcc2a65
once created will be kept open for the duration of the client object
Jakub Ruzicka fcc2a65
lifetime.
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
Please note: client instances are not thread safe. As can be seen from
Jakub Ruzicka fcc2a65
above forking example - if you wish to use threading/multiprocessing,
Jakub Ruzicka fcc2a65
you *must not* share client instances.
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
DocImpact
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
Related-bug: #1247056
Jakub Ruzicka fcc2a65
Closes-Bug: #1297796
Jakub Ruzicka fcc2a65
Co-authored-by: Nikola Dipanov <ndipanov@redhat.com>
Jakub Ruzicka fcc2a65
Change-Id: Id59e48f61bb3f3c6223302355c849e1e99673410
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
Conflicts:
Jakub Ruzicka fcc2a65
	novaclient/client.py
Jakub Ruzicka fcc2a65
	novaclient/tests/test_client.py
Jakub Ruzicka fcc2a65
	novaclient/tests/test_http.py
Jakub Ruzicka fcc2a65
	novaclient/v1_1/client.py
Jakub Ruzicka fcc2a65
	novaclient/v3/client.py
Jakub Ruzicka fcc2a65
---
Jakub Ruzicka fcc2a65
 novaclient/client.py                  |  74 +++++++++++++++--------
Jakub Ruzicka fcc2a65
 novaclient/tests/test_auth_plugins.py |   8 +--
Jakub Ruzicka fcc2a65
 novaclient/tests/test_client.py       | 110 +++++++++++++++++++++++++++++++++-
Jakub Ruzicka fcc2a65
 novaclient/tests/test_http.py         |   8 +--
Jakub Ruzicka fcc2a65
 novaclient/tests/v1_1/test_auth.py    |  12 ++--
Jakub Ruzicka fcc2a65
 novaclient/v1_1/client.py             |  27 ++++++++-
Jakub Ruzicka fcc2a65
 novaclient/v3/client.py               |  27 ++++++++-
Jakub Ruzicka fcc2a65
 7 files changed, 221 insertions(+), 45 deletions(-)
Jakub Ruzicka fcc2a65
Jakub Ruzicka fcc2a65
diff --git a/novaclient/client.py b/novaclient/client.py
Jakub Ruzicka fcc2a65
index 0b9aeee..7f5032b 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/client.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/client.py
Jakub Ruzicka fcc2a65
@@ -39,17 +39,19 @@ from novaclient import service_catalog
Jakub Ruzicka fcc2a65
 from novaclient import utils
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-_ADAPTERS = {}
Jakub Ruzicka fcc2a65
+class _ClientConnectionPool(object):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
+    def __init__(self):
Jakub Ruzicka fcc2a65
+        self._adapters = {}
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-def _adapter_pool(url):
Jakub Ruzicka fcc2a65
-    """
Jakub Ruzicka fcc2a65
-    Store and reuse HTTP adapters per Service URL.
Jakub Ruzicka fcc2a65
-    """
Jakub Ruzicka fcc2a65
-    if url not in _ADAPTERS:
Jakub Ruzicka fcc2a65
-        _ADAPTERS[url] = adapters.HTTPAdapter()
Jakub Ruzicka fcc2a65
+    def get(self, url):
Jakub Ruzicka fcc2a65
+        """
Jakub Ruzicka fcc2a65
+        Store and reuse HTTP adapters per Service URL.
Jakub Ruzicka fcc2a65
+        """
Jakub Ruzicka fcc2a65
+        if url not in self._adapters:
Jakub Ruzicka fcc2a65
+            self._adapters[url] = adapters.HTTPAdapter()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-    return _ADAPTERS[url]
Jakub Ruzicka fcc2a65
+        return self._adapters[url]
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
 class HTTPClient(object):
Jakub Ruzicka fcc2a65
@@ -64,12 +66,16 @@ class HTTPClient(object):
Jakub Ruzicka fcc2a65
                  os_cache=False, no_cache=True,
Jakub Ruzicka fcc2a65
                  http_log_debug=False, auth_system='keystone',
Jakub Ruzicka fcc2a65
                  auth_plugin=None, auth_token=None,
Jakub Ruzicka fcc2a65
-                 cacert=None, tenant_id=None):
Jakub Ruzicka fcc2a65
+                 cacert=None, tenant_id=None,
Jakub Ruzicka fcc2a65
+                 connection_pool=False):
Jakub Ruzicka fcc2a65
         self.user = user
Jakub Ruzicka fcc2a65
         self.password = password
Jakub Ruzicka fcc2a65
         self.projectid = projectid
Jakub Ruzicka fcc2a65
         self.tenant_id = tenant_id
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
+        self._connection_pool = (_ClientConnectionPool()
Jakub Ruzicka fcc2a65
+                                if connection_pool else None)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
         # This will be called by #_get_password if self.password is None.
Jakub Ruzicka fcc2a65
         # EG if a password can only be obtained by prompting the user, but a
Jakub Ruzicka fcc2a65
         # token is available, you don't want to prompt until the token has
Jakub Ruzicka fcc2a65
@@ -118,8 +124,8 @@ class HTTPClient(object):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         self.auth_system = auth_system
Jakub Ruzicka fcc2a65
         self.auth_plugin = auth_plugin
Jakub Ruzicka fcc2a65
+        self._session = None
Jakub Ruzicka fcc2a65
         self._current_url = None
Jakub Ruzicka fcc2a65
-        self._http = None
Jakub Ruzicka fcc2a65
         self._logger = logging.getLogger(__name__)
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         if self.http_log_debug and not self._logger.handlers:
Jakub Ruzicka fcc2a65
@@ -180,19 +186,33 @@ class HTTPClient(object):
Jakub Ruzicka fcc2a65
                                              'headers': resp.headers,
Jakub Ruzicka fcc2a65
                                              'text': resp.text})
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-    def http(self, url):
Jakub Ruzicka fcc2a65
-        magic_tuple = parse.urlsplit(url)
Jakub Ruzicka fcc2a65
-        scheme, netloc, path, query, frag = magic_tuple
Jakub Ruzicka fcc2a65
-        service_url = '%s://%s' % (scheme, netloc)
Jakub Ruzicka fcc2a65
-        if self._current_url != service_url:
Jakub Ruzicka fcc2a65
-            # Invalidate Session object in case the url is somehow changed
Jakub Ruzicka fcc2a65
-            if self._http:
Jakub Ruzicka fcc2a65
-                self._http.close()
Jakub Ruzicka fcc2a65
-            self._current_url = service_url
Jakub Ruzicka fcc2a65
-            self._logger.debug("New session created for: (%s)" % service_url)
Jakub Ruzicka fcc2a65
-            self._http = requests.Session()
Jakub Ruzicka fcc2a65
-            self._http.mount(service_url, _adapter_pool(service_url))
Jakub Ruzicka fcc2a65
-        return self._http
Jakub Ruzicka fcc2a65
+    def open_session(self):
Jakub Ruzicka fcc2a65
+        if not self._connection_pool:
Jakub Ruzicka fcc2a65
+            self._session = requests.Session()
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def close_session(self):
Jakub Ruzicka fcc2a65
+        if self._session and not self._connection_pool:
Jakub Ruzicka fcc2a65
+            self._session.close()
Jakub Ruzicka fcc2a65
+            self._session = None
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def _get_session(self, url):
Jakub Ruzicka fcc2a65
+        if self._connection_pool:
Jakub Ruzicka fcc2a65
+            magic_tuple = parse.urlsplit(url)
Jakub Ruzicka fcc2a65
+            scheme, netloc, path, query, frag = magic_tuple
Jakub Ruzicka fcc2a65
+            service_url = '%s://%s' % (scheme, netloc)
Jakub Ruzicka fcc2a65
+            if self._current_url != service_url:
Jakub Ruzicka fcc2a65
+                # Invalidate Session object in case the url is somehow changed
Jakub Ruzicka fcc2a65
+                if self._session:
Jakub Ruzicka fcc2a65
+                    self._session.close()
Jakub Ruzicka fcc2a65
+                self._current_url = service_url
Jakub Ruzicka fcc2a65
+                self._logger.debug(
Jakub Ruzicka fcc2a65
+                        "New session created for: (%s)" % service_url)
Jakub Ruzicka fcc2a65
+                self._session = requests.Session()
Jakub Ruzicka fcc2a65
+                self._session.mount(service_url,
Jakub Ruzicka fcc2a65
+                        self._connection_pool.get(service_url))
Jakub Ruzicka fcc2a65
+            return self._session
Jakub Ruzicka fcc2a65
+        elif self._session:
Jakub Ruzicka fcc2a65
+            return self._session
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
     def request(self, url, method, **kwargs):
Jakub Ruzicka fcc2a65
         kwargs.setdefault('headers', kwargs.get('headers', {}))
Jakub Ruzicka fcc2a65
@@ -207,7 +227,13 @@ class HTTPClient(object):
Jakub Ruzicka fcc2a65
         kwargs['verify'] = self.verify_cert
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         self.http_log_req(method, url, kwargs)
Jakub Ruzicka fcc2a65
-        resp = self.http(url).request(
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        request_func = requests.request
Jakub Ruzicka fcc2a65
+        session = self._get_session(url)
Jakub Ruzicka fcc2a65
+        if session:
Jakub Ruzicka fcc2a65
+            request_func = session.request
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        resp = request_func(
Jakub Ruzicka fcc2a65
             method,
Jakub Ruzicka fcc2a65
             url,
Jakub Ruzicka fcc2a65
             **kwargs)
Jakub Ruzicka fcc2a65
diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py
Jakub Ruzicka fcc2a65
index a084594..1761b98 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/tests/test_auth_plugins.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/tests/test_auth_plugins.py
Jakub Ruzicka fcc2a65
@@ -92,7 +92,7 @@ class DeprecatedAuthPluginTest(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         @mock.patch.object(pkg_resources, "iter_entry_points",
Jakub Ruzicka fcc2a65
                            mock_iter_entry_points)
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             plugin = auth_plugin.DeprecatedAuthPlugin("fake")
Jakub Ruzicka fcc2a65
             cs = client.Client("username", "password", "project_id",
Jakub Ruzicka fcc2a65
@@ -121,7 +121,7 @@ class DeprecatedAuthPluginTest(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         @mock.patch.object(pkg_resources, "iter_entry_points",
Jakub Ruzicka fcc2a65
                            mock_iter_entry_points)
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             auth_plugin.discover_auth_systems()
Jakub Ruzicka fcc2a65
             plugin = auth_plugin.DeprecatedAuthPlugin("notexists")
Jakub Ruzicka fcc2a65
@@ -164,7 +164,7 @@ class DeprecatedAuthPluginTest(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         @mock.patch.object(pkg_resources, "iter_entry_points",
Jakub Ruzicka fcc2a65
                            mock_iter_entry_points)
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl")
Jakub Ruzicka fcc2a65
             cs = client.Client("username", "password", "project_id",
Jakub Ruzicka fcc2a65
@@ -197,7 +197,7 @@ class DeprecatedAuthPluginTest(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
 class AuthPluginTest(utils.TestCase):
Jakub Ruzicka fcc2a65
-    @mock.patch.object(requests.Session, "request")
Jakub Ruzicka fcc2a65
+    @mock.patch.object(requests, "request")
Jakub Ruzicka fcc2a65
     @mock.patch.object(pkg_resources, "iter_entry_points")
Jakub Ruzicka fcc2a65
     def test_auth_system_success(self, mock_iter_entry_points, mock_request):
Jakub Ruzicka fcc2a65
         """Test that we can authenticate using the auth system."""
Jakub Ruzicka fcc2a65
diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py
Jakub Ruzicka fcc2a65
index d586702..96901b9 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/tests/test_client.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/tests/test_client.py
Jakub Ruzicka fcc2a65
@@ -27,6 +27,16 @@ import novaclient.v3.client
Jakub Ruzicka fcc2a65
 import json
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
+class ClientConnectionPoolTest(utils.TestCase):
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    @mock.patch("novaclient.client.adapters.HTTPAdapter")
Jakub Ruzicka fcc2a65
+    def test_get(self, mock_http_adapter):
Jakub Ruzicka fcc2a65
+        mock_http_adapter.side_effect = lambda: mock.Mock()
Jakub Ruzicka fcc2a65
+        pool = novaclient.client._ClientConnectionPool()
Jakub Ruzicka fcc2a65
+        self.assertEqual(pool.get("abc"), pool.get("abc"))
Jakub Ruzicka fcc2a65
+        self.assertNotEqual(pool.get("abc"), pool.get("def"))
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
 class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
     def test_client_with_timeout(self):
Jakub Ruzicka fcc2a65
@@ -43,9 +53,9 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
             'x-server-management-url': 'blah.com',
Jakub Ruzicka fcc2a65
             'x-auth-token': 'blah',
Jakub Ruzicka fcc2a65
         }
Jakub Ruzicka fcc2a65
-        with mock.patch('requests.Session.request', mock_request):
Jakub Ruzicka fcc2a65
+        with mock.patch('requests.request', mock_request):
Jakub Ruzicka fcc2a65
             instance.authenticate()
Jakub Ruzicka fcc2a65
-            requests.Session.request.assert_called_with(mock.ANY, mock.ANY,
Jakub Ruzicka fcc2a65
+            requests.request.assert_called_with(mock.ANY, mock.ANY,
Jakub Ruzicka fcc2a65
                                                         timeout=2,
Jakub Ruzicka fcc2a65
                                                         headers=mock.ANY,
Jakub Ruzicka fcc2a65
                                                         verify=mock.ANY)
Jakub Ruzicka fcc2a65
@@ -61,7 +71,7 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
         instance.version = 'v2.0'
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock()
Jakub Ruzicka fcc2a65
         mock_request.side_effect = novaclient.exceptions.Unauthorized(401)
Jakub Ruzicka fcc2a65
-        with mock.patch('requests.Session.request', mock_request):
Jakub Ruzicka fcc2a65
+        with mock.patch('requests.request', mock_request):
Jakub Ruzicka fcc2a65
             try:
Jakub Ruzicka fcc2a65
                 instance.get('/servers/detail')
Jakub Ruzicka fcc2a65
             except Exception:
Jakub Ruzicka fcc2a65
@@ -197,6 +207,26 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
         cs.authenticate()
Jakub Ruzicka fcc2a65
         self.assertTrue(mock_authenticate.called)
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
+    @mock.patch('novaclient.client.HTTPClient')
Jakub Ruzicka fcc2a65
+    def test_contextmanager_v1_1(self, mock_http_client):
Jakub Ruzicka fcc2a65
+        fake_client = mock.Mock()
Jakub Ruzicka fcc2a65
+        mock_http_client.return_value = fake_client
Jakub Ruzicka fcc2a65
+        with novaclient.v1_1.client.Client("user", "password", "project_id",
Jakub Ruzicka fcc2a65
+                auth_url="foo/v2") as client:
Jakub Ruzicka fcc2a65
+            pass
Jakub Ruzicka fcc2a65
+        self.assertTrue(fake_client.open_session.called)
Jakub Ruzicka fcc2a65
+        self.assertTrue(fake_client.close_session.called)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    @mock.patch('novaclient.client.HTTPClient')
Jakub Ruzicka fcc2a65
+    def test_contextmanager_v3(self, mock_http_client):
Jakub Ruzicka fcc2a65
+        fake_client = mock.Mock()
Jakub Ruzicka fcc2a65
+        mock_http_client.return_value = fake_client
Jakub Ruzicka fcc2a65
+        with novaclient.v3.client.Client("user", "password", "project_id",
Jakub Ruzicka fcc2a65
+                auth_url="foo/v2") as client:
Jakub Ruzicka fcc2a65
+            pass
Jakub Ruzicka fcc2a65
+        self.assertTrue(fake_client.open_session.called)
Jakub Ruzicka fcc2a65
+        self.assertTrue(fake_client.close_session.called)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
     def test_get_password_simple(self):
Jakub Ruzicka fcc2a65
         cs = novaclient.client.HTTPClient("user", "password", "", "")
Jakub Ruzicka fcc2a65
         cs.password_func = mock.Mock()
Jakub Ruzicka fcc2a65
@@ -216,3 +246,77 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
         cs.password_func = mock.Mock()
Jakub Ruzicka fcc2a65
         self.assertEqual(cs._get_password(), "password")
Jakub Ruzicka fcc2a65
         self.assertFalse(cs.password_func.called)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def test_auth_url_rstrip_slash(self):
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", "password", "project_id",
Jakub Ruzicka fcc2a65
+                                          auth_url="foo/v2/")
Jakub Ruzicka fcc2a65
+        self.assertEqual(cs.auth_url, "foo/v2")
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def test_token_and_bypass_url(self):
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient(None, None, None,
Jakub Ruzicka fcc2a65
+                                          auth_token="12345",
Jakub Ruzicka fcc2a65
+                                          bypass_url="compute/v100/")
Jakub Ruzicka fcc2a65
+        self.assertIsNone(cs.auth_url)
Jakub Ruzicka fcc2a65
+        self.assertEqual(cs.auth_token, "12345")
Jakub Ruzicka fcc2a65
+        self.assertEqual(cs.bypass_url, "compute/v100")
Jakub Ruzicka fcc2a65
+        self.assertEqual(cs.management_url, "compute/v100")
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    @mock.patch("novaclient.client.requests.Session")
Jakub Ruzicka fcc2a65
+    def test_session(self, mock_session):
Jakub Ruzicka fcc2a65
+        fake_session = mock.Mock()
Jakub Ruzicka fcc2a65
+        mock_session.return_value = fake_session
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "", "")
Jakub Ruzicka fcc2a65
+        cs.open_session()
Jakub Ruzicka fcc2a65
+        self.assertEqual(cs._session, fake_session)
Jakub Ruzicka fcc2a65
+        cs.close_session()
Jakub Ruzicka fcc2a65
+        self.assertIsNone(cs._session)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def test_session_connection_pool(self):
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "",
Jakub Ruzicka fcc2a65
+                                          "", connection_pool=True)
Jakub Ruzicka fcc2a65
+        cs.open_session()
Jakub Ruzicka fcc2a65
+        self.assertIsNone(cs._session)
Jakub Ruzicka fcc2a65
+        cs.close_session()
Jakub Ruzicka fcc2a65
+        self.assertIsNone(cs._session)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def test_get_session(self):
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "", "")
Jakub Ruzicka fcc2a65
+        self.assertIsNone(cs._get_session("http://nooooooooo.com"))
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    @mock.patch("novaclient.client.requests.Session")
Jakub Ruzicka fcc2a65
+    def test_get_session_open_session(self, mock_session):
Jakub Ruzicka fcc2a65
+        fake_session = mock.Mock()
Jakub Ruzicka fcc2a65
+        mock_session.return_value = fake_session
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "", "")
Jakub Ruzicka fcc2a65
+        cs.open_session()
Jakub Ruzicka fcc2a65
+        self.assertEqual(fake_session, cs._get_session("http://example.com"))
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    @mock.patch("novaclient.client.requests.Session")
Jakub Ruzicka fcc2a65
+    @mock.patch("novaclient.client._ClientConnectionPool")
Jakub Ruzicka fcc2a65
+    def test_get_session_connection_pool(self, mock_pool, mock_session):
Jakub Ruzicka fcc2a65
+        service_url = "http://example.com"
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        pool = mock.MagicMock()
Jakub Ruzicka fcc2a65
+        pool.get.return_value = "http_adapter"
Jakub Ruzicka fcc2a65
+        mock_pool.return_value = pool
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "",
Jakub Ruzicka fcc2a65
+                                          "", connection_pool=True)
Jakub Ruzicka fcc2a65
+        cs._current_url = "http://another.com"
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        session = cs._get_session(service_url)
Jakub Ruzicka fcc2a65
+        self.assertEqual(session, mock_session.return_value)
Jakub Ruzicka fcc2a65
+        pool.get.assert_called_once_with(service_url)
Jakub Ruzicka fcc2a65
+        mock_session().mount.assert_called_once_with(service_url,
Jakub Ruzicka fcc2a65
+                                                     'http_adapter')
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def test_init_without_connection_pool(self):
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "", "")
Jakub Ruzicka fcc2a65
+        self.assertIsNone(cs._connection_pool)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    @mock.patch("novaclient.client._ClientConnectionPool")
Jakub Ruzicka fcc2a65
+    def test_init_with_proper_connection_pool(self, mock_pool):
Jakub Ruzicka fcc2a65
+        fake_pool = mock.Mock()
Jakub Ruzicka fcc2a65
+        mock_pool.return_value = fake_pool
Jakub Ruzicka fcc2a65
+        cs = novaclient.client.HTTPClient("user", None, "",
Jakub Ruzicka fcc2a65
+                                          connection_pool=True)
Jakub Ruzicka fcc2a65
+        self.assertEqual(cs._connection_pool, fake_pool)
Jakub Ruzicka fcc2a65
diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py
Jakub Ruzicka fcc2a65
index e2fc4fa..aaa3a46 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/tests/test_http.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/tests/test_http.py
Jakub Ruzicka fcc2a65
@@ -56,7 +56,7 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
     def test_get(self):
Jakub Ruzicka fcc2a65
         cl = get_authed_client()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         @mock.patch('time.time', mock.Mock(return_value=1234))
Jakub Ruzicka fcc2a65
         def test_get_call():
Jakub Ruzicka fcc2a65
             resp, body = cl.get("/hi")
Jakub Ruzicka fcc2a65
@@ -78,7 +78,7 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
     def test_post(self):
Jakub Ruzicka fcc2a65
         cl = get_authed_client()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_post_call():
Jakub Ruzicka fcc2a65
             cl.post("/hi", body=[1, 2, 3])
Jakub Ruzicka fcc2a65
             headers = {
Jakub Ruzicka fcc2a65
@@ -110,7 +110,7 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
     def test_connection_refused(self):
Jakub Ruzicka fcc2a65
         cl = get_client()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", refused_mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", refused_mock_request)
Jakub Ruzicka fcc2a65
         def test_refused_call():
Jakub Ruzicka fcc2a65
             self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi")
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
@@ -119,7 +119,7 @@ class ClientTest(utils.TestCase):
Jakub Ruzicka fcc2a65
     def test_bad_request(self):
Jakub Ruzicka fcc2a65
         cl = get_client()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", bad_req_mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", bad_req_mock_request)
Jakub Ruzicka fcc2a65
         def test_refused_call():
Jakub Ruzicka fcc2a65
             self.assertRaises(exceptions.BadRequest, cl.get, "/hi")
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py
Jakub Ruzicka fcc2a65
index 7344bc7..7877145 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/tests/v1_1/test_auth.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/tests/v1_1/test_auth.py
Jakub Ruzicka fcc2a65
@@ -57,7 +57,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock(return_value=(auth_response))
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             cs.client.authenticate()
Jakub Ruzicka fcc2a65
             headers = {
Jakub Ruzicka fcc2a65
@@ -160,7 +160,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock(side_effect=side_effect)
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             cs.client.authenticate()
Jakub Ruzicka fcc2a65
             headers = {
Jakub Ruzicka fcc2a65
@@ -248,7 +248,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock(side_effect=side_effect)
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             cs.client.authenticate()
Jakub Ruzicka fcc2a65
             headers = {
Jakub Ruzicka fcc2a65
@@ -373,7 +373,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock(return_value=(auth_response))
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        with mock.patch.object(requests.Session, "request", mock_request):
Jakub Ruzicka fcc2a65
+        with mock.patch.object(requests, "request", mock_request):
Jakub Ruzicka fcc2a65
             cs.client.authenticate()
Jakub Ruzicka fcc2a65
             headers = {
Jakub Ruzicka fcc2a65
                 'User-Agent': cs.client.USER_AGENT,
Jakub Ruzicka fcc2a65
@@ -432,7 +432,7 @@ class AuthenticationTests(utils.TestCase):
Jakub Ruzicka fcc2a65
         })
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock(return_value=(auth_response))
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             cs.client.authenticate()
Jakub Ruzicka fcc2a65
             headers = {
Jakub Ruzicka fcc2a65
@@ -460,7 +460,7 @@ class AuthenticationTests(utils.TestCase):
Jakub Ruzicka fcc2a65
         auth_response = utils.TestResponse({'status_code': 401})
Jakub Ruzicka fcc2a65
         mock_request = mock.Mock(return_value=(auth_response))
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
-        @mock.patch.object(requests.Session, "request", mock_request)
Jakub Ruzicka fcc2a65
+        @mock.patch.object(requests, "request", mock_request)
Jakub Ruzicka fcc2a65
         def test_auth_call():
Jakub Ruzicka fcc2a65
             self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py
Jakub Ruzicka fcc2a65
index efeb5c9..ce4aea1 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/v1_1/client.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/v1_1/client.py
Jakub Ruzicka fcc2a65
@@ -61,6 +61,20 @@ class Client(object):
Jakub Ruzicka fcc2a65
         >>> client.flavors.list()
Jakub Ruzicka fcc2a65
         ...
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
+    It is also possible to use an instance as a context manager in which
Jakub Ruzicka fcc2a65
+    case there will be a session kept alive for the duration of the with
Jakub Ruzicka fcc2a65
+    statement::
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client:
Jakub Ruzicka fcc2a65
+        ...     client.servers.list()
Jakub Ruzicka fcc2a65
+        ...     client.flavors.list()
Jakub Ruzicka fcc2a65
+        ...
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    It is also possible to have a permanent (process-long) connection pool,
Jakub Ruzicka fcc2a65
+    by passing a connection_pool=True::
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        >>> client = Client(USERNAME, PASSWORD, PROJECT_ID,
Jakub Ruzicka fcc2a65
+        ...     AUTH_URL, connection_pool=True)
Jakub Ruzicka fcc2a65
     """
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
     # FIXME(jesse): project_id isn't required to authenticate
Jakub Ruzicka fcc2a65
@@ -73,7 +87,8 @@ class Client(object):
Jakub Ruzicka fcc2a65
                   bypass_url=None, os_cache=False, no_cache=True,
Jakub Ruzicka fcc2a65
                   http_log_debug=False, auth_system='keystone',
Jakub Ruzicka fcc2a65
                   auth_plugin=None, auth_token=None,
Jakub Ruzicka fcc2a65
-                  cacert=None, tenant_id=None):
Jakub Ruzicka fcc2a65
+                  cacert=None, tenant_id=None,
Jakub Ruzicka fcc2a65
+                  connection_pool=False):
Jakub Ruzicka fcc2a65
         # FIXME(comstud): Rename the api_key argument above when we
Jakub Ruzicka fcc2a65
         # know it's not being used as keyword argument
Jakub Ruzicka fcc2a65
         password = api_key
Jakub Ruzicka fcc2a65
@@ -145,7 +160,15 @@ class Client(object):
Jakub Ruzicka fcc2a65
                                     bypass_url=bypass_url,
Jakub Ruzicka fcc2a65
                                     os_cache=self.os_cache,
Jakub Ruzicka fcc2a65
                                     http_log_debug=http_log_debug,
Jakub Ruzicka fcc2a65
-                                    cacert=cacert)
Jakub Ruzicka fcc2a65
+                                    cacert=cacert,
Jakub Ruzicka fcc2a65
+                                    connection_pool=connection_pool)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def __enter__(self):
Jakub Ruzicka fcc2a65
+        self.client.open_session()
Jakub Ruzicka fcc2a65
+        return self
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def __exit__(self, t, v, tb):
Jakub Ruzicka fcc2a65
+        self.client.close_session()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
     def set_management_url(self, url):
Jakub Ruzicka fcc2a65
         self.client.set_management_url(url)
Jakub Ruzicka fcc2a65
diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py
Jakub Ruzicka fcc2a65
index f26fdcf..214ba57 100644
Jakub Ruzicka fcc2a65
--- a/novaclient/v3/client.py
Jakub Ruzicka fcc2a65
+++ b/novaclient/v3/client.py
Jakub Ruzicka fcc2a65
@@ -47,6 +47,20 @@ class Client(object):
Jakub Ruzicka fcc2a65
         >>> client.flavors.list()
Jakub Ruzicka fcc2a65
         ...
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
+    It is also possible to use an instance as a context manager in which
Jakub Ruzicka fcc2a65
+    case there will be a session kept alive for the duration of the with
Jakub Ruzicka fcc2a65
+    statement::
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client:
Jakub Ruzicka fcc2a65
+        ...     client.servers.list()
Jakub Ruzicka fcc2a65
+        ...     client.flavors.list()
Jakub Ruzicka fcc2a65
+        ...
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    It is also possible to have a permanent (process-long) connection pool,
Jakub Ruzicka fcc2a65
+    by passing a connection_pool=True::
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+        >>> client = Client(USERNAME, PASSWORD, PROJECT_ID,
Jakub Ruzicka fcc2a65
+        ...     AUTH_URL, connection_pool=True)
Jakub Ruzicka fcc2a65
     """
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
     # FIXME(jesse): project_id isn't required to authenticate
Jakub Ruzicka fcc2a65
@@ -59,7 +73,8 @@ class Client(object):
Jakub Ruzicka fcc2a65
                   bypass_url=None, os_cache=False, no_cache=True,
Jakub Ruzicka fcc2a65
                   http_log_debug=False, auth_system='keystone',
Jakub Ruzicka fcc2a65
                   auth_plugin=None, auth_token=None,
Jakub Ruzicka fcc2a65
-                  cacert=None, tenant_id=None):
Jakub Ruzicka fcc2a65
+                  cacert=None, tenant_id=None,
Jakub Ruzicka fcc2a65
+                  connection_pool=False):
Jakub Ruzicka fcc2a65
         self.projectid = project_id
Jakub Ruzicka fcc2a65
         self.tenant_id = tenant_id
Jakub Ruzicka fcc2a65
         self.os_cache = os_cache or not no_cache
Jakub Ruzicka fcc2a65
@@ -110,7 +125,15 @@ class Client(object):
Jakub Ruzicka fcc2a65
                                     bypass_url=bypass_url,
Jakub Ruzicka fcc2a65
                                     os_cache=os_cache,
Jakub Ruzicka fcc2a65
                                     http_log_debug=http_log_debug,
Jakub Ruzicka fcc2a65
-                                    cacert=cacert)
Jakub Ruzicka fcc2a65
+                                    cacert=cacert,
Jakub Ruzicka fcc2a65
+                                    connection_pool=connection_pool)
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def __enter__(self):
Jakub Ruzicka fcc2a65
+        self.client.open_session()
Jakub Ruzicka fcc2a65
+        return self
Jakub Ruzicka fcc2a65
+
Jakub Ruzicka fcc2a65
+    def __exit__(self, t, v, tb):
Jakub Ruzicka fcc2a65
+        self.client.close_session()
Jakub Ruzicka fcc2a65
 
Jakub Ruzicka fcc2a65
     def set_management_url(self, url):
Jakub Ruzicka fcc2a65
         self.client.set_management_url(url)