From 60b6d9797c230f597133f378f2fef4dd3beac4d9 Mon Sep 17 00:00:00 2001 From: Czémán Arnold <czeman.arnold@cloud.bme.hu> Date: Fri, 1 Dec 2017 01:14:52 +0100 Subject: [PATCH] network, settings: add basic network topology editor, eleminate pipeline cache, add ES6 compiler --- circle/bower.json | 3 ++- circle/circle/settings/base.py | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------- circle/circle/settings/local.py | 1 + circle/network/static/network/editor.es6 | 400 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/network/templates/network/editor.html | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/network/urls.py | 6 +++++- circle/network/views.py | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 7 files changed, 741 insertions(+), 75 deletions(-) create mode 100644 circle/network/static/network/editor.es6 create mode 100644 circle/network/templates/network/editor.html diff --git a/circle/bower.json b/circle/bower.json index a0695bd..8463de2 100644 --- a/circle/bower.json +++ b/circle/bower.json @@ -22,6 +22,7 @@ "favico.js": "~0.3.5", "datatables": "~1.10.4", "chart.js": "2.3.0", - "clipboard": "~1.6.1" + "clipboard": "~1.6.1", + "jsPlumb": "2.5.7" } } diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index ab6d3fe..45b1864 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -165,92 +165,112 @@ p = normpath(join(SITE_ROOT, '../../site-circle/static')) if exists(p): STATICFILES_DIRS.append(p) -STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' +STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage' PIPELINE = { - 'COMPILERS' : ('pipeline.compilers.less.LessCompiler',), + 'COMPILERS' : ('pipeline.compilers.less.LessCompiler', + 'pipeline.compilers.es6.ES6Compiler', ), 'LESS_ARGUMENTS': u'--include-path={}'.format(':'.join(STATICFILES_DIRS)), 'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor', + 'BABEL_ARGUMENTS': u'--presets env', 'JS_COMPRESSOR': None, 'DISABLE_WRAPPER': True, 'STYLESHEETS': { - "all": {"source_filenames": ( - "compile_bootstrap.less", - "bootstrap/dist/css/bootstrap-theme.css", - "fontawesome/css/font-awesome.css", - "jquery-simple-slider/css/simple-slider.css", - "intro.js/introjs.css", - "template.less", - "dashboard/dashboard.less", - "network/network.less", - "autocomplete_light/vendor/select2/dist/css/select2.css", - "autocomplete_light/select2.css", - ), + "all": { + "source_filenames": ( + "compile_bootstrap.less", + "bootstrap/dist/css/bootstrap-theme.css", + "fontawesome/css/font-awesome.css", + "jquery-simple-slider/css/simple-slider.css", + "intro.js/introjs.css", + "template.less", + "dashboard/dashboard.less", + "network/network.less", + "autocomplete_light/vendor/select2/dist/css/select2.css", + "autocomplete_light/select2.css", + ), "output_filename": "all.css", - } + }, + "network-editor": { + "source_filenames": ( + "network/editor.css", + ), + "output_filename": "network-editor.css", + }, }, 'JAVASCRIPT': { - "all": {"source_filenames": ( - # "jquery/dist/jquery.js", # included separately - "bootbox/bootbox.js", - "bootstrap/dist/js/bootstrap.js", - "intro.js/intro.js", - "jquery-knob/dist/jquery.knob.min.js", - "jquery-simple-slider/js/simple-slider.js", - "favico.js/favico.js", - "datatables/media/js/jquery.dataTables.js", - "autocomplete_light/jquery.init.js", - "autocomplete_light/autocomplete.init.js", - "autocomplete_light/vendor/select2/dist/js/select2.js", - "autocomplete_light/select2.js", - "dashboard/dashboard.js", - "dashboard/activity.js", - "dashboard/group-details.js", - "dashboard/group-list.js", - "dashboard/js/stupidtable.min.js", # no bower file - "dashboard/node-create.js", - "dashboard/node-details.js", - "dashboard/node-list.js", - "dashboard/profile.js", - "dashboard/store.js", - "dashboard/template-list.js", - "dashboard/vm-common.js", - "dashboard/vm-create.js", - "dashboard/vm-list.js", - "dashboard/help.js", - "js/host.js", - "js/network.js", - "js/switch-port.js", - "js/host-list.js", - ), + "all": { + "source_filenames": ( + # "jquery/dist/jquery.js", # included separately + "bootbox/bootbox.js", + "bootstrap/dist/js/bootstrap.js", + "intro.js/intro.js", + "jquery-knob/dist/jquery.knob.min.js", + "jquery-simple-slider/js/simple-slider.js", + "favico.js/favico.js", + "datatables/media/js/jquery.dataTables.js", + "autocomplete_light/jquery.init.js", + "autocomplete_light/autocomplete.init.js", + "autocomplete_light/vendor/select2/dist/js/select2.js", + "autocomplete_light/select2.js", + "jsPlumb/dist/js/dom.jsPlumb-1.7.5-min.js", + "dashboard/dashboard.js", + "dashboard/activity.js", + "dashboard/group-details.js", + "dashboard/group-list.js", + "dashboard/js/stupidtable.min.js", # no bower file + "dashboard/node-create.js", + "dashboard/node-details.js", + "dashboard/node-list.js", + "dashboard/profile.js", + "dashboard/store.js", + "dashboard/template-list.js", + "dashboard/vm-common.js", + "dashboard/vm-create.js", + "dashboard/vm-list.js", + "dashboard/help.js", + "js/host.js", + "js/network.js", + "js/switch-port.js", + "js/host-list.js", + ), "output_filename": "all.js", }, - "vm-detail": {"source_filenames": ( - "clipboard/dist/clipboard.min.js", - "dashboard/vm-details.js", - "no-vnc/include/util.js", - "no-vnc/include/webutil.js", - "no-vnc/include/base64.js", - "no-vnc/include/websock.js", - "no-vnc/include/des.js", - "no-vnc/include/keysym.js", - "no-vnc/include/keysymdef.js", - "no-vnc/include/keyboard.js", - "no-vnc/include/input.js", - "no-vnc/include/display.js", - "no-vnc/include/jsunzip.js", - "no-vnc/include/rfb.js", - "dashboard/vm-console.js", - "dashboard/vm-tour.js", - ), + "vm-detail": { + "source_filenames": ( + "clipboard/dist/clipboard.min.js", + "dashboard/vm-details.js", + "no-vnc/include/util.js", + "no-vnc/include/webutil.js", + "no-vnc/include/base64.js", + "no-vnc/include/websock.js", + "no-vnc/include/des.js", + "no-vnc/include/keysym.js", + "no-vnc/include/keysymdef.js", + "no-vnc/include/keyboard.js", + "no-vnc/include/input.js", + "no-vnc/include/display.js", + "no-vnc/include/jsunzip.js", + "no-vnc/include/rfb.js", + "dashboard/vm-console.js", + "dashboard/vm-tour.js", + ), "output_filename": "vm-detail.js", }, - "datastore": {"source_filenames": ( - "chart.js/dist/Chart.min.js", - "dashboard/datastore-details.js" - ), + "datastore": { + "source_filenames": ( + "chart.js/dist/Chart.min.js", + "dashboard/datastore-details.js" + ), "output_filename": "datastore.js", }, + "network-editor": { + "source_filenames": ( + "jsPlumb/dist/js/jsplumb.min.js", + "network/editor.es6", + ), + "output_filename": "network-editor.js", + }, }, } diff --git a/circle/circle/settings/local.py b/circle/circle/settings/local.py index cc5b2ae..788fb3f 100644 --- a/circle/circle/settings/local.py +++ b/circle/circle/settings/local.py @@ -112,6 +112,7 @@ if DEBUG: PIPELINE["COMPILERS"] = ( 'dashboard.compilers.DummyLessCompiler', + 'pipeline.compilers.es6.ES6Compiler', ) ADMIN_ENABLED = True diff --git a/circle/network/static/network/editor.es6 b/circle/network/static/network/editor.es6 new file mode 100644 index 0000000..1ae3c5d --- /dev/null +++ b/circle/network/static/network/editor.es6 @@ -0,0 +1,400 @@ +/* jshint esversion: 6 */ + +function renderListElement(elem){ + return ` +<div class="unused-element" + type="${ elem.type }" + description="${ elem.description }" + id="${ elem.id }" + icon="${ elem.icon }" + name="${ elem.name }" + free_port_num="${ elem.free_port_num }"> + <i class="fa ${ elem.type == 'vm' ? 'fa-desktop' : 'fa-sitemap' }"></i> + ${ elem.name } +</div>`; +} + +function renderBoardElement(elem){ + return ` +<div class="element" + name="${ elem.name }" + type="${ elem.type }" + id="${ elem.id }" + description="${ elem.description }" + icon="${ elem.icon }" + free_port_num="${ elem.free_port_num }" + ondragstart="return false;"> + <i class="fa ${ elem.type == 'vm' ? 'fa-desktop' : 'fa-sitemap'}"></i> + ${ elem.name } +</div>`; +} + + +var add_interfaces = []; +var remove_interfaces = []; +var old_connections = []; + +var add_nodes = new Set(); +var remove_nodes = new Set(); +var old_nodes = new Set(); + +function convertConnection(connection) { + var con = { + source: connection.source.id, + target: connection.target.id, + equals: function(other) { + return this.source === other.source && + this.target === other.target; + } + }; + + if(con.source.startsWith('net')){ + var tmp = con.source; + con.source = con.target; + con.target = tmp; + } + + con.source = con.source.slice(3); + con.target = con.target.slice(4); + + return con; +} + +function uniqueAddToList(list, value){ + var hit = list.find((val) => { + return val.equals(value); + }); + if(hit) return; + list.push(value); +} + +function removeFromList(list, value){ + var index = list.findIndex((val) => { + return val.equals(value); + }); + if(index === -1) return; + list.splice(index, 1); +} + +function isOldConnection(connection){ + return old_connections.find((val) => { + return val.equals(connection); + }); +} + +function cleanListElements(list){ + return list.map((value) => { + return { + source: value.source, + target: value.target + }; + }); +} + +function addInterface(connection) { + var con = convertConnection(connection); + if(!isOldConnection(con)) + uniqueAddToList(add_interfaces, con); + removeFromList(remove_interfaces, con); +} + +function removeInterface(connection) { + var con = convertConnection(connection); + if(isOldConnection(con)) + uniqueAddToList(remove_interfaces, con); + removeFromList(add_interfaces, con); +} + +function getNodeId(node) { + return node.id; +} + +function isOldNode(id) { + return old_nodes.has(id); +} + +function addNode(node) { + var id = getNodeId(node); + add_nodes.add(id); + remove_nodes.delete(id); +} + +function removeNode(node) { + var id = getNodeId(node); + if(isOldNode(id)) + remove_nodes.add(id); + add_nodes.delete(id); +} + +function checkLoopback(connection) { + return connection.target !== connection.source; +} + +function checkCompatibility(connection) { + var target_type = $(connection.target).attr('type'); + var source_type = $(connection.source).attr('type'); + + return target_type !== source_type; +} + +// Before the connection is established +function beforeDrop(info) { + return checkCompatibility(info.connection); +} + +function onConnect(info) { + addInterface(info.connection); +} + +function onDetach(info) { + removeInterface(info.connection); +} + +function FakeConnection(sourceId, targetId) { + this.source = {id: sourceId}; + this.target = {id: targetId}; + return this; +} + +function onConnectionMoved(info) { + var fakeOrigCon = new FakeConnection(info.originalSourceId, + info.originalTargetId); + removeInterface(fakeOrigCon); + var fakeNewCon = new FakeConnection(info.newSourceId, + info.newTargetId); + addInterface(fakeNewCon); +} + +function onConnectionAborted(connection) { + addInterface(connection); +} + +function randInt(from, to) { + if(from > to){ + var tmp = to; + to = from; + from = tmp; + } + var size = to - from; + return Math.floor((Math.random() * size) + from); +} + +function convertElement(elem) { + return { + id: elem.attr('id'), + type: elem.attr('type'), + description: elem.attr('description'), + name: elem.attr('name'), + icon: elem.attr('icon'), + free_port_num: elem.attr('free_port_num') + }; +} + +function convertListForSaving(list) { + const getId = (id, type) => + (type === 'vm') ? id.slice(3) : id.slice(4); + var retv = []; + list.forEach( (i, id) => { + var e=$('#' + id); + retv.push({ + id: getId(e.attr('id'), e.attr('type')), + type: e.attr('type'), + x: e.css('left').replace('px', ''), + y: e.css('top').replace('px', ''), + free_port_num: e.attr('free_port_num') + }); + }); + return retv; +} + + +class SwitchButton { + constructor(id, checked) { + this.element = $('#' + id); + this.element.css('cursor', 'pointer'); + this.afterChanged(() => {}); + this.setClass(checked); + } + + setClass(checked){ + this.element.removeClass('fa'); + this.element.removeClass('fa-toggle-on'); + this.element.removeClass('fa-toggle-off'); + this.element.addClass('fa'); + this.element.addClass( checked ? 'fa-toggle-on' : 'fa-toggle-off'); + } + + isChecked() { + return this.element.hasClass('fa-toggle-on'); + } + + afterChanged(func) { + this.element.off('click'); + var thiz = this; + this.element.click(function() { + thiz.setClass(!thiz.isChecked()); + func(); + }); + } +} + + +jsPlumb.ready(() => { + var endpointOptions = { + anchor: 'Continuous', + isSource: true, + isTarget: true, + maxConnections: 1 + }; + var jsPlumbInstance = jsPlumb; + + jsPlumbInstance.setContainer('#dropContainer'); + jsPlumbInstance.bind('beforeDrop', beforeDrop); + jsPlumbInstance.bind('connection', onConnect); + jsPlumbInstance.bind('connectionDetached', onDetach); + jsPlumbInstance.bind('connectionMoved', onConnectionMoved); + jsPlumbInstance.bind('connectionAborted', onConnectionAborted); + + function connectEndpoints(connection) { + return jsPlumbInstance.connect(connection, { + allowLoopback: false, + newConnection: true, + anchor: 'Continuous', + deleteEndpointsOnDetach: true + }); + } + + function addEndpoint(elem){ + jsPlumbInstance.addEndpoint(elem.id, endpointOptions); + } + + function generatePosition(){ + var dc = $('#dropContainer'); + var width = dc.css("width").replace('px', ''); + var height = dc.css("height").replace('px', ''); + return { + x: randInt(0, width), + y: randInt(0, height) + }; + } + + function addElementToBoard(element, random) { + var pos; + if(random) + pos = generatePosition(); + else + pos = { + x: element.x, + y: element.y + }; + + var newe = $(renderBoardElement(element)) + .css('top', pos.y + 'px') + .css('left', pos.x + 'px')[0]; + + $('#dropContainer').append(newe); + + for(var i = 0; i < element.free_port_num; ++i){ + addEndpoint(newe); + } + jsPlumbInstance.draggable(newe.id, {containment: true}); + jsPlumbInstance.repaint(newe.id); + + $(newe).bind('contextmenu', removeElement); + jsPlumbInstance.repaintEverything(); + } + + function selectElementFromList(ev) { + var elem = $(ev.target); + var obj = convertElement(elem); + elem.detach(); + addElementToBoard(obj, true); + addNode(obj); + } + + function addElementToList(elem) { + $('#dragContainer').append(renderListElement(elem)); + $('#' + elem.id).click(selectElementFromList); + } + + function removeElement(ev) { + var elem = $(ev.target); + var obj = convertElement(elem); + jsPlumbInstance.removeAllEndpoints(elem.attr('id')); + elem.detach(); + addElementToList(obj); + removeNode(obj); + } + + function clearWorkspace() { + jsPlumbInstance.deleteEveryConnection(); + jsPlumbInstance.deleteEveryEndpoint(); + $('.element').detach(); + $('.unused-element').detach(); + } + + function initialize(result){ + clearWorkspace(); + + old_nodes = new Set(); + add_nodes = new Set(); + remove_nodes = new Set(); + + $.each(result.elements, (i, element) => { + addElementToBoard(element); + old_nodes.add(element.id); + add_nodes.add(element.id); + }); + $.each(result.nongraph_elements, (i, element) => { + addElementToBoard(element, true); + add_nodes.add(element.id); + }); + $.each(result.unused_elements, (i, element) => { + addElementToList(element); + }); + old_connections = []; + $.each(result.connections, (i, connection) => { + var con = connectEndpoints(connection); + old_connections.push(convertConnection(con)); + }); + add_interfaces = []; // Because of a 'connect' event, + // connections are added to this list, + // but this is not necessary. + remove_interfaces = []; + } + + function save() { + var data = { + add_interfaces: cleanListElements(add_interfaces), + remove_interfaces: cleanListElements(remove_interfaces), + add_nodes: convertListForSaving(add_nodes), + remove_nodes: convertListForSaving(remove_nodes) + }; + $.post('', JSON.stringify(data), initialize, 'json'); + } + + $.get('', initialize); + $("#saveButton").click(save); + + $('#searchField').on('keyup', filter); + + var vmFilter = new SwitchButton('vm-filter', true); + vmFilter.afterChanged(filter); + var netFilter = new SwitchButton('net-filter', true); + netFilter.afterChanged(filter); + + function filter() { + $(".unused-element").each((i, elem) => { + elem = $(elem); + elem.hide(); + var key = $("#searchField").val().toLowerCase(); + var type = elem.attr('type'); + var network_on = netFilter.isChecked(); + var vm_on = vmFilter.isChecked(); + if(elem.attr("name").toLowerCase().indexOf(key) >= 0 && + ((type === "network" && network_on) || (type === "vm" && vm_on))) + elem.show(); + }); + } + +}); diff --git a/circle/network/templates/network/editor.html b/circle/network/templates/network/editor.html new file mode 100644 index 0000000..8e317e9 --- /dev/null +++ b/circle/network/templates/network/editor.html @@ -0,0 +1,53 @@ +{% extends "dashboard/base.html" %} +{% load staticfiles %} +{% load i18n %} +{% load pipeline %} + +{% block title-page %}{% trans 'Network Editor' %}{% endblock %} + +{% block extra_css %} + {% stylesheet "network-editor" %} +{% endblock %} + +{% block content %} + +<div class="flex-container" id="workspace"> + + <div class="panel panel-default text-center" id="dragPanel"> + <div class="panel-heading"> + <div class="row"> + <div class="col-md-9 text-left"> + <h3 class="no-margin"><i class="fa fa-sitemap"></i> {% trans 'Editor' %}</h3> + </div> + <div class="col-md-3 text-left"> + <button class="btn btn-success btn-xs" id="saveButton"><i class="fa fa-floppy-o"></i></button> + </div> + </div> + </div> + <div class="panel-heading text-center"> + <div id="filterConatiner"> + <div class="row"> + <input type="text" class="form-control" id="searchField" placeholder="{% trans 'Search' %}"/><br /> + </div> + <div class="row"> + <div class="col-md-6"> + <i id="vm-filter"></i> <i class="fa fa-desktop"></i> vm + </div> + <div class="col-md-6"> + <i id="net-filter"></i> <i class="fa fa-sitemap"></i> net + </div> + </div> + </div> + </div> + <div class="panel-body" id="dragContainer"> + </div> + </div> + + <div class="" id="dropContainer" oncontextmenu="return false;"></div> + +</div> +{% endblock %} + +{% block extra_js %} + {% javascript "network-editor" %} +{% endblock %} diff --git a/circle/network/urls.py b/circle/network/urls.py index 71e0c32..1dd5931 100644 --- a/circle/network/urls.py +++ b/circle/network/urls.py @@ -31,7 +31,7 @@ from .views import ( FirewallList, FirewallDetail, FirewallCreate, FirewallDelete, remove_host_group, add_host_group, remove_switch_port_device, add_switch_port_device, - VlanAclUpdateView + VlanAclUpdateView, NetworkEditorView ) urlpatterns = [ @@ -135,6 +135,10 @@ urlpatterns = [ url('^vxlans/delete/(?P<vni>\d+)/$', VxlanDelete.as_view(), name="network.vxlan-delete"), + # editor + url('^editor/$', NetworkEditorView.as_view(), + name="network.editor"), + # non class based views url('^hosts/(?P<pk>\d+)/remove/(?P<group_pk>\d+)/$', remove_host_group, name='network.remove_host_group'), diff --git a/circle/network/views.py b/circle/network/views.py index afc7741..cf0a89e 100644 --- a/circle/network/views.py +++ b/circle/network/views.py @@ -17,11 +17,12 @@ import logging import random +import json from collections import OrderedDict from netaddr import IPNetwork from django.views.generic import ( - TemplateView, UpdateView, DeleteView, CreateView + TemplateView, UpdateView, DeleteView, CreateView, ) from django.core.exceptions import ( ValidationError, PermissionDenied, ImproperlyConfigured @@ -38,8 +39,8 @@ from firewall.models import ( Host, Vlan, Domain, Group, Record, BlacklistItem, Rule, VlanGroup, SwitchPort, EthernetDevice, Firewall ) -from network.models import Vxlan -from vm.models import Interface +from network.models import Vxlan, EditorElement +from vm.models import Interface, Instance from common.views import CreateLimitedResourceMixin from acl.views import CheckedObjectMixin from .tables import ( @@ -1107,6 +1108,192 @@ class VxlanDelete(LoginRequiredMixin, CheckedObjectMixin, DeleteView): return context +class NetworkEditorView(LoginRequiredMixin, TemplateView): + template_name = 'network/editor.html' + + def get(self, *args, **kwargs): + if self.request.is_ajax(): + connections = self._get_connections() + + ngelements = self._get_nongraph_elements(connections) + ngelements = self._serialize_elements(ngelements) + + connections = map(lambda con: { + 'source': 'vm-%s' % con['source'].pk, + 'target': 'net-%s' % con['target'].vni, + }, connections['connections']) + + unused_elements = self._get_unused_elements() + unused_elements = self._serialize_elements(unused_elements) + + return JsonResponse({ + 'elements': map(lambda e: e.as_data(), + EditorElement.objects.filter( + owner=self.request.user)), + 'nongraph_elements': ngelements, + 'unused_elements': unused_elements, + 'connections': connections, + }) + return super(NetworkEditorView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + data = json.loads(self.request.body) + add_ifs = data.get('add_interfaces', []) + remove_ifs = data.get('remove_interfaces', []) + add_nodes = data.get('add_nodes', []) + remove_nodes = data.get('remove_nodes', []) + + # Add editor element + self._element_list_operation(add_nodes, self._update_element) + # Remove editor element + self._element_list_operation(remove_nodes, self._remove_element) + + # Add interface + self._interface_list_operation(add_ifs, self._add_interface) + # Remove interface + self._interface_list_operation(remove_ifs, self._remove_interface) + + return self.get(*args, **kwargs) + + def _max_port_num_helper(self, model, attr_name): + if not hasattr(self, attr_name): + value = model.get_objects_with_level( + 'user', self.request.user).count() + setattr(self, attr_name, value) + return getattr(self, attr_name) + + @property + def vm_max_port_num(self): + return self._max_port_num_helper(Vxlan, '_vm_max_port_num') + + @property + def vxlan_max_port_num(self): + return self._max_port_num_helper(Instance, '_vxlan_max_port_num') + + def _vm_serializer(self, vm): + max_port_num = self.vm_max_port_num + vxlans = Vxlan.get_objects_with_level( + 'user', self.request.user).values_list('pk', flat=True) + free_port_num = max_port_num - vm.interface_set.filter( + vxlan__pk__in=vxlans).count() + return { + 'name': unicode(vm), + 'id': 'vm-%s' % vm.pk, + 'description': vm.description, + 'type': 'vm', + 'icon': 'fa-desktop', + 'free_port_num': free_port_num, + } + + def _vxlan_serializer(self, vxlan): + max_port_num = self.vxlan_max_port_num + vms = Instance.get_objects_with_level( + 'user', self.request.user).values_list('pk', flat=True) + free_port_num = max_port_num - Interface.objects.filter( + vxlan=vxlan, instance__pk__in=vms).count() + return { + 'name': vxlan.name, + 'id': 'net-%s' % vxlan.vni, + 'description': vxlan.description, + 'type': 'network', + 'icon': 'fa-sitemap', + 'free_port_num': free_port_num, + } + + def _get_unused_elements(self): + connections = self._get_connections() + vms = map(lambda vm: vm.id, connections['vms']) + vxlans = map(lambda vxlan: vxlan.vni, connections['vxlans']) + eelems = EditorElement.objects.filter(owner=self.request.user) + + vm_query = Q(pk__in=vms) | Q(editor_elements__in=eelems) + vms = Instance.get_objects_with_level( + 'user', self.request.user).exclude(vm_query) + vxlan_query = Q(vni__in=vms) | Q(editor_elements__in=eelems) + vxlans = Vxlan.get_objects_with_level( + 'user', self.request.user).exclude(vxlan_query) + return { + 'vms': vms, + 'vxlans': vxlans, + } + + def _get_nongraph_elements(self, connections): + return { + 'vms': filter(lambda v: not v.editor_elements.exists(), + connections['vms']), + 'vxlans': filter(lambda v: not v.editor_elements.exists(), + connections['vxlans']), + } + + def _get_connections(self): + """ Returns connections and theirs participants. """ + vms = Instance.get_objects_with_level('user', self.request.user) + connections = [] + vm_set = set() + vxlan_set = set() + for vm in vms: + for intf in vm.interface_set.filter(vxlan__isnull=False): + vm_set.add(vm) + vxlan_set.add(intf.vxlan) + connections.append({ + 'source': vm, + 'target': intf.vxlan, + }) + return { + 'connections': connections, + 'vms': vm_set, + 'vxlans': vxlan_set, + } + + def _serialize_elements(self, elements): + return (map(self._vm_serializer, elements['vms']) + + map(self._vxlan_serializer, elements['vxlans'])) + + def _get_modifiable_object(self, model, connection, + attr_name, filter_attr): + value = connection.get(attr_name) + if value is not None: + value = model.get_objects_with_level( + 'user', self.request.user).filter( + **{filter_attr: value}).first() + return value + + def _element_list_operation(self, node_list, operation): + for e in node_list: + elem = dict(e) + type = elem.pop('type') + id = elem.pop('id') + model = Instance if type == 'vm' else Vxlan + filter = {'pk': id} if type == 'vm' else {'vni': id} + object = model.get_objects_with_level( + 'user', self.request.user).get(**filter) + operation(object.editor_elements, elem) + + def _update_element(self, elements, elem): + elements.update_or_create(owner=self.request.user, + defaults=elem) + + def _remove_element(self, elements, elem): + elements.filter(owner=self.request.user).delete() + + def _interface_list_operation(self, if_list, operation): + for con in if_list: + vm = self._get_modifiable_object(Instance, con, 'source', 'pk') + vxlan = self._get_modifiable_object(Vxlan, con, 'target', 'vni') + if vm and vxlan: + operation(vm, vxlan) + + def _add_interface(self, vm, vxlan): + vm.add_user_interface( + user=self.request.user, vxlan=vxlan, system=vm.system) + + def _remove_interface(self, vm, vxlan): + intf = vm.interface_set.filter(vxlan=vxlan).first() + if intf: + vm.remove_user_interface( + interface=intf, user=self.request.user, system=vm.system) + + def remove_host_group(request, **kwargs): host = Host.objects.get(pk=kwargs['pk']) group = Group.objects.get(pk=kwargs['group_pk']) -- libgit2 0.26.0