From 8483e1b1398d9230aef5f1a9d27720780f8b4340 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Tue, 30 Jan 2018 08:22:47 -0600
Subject: [PATCH] Add OpenStackCloud object to Connection

Make a cloud attribute on Connection so that people with a Connection
can also use shade features.

This changes the default for shade's list_flavors to NOT fetching
extra_specs, which is very much yay.

Change-Id: I45a5f7f11a9c5ab3c77443a8f5df26089243334c
---
 openstack/cloud/openstackcloud.py             | 22 +++-------
 openstack/config/cloud_region.py              | 20 +++++----
 openstack/connection.py                       |  5 +++
 .../tests/functional/cloud/test_flavor.py     |  6 ++-
 openstack/tests/unit/cloud/test_caching.py    |  6 ---
 openstack/tests/unit/cloud/test_flavors.py    | 44 ++++++++++++++++++-
 6 files changed, 69 insertions(+), 34 deletions(-)

diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py
index c5dfe9d49..956514a04 100644
--- a/openstack/cloud/openstackcloud.py
+++ b/openstack/cloud/openstackcloud.py
@@ -50,7 +50,6 @@ from openstack.cloud import meta
 from openstack.cloud import _utils
 import openstack.config
 import openstack.config.defaults
-import openstack.connection
 from openstack import task_manager
 from openstack import utils
 
@@ -143,6 +142,7 @@ class OpenStackCloud(_normalize.Normalizer):
             app_name=None,
             app_version=None,
             use_direct_get=False,
+            conn=None,
             **kwargs):
 
         self.log = _log.setup_logging('openstack')
@@ -162,12 +162,6 @@ class OpenStackCloud(_normalize.Normalizer):
         self.secgroup_source = cloud_config.config['secgroup_source']
         self.force_ipv4 = cloud_config.force_ipv4
         self.strict_mode = strict
-        # TODO(shade) The openstack.cloud default for get_flavor_extra_specs
-        #             should be changed and this should be removed completely
-        self._extra_config = cloud_config._openstack_config.get_extra_config(
-            'shade', {
-                'get_flavor_extra_specs': True,
-            })
 
         if manager is not None:
             self.manager = manager
@@ -303,11 +297,14 @@ class OpenStackCloud(_normalize.Normalizer):
             _utils.localhost_supports_ipv6() if not self.force_ipv4 else False)
 
         self.cloud_config = cloud_config
-        self._conn_object = None
+        self._conn_object = conn
 
     @property
     def _conn(self):
         if not self._conn_object:
+            # Importing late to avoid import cycle. If the OpenStackCloud
+            # object comes via Connection, it'll have connection passed in.
+            import openstack.connection
             self._conn_object = openstack.connection.Connection(
                 config=self.cloud_config, session=self._keystone_session)
         return self._conn_object
@@ -1938,7 +1935,7 @@ class OpenStackCloud(_normalize.Normalizer):
         return ret
 
     @_utils.cache_on_arguments()
-    def list_flavors(self, get_extra=None):
+    def list_flavors(self, get_extra=False):
         """List all available flavors.
 
         :param get_extra: Whether or not to fetch extra specs for each flavor.
@@ -1948,8 +1945,6 @@ class OpenStackCloud(_normalize.Normalizer):
         :returns: A list of flavor ``munch.Munch``.
 
         """
-        if get_extra is None:
-            get_extra = self._extra_config['get_flavor_extra_specs']
         data = _adapter._json_response(
             self._conn.compute.get(
                 '/flavors/detail', params=dict(is_public='None')),
@@ -2986,7 +2981,7 @@ class OpenStackCloud(_normalize.Normalizer):
             self.search_flavors, get_extra=get_extra)
         return _utils._get_entity(self, search_func, name_or_id, filters)
 
-    def get_flavor_by_id(self, id, get_extra=True):
+    def get_flavor_by_id(self, id, get_extra=False):
         """ Get a flavor by ID
 
         :param id: ID of the flavor.
@@ -3002,9 +2997,6 @@ class OpenStackCloud(_normalize.Normalizer):
         flavor = self._normalize_flavor(
             self._get_and_munchify('flavor', data))
 
-        if get_extra is None:
-            get_extra = self._extra_config['get_flavor_extra_specs']
-
         if not flavor.extra_specs and get_extra:
             endpoint = "/flavors/{id}/os-extra_specs".format(
                 id=flavor.id)
diff --git a/openstack/config/cloud_region.py b/openstack/config/cloud_region.py
index 43042d558..422bee3bf 100644
--- a/openstack/config/cloud_region.py
+++ b/openstack/config/cloud_region.py
@@ -353,6 +353,7 @@ class CloudRegion(object):
     def get_cache_expiration_time(self):
         if self._openstack_config:
             return self._openstack_config.get_cache_expiration_time()
+        return 0
 
     def get_cache_path(self):
         if self._openstack_config:
@@ -361,6 +362,7 @@ class CloudRegion(object):
     def get_cache_class(self):
         if self._openstack_config:
             return self._openstack_config.get_cache_class()
+        return 'dogpile.cache.null'
 
     def get_cache_arguments(self):
         if self._openstack_config:
@@ -400,56 +402,56 @@ class CloudRegion(object):
     def get_external_networks(self):
         """Get list of network names for external networks."""
         return [
-            net['name'] for net in self.config['networks']
+            net['name'] for net in self.config.get('networks', [])
             if net['routes_externally']]
 
     def get_external_ipv4_networks(self):
         """Get list of network names for external IPv4 networks."""
         return [
-            net['name'] for net in self.config['networks']
+            net['name'] for net in self.config.get('networks', [])
             if net['routes_ipv4_externally']]
 
     def get_external_ipv6_networks(self):
         """Get list of network names for external IPv6 networks."""
         return [
-            net['name'] for net in self.config['networks']
+            net['name'] for net in self.config.get('networks', [])
             if net['routes_ipv6_externally']]
 
     def get_internal_networks(self):
         """Get list of network names for internal networks."""
         return [
-            net['name'] for net in self.config['networks']
+            net['name'] for net in self.config.get('networks', [])
             if not net['routes_externally']]
 
     def get_internal_ipv4_networks(self):
         """Get list of network names for internal IPv4 networks."""
         return [
-            net['name'] for net in self.config['networks']
+            net['name'] for net in self.config.get('networks', [])
             if not net['routes_ipv4_externally']]
 
     def get_internal_ipv6_networks(self):
         """Get list of network names for internal IPv6 networks."""
         return [
-            net['name'] for net in self.config['networks']
+            net['name'] for net in self.config.get('networks', [])
             if not net['routes_ipv6_externally']]
 
     def get_default_network(self):
         """Get network used for default interactions."""
-        for net in self.config['networks']:
+        for net in self.config.get('networks', []):
             if net['default_interface']:
                 return net['name']
         return None
 
     def get_nat_destination(self):
         """Get network used for NAT destination."""
-        for net in self.config['networks']:
+        for net in self.config.get('networks', []):
             if net['nat_destination']:
                 return net['name']
         return None
 
     def get_nat_source(self):
         """Get network used for NAT source."""
-        for net in self.config['networks']:
+        for net in self.config.get('networks', []):
             if net.get('nat_source'):
                 return net['name']
         return None
diff --git a/openstack/connection.py b/openstack/connection.py
index cc8bd33b4..a7cad826d 100644
--- a/openstack/connection.py
+++ b/openstack/connection.py
@@ -168,6 +168,7 @@ import six
 
 from openstack import _log
 from openstack import _meta
+from openstack import cloud as _cloud
 from openstack import config as _config
 from openstack.config import cloud_region
 from openstack import exceptions
@@ -311,6 +312,10 @@ class Connection(six.with_metaclass(_meta.ConnectionMeta)):
         self.session._sdk_connection = self
 
         self._proxies = {}
+        self.cloud = _cloud.OpenStackCloud(
+            cloud_config=self.config,
+            manager=self.task_manager,
+            conn=self)
 
     def add_service(self, service):
         """Add a service to the Connection.
diff --git a/openstack/tests/functional/cloud/test_flavor.py b/openstack/tests/functional/cloud/test_flavor.py
index 9bdcf8dd0..e4d4a7228 100644
--- a/openstack/tests/functional/cloud/test_flavor.py
+++ b/openstack/tests/functional/cloud/test_flavor.py
@@ -157,7 +157,8 @@ class TestFlavor(base.BaseFunctionalTestCase):
         # Now set them
         extra_specs = {'foo': 'aaa', 'bar': 'bbb'}
         self.operator_cloud.set_flavor_specs(new_flavor['id'], extra_specs)
-        mod_flavor = self.operator_cloud.get_flavor(new_flavor['id'])
+        mod_flavor = self.operator_cloud.get_flavor(
+            new_flavor['id'], get_extra=True)
 
         # Verify extra_specs were set
         self.assertIn('extra_specs', mod_flavor)
@@ -165,7 +166,8 @@ class TestFlavor(base.BaseFunctionalTestCase):
 
         # Unset the 'foo' value
         self.operator_cloud.unset_flavor_specs(mod_flavor['id'], ['foo'])
-        mod_flavor = self.operator_cloud.get_flavor_by_id(new_flavor['id'])
+        mod_flavor = self.operator_cloud.get_flavor_by_id(
+            new_flavor['id'], get_extra=True)
 
         # Verify 'foo' is unset and 'bar' is still set
         self.assertEqual({'bar': 'bbb'}, mod_flavor['extra_specs'])
diff --git a/openstack/tests/unit/cloud/test_caching.py b/openstack/tests/unit/cloud/test_caching.py
index bcfc7cd63..b1bcc38d8 100644
--- a/openstack/tests/unit/cloud/test_caching.py
+++ b/openstack/tests/unit/cloud/test_caching.py
@@ -437,12 +437,6 @@ class TestMemoryCache(base.RequestsMockTestCase):
             dict(method='GET', uri=mock_uri,
                  json={'flavors': fakes.FAKE_FLAVOR_LIST})
         ]
-        uris_to_mock.extend([
-            dict(method='GET',
-                 uri='{endpoint}/flavors/{id}/os-extra_specs'.format(
-                     endpoint=fakes.COMPUTE_ENDPOINT, id=flavor['id']),
-                 json={'extra_specs': {}})
-            for flavor in fakes.FAKE_FLAVOR_LIST])
 
         self.register_uris(uris_to_mock)
 
diff --git a/openstack/tests/unit/cloud/test_flavors.py b/openstack/tests/unit/cloud/test_flavors.py
index e23e67b1f..0fa5ae877 100644
--- a/openstack/tests/unit/cloud/test_flavors.py
+++ b/openstack/tests/unit/cloud/test_flavors.py
@@ -82,6 +82,30 @@ class TestFlavors(base.RequestsMockTestCase):
                           self.cloud.delete_flavor, 'vanilla')
 
     def test_list_flavors(self):
+        uris_to_mock = [
+            dict(method='GET',
+                 uri='{endpoint}/flavors/detail?is_public=None'.format(
+                     endpoint=fakes.COMPUTE_ENDPOINT),
+                 json={'flavors': fakes.FAKE_FLAVOR_LIST}),
+        ]
+        self.register_uris(uris_to_mock)
+
+        flavors = self.cloud.list_flavors()
+
+        # test that new flavor is created correctly
+        found = False
+        for flavor in flavors:
+            if flavor['name'] == 'vanilla':
+                found = True
+                break
+        self.assertTrue(found)
+        needed_keys = {'name', 'ram', 'vcpus', 'id', 'is_public', 'disk'}
+        if found:
+            # check flavor content
+            self.assertTrue(needed_keys.issubset(flavor.keys()))
+        self.assert_calls()
+
+    def test_list_flavors_with_extra(self):
         uris_to_mock = [
             dict(method='GET',
                  uri='{endpoint}/flavors/detail?is_public=None'.format(
@@ -96,7 +120,7 @@ class TestFlavors(base.RequestsMockTestCase):
             for flavor in fakes.FAKE_FLAVOR_LIST])
         self.register_uris(uris_to_mock)
 
-        flavors = self.cloud.list_flavors()
+        flavors = self.cloud.list_flavors(get_extra=True)
 
         # test that new flavor is created correctly
         found = False
@@ -238,6 +262,22 @@ class TestFlavors(base.RequestsMockTestCase):
         self.assert_calls()
 
     def test_get_flavor_by_id(self):
+        flavor_uri = '{endpoint}/flavors/1'.format(
+            endpoint=fakes.COMPUTE_ENDPOINT)
+        flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')}
+
+        self.register_uris([
+            dict(method='GET', uri=flavor_uri, json=flavor_json),
+        ])
+
+        flavor1 = self.cloud.get_flavor_by_id('1')
+        self.assertEqual('1', flavor1['id'])
+        self.assertEqual({}, flavor1.extra_specs)
+        flavor2 = self.cloud.get_flavor_by_id('1')
+        self.assertEqual('1', flavor2['id'])
+        self.assertEqual({}, flavor2.extra_specs)
+
+    def test_get_flavor_with_extra_specs(self):
         flavor_uri = '{endpoint}/flavors/1'.format(
             endpoint=fakes.COMPUTE_ENDPOINT)
         flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format(
@@ -250,7 +290,7 @@ class TestFlavors(base.RequestsMockTestCase):
             dict(method='GET', uri=flavor_extra_uri, json=flavor_extra_json),
         ])
 
-        flavor1 = self.cloud.get_flavor_by_id('1')
+        flavor1 = self.cloud.get_flavor_by_id('1', get_extra=True)
         self.assertEqual('1', flavor1['id'])
         self.assertEqual({'name': 'test'}, flavor1.extra_specs)
         flavor2 = self.cloud.get_flavor_by_id('1', get_extra=False)