]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
vmware: Use SessionIsActive to find stale session
authorVipin Balachandran <vbala@vmware.com>
Wed, 19 Mar 2014 12:12:56 +0000 (17:42 +0530)
committerVipin Balachandran <vbala@vmware.com>
Thu, 20 Mar 2014 06:53:12 +0000 (12:23 +0530)
An API invocation with a stale session returns an empty response. In order
to distinguish it from an API returning valid empty response, the session
is recreated and the API is retried. If an empty response is received even
after the retry, it can be assumed that the API response is actually empty.
But this behavior results in authentication error from the VMware server
when there is an active session and the API response is actually empty.
This change fix this behavior by using the SessionIsActive check to
identify stale session.

Change-Id: I6ddc4028bc5319cd22006de0590c13d6868c3494
Closes-Bug: #1284979

cinder/tests/test_vmware_api.py [new file with mode: 0644]
cinder/tests/test_vmware_vmdk.py
cinder/volume/drivers/vmware/api.py

diff --git a/cinder/tests/test_vmware_api.py b/cinder/tests/test_vmware_api.py
new file mode 100644 (file)
index 0000000..09aa536
--- /dev/null
@@ -0,0 +1,233 @@
+# Copyright (c) 2014 VMware, Inc.
+# All Rights Reserved.
+#
+#    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.
+
+"""
+Unit tests for session management and API invocation classes.
+"""
+
+from eventlet import greenthread
+import mock
+
+from cinder import test
+from cinder.volume.drivers.vmware import api
+from cinder.volume.drivers.vmware import error_util
+
+
+class RetryTest(test.TestCase):
+    """Tests for retry decorator class."""
+
+    def test_retry(self):
+        result = "RESULT"
+
+        @api.Retry()
+        def func(*args, **kwargs):
+            return result
+
+        self.assertEqual(result, func())
+
+        def func2(*args, **kwargs):
+            return result
+
+        retry = api.Retry()
+        self.assertEqual(result, retry(func2)())
+        self.assertTrue(retry._retry_count == 0)
+
+    def test_retry_with_expected_exceptions(self):
+        result = "RESULT"
+        responses = [error_util.SessionOverLoadException(None),
+                     error_util.SessionOverLoadException(None),
+                     result]
+
+        def func(*args, **kwargs):
+            response = responses.pop(0)
+            if isinstance(response, Exception):
+                raise response
+            return response
+
+        sleep_time_incr = 0.01
+        retry_count = 2
+        retry = api.Retry(10, sleep_time_incr, 10,
+                          (error_util.SessionOverLoadException,))
+        self.assertEqual(result, retry(func)())
+        self.assertTrue(retry._retry_count == retry_count)
+        self.assertEqual(retry_count * sleep_time_incr, retry._sleep_time)
+
+    def test_retry_with_max_retries(self):
+        responses = [error_util.SessionOverLoadException(None),
+                     error_util.SessionOverLoadException(None),
+                     error_util.SessionOverLoadException(None)]
+
+        def func(*args, **kwargs):
+            response = responses.pop(0)
+            if isinstance(response, Exception):
+                raise response
+            return response
+
+        retry = api.Retry(2, 0, 0, (error_util.SessionOverLoadException,))
+        self.assertRaises(error_util.SessionOverLoadException, retry(func))
+        self.assertTrue(retry._retry_count == 2)
+
+    def test_retry_with_unexpected_exception(self):
+
+        def func(*args, **kwargs):
+            raise error_util.VimException(None)
+
+        retry = api.Retry()
+        self.assertRaises(error_util.VimException, retry(func))
+        self.assertTrue(retry._retry_count == 0)
+
+
+class VMwareAPISessionTest(test.TestCase):
+    """Tests for VMwareAPISession."""
+
+    SERVER_IP = '10.1.2.3'
+    USERNAME = 'admin'
+    PASSWORD = 'password'
+
+    def setUp(self):
+        super(VMwareAPISessionTest, self).setUp()
+        patcher = mock.patch('cinder.volume.drivers.vmware.vim.Vim')
+        self.addCleanup(patcher.stop)
+        self.VimMock = patcher.start()
+        self.VimMock.side_effect = lambda *args, **kw: mock.Mock()
+
+    def _create_api_session(self, _create_session, retry_count=10,
+                            task_poll_interval=1):
+        return api.VMwareAPISession(VMwareAPISessionTest.SERVER_IP,
+                                    VMwareAPISessionTest.USERNAME,
+                                    VMwareAPISessionTest.PASSWORD,
+                                    retry_count,
+                                    task_poll_interval,
+                                    'https',
+                                    _create_session)
+
+    def test_create_session(self):
+        session = mock.Mock()
+        session.key = "12345"
+        api_session = self._create_api_session(False)
+        vim_obj = api_session.vim
+        vim_obj.Login.return_value = session
+        pbm_client = mock.Mock()
+        api_session._pbm = pbm_client
+
+        api_session.create_session()
+        session_manager = vim_obj.service_content.sessionManager
+        vim_obj.Login.assert_called_once_with(
+            session_manager, userName=VMwareAPISessionTest.USERNAME,
+            password=VMwareAPISessionTest.PASSWORD)
+        self.assertFalse(vim_obj.TerminateSession.called)
+        self.assertEqual(session.key, api_session._session_id)
+        pbm_client.set_cookie.assert_called_once_with()
+
+    def test_create_session_with_existing_session(self):
+        old_session_key = '12345'
+        new_session_key = '67890'
+        session = mock.Mock()
+        session.key = new_session_key
+        api_session = self._create_api_session(False)
+        api_session._session_id = old_session_key
+        vim_obj = api_session.vim
+        vim_obj.Login.return_value = session
+
+        api_session.create_session()
+        session_manager = vim_obj.service_content.sessionManager
+        vim_obj.Login.assert_called_once_with(
+            session_manager, userName=VMwareAPISessionTest.USERNAME,
+            password=VMwareAPISessionTest.PASSWORD)
+        vim_obj.TerminateSession.assert_called_once_with(
+            session_manager, sessionId=[old_session_key])
+        self.assertEqual(new_session_key, api_session._session_id)
+
+    def test_invoke_api(self):
+        api_session = self._create_api_session(True)
+        response = mock.Mock()
+
+        def api(*args, **kwargs):
+            return response
+
+        module = mock.Mock()
+        module.api = api
+        ret = api_session.invoke_api(module, 'api')
+        self.assertEqual(response, ret)
+
+    def test_invoke_api_with_expected_exception(self):
+        api_session = self._create_api_session(True)
+        ret = mock.Mock()
+        responses = [error_util.VimException(None), ret]
+
+        def api(*args, **kwargs):
+            response = responses.pop(0)
+            if isinstance(response, Exception):
+                raise response
+            return response
+
+        module = mock.Mock()
+        module.api = api
+        with mock.patch.object(greenthread, 'sleep'):
+            self.assertEqual(ret, api_session.invoke_api(module, 'api'))
+
+    def test_invoke_api_with_vim_fault_exception(self):
+        api_session = self._create_api_session(True)
+
+        def api(*args, **kwargs):
+            raise error_util.VimFaultException([], "error")
+
+        module = mock.Mock()
+        module.api = api
+        self.assertRaises(error_util.VimFaultException,
+                          lambda: api_session.invoke_api(module, 'api'))
+
+    def test_invoke_api_with_empty_response(self):
+        api_session = self._create_api_session(True)
+        vim_obj = api_session.vim
+        vim_obj.SessionIsActive.return_value = True
+
+        def api(*args, **kwargs):
+            raise error_util.VimFaultException(
+                [error_util.NOT_AUTHENTICATED], "error")
+
+        module = mock.Mock()
+        module.api = api
+        ret = api_session.invoke_api(module, 'api')
+        self.assertEqual([], ret)
+        vim_obj.SessionIsActive.assert_called_once_with(
+            vim_obj.service_content.sessionManager,
+            sessionID=api_session._session_id,
+            userName=api_session._session_username)
+
+    def test_invoke_api_with_stale_session(self):
+        api_session = self._create_api_session(True)
+        api_session.create_session = mock.Mock()
+        vim_obj = api_session.vim
+        vim_obj.SessionIsActive.return_value = False
+        result = mock.Mock()
+        responses = [error_util.VimFaultException(
+            [error_util.NOT_AUTHENTICATED], "error"), result]
+
+        def api(*args, **kwargs):
+            response = responses.pop(0)
+            if isinstance(response, Exception):
+                raise response
+            return response
+
+        module = mock.Mock()
+        module.api = api
+        ret = api_session.invoke_api(module, 'api')
+        self.assertEqual(result, ret)
+        vim_obj.SessionIsActive.assert_called_once_with(
+            vim_obj.service_content.sessionManager,
+            sessionID=api_session._session_id,
+            userName=api_session._session_username)
+        api_session.create_session.assert_called_once_with()
index 0864a66dc9285b3b422c967a811d58021a3edeb1..1bca1605ae09d8a3804a25e9ac7b6b4967d23764 100644 (file)
@@ -55,6 +55,9 @@ class FakeVim(object):
     def TerminateSession(self, session_manager, sessionId):
         pass
 
+    def SessionIsActive(self, session_manager, sessionID, userName):
+        pass
+
 
 class FakeTaskInfo(object):
     def __init__(self, state, result=None):
index ecb436d06c4776513c39a9d69f7c87a5bec2eeb2..68e6844fc97c7b1cd6de383b1e6b4522efd1cd4e 100644 (file)
@@ -123,6 +123,7 @@ class VMwareAPISession(object):
         self._task_poll_interval = task_poll_interval
         self._scheme = scheme
         self._session_id = None
+        self._session_username = None
         self._vim = None
         self._pbm_wsdl = pbm_wsdl
         self._pbm = None
@@ -168,6 +169,14 @@ class VMwareAPISession(object):
                 LOG.exception(_("Error while terminating session: %s.") %
                               excep)
         self._session_id = session.key
+
+        # We need to save the username in the session since we may need it
+        # later to check active session. The SessionIsActive method requires
+        # the username parameter to be exactly same as that in the session
+        # object. We can't use the username used for login since the Login
+        # method ignores the case.
+        self._session_username = session.userName
+
         if self.pbm:
             self.pbm.set_cookie()
         LOG.info(_("Successfully established connection to the server."))
@@ -206,7 +215,6 @@ class VMwareAPISession(object):
         @Retry(max_retry_count=self._api_retry_count,
                exceptions=(error_util.VimException))
         def _invoke_api(module, method, *args, **kwargs):
-            last_fault_list = []
             while True:
                 try:
                     api_method = getattr(module, method)
@@ -217,25 +225,54 @@ class VMwareAPISession(object):
                     # If it is a not-authenticated fault, we re-authenticate
                     # the user and retry the API invocation.
 
-                    # Because of the idle session returning an empty
-                    # RetrieveProperties response and also the same is
-                    # returned when there is an empty answer to a query
-                    # (e.g. no VMs on the host), we have no way to
-                    # differentiate.
-                    # So if the previous response was also an empty
-                    # response and after creating a new session, we get
-                    # the same empty response, then we are sure of the
-                    # response being an empty response.
-                    if error_util.NOT_AUTHENTICATED in last_fault_list:
+                    # The not-authenticated fault is set by the fault checker
+                    # due to an empty response. An empty response could be a
+                    # valid response; for e.g., response for the query to
+                    # return the VMs in an ESX server which has no VMs in it.
+                    # Also, the server responds with an empty response in the
+                    # case of an inactive session. Therefore, we need a way to
+                    # differentiate between these two cases.
+                    if self._is_current_session_active():
+                        LOG.debug(_("Returning empty response for "
+                                    "%(module)s.%(method)s invocation."),
+                                  {'module': module,
+                                   'method': method})
                         return []
-                    last_fault_list = excep.fault_list
-                    LOG.exception(_("Not authenticated error occurred. "
-                                    "Will create session and try "
-                                    "API call again: %s.") % excep)
+
+                    # empty response is due to an inactive session
+                    LOG.warn(_("Current session: %(session)s is inactive; "
+                               "re-creating the session while invoking "
+                               "method %(module)s.%(method)s."),
+                             {'session': self._session_id,
+                              'module': module,
+                              'method': method},
+                             exc_info=True)
                     self.create_session()
 
         return _invoke_api(module, method, *args, **kwargs)
 
+    def _is_current_session_active(self):
+        """Check if current session is active.
+
+        :returns: True if the session is active; False otherwise
+        """
+        LOG.debug(_("Checking if the current session: %s is active."),
+                  self._session_id)
+
+        is_active = False
+        try:
+            is_active = self.vim.SessionIsActive(
+                self.vim.service_content.sessionManager,
+                sessionID=self._session_id,
+                userName=self._session_username)
+        except error_util.VimException:
+            LOG.warn(_("Error occurred while checking whether the "
+                       "current session: %s is active."),
+                     self._session_id,
+                     exc_info=True)
+
+        return is_active
+
     def wait_for_task(self, task):
         """Return a deferred that will give the result of the given task.