From 85e9f272bf14fd54ca62f3927fb8c6064f1a5673 Mon Sep 17 00:00:00 2001 From: Georgy Okrokvertskhov Date: Tue, 25 Feb 2014 20:43:39 -0800 Subject: [PATCH] Add Topology Tab for Services This makes the following changes on Environment->Service page: * Adds Tab for Services List inside Environment * Adds a new Tab for Topology Graph Added new API function to render d3 data for topology graph. Currently heat images are used. Change-Id: Icae70a3d2668a9b18d1c716d7a60ebdcf2faf5ea Implements: blueprint environment-topology-view --- muranodashboard/environments/api.py | 68 ++++ muranodashboard/environments/format.py | 86 +++++ muranodashboard/environments/tabs.py | 58 +++- muranodashboard/environments/urls.py | 7 +- muranodashboard/environments/views.py | 47 ++- .../js/horizon.muranotopology.js | 294 ++++++++++++++++++ .../templates/services/_application_info.html | 4 + .../templates/services/_detail_topology.html | 10 + .../templates/services/_environment_info.html | 2 + .../templates/services/_page_header.html | 2 + .../templates/services/_service_list.html | 5 + .../templates/services/_unit_info.html | 5 + muranodashboard/templates/services/index.html | 8 +- 13 files changed, 560 insertions(+), 36 deletions(-) create mode 100644 muranodashboard/environments/format.py create mode 100644 muranodashboard/static/muranodashboard/js/horizon.muranotopology.js create mode 100644 muranodashboard/templates/services/_application_info.html create mode 100644 muranodashboard/templates/services/_detail_topology.html create mode 100644 muranodashboard/templates/services/_environment_info.html create mode 100644 muranodashboard/templates/services/_service_list.html create mode 100644 muranodashboard/templates/services/_unit_info.html diff --git a/muranodashboard/environments/api.py b/muranodashboard/environments/api.py index 0b803b761..87279aefc 100644 --- a/muranodashboard/environments/api.py +++ b/muranodashboard/environments/api.py @@ -14,6 +14,7 @@ import logging import bunch +import json from django.conf import settings from horizon.exceptions import ServiceCatalogException from openstack_dashboard.api.base import url_for @@ -23,6 +24,8 @@ from muranoclient.common.exceptions import HTTPForbidden, HTTPNotFound from consts import STATUS_ID_READY, STATUS_ID_NEW from .network import get_network_params +from muranodashboard.environments import format + log = logging.getLogger(__name__) @@ -330,3 +333,68 @@ def get_deployment_descr(request, environment_id, deployment_id): request, service['type']) return descr return None + + +def load_environment_data(request, environment_id): + environment = environment_get(request, environment_id) + return render_d3_data(environment, environment.services) + + +def render_d3_data(environment, services): + ext_net_name = None + d3_data = {"nodes": [], "environment": {}} + if environment: + environment_image = '/static/dashboard/img/stack-green.svg' + in_progress, status_message = \ + format.get_environment_status_message(environment) + environment_node = format.create_empty_node() + environment_node['id'] = environment.id + environment_node['name'] = environment.name + environment_node['status'] = status_message + environment_node['image'] = environment_image + environment_node['in_progress'] = in_progress + environment_node['info_box'] = \ + format.environment_info(environment, status_message) + d3_data['environment'] = environment_node + + if services: + for service in services: + service_image = '/static/dashboard/img/stack-green.svg' + in_progress, status_message = \ + format.get_environment_status_message(service) + required_by = None + if service.get('assignFloatingIP', False): + if ext_net_name: + required_by = ext_net_name + else: + ext_net_name = 'External_Network' + d3_data['nodes'].append(format.create_ext_network_node( + ext_net_name)) + required_by = ext_net_name + service_node = format.create_empty_node() + service_node['name'] = service['name'] + service_node['status'] = status_message + service_node['image'] = service_image + service_node['link_type'] = "unit" + service_node['in_progress'] = in_progress + service_node['info_box'] = format.appication_info(service, + service_image, + status_message) + if required_by: + service_node['required_by'] = [required_by] + d3_data['nodes'].append(service_node) + + for unit in service['units']: + unit_image = '/static/dashboard/img/server-green.svg' + node = format.create_empty_node() + node['name'] = unit['name'] + node['id'] = unit['id'] + node['required_by'] = [service['name']] + node['flavor'] = service['flavor'] + node['info_box'] = \ + format.unit_info(service, unit, unit_image) + node['image'] = unit_image + node['link_type'] = "unit" + node['in_progress'] = in_progress + d3_data['nodes'].append(node) + return json.dumps(d3_data) diff --git a/muranodashboard/environments/format.py b/muranodashboard/environments/format.py new file mode 100644 index 000000000..7b60ac4f8 --- /dev/null +++ b/muranodashboard/environments/format.py @@ -0,0 +1,86 @@ +# 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.template.loader import render_to_string + + +def get_environment_status_message(entity): + try: + status = entity['status'] + except TypeError: + status = entity.status + + in_progress = True + if status in ('pending', 'ready'): + in_progress = False + if status == 'pending': + status_message = 'Waiting for deployment' + elif status == 'ready': + status_message = 'Deployed' + elif status == 'deploying': + status_message = 'Deployment is in progress' + return in_progress, status_message + + +def appication_info(application, app_image, status): + context = {} + context['name'] = application['name'] + context['type'] = application['type'] + context['status'] = status + context['app_image'] = app_image + return render_to_string('services/_application_info.html', + context) + + +def unit_info(service, unit, unit_image): + context = {} + context['name'] = unit['name'] + context['os'] = service['osImage']['type'] + context['image'] = service['osImage']['name'] + context['flavor'] = service['flavor'] + context['unit_image'] = unit_image + return render_to_string('services/_unit_info.html', + context) + + +def environment_info(environment, status): + context = {} + context['name'] = environment.name + context['status'] = status + return render_to_string('services/_environment_info.html', + context) + + +def create_empty_node(): + node = { + 'name': '', + 'status': 'ready', + 'image': '', + 'image_size': 60, + 'required_by': [], + 'image_x': -30, + 'image_y': -30, + 'text_x': 40, + 'text_y': ".35em", + 'link_type': "relation", + 'in_progress': False, + 'info_box': '' + } + return node + + +def create_ext_network_node(name): + node = create_empty_node() + node['name'] = name + node['image'] = '/static/dashboard/img/lb-green.svg' + node['link_type'] = "relation" + return node diff --git a/muranodashboard/environments/tabs.py b/muranodashboard/environments/tabs.py index 588c301b7..e9089b33d 100644 --- a/muranodashboard/environments/tabs.py +++ b/muranodashboard/environments/tabs.py @@ -14,17 +14,21 @@ import logging +from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.utils.datastructures import SortedDict +from horizon import exceptions from horizon import tabs -from muranodashboard.environments.consts import LOG_LEVEL_TO_COLOR -from muranodashboard.environments.consts import LOG_LEVEL_TO_TEXT + from openstack_dashboard.api import nova as nova_api from openstack_dashboard.api import heat as heat_api +from muranoclient.common import exceptions as exc from muranodashboard.environments import api +from muranodashboard.environments.consts import LOG_LEVEL_TO_COLOR +from muranodashboard.environments.consts import LOG_LEVEL_TO_TEXT from muranodashboard.environments.tables import STATUS_DISPLAY_CHOICES -from muranodashboard.environments.tables import EnvConfigTable +from muranodashboard.environments.tables import EnvConfigTable, ServicesTable LOG = logging.getLogger(__name__) @@ -166,6 +170,54 @@ class EnvConfigTab(tabs.TableTab): return deployment.get('services') +class EnvironmentTopologyTab(tabs.Tab): + name = _("Topology") + slug = "topology" + template_name = "services/_detail_topology.html" + preload = False + + def get_context_data(self, request): + context = {} + environment_id = self.tab_group.kwargs['environment_id'] + context['environment_id'] = environment_id + d3_data = api.load_environment_data(self.request, environment_id) + context['d3_data'] = d3_data + return context + + +class EnvironmentServicesTab(tabs.TableTab): + name = _("Services") + slug = "serviceslist" + table_classes = (ServicesTable,) + template_name = "services/_service_list.html" + preload = False + + def get_services_data(self): + services = [] + self.environment_id = self.tab_group.kwargs['environment_id'] + ns_url = "horizon:murano:environments:index" + try: + services = api.services_list(self.request, self.environment_id) + except exc.HTTPForbidden: + msg = _('Unable to retrieve list of services. This environment ' + 'is deploying or already deployed by other user.') + exceptions.handle(self.request, msg, redirect=reverse(ns_url)) + + except (exc.HTTPInternalServerError, exc.HTTPNotFound): + msg = _("Environment with id %s doesn't exist anymore" + % self.environment_id) + exceptions.handle(self.request, msg, redirect=reverse(ns_url)) + except exc.HTTPUnauthorized: + exceptions.handle(self.request) + + return services + + +class EnvironmentDetailsTabs(tabs.TabGroup): + slug = "environemnt_details" + tabs = (EnvironmentServicesTab, EnvironmentTopologyTab) + + class ServicesTabs(tabs.TabGroup): slug = "services_details" tabs = (OverviewTab, ServiceLogsTab) diff --git a/muranodashboard/environments/urls.py b/muranodashboard/environments/urls.py index ceb2717b4..eb40bdb36 100644 --- a/muranodashboard/environments/urls.py +++ b/muranodashboard/environments/urls.py @@ -15,7 +15,7 @@ from django.conf.urls import patterns, url from views import IndexView, DeploymentDetailsView -from views import Services +from views import JSONView, EnvironmentDetails from views import CreateEnvironmentView from views import DetailServiceView from views import DeploymentsView @@ -50,9 +50,12 @@ urlpatterns = patterns( EditEnvironmentView.as_view(), name='update_environment'), - url(ENVIRONMENT_ID + r'/services$', Services.as_view(), + url(ENVIRONMENT_ID + r'/services$', EnvironmentDetails.as_view(), name='services'), + url(ENVIRONMENT_ID + r'/services/get_d3_data$', + JSONView.as_view(), name='d3_data'), + url(ENVIRONMENT_ID + r'/(?P[^/]+)/$', DetailServiceView.as_view(), name='service_details'), diff --git a/muranodashboard/environments/views.py b/muranodashboard/environments/views.py index a989342d0..559a0c2e9 100644 --- a/muranodashboard/environments/views.py +++ b/muranodashboard/environments/views.py @@ -21,7 +21,9 @@ from functools import update_wrapper from django.core.urlresolvers import reverse, reverse_lazy from django.utils.translation import ugettext_lazy as _ from django.contrib.formtools.wizard.views import SessionWizardView -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect # noqa +from django.http import HttpResponse # noqa +from django.views import generic from horizon import exceptions from horizon import tabs from horizon import tables @@ -33,7 +35,7 @@ from tables import ServicesTable from tables import DeploymentsTable from tables import EnvConfigTable from workflows import CreateEnvironment, UpdateEnvironment -from tabs import ServicesTabs, DeploymentTabs +from tabs import ServicesTabs, DeploymentTabs, EnvironmentDetailsTabs from . import api from muranoclient.common.exceptions import HTTPUnauthorized, \ CommunicationError, HTTPInternalServerError, HTTPForbidden, HTTPNotFound @@ -193,46 +195,24 @@ class IndexView(tables.DataTableView): return environments -class Services(tables.DataTableView): - table_class = ServicesTable +class EnvironmentDetails(tabs.TabbedTableView): + tab_group_class = EnvironmentDetailsTabs template_name = 'services/index.html' def get_context_data(self, **kwargs): - context = super(Services, self).get_context_data(**kwargs) + context = super(EnvironmentDetails, self).get_context_data(**kwargs) try: + self.environment_id = self.kwargs['environment_id'] env = api.environment_get(self.request, self.environment_id) context['environment_name'] = env.name except: - msg = _("Sorry, this environment does't exist anymore") + msg = _("Sorry, this environment doesn't exist anymore") redirect = reverse("horizon:murano:environments:index") exceptions.handle(self.request, msg, redirect=redirect) return context - def get_data(self): - services = [] - self.environment_id = self.kwargs['environment_id'] - ns_url = "horizon:murano:environments:index" - try: - services = api.services_list(self.request, self.environment_id) - except HTTPForbidden: - msg = _('Unable to retrieve list of services. This environment ' - 'is deploying or already deployed by other user.') - exceptions.handle(self.request, msg, redirect=reverse(ns_url)) - - except HTTPInternalServerError: - msg = _("Environment with id %s doesn't exist anymore" - % self.environment_id) - exceptions.handle(self.request, msg, redirect=reverse(ns_url)) - except HTTPUnauthorized: - exceptions.handle(self.request) - except HTTPNotFound: - msg = _("Environment with id %s doesn't exist anymore" - % self.environment_id) - exceptions.handle(self.request, msg, redirect=reverse(ns_url)) - return services - class DetailServiceView(tabs.TabView): tab_group_class = ServicesTabs @@ -396,3 +376,12 @@ class DeploymentDetailsView(tabs.TabbedTableView): return self.tab_group_class(request, deployment=deployment, logs=logs, **kwargs) + + +class JSONView(generic.View): + + def get(self, request, **kwargs): + self.environment_id = kwargs['environment_id'] + data = api.load_environment_data(request, self.environment_id) + return HttpResponse(data, + content_type="application/json") diff --git a/muranodashboard/static/muranodashboard/js/horizon.muranotopology.js b/muranodashboard/static/muranodashboard/js/horizon.muranotopology.js new file mode 100644 index 000000000..d4465dae1 --- /dev/null +++ b/muranodashboard/static/muranodashboard/js/horizon.muranotopology.js @@ -0,0 +1,294 @@ +/** + * Adapted for Murano js topology generator. + * Based on: + * HeatTop JS Framework + * Dependencies: jQuery 1.7.1 or later, d3 v3 or later + * Date: June 2013 + * Description: JS Framework that subclasses the D3 Force Directed Graph library to create + * Heat-specific objects and relationships with the purpose of displaying + * Stacks, Resources, and related Properties in a Resource Topology Graph. + * + * 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. + */ + +var murano_container = "#murano_application_topology"; + +var diagonal = d3.svg.diagonal() + .projection(function(d) { return [d.y, d.x]; }); + +function update(){ + node = node.data(nodes, function(d) { return d.name; }); + link = link.data(links); + + var nodeEnter = node.enter().append("g") + .attr("class", "node") + .attr("node_name", function(d) { return d.name; }) + .attr("node_id", function(d) { return d.instance; }) + .call(force.drag); + + nodeEnter.append("image") + .attr("xlink:href", function(d) { return d.image; }) + .attr("id", function(d){ return "image_"+ d.name; }) + .attr("x", function(d) { return d.image_x; }) + .attr("y", function(d) { return d.image_y; }) + .attr("width", function(d) { return d.image_size; }) + .attr("height", function(d) { return d.image_size; }); + node.exit().remove(); + + link.enter().insert("path", "g.node") + .attr("class", function(d) { return "link " + d.link_type; }); + + link.exit().remove(); + //Setup click action for all nodes + node.on("mouseover", function(d) { + $("#info_box").html(d.info_box); + current_info = d.name; + }); + node.on("mouseout", function(d) { + $("#info_box").html(''); + }); + + force.start(); +} + +function tick() { + link.attr("d", linkArc); + node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); +} + +function linkArc(d) { + var dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy); + return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; +} + + +function set_in_progress(stack, nodes) { + if (stack.in_progress === true) { in_progress = true; } + for (var i = 0; i < nodes.length; i++) { + var d = nodes[i]; + if (d.in_progress === true){ in_progress = true; return false; } + } +} + +function findNode(name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].name === name){ return nodes[i]; } + } +} + +function findNodeIndex(name) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].name === name){ return i; } + } +} + +function addNode (node) { + nodes.push(node); + needs_update = true; +} + +function removeNode (name) { + var i = 0; + var n = findNode(name); + while (i < links.length) { + if (links[i].source === n || links[i].target === n) { + links.splice(i, 1); + } else { + i++; + } + } + nodes.splice(findNodeIndex(name),1); + needs_update = true; +} + +function remove_nodes(old_nodes, new_nodes){ + //Check for removed nodes + for (var i=0;i +

Name: {{ name }}

+

Type: {{ type }}

+

Status: {{ status }}

\ No newline at end of file diff --git a/muranodashboard/templates/services/_detail_topology.html b/muranodashboard/templates/services/_detail_topology.html new file mode 100644 index 000000000..ffc9bd042 --- /dev/null +++ b/muranodashboard/templates/services/_detail_topology.html @@ -0,0 +1,10 @@ +{% load i18n sizeformat %} +{% load static %} +
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/muranodashboard/templates/services/_environment_info.html b/muranodashboard/templates/services/_environment_info.html new file mode 100644 index 000000000..d469d8fea --- /dev/null +++ b/muranodashboard/templates/services/_environment_info.html @@ -0,0 +1,2 @@ +

Environment: {{ name }}

+

Status: {{ status }}

\ No newline at end of file diff --git a/muranodashboard/templates/services/_page_header.html b/muranodashboard/templates/services/_page_header.html index b1850bc6d..032415fb0 100644 --- a/muranodashboard/templates/services/_page_header.html +++ b/muranodashboard/templates/services/_page_header.html @@ -1,6 +1,8 @@ {% load i18n %} {% load url from future %} +{% load static %} {% block page_header %} +