1
1
/* Namespace for core functionality related to Network Topology. */
2
3
horizon.network_topology = {
5
topologyCanvas_padding: 120,
6
min_network_height:500,
8
device_initial_position : 40,
9
device_last_position : 0,
10
device_left_position : 90,
12
device_min_height : 45,
13
port_initial_position: 1,
5
svg:'#topology_canvas',
6
svg_container:'#topologyCanvasContainer',
7
post_messages:'#topologyMessages',
9
small:'#topology_template > .network_container_small',
10
normal:'#topology_template > .network_container_normal'
13
small:'#topology_template > .router_small',
14
normal:'#topology_template > .router_normal'
17
small:'#topology_template > .instance_small',
18
normal:'#topology_template > .instance_normal'
21
balloon_device_tmpl : null,
22
balloon_port_tmpl : null,
15
network_color_unit: 0,
16
network_saturation: 1,
17
network_lightness: 0.7,
18
25
reload_duration: 10000,
28
previous_message : null,
32
network_min_height:500,
41
port_text_margin:{x:6,y:-4},
44
balloon_margin:{x:12,y:-12}
48
network_min_height:400,
57
port_text_margin:{x:0,y:0},
60
balloon_margin:{x:12,y:-30}
63
device_name_max_size:9,
64
device_name_suffix:'..'
22
$("#topologyCanvas").spin(horizon.conf.spinner_options.modal);
23
self.retrieve_network_info();
68
$(self.svg_container).spin(horizon.conf.spinner_options.modal);
69
if($('#networktopology').length === 0) {
72
self.color = d3.scale.category10();
73
self.balloon_tmpl = Hogan.compile($('#balloon_container').html());
74
self.balloon_device_tmpl = Hogan.compile($('#balloon_device').html());
75
self.balloon_port_tmpl = Hogan.compile($('#balloon_port').html());
78
.on('click', 'a.closeTopologyBalloon', function(e) {
80
self.delete_balloon();
82
.on('click', '.topologyBalloon', function(e) {
85
.on('click', 'a.vnc_window', function(e) {
87
var vnc_window = window.open($(this).attr('href'), vnc_window, 'width=760,height=560');
88
self.delete_balloon();
91
self.delete_balloon();
94
$('.toggleView > .btn').click(function(){
95
self.draw_mode = $(this).data('value');
96
$('g.network').remove();
97
$.cookie('ntp_draw_mode',self.draw_mode);
102
.on('message',function(e){
103
var message = JSON.parse(e.originalEvent.data);
104
if (self.previous_message != message.message) {
105
horizon.alert(message.type, message.message);
106
horizon.autoDismissAlerts();
107
self.previous_message = message.message;
108
self.delete_post_message(message.iframe_id);
109
self.load_network_info();
110
setTimeout(function() {
111
self.previous_message = null;
116
self.load_network_info();
24
117
setInterval(function(){
25
self.retrieve_network_info();
118
self.load_network_info();
26
119
}, self.reload_duration);
28
retrieve_network_info: function(){
121
load_network_info:function(){
30
if($("#networktopology").length === 0) {
123
if($('#networktopology').length === 0) {
33
$.getJSON($("#networktopology").data("networktopology"),
126
$.getJSON($('#networktopology').data('networktopology') + '?' + $.now(),
35
self.draw_graph(data);
39
draw_loading: function () {
40
$("#topologyCanvas").spin(horizon.conf.spinner_options.modal);
42
draw_graph: function(data){
43
var canvas = $("#topologyCanvas");
44
var networks = $("#topologyCanvas > .networks");
45
var nodata = $("#topologyCanvas > .nodata");
51
this.device_last_position = this.device_initial_position;
52
var network_elements = this.draw_networks();
53
var router_elements = this.draw_routers();
54
var server_elements = this.draw_servers();
55
if ((network_elements + router_elements + server_elements) <= 0){
133
select_draw_mode:function() {
135
var draw_mode = $.cookie('ntp_draw_mode');
136
if (draw_mode && (draw_mode == 'normal'| draw_mode == 'small')) {
137
self.draw_mode = draw_mode;
60
Math.max(this.device_last_position + this.topologyCanvas_padding, this.min_network_height)
63
this.model.networks.length * this.network_margin
67
network_color: function(network_id){
69
var num_network = this.model.networks.length;
75
max_hue/num_network*(this.network_index(network_id) + 1));
77
hue, this.network_saturation, this.network_lightness);
79
//see http://en.wikipedia.org/wiki/HSL_and_HSV
80
hsv2rgb:function (h, s, v) {
81
var hi = Math.round(h/60) % 6;
85
var t = v*(1 - (1 - f)*s);
118
return "rgb(" + Math.round(r*255) + "," + Math.round(g*255) + "," + Math.round(b*255) + ")";
120
draw_networks: function(){
139
if (self.model.networks.length *
140
self.element_properties.normal.network_width > $('#topologyCanvas').width()) {
141
self.draw_mode = 'small';
143
self.draw_mode = 'normal';
145
$.cookie('ntp_draw_mode',self.draw_mode);
147
$('.toggleView > .btn').each(function(){
149
if($this.hasClass(self.draw_mode)) {
150
$this.addClass('active');
154
data_convert:function() {
122
var networks = $("#topologyCanvas > .networks");
123
$.each(self.model.networks, function(index, network){
124
var label = (network.name != "")? network.name : network.id;
125
if(network['router:external']){
126
label += " (external) ";
156
var model = self.model;
157
$.each(model.networks, function(index, network) {
128
158
self.network_index[network.id] = index;
129
var network_html = $("<div class='network' />").attr("id", network.id);
130
var nicname_html = $("<div class='nicname'><h3>" + label +
131
"</h3><span class='ip'>" + self.select_cidr(network.id) + "</span></div>");
132
if (network.url == undefined) {
133
nicname_html.addClass("nourl");
160
self.select_draw_mode();
161
var element_properties = self.element_properties[self.draw_mode];
162
self.network_height = element_properties.top_margin;
164
{model:model.routers, type:'router'},
165
{model:model.servers, type:'instance'}
166
], function(index, devices) {
167
var type = devices.type;
168
var model = devices.model;
169
$.each(model, function(index, device) {
171
device.ports = self.select_port(device.id);
172
var hasports = (device.ports.length <= 0) ? false : true;
173
device.parent_network = (hasports) ?
174
self.select_main_port(device.ports).network_id : self.model.networks[0].id;
175
var height = element_properties.port_margin*(device.ports.length - 1);
177
(self.draw_mode == 'normal' && height > element_properties.default_height) ? height :
178
element_properties.default_height;
179
device.pos_y = self.network_height;
181
(self.draw_mode == 'small' && height > device.height) ? 1 :
182
element_properties.port_height;
184
(self.draw_mode == 'small' && height > device.height) ?
185
device.height/device.ports.length :
186
element_properties.port_margin;
187
self.network_height += device.height + element_properties.margin;
190
$.each(model.networks, function(index, network) {
191
network.devices = [];
192
$.each([model.routers, model.servers],function(index, devices) {
193
$.each(devices,function(index, device) {
194
if(network.id == device.parent_network) {
195
network.devices.push(device);
200
self.network_height += element_properties.top_margin;
201
self.network_height = (self.network_height > element_properties.network_min_height) ?
202
self.network_height : element_properties.network_min_height;
203
self.draw_topology();
205
draw_topology:function() {
207
$(self.svg_container).spin(false);
208
$(self.svg_container).removeClass('noinfo');
209
if (self.model.networks.length <= 0) {
210
$('g.network').remove();
211
$(self.svg_container).addClass('noinfo');
214
var svg = d3.select(self.svg);
215
var element_properties = self.element_properties[self.draw_mode];
217
.attr('width',self.model.networks.length*element_properties.network_width)
218
.attr('height',self.network_height);
220
var network = svg.selectAll('g.network')
221
.data(self.model.networks);
223
var network_enter = network.enter()
225
.attr('class','network')
227
this.appendChild(d3.select(self.network_tmpl[self.draw_mode]).node().cloneNode(true));
228
var $this = d3.select(this).select('.network-rect');
230
var $this = d3.select(this).select('.network-rect');
232
.on('mouseover',function(){
233
$this.transition().style('fill',
234
function() { return d3.rgb(self.network_color(d.id)).brighter(0.5)});
236
.on('mouseout',function(){
237
$this.transition().style('fill',
238
function() { return self.network_color(d.id)});
240
.on('click',function(){
241
window.location.href = d.url;
244
$this.classed('nourl', true);
249
.attr('id',function(d) { return 'id_' + d.id; })
250
.attr('transform',function(d,i){
251
return 'translate(' + element_properties.network_width * i + ',' + 0 + ')'})
252
.select('.network-rect')
253
.attr('height', function(d) { return self.network_height})
254
.style('fill', function(d) { return self.network_color(d.id)});
256
.select('.network-name')
257
.attr('x', function(d) { return self.network_height/2 })
258
.text(function(d) { return d.name; });
260
.select('.network-cidr')
261
.attr('x', function(d) { return self.network_height - self.element_properties.cidr_margin })
263
var cidr = $.map(d.subnets,function(n, i){
266
return cidr.join(', ');
269
network.exit().remove();
271
var device = network.selectAll('g.device')
272
.data(function(d) { return d.devices; });
274
var device_enter = device.enter()
276
.attr('class','device')
278
var device_template = self[d.type + '_tmpl'][self.draw_mode];
279
this.appendChild(d3.select(device_template).node().cloneNode(true));
282
device_enter.on('mouseenter',function(d){
284
self.show_balloon(d,$this);
286
.on('click',function(){
287
d3.event.stopPropagation();
291
.attr('id',function(d) { return 'id_' + d.id; })
292
.attr('transform',function(d,i){
293
return 'translate(' + element_properties.device_x + ',' + d.pos_y + ')';
296
.attr('height',function(d) { return d.height; });
299
.attr('y',function(d) {
300
return element_properties.texts_bg_y + d.height - element_properties.default_height;
304
.attr('y',function(d) {
305
return element_properties.type_y + d.height - element_properties.default_height;
309
.text(function(d) { return self.string_truncate(d.name); });
310
device.each(function(d) {
311
if (d.status == 'BUILD') {
312
d3.select(this).classed('loading',true);
313
} else if (d.task == 'deleting') {
314
d3.select(this).classed('loading',true);
315
if ('bl_' + d.id == self.balloon_id) {
316
self.delete_balloon();
135
nicname_html.click(function (){
136
window.location.href = network.url;
141
{'background-color':self.network_color(network.id)})
142
.appendTo(network_html);
143
networks.append(network_html);
145
return self.model.networks.length;
147
select_cidr:function(network_id){
149
$.each(this.model.subnets, function(index, subnet){
150
if(subnet.network_id != network_id){
319
d3.select(this).classed('loading',false);
320
if ('bl_' + d.id == self.balloon_id) {
322
self.show_balloon(d,$this);
153
cidr.push(subnet.cidr);
155
return cidr.join(', ');
157
draw_devices: function(type){
159
$.each(self.model[type + 's'], function(index, device){
161
var name = (device.name != "")? device.name : device.id;
162
var ports = self.select_port(id);
163
if(ports.length <= 0){
166
var main_port = self.select_main_port(ports);
167
var parent_network = main_port.network_id;
168
var device_html = $("<div class='" + type + "'></div>");
170
.attr('id', device.id)
171
.css({top: self.device_last_position, position: 'absolute'})
172
.append($("<span class='devicename'><i></i>" + type + "</span>"))
175
window.location.href = device.url;
327
device.exit().each(function(d){
328
if ('bl_' + d.id == self.balloon_id) {
329
self.delete_balloon();
333
var port = device.select('g.ports')
335
.data(function(d) { return d.ports; });
337
var port_enter = port.enter()
339
.attr('class','port')
340
.attr('id',function(d) { return 'id_' + d.id; });
344
.attr('class','port_line');
348
.attr('class','port_text');
350
device.select('g.ports').each(function(d,i){
352
this._portdata.ports_length = d.ports.length;
353
this._portdata.parent_network = d.parent_network;
354
this._portdata.device_height = d.height;
355
this._portdata.port_height = d.port_height;
356
this._portdata.port_margin = d.port_margin;
357
this._portdata.left = 0;
358
this._portdata.right = 0;
359
$(this).mouseenter(function(e){
364
port.each(function(d,i){
365
var index_diff = self.network_index(this.parentNode._portdata.parent_network) -
366
self.network_index(d.network_id);
367
this._index_diff = index_diff = (index_diff >= 0)? ++index_diff : index_diff;
368
this._direction = (this._index_diff < 0)? 'right' : 'left';
369
this._index = this.parentNode._portdata[this._direction] ++;
373
port.attr('transform',function(d,i){
374
var x = (this._direction == 'left') ? 0 : element_properties.device_width;
375
var ports_length = this.parentNode._portdata[this._direction];
376
var distance = this.parentNode._portdata.port_margin;
377
var y = (this.parentNode._portdata.device_height -
378
(ports_length -1)*distance)/2 + this._index*distance;
379
return 'translate(' + x + ',' + y + ')';
383
.select('.port_line')
384
.attr('stroke-width',function(d,i) {
385
return this.parentNode.parentNode._portdata.port_height;
387
.attr('stroke',function(d,i) {return self.network_color(d.network_id)})
388
.attr('x1',0).attr('y1',0).attr('y2',0)
389
.attr('x2',function(d,i) {
390
var parent = this.parentNode;
391
var width = (Math.abs(parent._index_diff) - 1)*element_properties.network_width +
392
element_properties.port_width;
393
return (parent._direction == 'left') ? -1*width : width;
397
.select('.port_text')
398
.attr('x',function(d) {
399
var parent = this.parentNode;
400
if (parent._direction == 'left') {
401
d3.select(this).classed('left',true);
402
return element_properties.port_text_margin.x*-1;
404
d3.select(this).classed('left',false);
405
return element_properties.port_text_margin.x;
408
.attr('y',function(d) { return element_properties.port_text_margin.y })
411
$.each(d.fixed_ips, function() {
412
ip_label.push(this.ip_address);
414
return ip_label.join(',');
177
var name_html = $("<span class='name'></span>")
179
.attr('title', device.name)
180
.appendTo(device_html);
181
var port_position = self.port_initial_position;
182
$.each(ports, function(){
184
var port_html = self.port_html(port);
185
port_position += self.port_margin;
186
self.port_css(port_html, port_position, parent_network, port.network_id);
187
device_html.append(port_html);
189
port_position += self.port_margin;
191
{height: Math.max(self.device_min_height, port_position) + "px"});
192
self.device_last_position += device_html.height() + self.device_margin;
193
$("#" + parent_network).append(device_html);
194
$('div.port span.ip').each(function(i, ip){
195
$(ip).css('top', '-'+$(ip).height()+'px');
198
return self.model[type + 's'].length;
200
sum_port_length: function(network_id, ports){
202
var sum_port_length = 0;
203
var base_index = self.network_index(network_id);
204
$.each(ports, function(index, port){
205
sum_port_length += base_index - self.network_index(port.network_id);
207
return sum_port_length;
417
port.exit().remove();
419
network_color: function(network_id) {
420
return this.color(this.network_index(network_id));
422
network_index: function(network_id) {
423
return this.network_index[network_id];
425
select_port: function(device_id){
426
return $.map(this.model.ports,function(port, index){
427
if (port.device_id == device_id) {
209
432
select_main_port: function(ports){
210
434
var main_port_index = 0;
211
435
var MAX_INT = 4294967295;
212
436
var min_port_length = MAX_INT;
213
437
$.each(ports, function(index, port){
214
port_length = horizon.network_topology.sum_port_length(port.network_id, ports)
438
var port_length = _self.sum_port_length(port.network_id, ports);
215
439
if(port_length < min_port_length){
216
440
min_port_length = port_length;
217
441
main_port_index = index;
220
444
return ports[main_port_index];
222
draw_routers: function(){
223
return this.draw_devices('router');
225
draw_servers: function(){
226
return this.draw_devices('server');
228
select_port: function(device_id){
229
return $.map(this.model.ports,function(port, index){
230
if (port.device_id == device_id) {
235
port_html: function(port){
237
var port_html = $('<div class="port"><div class="dot"></div></div>');
239
$.each(port.fixed_ips, function(){
240
ip_label += this.ip_address + "<br />";
446
sum_port_length: function(network_id, ports){
448
var sum_port_length = 0;
449
var base_index = self.network_index(network_id);
450
$.each(ports, function(index, port){
451
sum_port_length += base_index - self.network_index(port.network_id);
453
return sum_port_length;
455
string_truncate: function(string) {
458
var max_size = self.element_properties.device_name_max_size;
459
var suffix = self.element_properties.device_name_suffix;
461
for (var i = 0; i < str.length; i++) {
462
bytes += str.charCodeAt(i) <= 255 ? 1 : 2;
463
if (bytes > max_size) {
464
str = str.substr(0, i) + suffix;
470
delete_device: function(type, device_id) {
472
var message = {id:device_id};
473
self.post_message(device_id,type,message);
475
delete_port: function(router_id, port_id) {
477
var message = {id:port_id};
478
self.post_message(port_id, 'router/' + router_id + '/', message);
480
show_balloon:function(d,element) {
482
var element_properties = self.element_properties[self.draw_mode];
483
if (self.balloon_id) {
484
self.delete_balloon();
486
var balloon_tmpl = self.balloon_tmpl;
487
var device_tmpl = self.balloon_device_tmpl;
488
var port_tmpl = self.balloon_port_tmpl;
489
var balloon_id = 'bl_' + d.id;
491
$.each(d.ports,function(i, port){
494
object.router_id = port.device_id;
495
object.url = port.url;
496
object.port_status = port.status;
497
object.port_status_css = (port.status == "ACTIVE")? 'active' : 'down';
500
ip_address = port.fixed_ips[0].ip_address;
502
ip_address = 'no info';
504
var device_owner = '';
506
device_owner = port.device_owner.replace('network:','');
508
device_owner = 'no info';
510
object.ip_address = ip_address;
511
object.device_owner = device_owner;
512
object.is_interface = (device_owner == 'router_interface') ? true : false;
516
balloon_id:balloon_id,
521
type_capital:d.type.replace(/^\w/, function($0) {return $0.toUpperCase()}),
524
status_class:(d.status == "ACTIVE")? 'active' : 'down'
526
if (d.type == 'router') {
527
html_data.port = ports;
528
html = balloon_tmpl.render(html_data,{
532
} else if (d.type == 'instance') {
533
html_data.console_id = d.id;
534
html = balloon_tmpl.render(html_data,{
540
$(self.svg_container).append(html);
541
var device_position = element.find('.frame');
542
var x = device_position.position().left +
543
element_properties.device_width +
544
element_properties.balloon_margin.x;
545
var y = device_position.position().top +
546
element_properties.balloon_margin.y;
547
$('#' + balloon_id).css({
242
var ip_html = $('<span class="ip" />').html(ip_label);
245
.css({'background-color':self.network_color(port.network_id)})
246
.click(function (e) {
248
if(port.url != undefined) {
249
window.location.href = port.url;
252
if(port.url == undefined) {
253
port_html.addClass("nourl");
257
port_css: function(port_html, position, network_a, network_b){
259
var index_diff = self.network_index(network_a) - self.network_index(network_b);
260
var width = self.network_margin * index_diff;
261
var direction = "left";
264
width += self.network_margin;
266
width = Math.abs(width) + self.device_left_position;
268
port_css['width'] = width + "px";
269
port_css['top'] = position + "px";
270
port_css[direction] = (-width -3) + "px";
271
port_html.addClass(direction).css(port_css);
273
network_index: function(network_id){
274
return horizon.network_topology.network_index[network_id];
552
var $balloon = $('#' + balloon_id);
553
if ($balloon.offset().left + $balloon.outerWidth() > $(window).outerWidth()) {
559
'left': device_position.position().left
560
- $balloon.outerWidth()
561
- element_properties.balloon_margin.x + 'px'
563
.addClass('leftPosition');
565
$balloon.find('.delete-device').click(function(e){
567
$this.addClass('deleting');
568
d3.select('#id_' + $this.data('device-id')).classed('loading',true);
569
self.delete_device($this.data('type'),$this.data('device-id'));
571
$balloon.find('.delete-port').click(function(e){
573
self.delete_port($this.data('router-id'),$this.data('port-id'));
575
self.balloon_id = balloon_id;
577
delete_balloon:function() {
579
if(self.balloon_id) {
580
$('#' + self.balloon_id).remove()
581
self.balloon_id = null;
584
post_message: function(id,url,message) {
586
var iframe_id = 'ifr_' + id;
587
var iframe = $('<iframe width="500" height="300" />')
588
.attr('id',iframe_id)
590
.appendTo(self.post_messages);
591
iframe.on('load',function() {
592
$(this).get(0).contentWindow.postMessage(
593
JSON.stringify(message, null, 2), '*');
596
delete_post_message: function(id) {
597
$('#' + id).remove();
278
601
horizon.addInitFunction(function () {
279
horizon.network_topology.init();
602
horizon.network_topology.init();