Adding network-topology view for quantum

Implements bp quantum-network-topology

- network-topology pane shows the topology of quantum network
- supports network, router , instances now
- support of load balancer is future work

Change-Id: Idb1bf2d3add43cf51ed23ebaac3ee740b03498aa
This commit is contained in:
Nachi Ueno 2012-09-24 20:34:33 +00:00
parent f2fb22cf59
commit 250d21a562
11 changed files with 779 additions and 1 deletions

View File

@ -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 = $("<div class='network' />").attr("id", network.id);
var nicname_html = $("<div class='nicname'><h3>" + label + "</h3></div>");
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 += " <span class=\"ip\">[ " + subnet.cidr + " ]</span>";
});
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 = $("<div class='" + type + "'></div>");
device_html
.attr('id', device.id)
.css({top: self.device_last_position, position: 'absolute'})
.append($("<span class='devicename'><i></i>" + type + "</span>"))
.click(function (e){
e.stopPropagation();
window.location.href = device.url;
});
var name_html = $("<span class='name'></span>")
.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 = $('<div class="port"><div class="dot"></div></div>');
var ip_label = "";
$.each(port.fixed_ips, function(){
ip_label += this.ip_address + " ";
})
var ip_html = $('<span class="ip" />').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();
});

View File

@ -32,6 +32,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.users.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.utils.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.projects.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.networktopology.js' type='text/javascript' charset='utf-8'></script>
{% endcompress %}
{% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %}

View File

@ -28,7 +28,8 @@ class BasePanels(horizon.PanelGroup):
'images_and_snapshots',
'access_and_security',
'networks',
'routers')
'routers',
'network_topology')
class ObjectStorePanels(horizon.PanelGroup):

View File

@ -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.

View File

@ -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)

View File

@ -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 %}
<style>
/* TODO(nati): The following styles are not work with compress, so put it here tempolary */
div.network .router:hover div.port,
div.network .server:hover div.port,
div.network .device:hover div.port {
background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(0, 0, 0, 0.25)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(0, 0, 0, 0.25)), color-stop(0.75, rgba(0, 0, 0, 0.25)), color-stop(0.75, transparent), to(transparent));
background-image: -webkit-linear-gradient(-45deg, rgba(0, 0, 0, 0.25) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.25) 50%, rgba(0, 0, 0, 0.25) 75%, transparent 75%, transparent);
background-image: -moz-linear-gradient(-45deg, rgba(0, 0, 0, 0.25) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.25) 50%, rgba(0, 0, 0, 0.25) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(-45deg, rgba(0, 0, 0, 0.25) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.25) 50%, rgba(0, 0, 0, 0.25) 75%, transparent 75%, transparent);
background-image: linear-gradient(-45deg, rgba(0, 0, 0, 0.25) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.25) 50%, rgba(0, 0, 0, 0.25) 75%, transparent 75%, transparent);
}
</style>
<noscript>
{%trans "This pane needs javascript support." %}
</noscript>
<div class="launchButtons">
<a href="{% url horizon:project:instances:launch %}" id="instances__action_launch" class="btn btn-small btn-launch ajax-modal">{%trans "Launch Instance" %}</a>
<a href="{% url horizon:project:networks:create %}" id="networks__action_create" class="btn btn-small ajax-modal btn-create">{%trans "Create Network" %}</a>
<a href="{% url horizon:project:routers:create %}" id="Routers__action_create" class="btn btn-small ajax-modal btn-create">{%trans "Create Router" %}</a>
</div>
<div id="topologyCanvas">
<div class="networks"></div>
</div>
<span data-networktopology="{% url horizon:project:network_topology:json %}" id="networktopology"></span>
{% endblock %}

View File

@ -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'),
)

View File

@ -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')

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -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;
}
}