From af87a9a795cd42c99ac9e0fb5bbb0f81ceb750ef Mon Sep 17 00:00:00 2001
From: Lennart Regebro <lregebro@redhat.com>
Date: Fri, 21 Nov 2014 16:42:14 +0100
Subject: [PATCH] Get the Satellite connection parameters from Heat

Change-Id: I1a66d2388f72b718169404232eac4861199f457c
---
 requirements.txt             |   2 +
 run_tests.sh                 |   2 +-
 tuskar_sat_ui/nodes/tabs.py  | 207 ++++++++++++++++++++++++++++-------
 tuskar_sat_ui/nodes/tests.py |  98 +++++++++++++++++
 4 files changed, 269 insertions(+), 40 deletions(-)
 create mode 100644 tuskar_sat_ui/nodes/tests.py

diff --git a/requirements.txt b/requirements.txt
index 7dfed9a..f32ad40 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,3 @@
 pbr>=0.6,!=0.7,<1.0
+oauthlib>=0.7.2,<0.8
+requests_oauthlib>=0.4.2,<0.5
diff --git a/run_tests.sh b/run_tests.sh
index 0e43740..f8b99fb 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -6,7 +6,7 @@ set -o errexit
 # Increment me any time the environment should be rebuilt.
 # This includes dependency changes, directory renames, etc.
 # Simple integer sequence: 1, 2, 3...
-environment_version=42
+environment_version=43
 #--------------------------------------------------------#
 
 function usage {
diff --git a/tuskar_sat_ui/nodes/tabs.py b/tuskar_sat_ui/nodes/tabs.py
index 80be470..bb033ea 100644
--- a/tuskar_sat_ui/nodes/tabs.py
+++ b/tuskar_sat_ui/nodes/tabs.py
@@ -12,14 +12,27 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 import collections
+import json
+import logging
 
+
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+import horizon.messages
 from horizon import tabs
 import requests
+import requests_oauthlib
+from tuskar_ui import api
 from tuskar_ui.infrastructure.nodes import tabs as nodes_tabs
 
 from tuskar_sat_ui.nodes import tables
 
 
+SAT_HOST_PARAM = 'SatelliteHost'
+SAT_AUTH_PARAM = 'SatelliteAuth'
+SAT_ORG_PARAM = 'SatelliteOrg'
+VERIFY_SSL = not getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
+LOG = logging.getLogger('tuskar_sat_ui')
 ErrataItem = collections.namedtuple('ErrataItem', [
     'title',
     'type',
@@ -28,52 +41,168 @@ ErrataItem = collections.namedtuple('ErrataItem', [
 ])
 
 
+class Error(Exception):
+    pass
+
+
+class NoConfigError(Error):
+    """Failed to find the Satellite configuration in Heat parameters."""
+
+    def __init__(self, param=None, *args, **kwargs):
+        super(NoConfigError, self).__init__(*args, **kwargs)
+        self.param = param
+
+
+class NodeNotFound(Error):
+    """Failed to find the Satellite node."""
+
+
+class BadAuthError(Error):
+    """Unknown authentication method for Satellite."""
+
+    def __init__(self, auth=None, *args, **kwargs):
+        super(BadAuthError, self).__init__(*args, **kwargs)
+        self.auth = auth
+
+
+class NoErrataError(Error):
+    """There is no errata for that node."""
+
+
+def _get_satellite_config(parameters):
+    """Find the Satellite configuration data.
+
+    The configuration data is store in Heat as parameters.  They may be
+    stored directly as Heat parameters, or in a the JSON structure stored
+    in ExtraConfig.
+    """
+
+    param = 'Satellite'
+    try:
+        config = parameters[param]
+    except KeyError:
+        try:
+            extra = json.loads(parameters['compute-1::ExtraConfig'])
+            config = extra[param]
+        except (KeyError, ValueError, TypeError):
+            raise NoConfigError(param, 'Parameter %r missing.' % param)
+
+    for param in [SAT_HOST_PARAM, SAT_AUTH_PARAM, SAT_ORG_PARAM]:
+        if param not in config:
+            raise NoConfigError(param, 'Parameter %r missing.' % param)
+
+    host = config[SAT_HOST_PARAM]
+    host = host.strip('/')  # Get rid of any trailing slash in the host url
+
+    try:
+        auth = config[SAT_AUTH_PARAM].split(':', 2)
+    except ValueError:
+        raise BadAuthError(auth=config[SAT_AUTH_PARAM])
+    if auth[0] == 'oauth':
+        auth = requests_oauthlib.OAuth1(auth[1], auth[2])
+    elif auth[0] == 'basic':
+        auth = auth[1], auth[2]
+    else:
+        raise BadAuthError(auth=auth[0])
+    organization = config[SAT_ORG_PARAM]
+    return host, auth, organization
+
+
+def _get_stack(request):
+    """Find the stack."""
+
+    # TODO(rdopiera) We probably should use the StackMixin instead.
+    try:
+        plan = api.tuskar.Plan.get_the_plan(request)
+        stack = api.heat.Stack.get_by_plan(request, plan)
+    except Exception as e:
+        LOG.exception(e)
+        horizon.messages.error(request, _("Could not retrieve errata."))
+        return None
+    return stack
+
+
+def _find_uuid_by_mac(host, auth, organization, addresses):
+    """Pick up the UUID from the MAC address.
+
+    This makes no sense, as we need both MAC address and the interface, and
+    we don't have the interface, so we need to make multiple slow searches.
+    If the Satellite UUID isn't the same as this one, and it probably
+    isn't, we need to store a mapping somewhere.
+    """
+
+    url = '{host}/katello/api/v2/systems'.format(host=host)
+    for mac in addresses:
+        for interface in ['eth0', 'eth1', 'en0', 'en1']:
+            q = 'facts.net.interface.{iface}.mac_address:{mac}'.format(
+                iface=interface, mac=mac)
+            params = {'search': q, 'organization_id': organization}
+            r = requests.get(url, params=params, auth=auth,
+                             verify=VERIFY_SSL)
+            r.raise_for_status()  # Raise an error if the request failed
+            contexts = r.json()['results']
+            if contexts:
+                return contexts[0]['uuid']
+    raise NodeNotFound()
+
+
+def _get_errata_data(self, host, auth, uuid):
+    """Get the errata here, while it's hot."""
+
+    url = '{host}/katello/api/v2/systems/{id}/errata'.format(host=host,
+                                                             id=uuid)
+    r = requests.get(url, auth=auth, verify=VERIFY_SSL)
+    r.raise_for_status()  # Raise an error if the request failed
+    errata = r.json()['contexts']
+    if not errata:
+        raise NoErrataError()
+    data = [ErrataItem(x['title'], x['type'], x['id'], x['issued'])
+            for x in errata]
+    return data
+
+
 class DetailOverviewTab(nodes_tabs.DetailOverviewTab):
     template_name = 'infrastructure/nodes/_detail_overview_sat.html'
 
-    def get_context_data(self, request):
-        result = super(DetailOverviewTab, self).get_context_data(request)
-        if result['node'].uuid is None:
-            return result
+    def get_context_data(self, request, **kwargs):
+        context = super(DetailOverviewTab,
+                        self).get_context_data(request, **kwargs)
+        if context['node'].uuid is None:
+            return context
 
-        # Some currently hardcoded values:
-        mac = '"52:54:00:4F:D8:65"'  # Hardcode for now
-        host = 'http://sat-perf-04.idm.lab.bos.redhat.com'  # Hardcode for now
-        auth = ('admin', 'changeme')
+        # TODO(rdopiera) We can probably get the stack from the context.
+        stack = _get_stack(request)
+        if stack is None:
+            return context
 
-        # Get the errata here
-        host = host.strip('/')  # Get rid of any trailing slash in the host url
+        try:
+            host, auth, organization = _get_satellite_config(stack.parameters)
+        except NoConfigError as e:
+            horizon.messages.error(request, _(
+                "No Satellite configuration found. "
+                "Missing parameter %r."
+            ) % e.param)
+            return context
+        except BadAuthError as e:
+            horizon.messages.error(request, _(
+                "Satellite configuration error, "
+                "unknown authentication method %r."
+            ) % e.auth)
+            return context
 
-        # Pick up the UUID from the MAC address This makes no sense, as we
-        # need both MAC address and the interface, and we don't have the
-        # interface, so we need to make multiple slow searches. If the
-        # Satellite UUID isn't the same as this one, and it probably isn't we
-        # need to store a mapping somewhere.
-        url = '{host}/katello/api/v2/systems'.format(host=host)
-        for interface in ['eth0', 'eth1', 'en0', 'en1']:
+        addresses = context['node'].addresses
+        try:
+            uuid = _find_uuid_by_mac(host, auth, organization, addresses)
+        except NodeNotFound:
+            return context
 
-            q = 'facts.net.interface.{iface}.mac_address:{mac}'.format(
-                iface=interface, mac=mac)
-            r = requests.get(url, params={'search': q}, auth=auth)
-            results = r.json()['results']
-            if results:
-                break
-        else:
-            # No node found
-            result['errata'] = None
-            return result
-
-        uuid = results[0]['uuid']
-        errata_url = '{host}/katello/api/v2/systems/{id}/errata'
-        r = requests.get(errata_url.format(host=host, id=uuid), auth=auth)
-        errata = r.json()['results']
-        if not errata:
-            result['errata'] = None
-        else:
-            data = [ErrataItem(x['title'], x['type'], x['id'], x['issued'])
-                    for x in errata]
-            result['errata'] = tables.ErrataTable(request, data=data)
-        return result
+        # TODO(rdopiera) Should probably catch that requests exception here.
+        try:
+            data = self._get_errata_data(host, auth, uuid)
+        except NoErrataError:
+            return context
+        context['errata'] = tables.ErrataTable(request, data=data)
+        return context
 
 
 class NodeDetailTabs(tabs.TabGroup):
diff --git a/tuskar_sat_ui/nodes/tests.py b/tuskar_sat_ui/nodes/tests.py
new file mode 100644
index 0000000..5a0d80f
--- /dev/null
+++ b/tuskar_sat_ui/nodes/tests.py
@@ -0,0 +1,98 @@
+# -*- coding: utf8 -*-
+#
+#    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.
+
+import json
+
+from tuskar_ui.test import helpers
+
+from tuskar_sat_ui.nodes import tabs
+
+
+class SatTests(helpers.BaseAdminViewTests):
+    def test_satellite_config_direct(self):
+        config = {
+            'Satellite': {
+                'SatelliteHost': 'http://example.com/',
+                'SatelliteAuth': 'basic:user:pass',
+                'SatelliteOrg': 'ACME',
+            },
+        }
+        host, auth, org = tabs._get_satellite_config(config)
+        self.assertEqual(host, 'http://example.com')
+        self.assertEqual(auth, ('user', 'pass'))
+        self.assertEqual(org, 'ACME')
+
+    def test_satellite_config_extra(self):
+        config = {
+            'compute-1::ExtraConfig': json.dumps({
+                'Satellite': {
+                    'SatelliteHost': 'http://example.com/',
+                    'SatelliteAuth': 'basic:user:pass',
+                    'SatelliteOrg': 'ACME',
+                }
+            }),
+        }
+        host, auth, org = tabs._get_satellite_config(config)
+        self.assertEqual(host, 'http://example.com')
+        self.assertEqual(auth, ('user', 'pass'))
+        self.assertEqual(org, 'ACME')
+
+    def test_satellite_config_missing_all(self):
+        config = {}
+        with self.assertRaises(tabs.NoConfigError) as e:
+            host, auth, org = tabs._get_satellite_config(config)
+        self.assertEqual(e.exception.param, 'Satellite')
+
+    def test_satellite_config_missing_one(self):
+        params = {
+            'SatelliteHost': 'http://example.com/',
+            'SatelliteAuth': 'basic:user:pass',
+            'SatelliteOrg': 'ACME',
+        }
+        for param in [
+            tabs.SAT_HOST_PARAM,
+            tabs.SAT_AUTH_PARAM,
+            tabs.SAT_ORG_PARAM,
+        ]:
+            broken_config = {
+                'Satellite': dict(kv for kv in params.items()
+                                  if kv[0] != param),
+            }
+            with self.assertRaises(tabs.NoConfigError) as e:
+                host, auth, org = tabs._get_satellite_config(broken_config)
+            self.assertEqual(e.exception.param, param)
+
+    def test_satellite_config_unknown_auth(self):
+        config = {
+            'Satellite': {
+                'SatelliteHost': 'http://example.com/',
+                'SatelliteAuth': 'bad:user:pass',
+                'SatelliteOrg': 'ACME',
+            },
+        }
+        with self.assertRaises(tabs.BadAuthError) as e:
+            host, auth, org = tabs._get_satellite_config(config)
+        self.assertEqual(e.exception.auth, 'bad')
+
+    def test_satellite_config_malformed_auth(self):
+        config = {
+            'Satellite': {
+                'SatelliteHost': 'http://example.com/',
+                'SatelliteAuth': 'bad',
+                'SatelliteOrg': 'ACME',
+            },
+        }
+        with self.assertRaises(tabs.BadAuthError) as e:
+            host, auth, org = tabs._get_satellite_config(config)
+        self.assertEqual(e.exception.auth, 'bad')