/** * 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. */ /* global Hogan */ /* Namespace for core functionality related to Network Topology. */ horizon.flat_network_topology = { model: null, fa_globe_glyph: '\uf0ac', fa_globe_glyph_width: 15, svg:'#topology_canvas', svg_container:'#flatTopologyCanvasContainer', network_tmpl:{ small:'#topology_template > .network_container_small', normal:'#topology_template > .network_container_normal' }, router_tmpl: { small:'#topology_template > .router_small', normal:'#topology_template > .router_normal' }, instance_tmpl: { small:'#topology_template > .instance_small', normal:'#topology_template > .instance_normal' }, balloon_tmpl : null, balloon_device_tmpl : null, balloon_port_tmpl : null, network_index: {}, balloon_id:null, reload_duration: 10000, draw_mode:'normal', network_height : 0, previous_message : null, element_properties:{ normal:{ network_width:270, network_min_height:500, top_margin:80, default_height:50, margin:20, device_x:98.5, device_width:90, port_margin:16, port_height:6, port_width:82, port_text_margin:{x:6,y:-4}, texts_bg_y:32, type_y:46, balloon_margin:{x:12,y:-12} }, small :{ network_width:100, network_min_height:400, top_margin:50, default_height:20, margin:30, device_x:47.5, device_width:20, port_margin:5, port_height:3, port_width:32.5, port_text_margin:{x:0,y:0}, texts_bg_y:0, type_y:0, balloon_margin:{x:12,y:-30} }, cidr_margin:5, device_name_max_size:9, device_name_suffix:'..' }, init:function() { var self = this; self.$container = $(self.svg_container); self.$loading_template = horizon.networktopologyloader.setup_loader($(self.$container)); if($('#networktopology').length === 0) { return; } self.color = d3.scale.category10(); self.balloon_tmpl = Hogan.compile($('#balloon_container').html()); self.balloon_device_tmpl = Hogan.compile($('#balloon_device').html()); self.balloon_port_tmpl = Hogan.compile($('#balloon_port').html()); $(document) .on('click', 'a.closeTopologyBalloon', function(e) { e.preventDefault(); self.delete_balloon(); }) .on('click', '.topologyBalloon', function(e) { e.stopPropagation(); }) .on('click', 'a.vnc_window', function(e) { e.preventDefault(); var vnc_window = window.open($(this).attr('href'), vnc_window, 'width=760,height=560'); self.delete_balloon(); }) .click(function(){ self.delete_balloon(); }); $('.toggle-view > .btn').click(function(){ self.draw_mode = $(this).data('value'); $('g.network').remove(); horizon.cookies.put('ntp_draw_mode',self.draw_mode); self.data_convert(); }); self.$loading_template.show(); $('#networktopology').on('change', function() { self.load_network_info(); }); // register for message notifications //horizon.networktopologymessager.addMessageHandler(self.handleMessage, this); }, /*handleMessage:function(message) { // noop },*/ load_network_info:function(){ var self = this; self.model = horizon.networktopologyloader.model; self.data_convert(); }, select_draw_mode:function() { var self = this; var draw_mode = 'normal'; try { draw_mode = horizon.cookies.get('ntp_draw_mode'); } catch(e) { // if the cookie does not exist, angular-cookie passes "undefined" to // JSON.parse which throws an exception } if (draw_mode && (draw_mode === 'normal' || draw_mode === 'small')) { self.draw_mode = draw_mode; } else { if (self.model.networks.length * self.element_properties.normal.network_width > $('#topologyCanvas').width()) { self.draw_mode = 'small'; } else { self.draw_mode = 'normal'; } horizon.cookies.put('ntp_draw_mode',self.draw_mode); } $('.toggle-view > .btn').each(function(){ var $this = $(this); if($this.data('value') === self.draw_mode) { $this.addClass('active'); } else { $this.removeClass('active'); } }); }, data_convert:function() { var self = this; var model = self.model; $.each(model.networks, function(index, network) { self.network_index[network.id] = index; }); self.select_draw_mode(); var element_properties = self.element_properties[self.draw_mode]; self.network_height = element_properties.top_margin; $.each([ {model:model.routers, type:'router'}, {model:model.servers, type:'instance'} ], function(index, devices) { var type = devices.type; var model = devices.model; $.each(model, function(index, device) { device.type = type; device.ports = self.select_port(device.id); var hasports = device.ports.length > 0; device.parent_network = (hasports) ? self.select_main_port(device.ports).network_id : self.model.networks[0].id; var height = element_properties.port_margin*(device.ports.length - 1); device.height = (self.draw_mode === 'normal' && height > element_properties.default_height) ? height : element_properties.default_height; device.pos_y = self.network_height; device.port_height = (self.draw_mode === 'small' && height > device.height) ? 1 : element_properties.port_height; device.port_margin = (self.draw_mode === 'small' && height > device.height) ? device.height/device.ports.length : element_properties.port_margin; self.network_height += device.height + element_properties.margin; }); }); $.each(model.networks, function(index, network) { network.devices = []; $.each([model.routers, model.servers],function(index, devices) { $.each(devices,function(index, device) { if(network.id === device.parent_network) { network.devices.push(device); } }); }); }); self.network_height += element_properties.top_margin; self.network_height = (self.network_height > element_properties.network_min_height) ? self.network_height : element_properties.network_min_height; self.draw_topology(); self.$loading_template.hide(); }, draw_topology:function() { var self = this; $(self.svg_container).removeClass('noinfo'); if (self.model.networks.length <= 0) { $('g.network').remove(); $(self.svg_container).addClass('noinfo'); return; } var svg = d3.select(self.svg); var element_properties = self.element_properties[self.draw_mode]; svg .attr('width',self.model.networks.length*element_properties.network_width) .attr('height',self.network_height); var network = svg.selectAll('g.network') .data(self.model.networks); network.enter() .append('g') .attr('class','network') .each(function(d){ this.appendChild(d3.select(self.network_tmpl[self.draw_mode]).node().cloneNode(true)); var $this = d3.select(this).select('.network-rect'); if (d.url) { $this .on('mouseover',function(){ $this.transition().style('fill', function() { return d3.rgb(self.get_network_color(d.id)).brighter(0.5); }); }) .on('mouseout',function(){ $this.transition().style('fill', function() { return self.get_network_color(d.id); }); }) .on('click',function(){ window.location.href = d.url; }); } else { $this.classed('nourl', true); } }); network .attr('id',function(d) { return 'id_' + d.id; }) .attr('transform',function(d,i){ return 'translate(' + element_properties.network_width * i + ',' + 0 + ')'; }) .select('.network-rect') .attr('height', function() { return self.network_height; }) .style('fill', function(d) { return self.get_network_color(d.id); }); network .select('.network-name') .attr('x', function() { return self.network_height/2; }) .text(function(d) { return d.name; }); network .select('.network-cidr') .attr('x', function(d) { var padding = isExternalNetwork(d) ? self.fa_globe_glyph_width : 0; return self.network_height - self.element_properties.cidr_margin - padding; }) .text(function(d) { var cidr = $.map(d.subnets,function(n){ return n.cidr; }); return cidr.join(', '); }); function isExternalNetwork(d) { return d['router:external']; } network .select('.network-type') .text(function(d) { return isExternalNetwork(d) ? self.fa_globe_glyph : ''; }) .attr('x', function() { return self.network_height - self.element_properties.cidr_margin; }); $('[data-toggle="tooltip"]').tooltip({container: 'body'}); network.exit().remove(); var device = network.selectAll('g.device') .data(function(d) { return d.devices; }); var device_enter = device.enter() .append("g") .attr('class','device') .each(function(d){ var device_template = self[d.type + '_tmpl'][self.draw_mode]; this.appendChild(d3.select(device_template).node().cloneNode(true)); }); device_enter .on('mouseenter',function(d){ var $this = $(this); self.show_balloon(d,$this); }) .on('click',function(){ d3.event.stopPropagation(); }); device .attr('id',function(d) { return 'id_' + d.id; }) .attr('transform',function(d){ return 'translate(' + element_properties.device_x + ',' + d.pos_y + ')'; }) .select('.frame') .attr('height',function(d) { return d.height; }); device .select('.texts_bg') .attr('y',function(d) { return element_properties.texts_bg_y + d.height - element_properties.default_height; }); device .select('.type') .attr('y',function(d) { return element_properties.type_y + d.height - element_properties.default_height; }); device .select('.name') .text(function(d) { return self.string_truncate(d.name); }); device.each(function(d) { if (d.status === 'BUILD') { d3.select(this).classed('loading',true); } else if (d.task === 'deleting') { d3.select(this).classed('loading',true); if ('bl_' + d.id === self.balloon_id) { self.delete_balloon(); } } else { d3.select(this).classed('loading',false); if ('bl_' + d.id === self.balloon_id) { var $this = $(this); self.show_balloon(d,$this); } } }); device.exit().each(function(d){ if ('bl_' + d.id === self.balloon_id) { self.delete_balloon(); } }).remove(); var port = device.select('g.ports') .selectAll('g.port') .data(function(d) { return d.ports; }); var port_enter = port.enter() .append('g') .attr('class','port') .attr('id',function(d) { return 'id_' + d.id; }); port_enter .append('line') .attr('class','port_line'); port_enter .append('text') .attr('class','port_text'); device.select('g.ports').each(function(d){ this._portdata = {}; this._portdata.ports_length = d.ports.length; this._portdata.parent_network = d.parent_network; this._portdata.device_height = d.height; this._portdata.port_height = d.port_height; this._portdata.port_margin = d.port_margin; this._portdata.left = 0; this._portdata.right = 0; $(this).mouseenter(function(e){ e.stopPropagation(); }); }); port.each(function(d){ var index_diff = self.get_network_index(this.parentNode._portdata.parent_network) - self.get_network_index(d.network_id); this._index_diff = index_diff = (index_diff >= 0)? ++index_diff : index_diff; this._direction = (this._index_diff < 0)? 'right' : 'left'; this._index = this.parentNode._portdata[this._direction] ++; }); port.attr('transform',function(){ var x = (this._direction === 'left') ? 0 : element_properties.device_width; var ports_length = this.parentNode._portdata[this._direction]; var distance = this.parentNode._portdata.port_margin; var y = (this.parentNode._portdata.device_height - (ports_length -1)*distance)/2 + this._index*distance; return 'translate(' + x + ',' + y + ')'; }); port .select('.port_line') .attr('stroke-width',function() { return this.parentNode.parentNode._portdata.port_height; }) .attr('stroke', function(d) { return self.get_network_color(d.network_id); }) .attr('x1',0).attr('y1',0).attr('y2',0) .attr('x2',function() { var parent = this.parentNode; var width = (Math.abs(parent._index_diff) - 1)*element_properties.network_width + element_properties.port_width; return (parent._direction === 'left') ? -1*width : width; }); port .select('.port_text') .attr('x',function() { var parent = this.parentNode; if (parent._direction === 'left') { d3.select(this).classed('left',true); return element_properties.port_text_margin.x*-1; } else { d3.select(this).classed('left',false); return element_properties.port_text_margin.x; } }) .attr('y',function() { return element_properties.port_text_margin.y; }) .text(function(d) { var ip_label = []; $.each(d.fixed_ips, function() { ip_label.push(this.ip_address); }); return ip_label.join(','); }); port.exit().remove(); }, get_network_color: function(network_id) { return this.color(this.get_network_index(network_id)); }, get_network_index: function(network_id) { return this.network_index[network_id]; }, select_port: function(device_id){ return $.map(this.model.ports,function(port){ if (port.device_id === device_id) { return port; } }); }, select_main_port: function(ports){ var _self = this; var main_port_index = 0; var MAX_INT = 4294967295; var min_port_length = MAX_INT; $.each(ports, function(index, port){ var port_length = _self.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]; }, sum_port_length: function(network_id, ports){ var self = this; var sum_port_length = 0; var base_index = self.get_network_index(network_id); $.each(ports, function(index, port){ sum_port_length += base_index - self.get_network_index(port.network_id); }); return sum_port_length; }, string_truncate: function(string) { var self = this; var str = string; var max_size = self.element_properties.device_name_max_size; var suffix = self.element_properties.device_name_suffix; var bytes = 0; for (var i = 0; i < str.length; i++) { bytes += str.charCodeAt(i) <= 255 ? 1 : 2; if (bytes > max_size) { str = str.substr(0, i) + suffix; break; } } return str; }, delete_device: function(device_type, device_id) { var message = {id:device_id}; var target = device_type === 'instance' ? 'instance?id=' + device_id : device_type; horizon.networktopologymessager.post_message(device_id, target, message, device_type, 'delete', data={}); }, delete_port: function(router_id, port_id, network_id) { var message = {id:port_id}; var data = {router_id: router_id, network_id: network_id}; horizon.networktopologymessager.post_message(port_id, 'router/' + router_id + '/', message, 'port', 'delete', data); }, show_balloon:function(d,element) { var self = this; var element_properties = self.element_properties[self.draw_mode]; if (self.balloon_id) { self.delete_balloon(); } var balloon_tmpl = self.balloon_tmpl; var device_tmpl = self.balloon_device_tmpl; var port_tmpl = self.balloon_port_tmpl; var balloon_id = 'bl_' + d.id; var ports = []; $.each(d.ports,function(i, port){ var object = {}; object.id = port.id; object.router_id = port.device_id; object.url = port.url; object.port_status = port.status; object.port_status_css = (port.status === "ACTIVE")? 'active' : 'down'; var ip_address = ''; try { ip_address = port.fixed_ips[0].ip_address; }catch(e){ ip_address = gettext('None'); } var device_owner = ''; try { device_owner = port.device_owner.replace('network:',''); }catch(e){ device_owner = gettext('None'); } var network_id = ''; try { network_id = port.network_id; }catch(e) { network_id = gettext('None'); } object.network_id = network_id; object.ip_address = ip_address; object.device_owner = device_owner; object.is_interface = (device_owner === 'router_interface' || device_owner === 'router_gateway'); ports.push(object); }); var html; var html_data = { balloon_id:balloon_id, id:d.id, url:d.url, name:d.name, type:d.type, delete_label: gettext("Delete"), status:d.status, status_class:(d.status === "ACTIVE")? 'active' : 'down', status_label: gettext("STATUS"), id_label: gettext("ID"), interfaces_label: gettext("Interfaces"), delete_interface_label: gettext("Delete Interface"), open_console_label: gettext("Open Console"), view_details_label: gettext("View Details") }; if (d.type === 'router') { html_data.delete_label = gettext("Delete Router"); html_data.view_details_label = gettext("View Router Details"); html_data.port = ports; html_data.add_interface_url = d.url + 'addinterface'; html_data.add_interface_label = gettext("Add Interface"); html = balloon_tmpl.render(html_data,{ table1:device_tmpl, table2:(ports.length > 0) ? port_tmpl : null }); } else if (d.type === 'instance') { html_data.delete_label = gettext("Delete Instance"); html_data.view_details_label = gettext("View Instance Details"); html_data.console_id = d.id; html_data.console = d.console; html = balloon_tmpl.render(html_data,{ table1:device_tmpl }); } else { return; } $(self.svg_container).append(html); var device_position = element.find('.frame'); var sidebar_width = $("#sidebar").width(); var navbar_height = $(".navbar").height(); var breadcrumb_height = $(".breadcrumb").outerHeight(true); var pageheader_height = $(".page-header").outerHeight(true); var launchbuttons_height = $(".launchButtons").height(); var height_offset = navbar_height + breadcrumb_height + pageheader_height + launchbuttons_height; var device_offset = device_position.offset(); var x = Math.round(device_offset.left + element_properties.device_width + element_properties.balloon_margin.x - sidebar_width); // 15 is magic pixel value that seems to make things line up var y = Math.round(device_offset.top + element_properties.balloon_margin.y - height_offset + 15); var $balloon = $('#' + balloon_id); $balloon.css({ 'left': '0px', 'top': y + 'px' }); var balloon_width = $balloon.outerWidth(); var left_x = device_offset.left - balloon_width - element_properties.balloon_margin.x - sidebar_width; var right_x = x + balloon_width + element_properties.balloon_margin.x + sidebar_width; if (left_x > 0 && right_x > $(window).outerWidth()) { x = left_x; $balloon.addClass('leftPosition'); } $balloon.css({ 'left': x + 'px' }).show(); $balloon.find('.delete-device').click(function(){ var $this = $(this); var delete_modal = horizon.datatables.confirm($this); delete_modal.find('.btn-primary').click(function () { $this.prop('disabled', true); d3.select('#id_' + $this.data('device-id')).classed('loading',true); self.delete_device($this.data('type'),$this.data('device-id')); horizon.modals.spinner.modal('hide'); }); }); $balloon.find('.delete-port').click(function(){ var $this = $(this); var delete_modal = horizon.datatables.confirm($this); delete_modal.find('.btn-primary').click(function () { $this.prop('disabled', true); self.delete_port($this.data('router-id'),$this.data('port-id'),$this.data('network-id')); horizon.modals.spinner.modal('hide'); }); }); self.balloon_id = balloon_id; }, delete_balloon:function() { var self = this; if(self.balloon_id) { $('#' + self.balloon_id).remove(); self.balloon_id = null; } } };