From 926330d3726e9ce7001190730c69bf6ee45b422a Mon Sep 17 00:00:00 2001
From: Tim Burke <tim.burke@gmail.com>
Date: Thu, 14 Apr 2016 14:18:16 -0700
Subject: [PATCH] Propagate AttributeErrors when lazily loading plugins

Previously, if an AttributeError was raised in a plugin's make_client
method, the plugin simply wouldn't be an attribute of the ClientManager,
producing tracebacks like

Traceback (most recent call last):
  File ".../openstackclient/shell.py", line 118, in run
    ret_val = super(OpenStackShell, self).run(argv)

  ...

  File ".../openstackclient/object/v1/container.py", line 150, in take_action
    data = self.app.client_manager.object_store.container_list(
  File ".../openstackclient/common/clientmanager.py", line 66, in __getattr__
    raise AttributeError(name)
AttributeError: object_store

This made writing minimal third-party auth plugins difficult, as it
obliterated the original AttributeError.

Now, AttributeErrors that are raised during plugin initialization will
be re-raised as PluginAttributeErrors, and the original traceback will
be preserved. This gives much more useful information to plugin
developers, as in

Traceback (most recent call last):
  File ".../openstackclient/shell.py", line 118, in run
    ret_val = super(OpenStackShell, self).run(argv)

  ...

  File ".../openstackclient/object/v1/container.py", line 150, in take_action
    data = self.app.client_manager.object_store.container_list(
  File ".../openstackclient/common/clientmanager.py", line 57, in __get__
    err_val, err_tb)
  File ".../openstackclient/common/clientmanager.py", line 51, in __get__
    self._handle = self.factory(instance)
  File ".../openstackclient/object/client.py", line 35, in make_client
    interface=instance._interface,
  File ".../openstackclient/common/clientmanager.py", line 258,
  in get_endpoint_for_service_type
    endpoint = self.auth_ref.service_catalog.url_for(
PluginAttributeError: 'NoneType' object has no attribute 'url_for'

Change-Id: I0eee7eba6eccc6d471a699a381185c4e76da10bd
---
 openstackclient/common/clientmanager.py            | 10 +++++++++-
 openstackclient/common/exceptions.py               |  7 +++++++
 openstackclient/tests/common/test_clientmanager.py |  8 ++++++++
 3 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index 6d23b55e64..8b0fb921be 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -22,8 +22,10 @@ import sys
 
 from oslo_utils import strutils
 import requests
+import six
 
 from openstackclient.api import auth
+from openstackclient.common import exceptions
 from openstackclient.common import session as osc_session
 from openstackclient.identity import client as identity_client
 
@@ -45,7 +47,13 @@ class ClientCache(object):
     def __get__(self, instance, owner):
         # Tell the ClientManager to login to keystone
         if self._handle is None:
-            self._handle = self.factory(instance)
+            try:
+                self._handle = self.factory(instance)
+            except AttributeError as err:
+                # Make sure the failure propagates. Otherwise, the plugin just
+                # quietly isn't there.
+                new_err = exceptions.PluginAttributeError(err)
+                six.reraise(new_err.__class__, new_err, sys.exc_info()[2])
         return self._handle
 
 
diff --git a/openstackclient/common/exceptions.py b/openstackclient/common/exceptions.py
index 5f81e6a6e9..bdc33ddb54 100644
--- a/openstackclient/common/exceptions.py
+++ b/openstackclient/common/exceptions.py
@@ -24,6 +24,13 @@ class AuthorizationFailure(Exception):
     pass
 
 
+class PluginAttributeError(Exception):
+    """A plugin threw an AttributeError while being lazily loaded."""
+    # This *must not* inherit from AttributeError;
+    # that would defeat the whole purpose.
+    pass
+
+
 class NoTokenLookupException(Exception):
     """This does not support looking up endpoints from an existing token."""
     pass
diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py
index 6fc5b41e69..fa6c3fcc25 100644
--- a/openstackclient/tests/common/test_clientmanager.py
+++ b/openstackclient/tests/common/test_clientmanager.py
@@ -41,6 +41,7 @@ auth.get_options_list()
 
 class Container(object):
     attr = clientmanager.ClientCache(lambda x: object())
+    buggy_attr = clientmanager.ClientCache(lambda x: x.foo)
 
     def __init__(self):
         pass
@@ -72,6 +73,13 @@ class TestClientCache(utils.TestCase):
         c = Container()
         self.assertEqual(c.attr, c.attr)
 
+    def test_attribute_error_propagates(self):
+        c = Container()
+        err = self.assertRaises(exc.PluginAttributeError,
+                                getattr, c, 'buggy_attr')
+        self.assertNotIsInstance(err, AttributeError)
+        self.assertEqual("'Container' object has no attribute 'foo'", str(err))
+
 
 class TestClientManager(utils.TestCase):