implement health checks in sahara-dashboard

following things are done in change:
 * action to verify cluster;
 * view to display health check in cluster details;
 * new column for the health overview in clusters list.

Implements blueprint: cluster-verification
Depends-On: I598753cbb33d7781b646e726ad8d614e924e4876
Change-Id: I6c47d564400872ec4f38bf9d5824f93f4599181c
This commit is contained in:
Vitaly Gridnev 2016-02-04 10:03:30 +03:00
parent bf067510da
commit 46d9adb665
13 changed files with 283 additions and 7 deletions

View File

@ -524,3 +524,7 @@ def job_execution_delete(request, jex_id):
def job_types_list(request):
return client(request).job_types.list()
def verification_update(request, cluster_id, status):
return client(request).clusters.verification_update(cluster_id, status)

View File

@ -77,6 +77,29 @@ class DeleteCluster(tables.DeleteAction):
saharaclient.cluster_delete(request, obj_id)
class CheckClusterAction(tables.BatchAction):
name = 'check_cluster'
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Start Verification",
u"Start Verifications",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Started Verification",
u"Started Verifications",
count
)
def action(self, request, datum_id):
saharaclient.verification_update(request, datum_id, status='START')
class UpdateClusterShares(tables.LinkAction):
name = "update_shares"
verbose_name = _("Update Shares")
@ -165,6 +188,22 @@ class MakeUnProtected(acl_utils.MakeUnProtected):
saharaclient.cluster_update(request, datum_id, **update_kwargs)
def get_health_status_info(cluster):
try:
return cluster.verification['status']
except (AttributeError, KeyError):
return 'UNKNOWN'
def get_health_filter(health):
mapper = {'GREEN': 'success', 'YELLOW': 'warning',
'RED': 'danger', 'CHECKING': 'info'}
label = mapper.get(health, 'default')
return render_to_string('clusters/_health_status.html',
{'status': health, 'label': label})
class ClustersTable(tables.DataTable):
name = tables.Column("name",
@ -184,6 +223,10 @@ class ClustersTable(tables.DataTable):
status=True,
filters=(rich_status_filter,))
health = tables.Column(get_health_status_info,
verbose_name=_("Health"),
filters=(get_health_filter,))
instances_count = tables.Column(get_instances_count,
verbose_name=_("Instances Count"))
@ -203,4 +246,5 @@ class ClustersTable(tables.DataTable):
row_actions = (ScaleCluster,
UpdateClusterShares,
DeleteCluster, MakePublic, MakePrivate,
MakeProtected, MakeUnProtected)
MakeProtected, MakeUnProtected,
CheckClusterAction)

View File

@ -229,8 +229,20 @@ class EventLogTab(tabs.Tab):
return kwargs
class HealthChecksTab(tabs.Tab):
name = _("Cluster health checks")
slug = 'cluster_health_checks'
template_name = "clusters/_health_checks_table.html"
def get_context_data(self, request, **kwargs):
cluster_id = self.tab_group.kwargs['cluster_id']
kwargs['cluster_id'] = cluster_id
kwargs['data_update_url'] = request.get_full_path()
return kwargs
class ClusterDetailsTabs(tabs.TabGroup):
slug = "cluster_details"
tabs = (GeneralTab, ClusterConfigsDetails, NodeGroupsTab, InstancesTab,
EventLogTab)
EventLogTab, HealthChecksTab)
sticky = True

View File

@ -73,6 +73,53 @@ class DataProcessingClusterTests(test.TestCase):
self.assertEqual(3, step_1["completed"])
self.assertEqual(0, len(step_1["events"]))
@test.create_stubs({api.sahara: ('cluster_get', )})
def test_health_checks_tab_sc1(self):
cluster = self.clusters.list()[-1]
api.sahara.cluster_get(IsA(http.HttpRequest),
"cl2").AndReturn(cluster)
self.mox.ReplayAll()
url = reverse(
'horizon:project:data_processing.clusters:verifications',
args=["cl2"])
res = self.client.get(url)
data = jsonutils.loads(res.content)
self.assertFalse(data['need_update'])
check0 = data['checks'][0]
check1 = data['checks'][1]
self.assertEqual('success', check0['label'])
self.assertEqual('danger', check1['label'])
self.assertEqual('GREEN', check0['status'])
self.assertEqual('RED', check1['status'])
self.assertEqual('0:07:40', check0['duration'])
@test.create_stubs({api.sahara: ('cluster_get', )})
def test_health_checks_tab_sc2(self):
cluster = self.clusters.list()[0]
cl1_id = 'ec9a0d28-5cfb-4028-a0b5-40afe23f1533'
api.sahara.cluster_get(IsA(http.HttpRequest),
cl1_id).AndReturn(cluster)
self.mox.ReplayAll()
url = reverse(
'horizon:project:data_processing.clusters:verifications',
args=[cl1_id])
res = self.client.get(url)
data = jsonutils.loads(res.content)
self.assertTrue(data['need_update'])
check0 = data['checks'][0]
check1 = data['checks'][1]
self.assertEqual('info', check0['label'])
self.assertEqual('danger', check1['label'])
self.assertEqual('CHECKING', check0['status'])
self.assertEqual('RED', check1['status'])
self.assertEqual('Houston, we have a problem', check1['description'])
@test.create_stubs({api.sahara: ('cluster_list',
'cluster_delete')})
def test_delete(self):

View File

@ -170,6 +170,49 @@ class ClusterEventsView(django_base.View):
content_type='application/json')
class ClusterHealthChecksView(django_base.View):
_date_format = "%Y-%m-%dT%H:%M:%S"
_status_in_progress = 'CHECKING'
def _get_checks(self, cluster):
try:
return cluster.verification['checks']
except (AttributeError, KeyError):
return []
def get(self, request, *args, **kwargs):
time_helpers = helpers.Helpers(request)
cluster_id = kwargs.get("cluster_id")
need_update, not_done_count, checks = False, 0, []
mapping_to_label_type = {'red': 'danger', 'yellow': 'warning',
'green': 'success', 'checking': 'info'}
try:
cluster = saharaclient.cluster_get(request, cluster_id)
for check in self._get_checks(cluster):
check['label'] = mapping_to_label_type.get(
check['status'].lower())
if not check['description']:
check['description'] = _("No description")
if check['status'] == self._status_in_progress:
not_done_count += 1
check['duration'] = time_helpers.get_duration(
check['created_at'], check['updated_at'])
checks.append(check)
except APIException:
need_update = False
checks = []
if not_done_count > 0:
need_update = True
context = {"checks": checks,
"need_update": need_update}
return HttpResponse(json.dumps(context),
content_type='application/json')
class CreateClusterView(workflows.WorkflowView):
workflow_class = create_flow.CreateCluster
success_url = \

View File

@ -0,0 +1,24 @@
{% load i18n %}
<h4>{% trans "Cluster health checks" %}</h4>
<table id="sahara_health_checks_table" class="table table-bordered datatable">
<thead>
<tr>
<th>{% trans "Status" %}</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
<tbody id="sahara_health_checks_body">
</tbody>
</table>
<script type="text/javascript">
var data_update_url = "{{ data_update_url }}"
$(function() {
// Initialize.
horizon.verifications.data_update_url = "{{ data_update_url }}"
horizon.verifications.update_health_checks();
});
</script>

View File

@ -0,0 +1,2 @@
<span class='label label-{{ label }}'>{{ status }}</span>

View File

@ -93,6 +93,9 @@ urlpatterns = patterns('',
url(r'^cluster/(?P<cluster_id>[^/]+)/scale$',
cluster_views.ScaleClusterView.as_view(),
name='scale'),
url(r'^cluster/(?P<cluster_id>[^/]+)/verifications$',
cluster_views.ClusterHealthChecksView.as_view(),
name='verifications'),
url(r'^cluster/(?P<cluster_id>[^/]+)/update_shares$',
cluster_views.UpdateClusterSharesView.as_view(),
name='update-shares'),

View File

@ -415,8 +415,7 @@ class NewClusterConfigAction(c_flow.GeneralConfigAction):
class Meta(object):
name = _("Configure Cluster")
help_text_template = (
"data_processing.clusters/_configure_general_help.html")
help_text_template = "clusters/_configure_general_help.html"
class ClusterGeneralConfig(workflows.Step):

View File

@ -102,7 +102,7 @@
var form = $(".hidden_create_field").closest("form");
var successful = false;
form.submit(function (e) {
var oldHref = $(".configure-nodegrouptemplate-btn")[0].href;
var oldHref = $(".create_cluster_btn")[0].href;
var plugin = $("#id_plugin_name option:selected").val();
var version = $("#id_" + plugin + "_version option:selected").val();
form.find(".close").click();

View File

@ -0,0 +1,58 @@
horizon.verifications = {
data_update_url: null,
update_health_checks: function() {
var url = this.data_update_url + "/verifications";
$.get(url).done(function(data) {
horizon.verifications.update_health_checks_view(data.checks);
horizon.verifications.schedule_next_update(data);
}).fail(function() {
horizon.alert("error", gettext("Verification is not available."));
});
},
update_health_checks_view: function(checks) {
// Clear health checks
$("#sahara_health_checks_body").find("tr").remove();
$(checks).each(function (i, check) {
horizon.verifications.create_check_row(check);
});
},
create_check_row: function(check) {
var check_row_template = "" +
"<tr id='%check_id%'>" +
"<td>%status%</td>" +
"<td>%name%</td>" +
"<td>%duration%</td>" +
"<td>%description%</td>" +
"</tr>";
var status_template = "" +
"<span class='label label-%label_type%'>%status_text%</span>";
var status = status_template
.replace(/%label_type%/g, check.label)
.replace(/%status_text%/g, check.status);
var row = check_row_template
.replace(/%check_id%/g, check.id)
.replace(/%name%/g, check.name)
.replace(/%duration%/g, check.duration)
.replace(/%status%/g, status)
.replace(/%description%/g, check.description);
$("#sahara_health_checks_body").append(row);
},
schedule_next_update: function(data) {
// 2-3 sec delay so that if there are multiple tabs polling the
// backend the requests are spread in time
var delay = 2000 + Math.floor((Math.random() * 1000) + 1);
if (data.need_update) {
setTimeout(function() {
horizon.verifications.update_health_checks(); }, delay);
}
}
};

View File

@ -29,7 +29,8 @@ ADD_INSTALLED_APPS = \
"sahara_dashboard.content.data_processing.clusters", ]
ADD_JS_FILES = [
'dashboard/project/data_processing/data_processing.event_log.js'
'dashboard/project/data_processing/data_processing.event_log.js',
'dashboard/project/data_processing/data_processing.verifications.js'
]
ADD_EXCEPTIONS = {

View File

@ -337,7 +337,27 @@ def data(TEST):
"updated_at": "2014-06-04T20:02:15",
"user_keypair_id": "stackboxkp"
}
cluster1_dict.update({
'verification': {
'status': 'CHECKING',
'checks': [
{
'status': 'CHECKING',
'name': "Stupid check",
'description': "Stupid description",
"created_at": "2015-03-27T15:51:54",
"updated_at": "2015-03-27T15:59:34",
},
{
'status': 'RED',
'name': "Stupid check",
'description': "Houston, we have a problem",
"created_at": "2015-03-27T15:51:54",
"updated_at": "2015-03-27T15:59:34",
},
]
}
})
cluster1 = clusters.Cluster(
clusters.ClusterManager(None), cluster1_dict)
TEST.clusters.add(cluster1)
@ -346,6 +366,25 @@ def data(TEST):
cluster2_dict.update({
"id": "cl2",
"name": "cl2_name",
'verification': {
'status': 'RED',
'checks': [
{
'status': 'GREEN',
'name': "Stupid check",
'description': "Stupid description",
"created_at": "2015-03-27T15:51:54",
"updated_at": "2015-03-27T15:59:34",
},
{
'status': 'RED',
'name': "Stupid check",
'description': "Houston, we have a problem",
"created_at": "2015-03-27T15:51:54",
"updated_at": "2015-03-27T15:59:34",
},
]
},
"provision_progress": [
{
"created_at": "2015-03-27T15:51:54",