diff --git a/sahara_dashboard/api/sahara.py b/sahara_dashboard/api/sahara.py index 4733010c..6813103c 100644 --- a/sahara_dashboard/api/sahara.py +++ b/sahara_dashboard/api/sahara.py @@ -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) diff --git a/sahara_dashboard/content/data_processing/clusters/clusters/tables.py b/sahara_dashboard/content/data_processing/clusters/clusters/tables.py index 4c2c7962..7819a6e1 100644 --- a/sahara_dashboard/content/data_processing/clusters/clusters/tables.py +++ b/sahara_dashboard/content/data_processing/clusters/clusters/tables.py @@ -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) diff --git a/sahara_dashboard/content/data_processing/clusters/clusters/tabs.py b/sahara_dashboard/content/data_processing/clusters/clusters/tabs.py index cb8832f9..6cc1c512 100644 --- a/sahara_dashboard/content/data_processing/clusters/clusters/tabs.py +++ b/sahara_dashboard/content/data_processing/clusters/clusters/tabs.py @@ -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 diff --git a/sahara_dashboard/content/data_processing/clusters/clusters/tests.py b/sahara_dashboard/content/data_processing/clusters/clusters/tests.py index 31da4e2a..a4dacf47 100644 --- a/sahara_dashboard/content/data_processing/clusters/clusters/tests.py +++ b/sahara_dashboard/content/data_processing/clusters/clusters/tests.py @@ -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): diff --git a/sahara_dashboard/content/data_processing/clusters/clusters/views.py b/sahara_dashboard/content/data_processing/clusters/clusters/views.py index ed4c1d9d..a5e17de1 100644 --- a/sahara_dashboard/content/data_processing/clusters/clusters/views.py +++ b/sahara_dashboard/content/data_processing/clusters/clusters/views.py @@ -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 = \ diff --git a/sahara_dashboard/content/data_processing/clusters/templates/clusters/_health_checks_table.html b/sahara_dashboard/content/data_processing/clusters/templates/clusters/_health_checks_table.html new file mode 100644 index 00000000..d02400f1 --- /dev/null +++ b/sahara_dashboard/content/data_processing/clusters/templates/clusters/_health_checks_table.html @@ -0,0 +1,24 @@ +{% load i18n %} + +

{% trans "Cluster health checks" %}

+ + + + + + + + + + + +
{% trans "Status" %}{% trans "Name" %}{% trans "Duration" %}{% trans "Description" %}
+ + diff --git a/sahara_dashboard/content/data_processing/clusters/templates/clusters/_health_status.html b/sahara_dashboard/content/data_processing/clusters/templates/clusters/_health_status.html new file mode 100644 index 00000000..36c0809c --- /dev/null +++ b/sahara_dashboard/content/data_processing/clusters/templates/clusters/_health_status.html @@ -0,0 +1,2 @@ +{{ status }} + diff --git a/sahara_dashboard/content/data_processing/clusters/urls.py b/sahara_dashboard/content/data_processing/clusters/urls.py index 7c36a78d..eb8e9ecf 100644 --- a/sahara_dashboard/content/data_processing/clusters/urls.py +++ b/sahara_dashboard/content/data_processing/clusters/urls.py @@ -93,6 +93,9 @@ urlpatterns = patterns('', url(r'^cluster/(?P[^/]+)/scale$', cluster_views.ScaleClusterView.as_view(), name='scale'), + url(r'^cluster/(?P[^/]+)/verifications$', + cluster_views.ClusterHealthChecksView.as_view(), + name='verifications'), url(r'^cluster/(?P[^/]+)/update_shares$', cluster_views.UpdateClusterSharesView.as_view(), name='update-shares'), diff --git a/sahara_dashboard/content/data_processing/jobs/job_templates/workflows/launch.py b/sahara_dashboard/content/data_processing/jobs/job_templates/workflows/launch.py index 7651a7e4..ffde62cb 100644 --- a/sahara_dashboard/content/data_processing/jobs/job_templates/workflows/launch.py +++ b/sahara_dashboard/content/data_processing/jobs/job_templates/workflows/launch.py @@ -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): diff --git a/sahara_dashboard/content/data_processing/jobs/templates/jobs/index.html b/sahara_dashboard/content/data_processing/jobs/templates/jobs/index.html index abbc82a5..895231d2 100644 --- a/sahara_dashboard/content/data_processing/jobs/templates/jobs/index.html +++ b/sahara_dashboard/content/data_processing/jobs/templates/jobs/index.html @@ -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(); diff --git a/sahara_dashboard/content/data_processing/static/dashboard/project/data_processing/data_processing.verifications.js b/sahara_dashboard/content/data_processing/static/dashboard/project/data_processing/data_processing.verifications.js new file mode 100644 index 00000000..d530c5ae --- /dev/null +++ b/sahara_dashboard/content/data_processing/static/dashboard/project/data_processing/data_processing.verifications.js @@ -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 = "" + + "" + + "%status%" + + "%name%" + + "%duration%" + + "%description%" + + ""; + + var status_template = "" + + "%status_text%"; + 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); + } + } + +}; diff --git a/sahara_dashboard/enabled/_1820_data_processing_clusters_panel.py b/sahara_dashboard/enabled/_1820_data_processing_clusters_panel.py index 1fe741b0..57d72ccc 100644 --- a/sahara_dashboard/enabled/_1820_data_processing_clusters_panel.py +++ b/sahara_dashboard/enabled/_1820_data_processing_clusters_panel.py @@ -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 = { diff --git a/sahara_dashboard/test/test_data/sahara_data.py b/sahara_dashboard/test/test_data/sahara_data.py index db9ca192..86e87ce3 100644 --- a/sahara_dashboard/test/test_data/sahara_data.py +++ b/sahara_dashboard/test/test_data/sahara_data.py @@ -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",