Get the Satellite connection parameters from Heat

Change-Id: I1a66d2388f72b718169404232eac4861199f457c
This commit is contained in:
Lennart Regebro 2014-11-21 16:42:14 +01:00
parent 6791631cd0
commit af87a9a795
4 changed files with 269 additions and 40 deletions

View File

@ -1 +1,3 @@
pbr>=0.6,!=0.7,<1.0
oauthlib>=0.7.2,<0.8
requests_oauthlib>=0.4.2,<0.5

View File

@ -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 {

View File

@ -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):

View File

@ -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')