Get the Satellite connection parameters from Heat
Change-Id: I1a66d2388f72b718169404232eac4861199f457c
This commit is contained in:
parent
6791631cd0
commit
af87a9a795
@ -1 +1,3 @@
|
|||||||
pbr>=0.6,!=0.7,<1.0
|
pbr>=0.6,!=0.7,<1.0
|
||||||
|
oauthlib>=0.7.2,<0.8
|
||||||
|
requests_oauthlib>=0.4.2,<0.5
|
||||||
|
@ -6,7 +6,7 @@ set -o errexit
|
|||||||
# Increment me any time the environment should be rebuilt.
|
# Increment me any time the environment should be rebuilt.
|
||||||
# This includes dependency changes, directory renames, etc.
|
# This includes dependency changes, directory renames, etc.
|
||||||
# Simple integer sequence: 1, 2, 3...
|
# Simple integer sequence: 1, 2, 3...
|
||||||
environment_version=42
|
environment_version=43
|
||||||
#--------------------------------------------------------#
|
#--------------------------------------------------------#
|
||||||
|
|
||||||
function usage {
|
function usage {
|
||||||
|
@ -12,14 +12,27 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
import collections
|
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
|
from horizon import tabs
|
||||||
import requests
|
import requests
|
||||||
|
import requests_oauthlib
|
||||||
|
from tuskar_ui import api
|
||||||
from tuskar_ui.infrastructure.nodes import tabs as nodes_tabs
|
from tuskar_ui.infrastructure.nodes import tabs as nodes_tabs
|
||||||
|
|
||||||
from tuskar_sat_ui.nodes import tables
|
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', [
|
ErrataItem = collections.namedtuple('ErrataItem', [
|
||||||
'title',
|
'title',
|
||||||
'type',
|
'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):
|
class DetailOverviewTab(nodes_tabs.DetailOverviewTab):
|
||||||
template_name = 'infrastructure/nodes/_detail_overview_sat.html'
|
template_name = 'infrastructure/nodes/_detail_overview_sat.html'
|
||||||
|
|
||||||
def get_context_data(self, request):
|
def get_context_data(self, request, **kwargs):
|
||||||
result = super(DetailOverviewTab, self).get_context_data(request)
|
context = super(DetailOverviewTab,
|
||||||
if result['node'].uuid is None:
|
self).get_context_data(request, **kwargs)
|
||||||
return result
|
if context['node'].uuid is None:
|
||||||
|
return context
|
||||||
|
|
||||||
# Some currently hardcoded values:
|
# TODO(rdopiera) We can probably get the stack from the context.
|
||||||
mac = '"52:54:00:4F:D8:65"' # Hardcode for now
|
stack = _get_stack(request)
|
||||||
host = 'http://sat-perf-04.idm.lab.bos.redhat.com' # Hardcode for now
|
if stack is None:
|
||||||
auth = ('admin', 'changeme')
|
return context
|
||||||
|
|
||||||
# Get the errata here
|
try:
|
||||||
host = host.strip('/') # Get rid of any trailing slash in the host url
|
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
|
addresses = context['node'].addresses
|
||||||
# need both MAC address and the interface, and we don't have the
|
try:
|
||||||
# interface, so we need to make multiple slow searches. If the
|
uuid = _find_uuid_by_mac(host, auth, organization, addresses)
|
||||||
# Satellite UUID isn't the same as this one, and it probably isn't we
|
except NodeNotFound:
|
||||||
# need to store a mapping somewhere.
|
return context
|
||||||
url = '{host}/katello/api/v2/systems'.format(host=host)
|
|
||||||
for interface in ['eth0', 'eth1', 'en0', 'en1']:
|
|
||||||
|
|
||||||
q = 'facts.net.interface.{iface}.mac_address:{mac}'.format(
|
# TODO(rdopiera) Should probably catch that requests exception here.
|
||||||
iface=interface, mac=mac)
|
try:
|
||||||
r = requests.get(url, params={'search': q}, auth=auth)
|
data = self._get_errata_data(host, auth, uuid)
|
||||||
results = r.json()['results']
|
except NoErrataError:
|
||||||
if results:
|
return context
|
||||||
break
|
context['errata'] = tables.ErrataTable(request, data=data)
|
||||||
else:
|
return context
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
class NodeDetailTabs(tabs.TabGroup):
|
class NodeDetailTabs(tabs.TabGroup):
|
||||||
|
98
tuskar_sat_ui/nodes/tests.py
Normal file
98
tuskar_sat_ui/nodes/tests.py
Normal 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')
|
Loading…
x
Reference in New Issue
Block a user