diff --git a/horizon/static/horizon/js/horizon.networktopology.js b/horizon/static/horizon/js/horizon.networktopology.js new file mode 100644 index 000000000..9007ddb84 --- /dev/null +++ b/horizon/static/horizon/js/horizon.networktopology.js @@ -0,0 +1,255 @@ +/* Namespace for core functionality related to Network Topology. */ +horizon.network_topology = { + model: null, + network_margin: 270, + min_network_height:500, + port_margin: 20, + device_initial_position : 40, + device_last_position : 0, + device_left_position : 90, + device_margin : 20, + device_min_height : 45, + port_initial_position: 1, + network_index: {}, + network_color_unit: 0, + network_saturation: 1, + network_lightness: 0.7, + reload_duration: 10000, + spinner:null, + init:function(){ + var self = this; + $("#topologyCanvas").spin(horizon.conf.spinner_options.modal); + self.retrieve_network_info(); + setInterval(horizon.network_topology.retrieve_network_info, + horizon.network_topology.reload_duration); + }, + retrieve_network_info: function(){ + var self = this; + if(!$("#networktopology")) { + return; + } + $.getJSON($("#networktopology").data('networktopology'), + function(data) { + self.draw_graph(data); + } + ); + }, + draw_loading: function () { + $("#topologyCanvas").spin(horizon.conf.spinner_options.modal); + }, + draw_graph: function(data){ + var canvas = $("#topologyCanvas"); + var networks = $("#topologyCanvas > .networks"); + canvas.spin(false); + networks.empty(); + this.model = data; + this.device_last_position = this.device_initial_position; + this.draw_networks(); + this.draw_routers(); + this.draw_servers(); + canvas.height( + Math.max(this.device_last_position,this.min_network_height) + ); + networks.width( + this.model.networks.length * this.network_margin + ); + }, + network_color: function(network_id){ + var max_hue = 360; + var num_network = this.model.networks.length; + if(num_network <= 0){ + return; + } + num_network ++; + var hue = Math.floor( + max_hue/num_network*(this.network_index(network_id) + 1)); + return this.hsv2rgb( + hue, this.network_saturation, this.network_lightness); + }, + //see http://en.wikipedia.org/wiki/HSL_and_HSV + hsv2rgb:function (h, s, v) { + var hi = Math.round(h/60) % 6; + var f = h/60 - hi; + var p = v*(1 - s); + var q = v*(1 - f*s); + var t = v*(1 - (1 - f)*s); + switch(hi){ + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; + } + return "rgb(" + Math.round(r*255) + "," + Math.round(g*255) + "," + Math.round(b*255) + ")"; + }, + draw_networks: function(){ + var self = this; + var networks = $("#topologyCanvas > .networks"); + $.each(self.model.networks, function(index, network){ + var label = (network.name != "")? network.name : network.id; + if(network['router:external']){ + label += " (external) "; + } + label += self.select_cidr(network.id); + self.network_index[network.id] = index; + var network_html = $("
").attr("id", network.id); + var nicname_html = $("

" + label + "

"); + nicname_html + .click(function (){ + window.location.href = network.url;}) + .css ( + {'background-color':self.network_color(network.id)}) + .appendTo(network_html); + networks.append(network_html); + }); + }, + select_cidr:function(network_id){ + var cidr = ""; + $.each(this.model.subnets, function(index, subnet){ + if(subnet.network_id != network_id){ + return; + } + cidr += " [ " + subnet.cidr + " ]"; + }); + return cidr; + }, + draw_devices: function(type){ + var self = this; + $.each(self.model[type + 's'], function(index, device){ + var id = device.id; + var name = (device.name != "")? device.name : device.id; + var ports = self.select_port(id); + if(ports.length <= 0){ + return; + } + var main_port = self.select_main_port(ports); + var parent_network = main_port.network_id; + var device_html = $("
"); + device_html + .attr('id', device.id) + .css({top: self.device_last_position, position: 'absolute'}) + .append($("" + type + "")) + .click(function (e){ + e.stopPropagation(); + window.location.href = device.url; + }); + var name_html = $("") + .html(device.name) + .attr('title', device.name) + .appendTo(device_html); + var port_position = self.port_initial_position; + $.each(ports, function(){ + var port = this; + var port_html = self.port_html(port); + port_position += self.port_margin; + self.port_css(port_html, port_position, parent_network, port.network_id); + device_html.append(port_html); + }); + port_position += self.port_margin; + device_html.css( + {height: Math.max(self.device_min_height, port_position) + "px"}); + self.device_last_position += device_html.height() + self.device_margin; + $("#" + parent_network).append(device_html); + }); + }, + sum_port_length: function(network_id, ports){ + var self = this; + var sum_port_length = 0; + var base_index = self.network_index(network_id); + $.each(ports, function(index, port){ + sum_port_length += base_index - self.network_index(port.network_id); + }); + return sum_port_length; + }, + select_main_port: function(ports){ + var main_port_index = 0; + var MAX_INT = 4294967295; + var min_port_length = MAX_INT; + $.each(ports, function(index, port){ + port_length = horizon.network_topology.sum_port_length(port.network_id, ports) + if(port_length < min_port_length){ + min_port_length = port_length; + main_port_index = index; + } + }) + return ports[main_port_index]; + }, + draw_routers: function(){ + this.draw_devices('router'); + }, + draw_servers: function(){ + this.draw_devices('server'); + }, + select_port: function(device_id){ + return $.map(this.model.ports,function(port, index){ + if (port.device_id == device_id) { + return port; + } + }); + }, + port_html: function(port){ + var self = this; + var port_html = $('
'); + var ip_label = ""; + $.each(port.fixed_ips, function(){ + ip_label += this.ip_address + " "; + }) + var ip_html = $('').html(ip_label); + port_html + .append(ip_html) + .css({'background-color':self.network_color(port.network_id)}) + .click(function (e){ + e.stopPropagation(); + window.location.href = port.url; + }); + return port_html; + }, + port_css: function(port_html, position, network_a, network_b){ + var self = this; + var index_diff = self.network_index(network_a) - self.network_index(network_b); + var width = self.network_margin * index_diff; + var direction = "left"; + if(width < 0){ + direction = "right"; + width += self.network_margin; + } + width = Math.abs(width) + self.device_left_position; + var port_css = {}; + port_css['width'] = width + "px"; + port_css['top'] = position + "px"; + port_css[direction] = (-width -3) + "px"; + port_html.addClass(direction).css(port_css); + }, + network_index: function(network_id){ + return horizon.network_topology.network_index[network_id]; + } +} + +horizon.addInitFunction(function () { + horizon.network_topology.init(); +}); diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html index 0055b8794..a76106f5a 100644 --- a/horizon/templates/horizon/_scripts.html +++ b/horizon/templates/horizon/_scripts.html @@ -32,6 +32,7 @@ + {% endcompress %} {% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %} diff --git a/openstack_dashboard/dashboards/project/dashboard.py b/openstack_dashboard/dashboards/project/dashboard.py index 79df86a64..419b0e66c 100644 --- a/openstack_dashboard/dashboards/project/dashboard.py +++ b/openstack_dashboard/dashboards/project/dashboard.py @@ -28,7 +28,8 @@ class BasePanels(horizon.PanelGroup): 'images_and_snapshots', 'access_and_security', 'networks', - 'routers') + 'routers', + 'network_topology') class ObjectStorePanels(horizon.PanelGroup): diff --git a/openstack_dashboard/dashboards/project/network_topology/__init__.py b/openstack_dashboard/dashboards/project/network_topology/__init__.py new file mode 100644 index 000000000..3b67d1057 --- /dev/null +++ b/openstack_dashboard/dashboards/project/network_topology/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2013 NTT MCL, Inc. +# +# 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. diff --git a/openstack_dashboard/dashboards/project/network_topology/panel.py b/openstack_dashboard/dashboards/project/network_topology/panel.py new file mode 100644 index 000000000..4277da735 --- /dev/null +++ b/openstack_dashboard/dashboards/project/network_topology/panel.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2013 NTT MCL, Inc. +# +# 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 _ + +import horizon + +from openstack_dashboard.dashboards.project import dashboard + + +class NetworkTopology(horizon.Panel): + name = _("Network Topology") + slug = 'network_topology' + permissions = ('openstack.services.network', ) + + +dashboard.Project.register(NetworkTopology) diff --git a/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/index.html b/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/index.html new file mode 100644 index 000000000..f5dd563b3 --- /dev/null +++ b/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/index.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Network Topology" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Network Topology") %} +{% endblock page_header %} + +{% block main %} + + + + +
+
+
+ +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/network_topology/urls.py b/openstack_dashboard/dashboards/project/network_topology/urls.py new file mode 100644 index 000000000..95c5e9fb0 --- /dev/null +++ b/openstack_dashboard/dashboards/project/network_topology/urls.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2013 NTT MCL, Inc. +# +# 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.conf.urls.defaults import url, patterns + +from .views import (NetworkTopology, JSONView) + + +urlpatterns = patterns( + 'openstack_dashboard.dashboards.project.network_topology.views', + url(r'^$', NetworkTopology.as_view(), name='index'), + url(r'^json$', JSONView.as_view(), name='json'), +) diff --git a/openstack_dashboard/dashboards/project/network_topology/views.py b/openstack_dashboard/dashboards/project/network_topology/views.py new file mode 100644 index 000000000..d96ea32b9 --- /dev/null +++ b/openstack_dashboard/dashboards/project/network_topology/views.py @@ -0,0 +1,97 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2013 NTT MCL Inc. +# +# 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.core.urlresolvers import reverse +from django.http import HttpResponse +from django.utils import simplejson +from django.views.generic import TemplateView +from django.views.generic import View + +from openstack_dashboard import api + + +class NetworkTopology(TemplateView): + template_name = 'project/network_topology/index.html' + + +class JSONView(View): + def add_resource_url(self, view, resources): + for resource in resources: + resource['url'] = reverse(view, None, [str(resource['id'])]) + + def _select_port_by_network_id(self, ports, network_id): + for port in ports: + if port['network_id'] == network_id: + return True + return False + + def get(self, request, *args, **kwargs): + data = {} + # Get nova data + novaclient = api.nova.novaclient(request) + servers = novaclient.servers.list() + data['servers'] = [{'name': server.name, + 'status': server.status, + 'id': server.id} for server in servers] + self.add_resource_url('horizon:project:instances:detail', + data['servers']) + # Get quantum data + quantumclient = api.quantum.quantumclient(request) + networks = quantumclient.list_networks() + subnets = quantumclient.list_subnets() + ports = quantumclient.list_ports() + routers = quantumclient.list_routers() + data['networks'] = sorted(networks.get('networks', []), + key=lambda x: x.get('router:external'), + reverse=True) + self.add_resource_url('horizon:project:networks:detail', + data['networks']) + data['subnets'] = subnets.get('subnets', []) + data['ports'] = ports.get('ports', []) + self.add_resource_url('horizon:project:networks:ports:detail', + data['ports']) + data['routers'] = routers.get('routers', []) + # user can't see port on shared network. so we are + # adding fake port based on router information + for router in data['routers']: + external_gateway_info = router.get('external_gateway_info') + if not external_gateway_info: + continue + external_network = external_gateway_info.get( + 'network_id') + if not external_network: + continue + if self._select_port_by_network_id(data['ports'], + external_network): + continue + fake_port = {'id': 'fake%s' % external_network, + 'network_id': external_network, + 'url': reverse( + 'horizon:project:networks:detail', + None, + [external_network]), + 'device_id': router['id'], + 'fixed_ips': []} + data['ports'].append(fake_port) + + self.add_resource_url('horizon:project:routers:detail', + data['routers']) + json_string = simplejson.dumps(data, ensure_ascii=False) + return HttpResponse(json_string, mimetype='text/json') diff --git a/openstack_dashboard/static/dashboard/img/router.png b/openstack_dashboard/static/dashboard/img/router.png new file mode 100644 index 000000000..2726dd9f3 Binary files /dev/null and b/openstack_dashboard/static/dashboard/img/router.png differ diff --git a/openstack_dashboard/static/dashboard/img/server.png b/openstack_dashboard/static/dashboard/img/server.png new file mode 100644 index 000000000..c0a236d9a Binary files /dev/null and b/openstack_dashboard/static/dashboard/img/server.png differ diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less index afd8d1fcc..c0f58466b 100644 --- a/openstack_dashboard/static/dashboard/less/horizon.less +++ b/openstack_dashboard/static/dashboard/less/horizon.less @@ -1744,6 +1744,311 @@ label.log-length { } +/* Styling for network topology */ +.box-sizing(@box: border-box) { + -webkit-box-sizing: @box; + -moz-box-sizing: @box; + -ms-box-sizing: @box; + -o-box-sizing: @box; + box-sizing: @box; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 20px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 20px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 20px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 20px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 20px 0; + } + to { + background-position: 0 0; + } +} + +#topologyCanvas { + .box-sizing(); + width: 100%; + height: 500px; + padding: 25px; + padding-left: 50px; + background: #efefef; +} +div.networks { + height: 100%; +} +div.network { + .box-sizing(); + float: left; + width: 270px; + height: 100%; + position: relative; + .nicname { + .box-sizing(); + height: 100%; + width: 17px; + border-radius: 17px; + z-index: 200; + color:#fff; + position: absolute; + left: -8px; + top: 0px; + cursor: pointer; + &:hover { + background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)); + background-image: -moz-linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)); + background-image: -ms-linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)); + background-image: -o-linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)); + background-image: linear-gradient(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15)); + background-size: 10px 10px; + } + h3 { + font-size: 12px; + line-height: 1; + position: relative; + font-weight: normal; + top:60%; + color:#fff; + left:-1px; + letter-spacing: 0.2em; + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); + white-space: nowrap; + text-shadow: 0px 0px 5px #000; + span.ip { + margin-left: 0.5em; + color: #000; + font-weight: normal; + font-size: 90%; + text-shadow: 0px 0px 0px #000; + } + } + } + .router, .server, .device { + .box-sizing(); + cursor: pointer; + width: 90px; + border: 3px solid #666; + position: absolute; + top:30px; + left:90px; + padding: 0 3px; + background: #666; + margin-bottom: 20px; + &:before,&:after { + content: ""; + width: 90px; + height: 34px; + text-align: center; + position: absolute; + border: 3px solid #666; + .box-sizing(); + background: #fff; + border-radius:50%; + top:-19px; + left:-3px; + } + &:after { + content:""; + color: #fff; + background:#666; + border-radius:50%; + top:auto; + bottom:-19px; + font-size: 11px; + line-height: 30px; + } + span.devicename { + position: absolute; + color: #fff; + bottom: -10px; + font-size: 12px; + line-height: 14px; + left:-4px; + width: 100%; + text-align: center; + z-index:300; + i { + display: inline-block; + width: 14px; + height:14px; + background: #fff url(/static/dashboard/img/router.png) no-repeat center center; + background-size: 12px 12px; + margin-right: 3px; + vertical-align: middle; + border-radius: 20px; + } + } + span.name { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: block; + font-size : 12px; + position: relative; + z-index:10; + text-align: center; + top:-10px; + padding: 0 3px; + } + div.port { + text-align: right; + min-width: 90px; + height: 10px; + font:0px/0px sans-serif; + position: absolute; + left:-91px; + top:8px; + background-color: #37a9e3; + background-image: none; + -webkit-background-size: 20px 20px; + -moz-background-size: 20px 20px; + -o-background-size: 20px 20px; + background-size: 20px 20px; + z-index:100; + span.ip { + .box-sizing(); + font-size: 9px; + line-height: 1; + text-shadow: 0px -1px #fff; + position: relative; + top:-1em; + width: 90px; + display: inline-block; + padding-right:8px; + padding-left: 8px; + word-wrap:break-word; + word-break:break-all; + } + &.right { + left:auto; + right:-92px; + width: 92px; + text-align: left; + span.ip { + } + } + } + &:hover { + div.port { + cursor:pointer; + background-color: #2688c0; + -webkit-animation: progress-bar-stripes 1s linear infinite; + -moz-animation: progress-bar-stripes 1s linear infinite; + -ms-animation: progress-bar-stripes 1s linear infinite; + -o-animation: progress-bar-stripes 1s linear infinite; + animation: progress-bar-stripes 1s linear infinite; + &:hover { + -webkit-animation: progress-bar-stripes 0.3s linear infinite; + -moz-animation: progress-bar-stripes 0.3s linear infinite; + -ms-animation: progress-bar-stripes 0.3s linear infinite; + -o-animation: progress-bar-stripes 0.3s linear infinite; + animation: progress-bar-stripes 0.3s linear infinite; + } + } + background-color: #444; + border-color: #444; + &:before { + border-color: #444; + } + &:after { + background-color: #444; + border-color: #444; + } + } + } + .device { + border:none; + background: transparent; + } + .server { + border-radius: 5px; + background: #fff; + span.devicename { + bottom: 0px; + font-size: 12px; + line-height: 14px; + left:-4px; + i { + display: inline-block; + width: 14px; + height:14px; + background: url(/static/dashboard/img/server.png) no-repeat center center; + background-size: 12px 12px; + margin-right: 3px; + vertical-align: middle; + } + } + span.name { + top:5px; + padding: 0 3px; + } + &:before { + border:none; + background: transparent; + } + &:after { + content: ""; + width: 100%; + line-height: 1.2; + position: absolute; + text-align: center; + border-radius: 0; + background: #666; + color: #fff; + font-size: 11px; + height: 1.5em; + bottom:0px; + left: 0px; + } + &:hover { + background: #fff; + } + } +} + +.launchButtons { + text-align: right; + margin: 10px 0px 15px 10px; + a.btn { + margin-left: 10px; + } +}