From af87a9a795cd42c99ac9e0fb5bbb0f81ceb750ef Mon Sep 17 00:00:00 2001 From: Lennart Regebro 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')