Commit a3bbce2c by Kálmán Viktor

Merge branch 'master' into issue-364

Conflicts:
	circle/dashboard/static/dashboard/dashboard.less
parents 5e6f5435 b0fd61ef
......@@ -35,9 +35,12 @@ circle/*.key
circle/*.pem
# collected static files:
circle/static
circle/static_collected
circle/bower_components
# jsi18n files
jsi18n
scripts.rc
# less
*.css
{
"name": "circle",
"version": "0.0.0",
"license": "GPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"bootstrap": "~3.2.0",
"fontawesome": "~4.2.0",
"jquery": "~2.1.1",
"no-vnc": "*",
"jquery-knob": "~1.2.9",
"jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git",
"bootbox": "~4.3.0",
"intro.js": "0.9.0"
}
}
......@@ -160,10 +160,88 @@ STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
########## END STATIC FILE CONFIGURATION
STATICFILES_DIRS = [normpath(join(SITE_ROOT, 'bower_components'))]
p = normpath(join(SITE_ROOT, '../../site-circle/static'))
if exists(p):
STATICFILES_DIRS = (p, )
STATICFILES_DIRS.append(p)
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
PIPELINE_COMPILERS = (
'pipeline.compilers.less.LessCompiler',
)
PIPELINE_CSS_COMPRESSOR = 'pipeline.compressors.yuglify.YuglifyCompressor'
PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.slimit.SlimItCompressor'
PIPELINE_DISABLE_WRAPPER = True
PIPELINE_LESS_ARGUMENTS = u'--include-path={}'.format(':'.join(STATICFILES_DIRS))
PIPELINE_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/style.css",
),
"output_filename": "all.css",
}
}
PIPELINE_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",
"dashboard/dashboard.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",
"js/host.js",
"js/network.js",
"js/switch-port.js",
"autocomplete_light/autocomplete.js",
"autocomplete_light/widget.js",
"autocomplete_light/addanother.js",
"autocomplete_light/text_widget.js",
"autocomplete_light/remote.js",
),
"output_filename": "all.js",
},
"vm-detail": {"source_filenames": (
"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",
},
}
########## SECRET CONFIGURATION
......@@ -266,6 +344,7 @@ THIRD_PARTY_APPS = (
'statici18n',
'django_sshkey',
'autocomplete_light',
'pipeline',
)
# Apps specific for this project go here.
......
......@@ -109,3 +109,7 @@ CRISPY_FAIL_SILENTLY = not DEBUG
if DEBUG:
from django.dispatch import Signal
Signal.send_robust = Signal.send
PIPELINE_DISABLED_COMPILERS = (
'pipeline.compilers.less.LessCompiler',
)
......@@ -58,3 +58,6 @@ for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level}
# Forbid store usage
STORE_URL = ""
# buildbot doesn't love pipeline
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
from django.core.management.base import BaseCommand
from common.management.commands.watch import LessUtils
class Command(BaseCommand):
help = "Compiles all LESS files."
def handle(self, *args, **kwargs):
print("Compiling LESS")
LessUtils.initial_compile()
import subprocess
import os
import pyinotify
from django.core.management.base import BaseCommand
from django.conf import settings
STATIC_FILES = u'--include-path={}'.format(':'.join(settings.STATICFILES_DIRS))
IGNORED_FOLDERS = ("static_collected", "bower_components", )
class LessUtils(object):
@staticmethod
def less_path_to_css_path(pathname):
return "%s.css" % pathname[:-1 * len(".less")]
@staticmethod
def compile_less(less_pathname, css_pathname):
cmd = ["lessc", STATIC_FILES, less_pathname, css_pathname]
print("\n%s" % ("=" * 30))
print("Compiling: %s" % os.path.basename(less_pathname))
try:
subprocess.check_output(cmd)
except subprocess.CalledProcessError as e:
print(e.output)
else:
print("Successfully compiled:\n%s\n->\n%s" % (
less_pathname, css_pathname))
@staticmethod
def initial_compile():
""" Walks through the project looking for LESS files
and compiles them into CSS.
"""
for root, dirs, files in os.walk(settings.SITE_ROOT):
for f in files:
if not f.endswith(".less"):
continue
relpath = os.path.relpath(root, settings.SITE_ROOT)
if relpath.startswith(IGNORED_FOLDERS):
continue
less_pathname = "%s/%s" % (root, f)
css_pathname = LessUtils.less_path_to_css_path(less_pathname)
LessUtils.compile_less(less_pathname, css_pathname)
@staticmethod
def start_watch():
""" Watches for changes in LESS files recursively from the
project's root and compiles the files
"""
wm = pyinotify.WatchManager()
class EventHandler(pyinotify.ProcessEvent):
def process_IN_MODIFY(self, event):
if not event.name.endswith(".less"):
return
relpath = os.path.relpath(event.pathname, settings.SITE_ROOT)
if relpath.startswith(IGNORED_FOLDERS):
return
css_pathname = LessUtils.less_path_to_css_path(event.pathname)
LessUtils.compile_less(event.pathname, css_pathname)
handler = EventHandler()
notifier = pyinotify.Notifier(wm, handler)
wm.add_watch(settings.SITE_ROOT, pyinotify.IN_MODIFY, rec=True)
notifier.loop()
class Command(BaseCommand):
help = "Compiles all LESS files then watches for changes."
def handle(self, *args, **kwargs):
# for first run compile everything
print("Initial LESS compiles")
LessUtils.initial_compile()
print("\n%s\n" % ("=" * 30))
print("End of initial LESS compiles\n")
# after first run watch less files
LessUtils.start_watch()
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from firewall.models import (Vlan, VlanGroup, Domain, Firewall, Rule,
SwitchPort, EthernetDevice, Host)
from storage.models import DataStore
from vm.models import Lease
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--force', action="store_true"),
make_option('--portal-ip'),
make_option('--external-net'),
make_option('--management-net'),
make_option('--vm-net'),
make_option('--external-if'),
make_option('--management-if'),
make_option('--trunk-if'),
make_option('--datastore-queue'),
make_option('--firewall-queue'),
make_option('--admin-user'),
make_option('--admin-pass'),
)
def create(self, model, field, **kwargs):
value = kwargs[field]
qs = model.objects.filter(**{field: value})[:1]
if not qs.exists():
obj = model.objects.create(**kwargs)
self.changed.append('New %s: %s' % (model, obj))
return obj
else:
return qs[0]
# http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html
def print_state(self):
changed = "yes" if len(self.changed) else "no"
print "\nchanged=%s comment='%s'" % (changed, ", ".join(self.changed))
def handle(self, *args, **options):
self.changed = []
if (DataStore.objects.exists() and Vlan.objects.exists()
and not options['force']):
return self.print_state()
admin = self.create(User, 'username', username=options['admin_user'],
is_superuser=True, is_staff=True)
admin.set_password(options['admin_pass'])
admin.save()
self.create(DataStore, 'path', path='/datastore', name='default',
hostname=options['datastore_queue'])
# leases
self.create(Lease, 'name', name='lab',
suspend_interval_seconds=3600 * 5,
delete_interval_seconds=3600 * 24 * 7)
self.create(Lease, 'name', name='project',
suspend_interval_seconds=3600 * 24 * 30,
delete_interval_seconds=3600 * 24 * 30 * 6)
self.create(Lease, 'name', name='server',
suspend_interval_seconds=3600 * 24 * 365,
delete_interval_seconds=3600 * 24 * 365 * 3)
domain = self.create(Domain, 'name', name='example.com', owner=admin)
# vlans
net = self.create(Vlan, 'name', name='net', vid=4,
network4=options['external_net'], domain=domain)
man = self.create(Vlan, 'name', name='man', vid=3, dhcp_pool='manual',
network4=options['management_net'], domain=domain,
snat_ip=options['external_net'].split('/')[0])
man.snat_to.add(net)
man.snat_to.add(man)
vm = self.create(Vlan, 'name', name='vm', vid=2, dhcp_pool='manual',
network4=options['vm_net'], domain=domain,
snat_ip=options['external_net'].split('/')[0])
vm.snat_to.add(net)
vm.snat_to.add(vm)
# default vlan groups
vg_all = self.create(VlanGroup, 'name', name='all')
vg_all.vlans.add(vm, man, net)
vg_pf = self.create(VlanGroup, 'name', name='portforward')
vg_pf.vlans.add(vm, man, net)
vg_net = self.create(VlanGroup, 'name', name='net')
vg_net.vlans.add(net)
# portal host
portal = self.create(Host, 'hostname', hostname='portal', vlan=man,
mac='11:22:33:44:55:66', owner=admin,
shared_ip=True, external_ipv4=man.snat_ip,
ipv4=options['portal_ip'])
portal.add_port(proto='tcp', public=443, private=443)
portal.add_port(proto='tcp', public=22, private=22)
# firewall rules
fw = self.create(Firewall, 'name', name=options['firewall_queue'])
self.create(Rule, 'description', description='default output rule',
direction='out', action='accept',
foreign_network=vg_all, firewall=fw)
self.create(Rule, 'description', description='default input rule',
direction='in', action='accept',
foreign_network=vg_all, firewall=fw)
# vlan rules
self.create(Rule, 'description', description='allow vm->net',
direction='out', action='accept',
foreign_network=vg_net, vlan=vm)
self.create(Rule, 'description', description='allow man->net',
direction='out', action='accept',
foreign_network=vg_net, vlan=man)
# switch
# uplink interface
sp_net = self.create(SwitchPort, 'untagged_vlan', untagged_vlan=net)
self.create(EthernetDevice, 'switch_port', switch_port=sp_net,
name=options['external_if'])
# management interface
if options['management_if']:
sp_man = self.create(
SwitchPort, 'untagged_vlan', untagged_vlan=man)
self.create(EthernetDevice, 'switch_port', switch_port=sp_man,
name=options['management_if'])
# vm interface
sp_trunk = self.create(
SwitchPort, 'tagged_vlans', untagged_vlan=man, tagged_vlans=vg_all)
self.create(EthernetDevice, 'switch_port', switch_port=sp_trunk,
name=options['trunk_if'])
return self.print_state()
// Core variables and mixins
@import "bootstrap/less/variables.less";
// Custom variables
@navbar-height: 45px;
@import "bootstrap/less/mixins.less";
// Reset and dependencies
@import "bootstrap/less/normalize.less";
@import "bootstrap/less/print.less";
// we don't use these, also they make collectstatic fail ...
// @import "bootstrap/less/glyphicons.less";
// Core CSS
@import "bootstrap/less/scaffolding.less";
@import "bootstrap/less/type.less";
@import "bootstrap/less/code.less";
@import "bootstrap/less/grid.less";
@import "bootstrap/less/tables.less";
@import "bootstrap/less/forms.less";
@import "bootstrap/less/buttons.less";
// Components
@import "bootstrap/less/component-animations.less";
@import "bootstrap/less/dropdowns.less";
@import "bootstrap/less/button-groups.less";
@import "bootstrap/less/input-groups.less";
@import "bootstrap/less/navs.less";
@import "bootstrap/less/navbar.less";
@import "bootstrap/less/breadcrumbs.less";
@import "bootstrap/less/pagination.less";
@import "bootstrap/less/pager.less";
@import "bootstrap/less/labels.less";
@import "bootstrap/less/badges.less";
@import "bootstrap/less/jumbotron.less";
@import "bootstrap/less/thumbnails.less";
@import "bootstrap/less/alerts.less";
@import "bootstrap/less/progress-bars.less";
@import "bootstrap/less/media.less";
@import "bootstrap/less/list-group.less";
@import "bootstrap/less/panels.less";
@import "bootstrap/less/responsive-embed.less";
@import "bootstrap/less/wells.less";
@import "bootstrap/less/close.less";
// Components w/ JavaScript
@import "bootstrap/less/modals.less";
@import "bootstrap/less/tooltip.less";
@import "bootstrap/less/popovers.less";
@import "bootstrap/less/carousel.less";
// Utility classes
@import "bootstrap/less/utilities.less";
@import "bootstrap/less/responsive-utilities.less";
/* =========================================================
* bootstrap-slider.js v2.0.0
* http://www.eyecon.ro/bootstrap-slider
* =========================================================
* Copyright 2012 Stefan Petre
*
* 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.
* ========================================================= */
!function( $ ) {
var Slider = function(element, options) {
this.element = $(element);
this.picker = $('<div class="slider">'+
'<div class="slider-track">'+
'<div class="slider-selection"></div>'+
'<div class="slider-handle"></div>'+
'<div class="slider-handle"></div>'+
'</div>'+
'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
'</div>')
.insertBefore(this.element)
.append(this.element);
this.id = this.element.data('slider-id')||options.id;
if (this.id) {
this.picker[0].id = this.id;
}
if (typeof Modernizr !== 'undefined' && Modernizr.touch) {
this.touchCapable = true;
}
var tooltip = this.element.data('slider-tooltip')||options.tooltip;
this.tooltip = this.picker.find('.tooltip');
this.tooltipInner = this.tooltip.find('div.tooltip-inner');
this.orientation = this.element.data('slider-orientation')||options.orientation;
switch(this.orientation) {
case 'vertical':
this.picker.addClass('slider-vertical');
this.stylePos = 'top';
this.mousePos = 'pageY';
this.sizePos = 'offsetHeight';
this.tooltip.addClass('right')[0].style.left = '100%';
break;
default:
this.picker
.addClass('slider-horizontal')
.css('width', this.element.outerWidth());
this.orientation = 'horizontal';
this.stylePos = 'left';
this.mousePos = 'pageX';
this.sizePos = 'offsetWidth';
this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px';
break;
}
this.min = this.element.data('slider-min')||options.min;
this.max = this.element.data('slider-max')||options.max;
this.step = this.element.data('slider-step')||options.step;
this.value = this.element.data('slider-value')||options.value;
if (this.value[1]) {
this.range = true;
}
this.selection = this.element.data('slider-selection')||options.selection;
this.selectionEl = this.picker.find('.slider-selection');
if (this.selection === 'none') {
this.selectionEl.addClass('hide');
}
this.selectionElStyle = this.selectionEl[0].style;
this.handle1 = this.picker.find('.slider-handle:first');
this.handle1Stype = this.handle1[0].style;
this.handle2 = this.picker.find('.slider-handle:last');
this.handle2Stype = this.handle2[0].style;
var handle = this.element.data('slider-handle')||options.handle;
switch(handle) {
case 'round':
this.handle1.addClass('round');
this.handle2.addClass('round');
break
case 'triangle':
this.handle1.addClass('triangle');
this.handle2.addClass('triangle');
break
}
if (this.range) {
this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
} else {
this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
this.handle2.addClass('hide');
if (this.selection == 'after') {
this.value[1] = this.max;
} else {
this.value[1] = this.min;
}
}
this.diff = this.max - this.min;
this.percentage = [
(this.value[0]-this.min)*100/this.diff,
(this.value[1]-this.min)*100/this.diff,
this.step*100/this.diff
];
this.offset = this.picker.offset();
this.size = this.picker[0][this.sizePos];
this.formater = options.formater;
this.layout();
if (this.touchCapable) {
// Touch: Bind touch events:
this.picker.on({
touchstart: $.proxy(this.mousedown, this)
});
} else {
this.picker.on({
mousedown: $.proxy(this.mousedown, this)
});
}
if (tooltip === 'show') {
this.picker.on({
mouseenter: $.proxy(this.showTooltip, this),
mouseleave: $.proxy(this.hideTooltip, this)
});
} else {
this.tooltip.addClass('hide');
}
};
Slider.prototype = {
constructor: Slider,
over: false,
inDrag: false,
showTooltip: function(){
this.tooltip.addClass('in');
//var left = Math.round(this.percent*this.width);
//this.tooltip.css('left', left - this.tooltip.outerWidth()/2);
this.over = true;
},
hideTooltip: function(){
if (this.inDrag === false) {
this.tooltip.removeClass('in');
}
this.over = false;
},
layout: function(){
this.handle1Stype[this.stylePos] = this.percentage[0]+'%';
this.handle2Stype[this.stylePos] = this.percentage[1]+'%';
if (this.orientation == 'vertical') {
this.selectionElStyle.top = Math.min(this.percentage[0], this.percentage[1]) +'%';
this.selectionElStyle.height = Math.abs(this.percentage[0] - this.percentage[1]) +'%';
} else {
this.selectionElStyle.left = Math.min(this.percentage[0], this.percentage[1]) +'%';
this.selectionElStyle.width = Math.abs(this.percentage[0] - this.percentage[1]) +'%';
}
if (this.range) {
this.tooltipInner.text(
this.formater(this.value[0]) +
' : ' +
this.formater(this.value[1])
);
this.tooltip[0].style[this.stylePos] = this.size * (this.percentage[0] + (this.percentage[1] - this.percentage[0])/2)/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px';
} else {
this.tooltipInner.text(
this.formater(this.value[0])
);
this.tooltip[0].style[this.stylePos] = this.size * this.percentage[0]/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px';
}
},
mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') {
ev = ev.originalEvent;
}
this.offset = this.picker.offset();
this.size = this.picker[0][this.sizePos];