Reimplement the Satellite integration in Python

The AngularJS implementation proved hard to maintain, and brittle.
Angulars obtuse error messages did not help. Reimplementing this
in Python with standard Horizon tables proved faster than trying
to solve the issues with the JS implementation.

Change-Id: I3cd80d14505b278273b19bfd6b875326c3b72f6a
This commit is contained in:
Lennart Regebro 2014-11-19 19:24:36 +01:00 committed by Lennart Regebro
parent 05c3a99b3f
commit 67cf6e1c04
8 changed files with 132 additions and 364 deletions

View File

@ -18,6 +18,7 @@ import horizon
from tuskar_ui.infrastructure import dashboard from tuskar_ui.infrastructure import dashboard
from tuskar_ui.infrastructure.nodes.panel import Nodes as TuskarNodes from tuskar_ui.infrastructure.nodes.panel import Nodes as TuskarNodes
class Nodes(horizon.Panel): class Nodes(horizon.Panel):
name = _("Nodes") name = _("Nodes")
slug = "nodes" slug = "nodes"

View File

@ -0,0 +1,36 @@
# -*- 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.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
def get_errata_link(errata):
return "http://www.google.com"
class ErrataTable(tables.DataTable):
title = tables.Column('title',
link=get_errata_link,
verbose_name=_("Title"))
type = tables.Column('type',
verbose_name=_("Type"))
id = tables.Column('id',
verbose_name=_("Errata ID"))
issued = tables.Column('issued',
verbose_name=_("Date Issued"))
class Meta:
name = "erratatable"
verbose_name = _("Errata")
template = "horizon/common/_enhanced_data_table.html"

View File

@ -0,0 +1,76 @@
# -*- 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 requests
import urllib
from collections import namedtuple
from horizon import tabs
from tuskar_ui.infrastructure.nodes import tabs as nodes_tabs
from .tables import ErrataTable
ErrataItem = namedtuple('ErrataItem', ['title', 'type', 'id', 'issued'])
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
# 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')
# Get the errata here
host = host.strip('/') # Get rid of any trailing slash in the host url
# 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']:
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'] = ErrataTable(request, data=data)
return result
class NodeDetailTabs(tabs.TabGroup):
slug = "node_details"
tabs = (DetailOverviewTab,)

View File

@ -13,7 +13,8 @@
# under the License. # under the License.
from tuskar_ui.infrastructure.nodes import views from tuskar_ui.infrastructure.nodes import views
from tuskar_sat_ui.nodes import tabs
class DetailView(views.DetailView): class DetailView(views.DetailView):
template_name = 'infrastructure/nodes/sat_detail.html' tab_group_class = tabs.NodeDetailTabs

View File

@ -1,84 +0,0 @@
angular.module('hz').factory('Base64', function() {
var keyStr = 'ABCDEFGHIJKLMNOP' +
'QRSTUVWXYZabcdef' +
'ghijklmnopqrstuv' +
'wxyz0123456789+/' +
'=';
return {
encode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
do {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
keyStr.charAt(enc1) +
keyStr.charAt(enc2) +
keyStr.charAt(enc3) +
keyStr.charAt(enc4);
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i < input.length);
return output;
},
decode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
var base64test = /[^A-Za-z0-9\+\/\=]/g;
if (base64test.exec(input)) {
alert("There were invalid base64 characters in the input text.\n" +
"Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
"Expect errors in decoding.");
}
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
do {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i < input.length);
return output;
}
};
});

View File

@ -1,96 +0,0 @@
angular.module('hz').factory
('SatelliteErrata', ['$resource', 'Base64',
function ($resource, Base64) {
var getAuthToken = function(username, password) {
var tokenize = username + ':' + password;
tokenize = Base64.encode(tokenize);
return "Basic " + tokenize;
};
var SatelliteErrata = $resource('https://sat-perf-05.idm.lab.bos.redhat.com/katello/api/v2/systems/:id/errata', {id: '@uuid'}, {
get: {
method: 'GET',
isArray: false,
headers: { 'Authorization': getAuthToken('admin', 'changeme') }
}
}
);
return SatelliteErrata;
}]);
angular.module('hz').directive({
satelliteErrata: [ function () {
return {
restrict: 'A',
// require: '^file',
transclude: true,
scope: {
uuid: '='
},
controller: ['$scope', 'SatelliteErrata', '$http', 'Base64', 'ngTableParams', '$filter',
function ($scope, SatelliteErrata, $http, Base64, ngTableParams, $filter) {
var baseUrl = 'https://sat-perf-05.idm.lab.bos.redhat.com';
var defaultParams = function (data) {
var params = new ngTableParams({
page: 1, // show first page
count: 10 // count per page
}, {
total: data.length, // length of data
getData: function($defer, params) {
var filteredData = params.filter() ?
$filter('filter')(data, params.filter()) :
data;
var orderedData = params.sorting() ?
$filter('orderBy')(filteredData, params.orderBy()) :
data;
$defer.resolve(orderedData.slice((params.page() - 1) * params.count(), params.page() * params.count()));
}
});
return params;
};
$scope.errataLink = function (errata) {
return baseUrl + '/content_hosts/' + $scope.uuid + '/errata/' + errata.errata_id;
};
var payload = SatelliteErrata.get({id: $scope.uuid});
payload.$promise.then(
function() {
$scope.errata = payload.results
$scope.errataParams = defaultParams($scope.errata);
});
}],
template:
'<table ng-table="errataParams" class="table">\n' +
'<tr ng-repeat="e in $data">\n' +
'<td data-title="\'Title\'" sortable="\'title\'">\n' +
'<a ng-href="{$errataLink(e)$}">{{e.title}}</a>\n' +
'</td>\n' +
'<td data-title="\'Type\'" sortable="\'type\'">{{ e.type }}</td>\n' +
'<td data-title="\'Errata ID\'" sortable="\'type\'">{{ e.errata_id }}</td>\n' +
'<td data-title="\'Date Issued\'" sortable="\'issued\'">{{ e.issued }}</td>\n' +
'</tr>\n'+
'</table>\n',
link: function (scope, element, attrs, modelCtrl, transclude) {
scope.modelCtrl = modelCtrl;
scope.$transcludeFn = transclude;
}
};
}]
});
angular.module('hz').controller({
ErrataController: ['$scope',
function ($scope ) {
}]});

View File

@ -0,0 +1,17 @@
{% extends "infrastructure/nodes/_detail_overview.html" %}
{% load i18n %}
{% block additional_data %}
{% if node.uuid %}
<div>
<h3>{% trans "Errata" %}</h3>
{% if errata %}
{{ errata.render }}
{% else %}
<p>{% trans "No errata found" %}</p>
{% endif %}
</div>
{% endif %}
{% endblock %}

View File

@ -1,183 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block page_header %}
{% include 'horizon/common/_page_header.html' with title=title %}
{% endblock page_header %}
{% block js %}
{{ block.super }}
<script src='{{ STATIC_URL }}infrastructure/js/angular/horizon.base64.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}infrastructure/js/angular/horizon.node_errata.js' type='text/javascript' charset='utf-8'></script>
{% endblock %}
{% block main %}
<div class="node node-details">
<div class="node-title">
<dl>
<span class="powerstate">{% trans "Powered" %} {{ node.power_state|default:"&mdash;" }}</span>
</dl>
</div>
<div class="node node-deployment">
<h4>{% trans "Deployment" %}</h4>
<dl class="clearfix">
<dt>{% trans "Deployment Role" %}</dt>
{% if stack and role %}
<dd><a href="{% url 'horizon:infrastructure:overcloud:role' stack.id role.id %}">{{ role.name }}</a></dd>
{% else %}
<dd>&mdash;</dd>
{% endif %}
<dt>{% trans "Provisioning" %}</dt>
<dd>
{{ node.provisioning_status|default:"&mdash;" }}
{% if node.instance_uuid %}
<br />{{ node.instance.created }}
{% endif %}
</dd>
<dt>{% trans "Image" %}</dt>
<dd>{{ node.image_name|default:"&mdash;" }}</dd>
<dt>{% trans "Instance UUID" %}</dt>
<dd>{{ node.instance_uuid|default:"&mdash;" }}</dd>
</dl>
</div>
<div class="node node-detail">
<h4>{% trans "Inventory" %}</h4>
<dl>
<dt>{% trans "Node UUID" %}</dt>
<dd>{{ node.uuid|default:"&mdash;" }}</dd>
<dt>{% trans "Driver" %}</dt>
<dd class="dropdown">
<span class="dropdown-toggle" type="button" data-toggle="dropdown">
{{ node.driver|default:"&mdash;" }}
<span class="caret"></span>
</span>
<ul class="dropdown-menu">
<li><dt>IP Address</dt> <dd>{{ node.driver_info.ipmi_address|default:"&mdash;" }}</dd></li>
<li><dt>IPMI User</dt> <dd>{{ node.driver_info.ipmi_username|default:"&mdash;" }}</dd></li>
</ul>
</dd>
<dt>{% trans "Network Cards" %}</dt>
<dd class="dropdown">
<span class="dropdown-toggle" type="button" data-toggle="dropdown">
{{ node.addresses|length }}
<span class="caret"></span>
</span>
<ul class="dropdown-menu">
{% for address in node.addresses %}
<li>{{ address }}</li>
{% endfor %}
</ul>
</dd>
<dt>{% trans "Registered HW" %}</dt>
<dd>
{{ node.cpus|default:"&mdash;" }} {% trans "CPU" %}<br />
{{ node.memory_mb|default:"&mdash;" }} {% trans "RAM (MB)" %}<br />
{{ node.local_gb|default:"&mdash;" }} {% trans "HDD (GB)" %}
</dd>
</dl>
</div>
<div id="node-performance" class="node">
<h4>{% trans "Performance and Capacity" %}</h4>
{% if meter_conf %}
<br />
{% url 'horizon:infrastructure:nodes:performance' node.uuid as node_perf_url %}
<div id="ceilometer-stats" class="clearfix">
<form class="form-horizontal" id="linechart_general_form">
<div class="control-group">
<label for="date_options" class="control-label">{% trans "Period" %}:&nbsp;</label>
<div class="controls">
<select data-line-chart-command="select_box_change"
id="date_options"
name="date_options"
class="span2">
<option value="1" selected="selected">{% trans "Last day" %}</option>
<option value="7">{% trans "Last week" %}</option>
<option value="{% now 'j' %}">{% trans "Month to date" %}</option>
<option value="15">{% trans "Last 15 days" %}</option>
<option value="30">{% trans "Last 30 days" %}</option>
<option value="365">{% trans "Last year" %}</option>
<option value="other">{% trans "Other" %}</option>
</select>
</div>
</div>
<div class="control-group" id="date_from">
<label for="date_from" class="control-label">{% trans "From" %}:&nbsp;</label>
<div class="controls">
<input data-line-chart-command="date_picker_change"
type="text"
id="date_from"
name="date_from"
class="span2 example"/>
</div>
</div>
<div class="control-group" id="date_to">
<label for="date_to" class="control-label">{% trans "To" %}:&nbsp;</label>
<div class="controls">
<input data-line-chart-command="date_picker_change"
type="text"
name="date_to"
class="span2 example"/>
</div>
</div>
</form>
</div>
<script type="text/javascript">
if (typeof $ !== 'undefined') {
show_hide_datepickers();
} else {
addHorizonLoadEvent(function() {
show_hide_datepickers();
});
}
function show_hide_datepickers() {
var date_options = $("#date_options");
date_options.change(function(evt) {
if ($(this).find("option:selected").val() == "other"){
evt.stopPropagation();
$("#date_from .controls input, #date_to .controls input").val('');
$("#date_from, #date_to").show();
} else {
$("#date_from, #date_to").hide();
}
});
if (date_options.find("option:selected").val() == "other"){
$("#date_from, #date_to").show();
} else {
$("#date_from, #date_to").hide();
}
}
</script>
<div id="node-charts" class="clearfix">
{% for meter_label, url_part, y_max in meter_conf %}
<div class="span3">
{% include "infrastructure/_performance_chart.html" with label=meter_label y_max=y_max url=node_perf_url|add:"?"|add:url_part only %}
</div>
{% endfor %}
{% else %}
{% trans 'Metering service is not enabled.' %}
{% endif %}
</div>
</div>
{% if node.uuid %}
<br />
<br />
<div>
<h4>{% trans "Errata" %}</h4>
<div ng-controller="ErrataController">
<div satellite-errata uuid="'{{ node.uuid }}'"></div>
</div>
</div>
{% endif %}
{% endblock %}