Commit 60b6d979 by Czémán Arnold

network, settings: add basic network topology editor, eleminate pipeline cache, add ES6 compiler

parent da10884e
......@@ -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"
}
}
......@@ -165,16 +165,19 @@ 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": (
"all": {
"source_filenames": (
"compile_bootstrap.less",
"bootstrap/dist/css/bootstrap-theme.css",
"fontawesome/css/font-awesome.css",
......@@ -187,10 +190,17 @@ PIPELINE = {
"autocomplete_light/select2.css",
),
"output_filename": "all.css",
}
},
"network-editor": {
"source_filenames": (
"network/editor.css",
),
"output_filename": "network-editor.css",
},
},
'JAVASCRIPT': {
"all": {"source_filenames": (
"all": {
"source_filenames": (
# "jquery/dist/jquery.js", # included separately
"bootbox/bootbox.js",
"bootstrap/dist/js/bootstrap.js",
......@@ -203,6 +213,7 @@ PIPELINE = {
"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",
......@@ -225,7 +236,8 @@ PIPELINE = {
),
"output_filename": "all.js",
},
"vm-detail": {"source_filenames": (
"vm-detail": {
"source_filenames": (
"clipboard/dist/clipboard.min.js",
"dashboard/vm-details.js",
"no-vnc/include/util.js",
......@@ -245,12 +257,20 @@ PIPELINE = {
),
"output_filename": "vm-detail.js",
},
"datastore": {"source_filenames": (
"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",
},
},
}
......
......@@ -112,6 +112,7 @@ if DEBUG:
PIPELINE["COMPILERS"] = (
'dashboard.compilers.DummyLessCompiler',
'pipeline.compilers.es6.ES6Compiler',
)
ADMIN_ENABLED = True
......
/* 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();
});
}
});
{% 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 %}
......@@ -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'),
......
......@@ -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'])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment