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 @@ ...@@ -22,6 +22,7 @@
"favico.js": "~0.3.5", "favico.js": "~0.3.5",
"datatables": "~1.10.4", "datatables": "~1.10.4",
"chart.js": "2.3.0", "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')) ...@@ -165,16 +165,19 @@ p = normpath(join(SITE_ROOT, '../../site-circle/static'))
if exists(p): if exists(p):
STATICFILES_DIRS.append(p) STATICFILES_DIRS.append(p)
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
PIPELINE = { PIPELINE = {
'COMPILERS' : ('pipeline.compilers.less.LessCompiler',), 'COMPILERS' : ('pipeline.compilers.less.LessCompiler',
'pipeline.compilers.es6.ES6Compiler', ),
'LESS_ARGUMENTS': u'--include-path={}'.format(':'.join(STATICFILES_DIRS)), 'LESS_ARGUMENTS': u'--include-path={}'.format(':'.join(STATICFILES_DIRS)),
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor', 'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'BABEL_ARGUMENTS': u'--presets env',
'JS_COMPRESSOR': None, 'JS_COMPRESSOR': None,
'DISABLE_WRAPPER': True, 'DISABLE_WRAPPER': True,
'STYLESHEETS': { 'STYLESHEETS': {
"all": {"source_filenames": ( "all": {
"source_filenames": (
"compile_bootstrap.less", "compile_bootstrap.less",
"bootstrap/dist/css/bootstrap-theme.css", "bootstrap/dist/css/bootstrap-theme.css",
"fontawesome/css/font-awesome.css", "fontawesome/css/font-awesome.css",
...@@ -187,10 +190,17 @@ PIPELINE = { ...@@ -187,10 +190,17 @@ PIPELINE = {
"autocomplete_light/select2.css", "autocomplete_light/select2.css",
), ),
"output_filename": "all.css", "output_filename": "all.css",
} },
"network-editor": {
"source_filenames": (
"network/editor.css",
),
"output_filename": "network-editor.css",
},
}, },
'JAVASCRIPT': { 'JAVASCRIPT': {
"all": {"source_filenames": ( "all": {
"source_filenames": (
# "jquery/dist/jquery.js", # included separately # "jquery/dist/jquery.js", # included separately
"bootbox/bootbox.js", "bootbox/bootbox.js",
"bootstrap/dist/js/bootstrap.js", "bootstrap/dist/js/bootstrap.js",
...@@ -203,6 +213,7 @@ PIPELINE = { ...@@ -203,6 +213,7 @@ PIPELINE = {
"autocomplete_light/autocomplete.init.js", "autocomplete_light/autocomplete.init.js",
"autocomplete_light/vendor/select2/dist/js/select2.js", "autocomplete_light/vendor/select2/dist/js/select2.js",
"autocomplete_light/select2.js", "autocomplete_light/select2.js",
"jsPlumb/dist/js/dom.jsPlumb-1.7.5-min.js",
"dashboard/dashboard.js", "dashboard/dashboard.js",
"dashboard/activity.js", "dashboard/activity.js",
"dashboard/group-details.js", "dashboard/group-details.js",
...@@ -225,7 +236,8 @@ PIPELINE = { ...@@ -225,7 +236,8 @@ PIPELINE = {
), ),
"output_filename": "all.js", "output_filename": "all.js",
}, },
"vm-detail": {"source_filenames": ( "vm-detail": {
"source_filenames": (
"clipboard/dist/clipboard.min.js", "clipboard/dist/clipboard.min.js",
"dashboard/vm-details.js", "dashboard/vm-details.js",
"no-vnc/include/util.js", "no-vnc/include/util.js",
...@@ -245,12 +257,20 @@ PIPELINE = { ...@@ -245,12 +257,20 @@ PIPELINE = {
), ),
"output_filename": "vm-detail.js", "output_filename": "vm-detail.js",
}, },
"datastore": {"source_filenames": ( "datastore": {
"source_filenames": (
"chart.js/dist/Chart.min.js", "chart.js/dist/Chart.min.js",
"dashboard/datastore-details.js" "dashboard/datastore-details.js"
), ),
"output_filename": "datastore.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: ...@@ -112,6 +112,7 @@ if DEBUG:
PIPELINE["COMPILERS"] = ( PIPELINE["COMPILERS"] = (
'dashboard.compilers.DummyLessCompiler', 'dashboard.compilers.DummyLessCompiler',
'pipeline.compilers.es6.ES6Compiler',
) )
ADMIN_ENABLED = True ADMIN_ENABLED = True
......
{% 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 ( ...@@ -31,7 +31,7 @@ from .views import (
FirewallList, FirewallDetail, FirewallCreate, FirewallDelete, FirewallList, FirewallDetail, FirewallCreate, FirewallDelete,
remove_host_group, add_host_group, remove_host_group, add_host_group,
remove_switch_port_device, add_switch_port_device, remove_switch_port_device, add_switch_port_device,
VlanAclUpdateView VlanAclUpdateView, NetworkEditorView
) )
urlpatterns = [ urlpatterns = [
...@@ -135,6 +135,10 @@ urlpatterns = [ ...@@ -135,6 +135,10 @@ urlpatterns = [
url('^vxlans/delete/(?P<vni>\d+)/$', VxlanDelete.as_view(), url('^vxlans/delete/(?P<vni>\d+)/$', VxlanDelete.as_view(),
name="network.vxlan-delete"), name="network.vxlan-delete"),
# editor
url('^editor/$', NetworkEditorView.as_view(),
name="network.editor"),
# non class based views # non class based views
url('^hosts/(?P<pk>\d+)/remove/(?P<group_pk>\d+)/$', remove_host_group, url('^hosts/(?P<pk>\d+)/remove/(?P<group_pk>\d+)/$', remove_host_group,
name='network.remove_host_group'), name='network.remove_host_group'),
......
...@@ -17,11 +17,12 @@ ...@@ -17,11 +17,12 @@
import logging import logging
import random import random
import json
from collections import OrderedDict from collections import OrderedDict
from netaddr import IPNetwork from netaddr import IPNetwork
from django.views.generic import ( from django.views.generic import (
TemplateView, UpdateView, DeleteView, CreateView TemplateView, UpdateView, DeleteView, CreateView,
) )
from django.core.exceptions import ( from django.core.exceptions import (
ValidationError, PermissionDenied, ImproperlyConfigured ValidationError, PermissionDenied, ImproperlyConfigured
...@@ -38,8 +39,8 @@ from firewall.models import ( ...@@ -38,8 +39,8 @@ from firewall.models import (
Host, Vlan, Domain, Group, Record, BlacklistItem, Rule, VlanGroup, Host, Vlan, Domain, Group, Record, BlacklistItem, Rule, VlanGroup,
SwitchPort, EthernetDevice, Firewall SwitchPort, EthernetDevice, Firewall
) )
from network.models import Vxlan from network.models import Vxlan, EditorElement
from vm.models import Interface from vm.models import Interface, Instance
from common.views import CreateLimitedResourceMixin from common.views import CreateLimitedResourceMixin
from acl.views import CheckedObjectMixin from acl.views import CheckedObjectMixin
from .tables import ( from .tables import (
...@@ -1107,6 +1108,192 @@ class VxlanDelete(LoginRequiredMixin, CheckedObjectMixin, DeleteView): ...@@ -1107,6 +1108,192 @@ class VxlanDelete(LoginRequiredMixin, CheckedObjectMixin, DeleteView):
return context 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): def remove_host_group(request, **kwargs):
host = Host.objects.get(pk=kwargs['pk']) host = Host.objects.get(pk=kwargs['pk'])
group = Group.objects.get(pk=kwargs['group_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