Commit b226eeef by Kálmán Viktor

Merge branch 'master' into featura-mount-store-op

parents 2c38e827 fec75c1e
......@@ -447,5 +447,16 @@ if graphite_host and graphite_port:
else:
GRAPHITE_URL = None
STORE_BASIC_AUTH = get_env_variable("STORE_BASIC_AUTH", "") == "True"
STORE_VERIFY_SSL = get_env_variable("STORE_VERIFY_SSL", "") == "True"
STORE_SSL_AUTH = get_env_variable("STORE_SSL_AUTH", "") == "True"
STORE_CLIENT_USER = get_env_variable("STORE_CLIENT_USER", "")
STORE_CLIENT_PASSWORD = get_env_variable("STORE_CLIENT_PASSWORD", "")
STORE_CLIENT_KEY = get_env_variable("STORE_CLIENT_KEY", "")
STORE_CLIENT_CERT = get_env_variable("STORE_CLIENT_CERT", "")
STORE_URL = get_env_variable("STORE_URL", "")
SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
(getnode() % 983)) & 0xffff)
MAX_NODE_RAM = get_env_variable("MAX_NODE_RAM", 1024)
......@@ -70,20 +70,14 @@ SERVER_EMAIL = EMAIL_HOST_USER
########## CACHE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches
try:
CACHES = {
from urlparse import urlsplit
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': get_env_variable('DJANGO_MEMCACHED'),
}
}
except ImproperlyConfigured:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': SITE_NAME,
}
'LOCATION': urlsplit(get_env_variable('CACHE_URI')).netloc,
}
}
########## END CACHE CONFIGURATION
......
......@@ -35,7 +35,11 @@ SOUTH_TESTS_MIGRATE = False
INSTALLED_APPS += (
'acl.tests',
'django_nose',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--with-doctest']
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
CACHES = {
'default': {
......@@ -52,3 +56,5 @@ LOGGING['handlers']['console'] = {'level': level,
'formatter': 'simple'}
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level}
# Forbid store usage
STORE_URL = ""
......@@ -23,6 +23,7 @@ from logging import getLogger
from time import time
from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
......@@ -46,17 +47,24 @@ class WorkerNotFound(Exception):
def activitycontextimpl(act, on_abort=None, on_commit=None):
try:
try:
yield act
except HumanReadableException as e:
result = e
raise
except BaseException as e:
# BaseException is the common parent of Exception and
# system-exiting exceptions, e.g. KeyboardInterrupt
handler = None if on_abort is None else lambda a: on_abort(a, e)
result = create_readable(ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: "
"%(error)s"),
result = create_readable(
ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: %(error)s"),
error=unicode(e))
raise
except:
logger.exception("Failed activity %s" % unicode(act))
handler = None if on_abort is None else lambda a: on_abort(a, e)
act.finish(succeeded=False, result=result, event_handler=handler)
raise e
raise
else:
act.finish(succeeded=True, event_handler=on_commit)
......@@ -70,11 +78,11 @@ activity_code_separator = '.'
def has_prefix(activity_code, *prefixes):
"""Determine whether the activity code has the specified prefix.
E.g.: has_prefix('foo.bar.buz', 'foo.bar') == True
has_prefix('foo.bar.buz', 'foo', 'bar') == True
has_prefix('foo.bar.buz', 'foo.bar', 'buz') == True
has_prefix('foo.bar.buz', 'foo', 'bar', 'buz') == True
has_prefix('foo.bar.buz', 'foo', 'buz') == False
>>> assert has_prefix('foo.bar.buz', 'foo.bar')
>>> assert has_prefix('foo.bar.buz', 'foo', 'bar')
>>> assert has_prefix('foo.bar.buz', 'foo.bar', 'buz')
>>> assert has_prefix('foo.bar.buz', 'foo', 'bar', 'buz')
>>> assert not has_prefix('foo.bar.buz', 'foo', 'buz')
"""
equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code)
......@@ -85,11 +93,11 @@ def has_prefix(activity_code, *prefixes):
def has_suffix(activity_code, *suffixes):
"""Determine whether the activity code has the specified suffix.
E.g.: has_suffix('foo.bar.buz', 'bar.buz') == True
has_suffix('foo.bar.buz', 'bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo.bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo', 'bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo', 'buz') == False
>>> assert has_suffix('foo.bar.buz', 'bar.buz')
>>> assert has_suffix('foo.bar.buz', 'bar', 'buz')
>>> assert has_suffix('foo.bar.buz', 'foo.bar', 'buz')
>>> assert has_suffix('foo.bar.buz', 'foo', 'bar', 'buz')
>>> assert not has_suffix('foo.bar.buz', 'foo', 'buz')
"""
equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code)
......@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel):
DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=value)
elif not hasattr(value, "to_dict"):
warn("Use HumanReadableObject.", DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=unicode(value))
self.result_data = None if value is None else value.to_dict()
......@@ -361,8 +373,9 @@ class HumanReadableObject(object):
@classmethod
def create(cls, user_text_template, admin_text_template=None, **params):
return cls(user_text_template,
admin_text_template or user_text_template, params)
return cls(user_text_template=user_text_template,
admin_text_template=(admin_text_template
or user_text_template), params=params)
def set(self, user_text_template, admin_text_template=None, **params):
self._set_values(user_text_template,
......@@ -407,10 +420,29 @@ create_readable = HumanReadableObject.create
class HumanReadableException(HumanReadableObject, Exception):
"""HumanReadableObject that is an Exception so can used in except clause.
"""
pass
def __init__(self, level=None, *args, **kwargs):
super(HumanReadableException, self).__init__(*args, **kwargs)
if level is not None:
if hasattr(messages, level):
self.level = level
else:
raise ValueError(
"Level should be the name of an attribute of django."
"contrib.messages (and it should be callable with "
"(request, message)). Like 'error', 'warning'.")
else:
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
getattr(messages, level or self.level)(request, msg)
def humanize_exception(message, exception=None, **params):
def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception.
......@@ -423,4 +455,7 @@ def humanize_exception(message, exception=None, **params):
Ex = type("HumanReadable" + type(exception).__name__,
(HumanReadableException, type(exception)),
exception.__dict__)
return Ex.create(message, **params)
ex = Ex.create(message, **params)
if level:
ex.level = level
return ex
......@@ -1322,7 +1322,7 @@
"user_permissions": [
115
],
"password": "pbkdf2_sha256$10000$KIoeMs78MiOj$PnVXn3YJMehbOciBO32CMzqL0ZnQrzrdb7+b5dE13os=",
"password": "md5$qLN4mQMOrsUJ$f07129fd1a289a0afb4e09f7a6816a4f",
"email": "test@example.org",
"date_joined": "2013-09-04T15:29:49.914Z"
}
......@@ -1382,7 +1382,7 @@
"pw": "ads",
"time_of_suspend": null,
"ram_size": 200,
"priority": 4,
"priority": 10,
"active_since": null,
"template": null,
"access_method": "nx",
......@@ -1412,7 +1412,7 @@
"pw": "ads",
"time_of_suspend": null,
"ram_size": 200,
"priority": 4,
"priority": 10,
"active_since": null,
"template": null,
"access_method": "nx",
......@@ -1518,7 +1518,7 @@
"ram_size": 1024,
"modified": "2014-01-24T00:58:19.654Z",
"system": "bubuntu",
"priority": 20,
"priority": 10,
"access_method": "ssh",
"raw_data": "",
"arch": "x86_64",
......
......@@ -97,11 +97,13 @@ class Migration(SchemaMigration):
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'smb_password': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
......
......@@ -98,11 +98,13 @@ class Migration(SchemaMigration):
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'smb_password': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
......
......@@ -93,11 +93,13 @@ class Migration(DataMigration):
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'smb_password': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
......
......@@ -96,11 +96,13 @@ class Migration(SchemaMigration):
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'smb_password': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
......
......@@ -29,10 +29,13 @@ from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, BooleanField
)
from django.db.models.signals import post_save, pre_delete
from django.db.models.signals import post_save, pre_delete, post_delete
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField
from jsonfield import JSONField
from model_utils.models import TimeStampedModel
......@@ -44,8 +47,12 @@ from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys
from .store_api import Store, NoStoreException, NotOkException
logger = getLogger(__name__)
pwgen = User.objects.make_random_password
class Favourite(Model):
instance = ForeignKey("vm.Instance")
......@@ -109,6 +116,18 @@ class Profile(Model):
email_notifications = BooleanField(
verbose_name=_("Email notifications"), default=True,
help_text=_('Whether user wants to get digested email notifications.'))
smb_password = CharField(
max_length=20,
verbose_name=_('Samba password'),
help_text=_(
'Generated password for accessing store from '
'virtual machines.'),
default=pwgen,
)
disk_quota = FileSizeField(
verbose_name=_('disk quota'),
default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.'))
def notify(self, subject, template, context=None, valid_until=None,
**kwargs):
......@@ -201,6 +220,11 @@ def create_profile(sender, user, request, **kwargs):
if not user.pk:
return False
profile, created = Profile.objects.get_or_create(user=user)
try:
Store(user).create_user(profile.smb_password, None, profile.disk_quota)
except:
logger.exception("Can't create user %s", unicode(user))
return created
user_logged_in.connect(create_profile)
......@@ -268,6 +292,44 @@ else:
logger.debug("Do not register save_org_id to djangosaml2 pre_user_save")
def update_store_profile(sender, **kwargs):
profile = kwargs.get('instance')
keys = [i.key for i in profile.user.userkey_set.all()]
try:
s = Store(profile.user)
s.create_user(profile.smb_password, keys,
profile.disk_quota)
except NoStoreException:
logger.debug("Store is not available.")
except NotOkException:
logger.critical("Store is not accepting connections.")
post_save.connect(update_store_profile, sender=Profile)
def update_store_keys(sender, **kwargs):
userkey = kwargs.get('instance')
try:
profile = userkey.user.profile
except ObjectDoesNotExist:
pass # If there is no profile the user is deleted
else:
keys = [i.key for i in profile.user.userkey_set.all()]
try:
s = Store(userkey.user)
s.create_user(profile.smb_password, keys,
profile.disk_quota)
except NoStoreException:
logger.debug("Store is not available.")
except NotOkException:
logger.critical("Store is not accepting connections.")
post_save.connect(update_store_keys, sender=UserKey)
post_delete.connect(update_store_keys, sender=UserKey)
def add_ssh_keys(sender, **kwargs):
from vm.models import Instance
......
......@@ -186,42 +186,6 @@ html {
text-decoration: none !important;
}
.slider {
display: inline-block;
}
.slider .track {
height: 20px;
top: 50%;
}
.slider > .dragger, .slider > .dragger:hover {
border-radius: 0px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
width: 8px;
height: 24px;
margin-top: -12px!important;
text-shadow: 0 1px 0 #fff;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9));
background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
background-repeat: repeat-x;
border-color: #2d6ca2;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
}
.slider > .dragger:hover {
background-color: #3071a9;
background-image: none;
border-color: #2d6ca2;
}
.slider > .highlight-track {
height: 20px;
top: 50%;
}
.slider + .output {
}
.rule-table tr >:nth-child(1) {
text-align: right;
}
......@@ -723,6 +687,82 @@ textarea[name="list-new-namelist"] {
}
#store-list-list {
list-style: none;
}
.store-list-item {
cursor: pointer;
}
.store-list-item:hover {
background: rgba(0, 0, 0, 0.6);
}
.store-list-item-icon {
width: 20px;
text-align: center;
display: inline-block;
margin-right: 15px;
float: left;
}
.store-list-item-size {
width: 70px;
text-align: right;
float: right;
}
.store-list-file-infos {
padding: 15px;
display: none;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
position: relative;
}
.store-list-item-new {
display: inline-block;
}
.store-list-item-new .badge {
margin-left: 5px;
background: #5bc0dc;
}
.store-list-item-icon-directory {
color: #ff8c00;
}
.store-remove-button {
margin-top: 8px;
}
#dashboard-files-toplist div.list-group-item {
color: #555;
}
#dashboard-files-toplist div.list-group-item:hover {
background: #eee;
}
.store-list-item-name {
max-width: 70%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
.dashboard-toplist-icon {
float: left;
padding: 2px 5px 0 0;
}
.no-hover:hover {
background: none !important;
}
#group-detail-permissions .filtered {
margin: 2px 0;
padding: 2px 3px;
......@@ -752,6 +792,74 @@ textarea[name="list-new-namelist"] {
margin-top: -6px;
}
.store-action-button {
margin-left: 5px;
}
#progress-marker-hard {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
right: 0;
background: red;
}
.progress-marker {
width: 6px;
height: 20px;
position: absolute;
}
#show-all-activities-container {
margin: 20px 0 0 10px;
}
#vm-details-resources-form {
margin-top: 15px;
}
#vm-details-resources-form .row, .resources-sliders .row {
margin-bottom: 15px;
}
#vm-create-disk-add-form {
max-width: 450px;
margin-top: 15px;
}
.vm-create-template {
max-width: 800px;
border: 1px solid black;
border-bottom: none;
}
.vm-create-template-list .vm-create-template:last-child {
border-bottom: 1px solid black;
}
.vm-create-template-summary {
padding: 15px;
cursor: pointer;
}
.vm-create-template:nth-child(odd) .vm-create-template-summary {
background: #F5F5F5;
}
.vm-create-template-list .vm-create-template-summary:hover {
background: #D2D2D2;
}
.vm-create-template-details {
border-top: 1px dashed #D3D3D3;
padding: 15px;
}
.vm-create-template-details ul {
list-style: none;
padding: 0 15px;
}
.vm-create-template-details li {
border-bottom: 1px dotted #aaa;
padding: 5px 0px;
}
......@@ -112,6 +112,7 @@ $(function () {
/* no js compatibility */
noJS();
$('.no-js-hidden').show();
$('.js-hidden').hide();
......@@ -349,9 +350,9 @@ $(function () {
}
}
for(var i=0; i<5 && i<search_result.length; i++)
html += generateGroupHTML(search_result[i].url, search_result[i].name);
html += generateGroupHTML(search_result[i].url, search_result[i].name, search_result.length < 5);
if(search_result.length == 0)
html += '<div class="list-group-item">No result</div>';
html += '<div class="list-group-item list-group-item-last">No result</div>';
$("#dashboard-group-list").html(html);
// if there is only one result and ENTER is pressed redirect
......@@ -370,7 +371,10 @@ $(function () {
});
/* don't close notifications window on missclick */
$(document).on("click", ".notification-messages", function() {
$(document).on("click", ".notification-messages", function(e) {
if($(e.target).closest("a").length)
return true
else
return false;
});
......@@ -395,8 +399,8 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
'</a>';
}
function generateGroupHTML(url, name) {
return '<a href="' + url + '" class="list-group-item real-link">'+
function generateGroupHTML(url, name, is_last) {
return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+
'<i class="fa fa-users"></i> '+ name +
'</a>';
}
......@@ -431,28 +435,63 @@ function compareVmByFav(a, b) {
return a.pk < b.pk ? -1 : 1;
}
$(document).on('shown.bs.tab', 'a[href="#resources"]', function (e) {
$(".cpu-priority-input").trigger("change");
$(".cpu-count-input, .ram-input").trigger("input");
})
function addSliderMiscs() {
$('.vm-slider').each(function() {
$("<span>").addClass("output").html($(this).val()).insertAfter($(this));
// set max values based on inputs
var cpu_count_range = "0, " + $(".cpu-count-input").prop("max");
var ram_range = "0, " + $(".ram-input").prop("max");
$(".cpu-count-slider").data("slider-range", cpu_count_range);
$(".ram-slider").data("slider-range", ram_range);
$(".vm-slider").simpleSlider();
$(".cpu-priority-slider").bind("slider:changed", function (event, data) {
var value = data.value + 0;
$('.cpu-priority-input option[value="' + value + '"]').attr("selected", "selected");
});
$('.vm-slider').slider()
.on('slide', function(e) {
$(this).val(e.value);
$(this).parent('div').nextAll("span").html(e.value)
$(".cpu-priority-input").change(function() {
var val = $(":selected", $(this)).val();
$(".cpu-priority-slider").simpleSlider("setValue", val);
});
refreshSliders();
}
$(".cpu-count-slider").bind("slider:changed", function (event, data) {
var value = data.value + 0;
$(".cpu-count-input").val(parseInt(value));
});
// ehhh
function refreshSliders() {
$('.vm-slider').each(function() {
$(this).val($(this).slider().data('slider').getValue());
$(this).parent('div').nextAll("span").html($(this).val());
$(".cpu-count-input").bind("input", function() {
var val = parseInt($(this).val());
if(!val) return;
$(".cpu-count-slider").simpleSlider("setValue", val);
});
var ram_fire = false;
$(".ram-slider").bind("slider:changed", function (event, data) {
if(ram_fire) {
ram_fire = false;
return;
}
var value = data.value + 0;
$(".ram-input").val(value);
});
$(".ram-input").bind("input", function() {
var val = $(this).val();
ram_fire = true;
$(".ram-slider").simpleSlider("setValue", parseInt(val));
});
$(".cpu-priority-input").trigger("change");
$(".cpu-count-input, .ram-input").trigger("input");
}
/* deletes the VM with the pk
* if dir is true, then redirect to the dashboard landing page
* else it adds a success message */
......@@ -561,3 +600,10 @@ function getCookie(name) {
}
return cookieValue;
}
/* no js compatibility */
function noJS() {
$('.no-js-hidden').show();
$('.js-hidden').hide();
}
jQuery Simple Slider: Unobtrusive Numerical Slider
==================================================
SimpleSlider is a jQuery plugin for turning your text inputs into draggable
numerical sliders.
It has no external dependencies other than jQuery, and you don't need to write
a single line of JavaScript to get it to work.
How to Use
-----------
Include the javascript file in your page:
<script src="simple-slider.js"></script>
Turn your text input into a slider:
<input type="text" data-slider="true">
Documentation, Features and Demos
---------------------------------
Full details and documentation can be found on the project page here:
<http://loopj.com/jquery-simple-slider/>
\ No newline at end of file
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="js/simple-slider.js"></script>
<link href="css/simple-slider.css" rel="stylesheet" type="text/css" />
<link href="css/simple-slider-volume.css" rel="stylesheet" type="text/css" />
<!-- These styles are only used for this page, not required for the slider -->
<style>
body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
[class^=slider] { display: inline-block; margin-bottom: 30px; }
.output { color: #888; font-size: 14px; padding-top: 1px; margin-left: 5px; vertical-align: top;}
h1 { font-size: 20px; }
h2 { clear: both; margin: 0; margin-bottom: 5px; font-size: 16px; }
p { font-size: 15px; margin-bottom: 30px; }
h2:first-of-type { margin-top: 0; }
</style>
</head>
<body>
<h1>jQuery Simple Slider Examples</h1>
<p>
Here are a few examples of the functionality available in Simple Slider.
</p>
<h2>Basic Example</h2>
<input type="text" data-slider="true">
<h2>Basic Example (Themed)</h2>
<input type="text" data-slider="true" data-slider-theme="volume">
<h2>Predefined Value</h2>
<input type="text" value="0.2" data-slider="true">
<h2>Steps</h2>
<input type="text" data-slider="true" data-slider-step="0.1">
<h2>Range</h2>
<input type="text" data-slider="true" data-slider-range="10,1000">
<h2>Range &amp; Steps</h2>
<input type="text" data-slider="true" data-slider-range="100,500" data-slider-step="100">
<h2>Range, Steps &amp; Snap</h2>
<input type="text" data-slider="true" data-slider-range="100,500" data-slider-step="100" data-slider-snap="true">
<h2>Predefined List of Values</h2>
<input type="text" data-slider="true" data-slider-values="0,100,500,800,2000">
<h2>Predefined List &amp; Snap</h2>
<input type="text" data-slider="true" data-slider-values="0,100,500,800,2000" data-slider-snap="true">
<h2>Predefined List, Equal Steps &amp; Snap</h2>
<input type="text" data-slider="true" data-slider-values="0,100,500,800,2000" data-slider-equal-steps="true" data-slider-snap="true">
<h2>Highlighted</h2>
<input type="text" data-slider="true" value="0.8" data-slider-highlight="true">
<h2>Highlighted (Themed)</h2>
<input type="text" data-slider="true" value="0.4" data-slider-highlight="true" data-slider-theme="volume">
<script>
$("[data-slider]")
.each(function () {
var input = $(this);
$("<span>")
.addClass("output")
.insertAfter($(this));
})
.bind("slider:ready slider:changed", function (event, data) {
$(this)
.nextAll(".output:first")
.html(data.value.toFixed(3));
});
</script>
</body>
</html>
module.exports = function(grunt) {
grunt.initConfig({
pkg: '<json:package.json>',
meta: {
banner:
'/*\n' +
' * <%= pkg.title || pkg.name %>: <%= pkg.description %>\n' +
' * Version <%= pkg.version %>\n' +
' *\n' +
' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %> (<%= pkg.author.url %>)\n' +
' *\n' +
' * Licensed under the <%= pkg.licenses[0].type %> license (<%= pkg.licenses[0].url %>)\n' +
' *\n' +
' */\n'
},
coffee: {
compile: {
files: {
'js/<%= pkg.name %>.js': 'js/*.coffee'
},
options: {
bare: true
}
}
},
watch: {
coffee: {
files: ['js/*.coffee'],
tasks: 'coffee growl:coffee'
}
},
growl: {
coffee: {
title: 'CoffeeScript',
message: 'Compiled successfully'
}
},
min: {
dist: {
src: ['<banner:meta.banner>', 'js/<%= pkg.name %>.js'],
dest: 'js/<%= pkg.name %>.min.js'
}
},
compress: {
zip: {
files: {
"<%= pkg.name %>-<%= pkg.version %>.zip": ["js/**", "demo.html", "README.md"]
}
}
}
});
// Lib tasks.
grunt.loadNpmTasks('grunt-contrib');
grunt.loadNpmTasks('grunt-growl');
// Default task.
grunt.registerTask('build', 'coffee min');
grunt.registerTask('serve', 'server watch:coffee');
grunt.registerTask('default', 'build');
};
\ No newline at end of file
/*
* jQuery Simple Slider: Unobtrusive Numerical Slider
* Version 1.0.0
*
* Copyright (c) 2013 James Smith (http://loopj.com)
*
* Licensed under the MIT license (http://mit-license.org/)
*
*/
var __slice=[].slice,__indexOf=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++)if(t in this&&this[t]===e)return t;return-1};(function(e,t){var n;return n=function(){function t(t,n){var r,i=this;this.input=t,this.defaultOptions={animate:!0,snapMid:!1,classPrefix:null,classSuffix:null,theme:null,highlight:!1},this.settings=e.extend({},this.defaultOptions,n),this.settings.theme&&(this.settings.classSuffix="-"+this.settings.theme),this.input.hide(),this.slider=e("<div>").addClass("slider"+(this.settings.classSuffix||"")).css({position:"relative",userSelect:"none",boxSizing:"border-box"}).insertBefore(this.input),this.input.attr("id")&&this.slider.attr("id",this.input.attr("id")+"-slider"),this.track=this.createDivElement("track").css({width:"100%"}),this.settings.highlight&&(this.highlightTrack=this.createDivElement("highlight-track").css({width:"0"})),this.dragger=this.createDivElement("dragger"),this.slider.css({minHeight:this.dragger.outerHeight(),marginLeft:this.dragger.outerWidth()/2,marginRight:this.dragger.outerWidth()/2}),this.track.css({marginTop:this.track.outerHeight()/-2}),this.settings.highlight&&this.highlightTrack.css({marginTop:this.track.outerHeight()/-2}),this.dragger.css({marginTop:this.dragger.outerWidth()/-2,marginLeft:this.dragger.outerWidth()/-2}),this.track.mousedown(function(e){return i.trackEvent(e)}),this.settings.highlight&&this.highlightTrack.mousedown(function(e){return i.trackEvent(e)}),this.dragger.mousedown(function(e){if(e.which!==1)return;return i.dragging=!0,i.dragger.addClass("dragging"),i.domDrag(e.pageX,e.pageY),!1}),e("body").mousemove(function(t){if(i.dragging)return i.domDrag(t.pageX,t.pageY),e("body").css({cursor:"pointer"})}).mouseup(function(t){if(i.dragging)return i.dragging=!1,i.dragger.removeClass("dragging"),e("body").css({cursor:"auto"})}),this.pagePos=0,this.input.val()===""?(this.value=this.getRange().min,this.input.val(this.value)):this.value=this.nearestValidValue(this.input.val()),this.setSliderPositionFromValue(this.value),r=this.valueToRatio(this.value),this.input.trigger("slider:ready",{value:this.value,ratio:r,position:r*this.slider.outerWidth(),el:this.slider})}return t.prototype.createDivElement=function(t){var n;return n=e("<div>").addClass(t).css({position:"absolute",top:"50%",userSelect:"none",cursor:"pointer"}).appendTo(this.slider),n},t.prototype.setRatio=function(e){var t;return e=Math.min(1,e),e=Math.max(0,e),t=this.ratioToValue(e),this.setSliderPositionFromValue(t),this.valueChanged(t,e,"setRatio")},t.prototype.setValue=function(e){var t;return e=this.nearestValidValue(e),t=this.valueToRatio(e),this.setSliderPositionFromValue(e),this.valueChanged(e,t,"setValue")},t.prototype.trackEvent=function(e){if(e.which!==1)return;return this.domDrag(e.pageX,e.pageY,!0),this.dragging=!0,!1},t.prototype.domDrag=function(e,t,n){var r,i,s;n==null&&(n=!1),r=e-this.slider.offset().left,r=Math.min(this.slider.outerWidth(),r),r=Math.max(0,r);if(this.pagePos!==r)return this.pagePos=r,i=r/this.slider.outerWidth(),s=this.ratioToValue(i),this.valueChanged(s,i,"domDrag"),this.settings.snap?this.setSliderPositionFromValue(s,n):this.setSliderPosition(r,n)},t.prototype.setSliderPosition=function(e,t){t==null&&(t=!1);if(t&&this.settings.animate){this.dragger.animate({left:e},200);if(this.settings.highlight)return this.highlightTrack.animate({width:e},200)}else{this.dragger.css({left:e});if(this.settings.highlight)return this.highlightTrack.css({width:e})}},t.prototype.setSliderPositionFromValue=function(e,t){var n;return t==null&&(t=!1),n=this.valueToRatio(e),this.setSliderPosition(n*this.slider.outerWidth(),t)},t.prototype.getRange=function(){return this.settings.allowedValues?{min:Math.min.apply(Math,this.settings.allowedValues),max:Math.max.apply(Math,this.settings.allowedValues)}:this.settings.range?{min:parseFloat(this.settings.range[0]),max:parseFloat(this.settings.range[1])}:{min:0,max:1}},t.prototype.nearestValidValue=function(t){var n,r,i,s;return i=this.getRange(),t=Math.min(i.max,t),t=Math.max(i.min,t),this.settings.allowedValues?(n=null,e.each(this.settings.allowedValues,function(){if(n===null||Math.abs(this-t)<Math.abs(n-t))return n=this}),n):this.settings.step?(r=(i.max-i.min)/this.settings.step,s=Math.floor((t-i.min)/this.settings.step),(t-i.min)%this.settings.step>this.settings.step/2&&s<r&&(s+=1),s*this.settings.step+i.min):t},t.prototype.valueToRatio=function(e){var t,n,r,i,s,o,u,a;if(this.settings.equalSteps){a=this.settings.allowedValues;for(i=o=0,u=a.length;o<u;i=++o){t=a[i];if(typeof n=="undefined"||n===null||Math.abs(t-e)<Math.abs(n-e))n=t,r=i}return this.settings.snapMid?(r+.5)/this.settings.allowedValues.length:r/(this.settings.allowedValues.length-1)}return s=this.getRange(),(e-s.min)/(s.max-s.min)},t.prototype.ratioToValue=function(e){var t,n,r,i,s;return this.settings.equalSteps?(s=this.settings.allowedValues.length,i=Math.round(e*s-.5),t=Math.min(i,this.settings.allowedValues.length-1),this.settings.allowedValues[t]):(n=this.getRange(),r=e*(n.max-n.min)+n.min,this.nearestValidValue(r))},t.prototype.valueChanged=function(t,n,r){var i;if(t.toString()===this.value.toString())return;return this.value=t,i={value:t,ratio:n,position:n*this.slider.outerWidth(),trigger:r,el:this.slider},this.input.val(t).trigger(e.Event("change",i)).trigger("slider:changed",i)},t}(),e.extend(e.fn,{simpleSlider:function(){var t,r,i;return i=arguments[0],t=2<=arguments.length?__slice.call(arguments,1):[],r=["setRatio","setValue"],e(this).each(function(){var s,o;return i&&__indexOf.call(r,i)>=0?(s=e(this).data("slider-object"),s[i].apply(s,t)):(o=i,e(this).data("slider-object",new n(e(this),o)))})}}),e(function(){return e("[data-slider]").each(function(){var t,n,r,i;return t=e(this),r={},n=t.data("slider-values"),n&&(r.allowedValues=function(){var e,t,r,s;r=n.split(","),s=[];for(e=0,t=r.length;e<t;e++)i=r[e],s.push(parseFloat(i));return s}()),t.data("slider-range")&&(r.range=t.data("slider-range").split(",")),t.data("slider-step")&&(r.step=t.data("slider-step")),r.snap=t.data("slider-snap"),r.equalSteps=t.data("slider-equal-steps"),t.data("slider-theme")&&(r.theme=t.data("slider-theme")),t.attr("data-slider-highlight")&&(r.highlight=t.data("slider-highlight")),t.data("slider-animate")!=null&&(r.animate=t.data("slider-animate")),t.simpleSlider(r)})})})(this.jQuery||this.Zepto,this);
\ No newline at end of file
{
"name" : "simple-slider",
"title" : "jQuery Simple Slider",
"description" : "Unobtrusive Numerical Slider",
"version" : "1.0.0",
"homepage" : "http://loopj.com/jquery-simple-slider",
"keywords" : [],
"author" : {
"name" : "James Smith",
"email" : "james@loopj.com",
"url" : "http://loopj.com"
},
"repository" : {
"type" : "git",
"url" : "https://github.com/loopj/jquery-simple-slider.git"
},
"bugs" : {
"url" : "https://github.com/loopj/jquery-simple-slider/issues"
},
"licenses": [{
"type": "MIT",
"url" : "http://mit-license.org/"
}],
"devDependencies" : {
"grunt" : "0.3.x",
"grunt-contrib": "0.2.x",
"grunt-growl": "git://github.com/loopj/grunt-growl.git#master"
},
"scripts": {
"test": "grunt"
}
}
\ No newline at end of file
......@@ -53,3 +53,50 @@
border-color: #496805;
}
.slider {
display: inline-block;
}
.slider .track {
height: 20px;
top: 50%;
}
.slider > .dragger, .slider > .dragger:hover {
border-radius: 0px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
width: 8px;
height: 24px;
margin-top: -12px!important;
text-shadow: 0 1px 0 #fff;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9));
background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%);
background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
background-repeat: repeat-x;
border-color: #2d6ca2;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
}
.slider > .dragger:hover {
background-color: #3071a9;
background-image: none;
border-color: #2d6ca2;
}
.slider > .highlight-track {
height: 20px;
top: 50%;
}
.slider + .output {
}
.slider > .track, .slider > .highlight-track {
border-radius: 5px;
}
/* review this later */
.slider {
width: 100%;
}
/*
jQuery Simple Slider
Copyright (c) 2012 James Smith (http://loopj.com)
Copyright (c) 2012, 2013 James Smith (http://loopj.com)
Copyright (c) 2013 Maarten van Grootel (http://maatenvangrootel.nl)
Copyright (c) 2013 Nathan Hunzaker (http://natehunzaker.com)
Copyright (c) 2013 Erik J. Nedwidek (http://github.com/nedwidek)
Licensed under the MIT license (http://mit-license.org/)
*/
......@@ -23,8 +26,12 @@ var __slice = [].slice,
classPrefix: null,
classSuffix: null,
theme: null,
highlight: false
highlight: false,
showScale: false
};
if(typeof options == 'undefined') {
options = this.loadDataOptions();
}
this.settings = $.extend({}, this.defaultOptions, options);
if (this.settings.theme) {
this.settings.classSuffix = "-" + this.settings.theme;
......@@ -106,6 +113,20 @@ var __slice = [].slice,
}
this.setSliderPositionFromValue(this.value);
ratio = this.valueToRatio(this.value);
if (this.settings.showScale) {
this.scale = this.createDivElement("scale");
this.minScale = this.createSpanElement("min-scale", this.scale);
this.maxScale = this.createSpanElement("max-scale", this.scale);
range = this.getRange();
this.minScale.html(range.min);
this.maxScale.html(range.max);
this.scale.css('marginTop', function(index, currentValue) {
return (parseInt(currentValue, 10) + this.previousSibling.offsetHeight / 2) + 'px';
});
}
this.input.trigger("slider:ready", {
value: this.value,
ratio: ratio,
......@@ -114,6 +135,44 @@ var __slice = [].slice,
});
}
SimpleSlider.prototype.loadDataOptions = function() {
var options = {};
allowedValues = this.input.data("slider-values");
if (allowedValues) {
options.allowedValues = (function() {
var _i, _len, _ref, _results;
_ref = allowedValues.split(",");
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
x = _ref[_i];
_results.push(parseFloat(x));
}
return _results;
})();
}
if (this.input.data("slider-range")) {
options.range = this.input.data("slider-range").split(",");
}
if (this.input.data("slider-step")) {
options.step = this.input.data("slider-step");
}
options.snap = this.input.data("slider-snap");
options.equalSteps = this.input.data("slider-equal-steps");
if (this.input.data("slider-theme")) {
options.theme = this.input.data("slider-theme");
}
if (this.input.attr("data-slider-highlight")) {
options.highlight = this.input.data("slider-highlight");
}
if (this.input.data("slider-animate") != null) {
options.animate = this.input.data("slider-animate");
}
if (this.input.data("slider-showscale") != null) {
options.showScale = this.input.data("slider-showscale");
}
return options;
}
SimpleSlider.prototype.createDivElement = function(classname) {
var item;
item = $("<div>").addClass(classname).css({
......@@ -125,6 +184,12 @@ var __slice = [].slice,
return item;
};
SimpleSlider.prototype.createSpanElement = function(classname, parent) {
var item;
item = $("<span>").addClass(classname).appendTo(parent);
return item;
};
SimpleSlider.prototype.setRatio = function(ratio) {
var value;
ratio = Math.min(1, ratio);
......@@ -322,42 +387,14 @@ var __slice = [].slice,
});
}
});
/*
return $(function() {
return $("[data-slider]").each(function() {
var $el, allowedValues, settings, x;
$el = $(this);
settings = {};
allowedValues = $el.data("slider-values");
if (allowedValues) {
settings.allowedValues = (function() {
var _i, _len, _ref, _results;
_ref = allowedValues.split(",");
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
x = _ref[_i];
_results.push(parseFloat(x));
}
return _results;
})();
}
if ($el.data("slider-range")) {
settings.range = $el.data("slider-range").split(",");
}
if ($el.data("slider-step")) {
settings.step = $el.data("slider-step");
}
settings.snap = $el.data("slider-snap");
settings.equalSteps = $el.data("slider-equal-steps");
if ($el.data("slider-theme")) {
settings.theme = $el.data("slider-theme");
}
if ($el.attr("data-slider-highlight")) {
settings.highlight = $el.data("slider-highlight");
}
if ($el.data("slider-animate") != null) {
settings.animate = $el.data("slider-animate");
}
return $el.simpleSlider(settings);
return $el.simpleSlider();
});
});
*/
})(this.jQuery || this.Zepto, this);
/*
* jQuery Simple Slider: Unobtrusive Numerical Slider
* Version 1.0.0
*
* Copyright (c) 2013 James Smith (http://loopj.com)
*
* Licensed under the MIT license (http://mit-license.org/)
*
*/
var __slice=[].slice,__indexOf=[].indexOf||function(c){for(var b=0,a=this.length;b<a;b++){if(b in this&&this[b]===c){return b}}return -1};(function(c,a){var b;b=(function(){function d(e,f){var g,h=this;this.input=e;this.defaultOptions={animate:true,snapMid:false,classPrefix:null,classSuffix:null,theme:null,highlight:false,showScale:false};if(typeof f=="undefined"){f=this.loadDataOptions()}this.settings=c.extend({},this.defaultOptions,f);if(this.settings.theme){this.settings.classSuffix="-"+this.settings.theme}this.input.hide();this.slider=c("<div>").addClass("slider"+(this.settings.classSuffix||"")).css({position:"relative",userSelect:"none",boxSizing:"border-box"}).insertBefore(this.input);if(this.input.attr("id")){this.slider.attr("id",this.input.attr("id")+"-slider")}this.track=this.createDivElement("track").css({width:"100%"});if(this.settings.highlight){this.highlightTrack=this.createDivElement("highlight-track").css({width:"0"})}this.dragger=this.createDivElement("dragger");this.slider.css({minHeight:this.dragger.outerHeight(),marginLeft:this.dragger.outerWidth()/2,marginRight:this.dragger.outerWidth()/2});this.track.css({marginTop:this.track.outerHeight()/-2});if(this.settings.highlight){this.highlightTrack.css({marginTop:this.track.outerHeight()/-2})}this.dragger.css({marginTop:this.dragger.outerWidth()/-2,marginLeft:this.dragger.outerWidth()/-2});this.track.mousedown(function(i){return h.trackEvent(i)});if(this.settings.highlight){this.highlightTrack.mousedown(function(i){return h.trackEvent(i)})}this.dragger.mousedown(function(i){if(i.which!==1){return}h.dragging=true;h.dragger.addClass("dragging");h.domDrag(i.pageX,i.pageY);return false});c("body").mousemove(function(i){if(h.dragging){h.domDrag(i.pageX,i.pageY);return c("body").css({cursor:"pointer"})}}).mouseup(function(i){if(h.dragging){h.dragging=false;h.dragger.removeClass("dragging");return c("body").css({cursor:"auto"})}});this.pagePos=0;if(this.input.val()===""){this.value=this.getRange().min;this.input.val(this.value)}else{this.value=this.nearestValidValue(this.input.val())}this.setSliderPositionFromValue(this.value);g=this.valueToRatio(this.value);if(this.settings.showScale){this.scale=this.createDivElement("scale");this.minScale=this.createSpanElement("min-scale",this.scale);this.maxScale=this.createSpanElement("max-scale",this.scale);range=this.getRange();this.minScale.html(range.min);this.maxScale.html(range.max);this.scale.css("marginTop",function(i,j){return(parseInt(j,10)+this.previousSibling.offsetHeight/2)+"px"})}this.input.trigger("slider:ready",{value:this.value,ratio:g,position:g*this.slider.outerWidth(),el:this.slider})}d.prototype.loadDataOptions=function(){var e={};allowedValues=this.input.data("slider-values");if(allowedValues){e.allowedValues=(function(){var i,g,h,f;h=allowedValues.split(",");f=[];for(i=0,g=h.length;i<g;i++){x=h[i];f.push(parseFloat(x))}return f})()}if(this.input.data("slider-range")){e.range=this.input.data("slider-range").split(",")}if(this.input.data("slider-step")){e.step=this.input.data("slider-step")}e.snap=this.input.data("slider-snap");e.equalSteps=this.input.data("slider-equal-steps");if(this.input.data("slider-theme")){e.theme=this.input.data("slider-theme")}if(this.input.attr("data-slider-highlight")){e.highlight=this.input.data("slider-highlight")}if(this.input.data("slider-animate")!=null){e.animate=this.input.data("slider-animate")}if(this.input.data("slider-showscale")!=null){e.showScale=this.input.data("slider-showscale")}return e};d.prototype.createDivElement=function(f){var e;e=c("<div>").addClass(f).css({position:"absolute",top:"50%",userSelect:"none",cursor:"pointer"}).appendTo(this.slider);return e};d.prototype.createSpanElement=function(g,e){var f;f=c("<span>").addClass(g).appendTo(e);return f};d.prototype.setRatio=function(e){var f;e=Math.min(1,e);e=Math.max(0,e);f=this.ratioToValue(e);this.setSliderPositionFromValue(f);return this.valueChanged(f,e,"setRatio")};d.prototype.setValue=function(f){var e;f=this.nearestValidValue(f);e=this.valueToRatio(f);this.setSliderPositionFromValue(f);return this.valueChanged(f,e,"setValue")};d.prototype.trackEvent=function(f){if(f.which!==1){return}this.domDrag(f.pageX,f.pageY,true);this.dragging=true;return false};d.prototype.domDrag=function(i,g,e){var f,h,j;if(e==null){e=false}f=i-this.slider.offset().left;f=Math.min(this.slider.outerWidth(),f);f=Math.max(0,f);if(this.pagePos!==f){this.pagePos=f;h=f/this.slider.outerWidth();j=this.ratioToValue(h);this.valueChanged(j,h,"domDrag");if(this.settings.snap){return this.setSliderPositionFromValue(j,e)}else{return this.setSliderPosition(f,e)}}};d.prototype.setSliderPosition=function(e,f){if(f==null){f=false}if(f&&this.settings.animate){this.dragger.animate({left:e},200);if(this.settings.highlight){return this.highlightTrack.animate({width:e},200)}}else{this.dragger.css({left:e});if(this.settings.highlight){return this.highlightTrack.css({width:e})}}};d.prototype.setSliderPositionFromValue=function(g,e){var f;if(e==null){e=false}f=this.valueToRatio(g);return this.setSliderPosition(f*this.slider.outerWidth(),e)};d.prototype.getRange=function(){if(this.settings.allowedValues){return{min:Math.min.apply(Math,this.settings.allowedValues),max:Math.max.apply(Math,this.settings.allowedValues)}}else{if(this.settings.range){return{min:parseFloat(this.settings.range[0]),max:parseFloat(this.settings.range[1])}}else{return{min:0,max:1}}}};d.prototype.nearestValidValue=function(i){var h,g,f,e;f=this.getRange();i=Math.min(f.max,i);i=Math.max(f.min,i);if(this.settings.allowedValues){h=null;c.each(this.settings.allowedValues,function(){if(h===null||Math.abs(this-i)<Math.abs(h-i)){return h=this}});return h}else{if(this.settings.step){g=(f.max-f.min)/this.settings.step;e=Math.floor((i-f.min)/this.settings.step);if((i-f.min)%this.settings.step>this.settings.step/2&&e<g){e+=1}return e*this.settings.step+f.min}else{return i}}};d.prototype.valueToRatio=function(l){var f,e,j,m,i,g,k,h;if(this.settings.equalSteps){h=this.settings.allowedValues;for(m=g=0,k=h.length;g<k;m=++g){f=h[m];if(!(typeof e!=="undefined"&&e!==null)||Math.abs(f-l)<Math.abs(e-l)){e=f;j=m}}if(this.settings.snapMid){return(j+0.5)/this.settings.allowedValues.length}else{return j/(this.settings.allowedValues.length-1)}}else{i=this.getRange();return(l-i.min)/(i.max-i.min)}};d.prototype.ratioToValue=function(h){var e,g,j,i,f;if(this.settings.equalSteps){f=this.settings.allowedValues.length;i=Math.round(h*f-0.5);e=Math.min(i,this.settings.allowedValues.length-1);return this.settings.allowedValues[e]}else{g=this.getRange();j=h*(g.max-g.min)+g.min;return this.nearestValidValue(j)}};d.prototype.valueChanged=function(h,f,e){var g;if(h.toString()===this.value.toString()){return}this.value=h;g={value:h,ratio:f,position:f*this.slider.outerWidth(),trigger:e,el:this.slider};return this.input.val(h).trigger(c.Event("change",g)).trigger("slider:changed",g)};return d})();c.extend(c.fn,{simpleSlider:function(){var e,d,f;f=arguments[0],e=2<=arguments.length?__slice.call(arguments,1):[];d=["setRatio","setValue"];return c(this).each(function(){var h,g;if(f&&__indexOf.call(d,f)>=0){h=c(this).data("slider-object");return h[f].apply(h,e)}else{g=f;return c(this).data("slider-object",new b(c(this),g))}})}});return c(function(){return c("[data-slider]").each(function(){var e,g,f,d;e=c(this);return e.simpleSlider()})})})(this.jQuery||this.Zepto,this);
$(function() {
$("#store-list-container").on("click", ".store-list-item", function() {
if($(this).data("item-type") == "D") {
$("#store-list-up-icon").removeClass("fa-reply").addClass("fa-refresh fa-spin");
var url = $(this).prop("href");
$.get(url, function(result) {
$("#store-list-container").html(result);
noJS();
$("[title]").tooltip();
history.pushState({}, "", url);
});
} else {
$(this).next(".store-list-file-infos").stop().slideToggle();
}
return false;
});
/* how upload works
* - user clicks on a "fake" browse button, this triggers a click event on the file upload
* - if the file input changes it adds the name of the file to form (or number of files if multiple is enabled)
* - and finally when we click on the upload button (this event handler) it firsts ask the store api where to upload
* then changes the form's action attr before sending the form itself
*/
$("#store-list-container").on("click", '#store-upload-form button[type="submit"]', function() {
var current_dir = $("#store-upload-form").find('[name="current_dir"]').val();
$.get($("#store-upload-form").data("action") + "?current_dir=" + current_dir, function(result) {
$('#store-upload-form button[type="submit"] i').addClass("fa-spinner fa-spin");
$("#store-upload-form").get(0).setAttribute("action", result['url']);
$("#store-upload-form").submit();
});
return false;
});
/* "fake" browse button */
$("#store-list-container").on("click", "#store-upload-browse", function() {
$('#store-upload-form input[type="file"]').click();
});
$("#store-list-container").on("change", "#store-upload-file", function() {
var input = $(this);
var numFiles = input.get(0).files ? input.get(0).files.length : 1;
var label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
input.trigger('fileselect', [numFiles, label]);
});
$("#store-list-container").on("fileselect", "#store-upload-file", function(event, numFiles, label) {
var input = $("#store-upload-filename");
var log = numFiles > 1 ? numFiles + ' files selected' : label;
if(input.length) {
input.val(log);
}
if(log) {
$('#store-upload-form button[type="submit"]').prop("disabled", false);
} else {
$('#store-upload-form button[type="submit"]').prop("disabled", true);
}
});
});
......@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation.btn', function(e) {
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({
......
......@@ -2,13 +2,17 @@ var vlans = [];
var disks = [];
$(function() {
if($(".vm-create-template-list").length) {
vmCreateLoaded();
} else {
vmCustomizeLoaded();
}
});
function vmCreateLoaded() {
$(".vm-create-template-details").hide();
$(".vm-create-template-summary").click(function() {
$(".vm-create-template-summary").unbind("click").click(function() {
$(this).next(".vm-create-template-details").slideToggle();
});
......@@ -64,9 +68,18 @@ function vmCreateLoaded() {
return false;
});
$('.progress-bar').each(function() {
var min = $(this).attr('aria-valuemin');
var max = $(this).attr('aria-valuemax');
var now = $(this).attr('aria-valuenow');
var siz = (now-min)*100/(max-min);
$(this).css('width', siz+'%');
});
}
function vmCustomizeLoaded() {
$("[title]").tooltip();
/* network thingies */
/* add network */
......@@ -92,7 +105,7 @@ function vmCustomizeLoaded() {
/* add dummy text if no more networks are available */
if($('#vm-create-network-add-select option').length < 1) {
$('#vm-create-network-add-button').attr('disabled', true);
$('#vm-create-network-add-select').html('<option value="-1">No more networks!</option>');
$('#vm-create-network-add-select').html('<option value="-1">' + gettext("No more networks.") + '</option>');
}
return false;
......@@ -124,7 +137,7 @@ function vmCustomizeLoaded() {
/* remove the selection from the multiple select */
$('#vm-create-network-add-vlan option[value="' + vlan_pk + '"]').prop('selected', false);
if ($('#vm-create-network-list').children('span').length < 1) {
$('#vm-create-network-list').append('Not added to any network!');
$('#vm-create-network-list').append(gettext("Not added to any network"));
}
});
return false;
......@@ -155,7 +168,7 @@ function vmCustomizeLoaded() {
// if all networks are added add a dummy and disable the add button
if($("#vm-create-network-add-select option").length < 1) {
$("#vm-create-network-add-select").html('<option value="-1">No more networks!</option>');
$("#vm-create-network-add-select").html('<option value="-1">' + gettext("No more networks.") + '</option>');
$('#vm-create-network-add-button').attr('disabled', true);
}
......@@ -170,54 +183,6 @@ function vmCustomizeLoaded() {
/* ----- end of networks thingies ----- */
/* add disk */
$('#vm-create-disk-add-button').click(function() {
var disk_pk = $('#vm-create-disk-add-select :selected').val();
var name = $('#vm-create-disk-add-select :selected').text();
if ($('#vm-create-disk-list').children('span').length < 1) {
$('#vm-create-disk-list').html('');
}
$('#vm-create-disk-list').append(
vmCreateDiskLabel(disk_pk, name)
);
/* select the disk from the multiple select */
$('#vm-create-disk-add-form option[value="' + disk_pk + '"]').prop('selected', true);
$('option:selected', $('#vm-create-disk-add-select')).remove();
/* add dummy text if no more disks are available */
if($('#vm-create-disk-add-select option').length < 1) {
$('#vm-create-disk-add-button').attr('disabled', true);
$('#vm-create-disk-add-select').html('<option value="-1">We are out of &lt;options&gt; hehe</option>');
}
return false;
});
/* remove disk */
// event for disk remove button (icon, X)
$('body').on('click', '.vm-create-remove-disk', function() {
var disk_pk = ($(this).parent('span').prop('id')).replace('disk-', '')
$(this).parent('span').fadeOut(500, function() {
/* remove the disk label */
$(this).remove();
var disk_name = $(this).text();
/* remove the selection from the multiple select */
$('#vm-create-disk-add-form option[value="' + disk_pk + '"]').prop('selected', false);
if ($('#vm-create-disk-list').children('span').length < 1) {
$('#vm-create-disk-list').append('No disks are added!');
}
});
return false;
});
/* copy disks from hidden select */
$('#vm-create-disk-add-form option').each(function() {
var text = $(this).text();
......@@ -244,6 +209,14 @@ function vmCustomizeLoaded() {
/* start vm button clicks */
$('#vm-create-customized-start').click(function() {
var error = false;
$(".cpu-count-input, .ram-input, #id_name, #id_amount ").each(function() {
if(!$(this)[0].checkValidity()) {
error = true;
}
});
if(error) return true;
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
......@@ -284,6 +257,7 @@ function vmCustomizeLoaded() {
/* for no js stuff */
$('.no-js-hidden').show();
$('.js-hidden').hide();
}
......@@ -294,5 +268,5 @@ function vmCreateNetworkLabel(pk, name, managed) {
function vmCreateDiskLabel(pk, name) {
var style = "float: left; margin: 5px 5px 5px 0;";
return '<span id="disk-' + pk + '" class="label label-primary" style="' + style + '"><i class="fa fa-file"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-disk"><i class="fa fa-times-circle"></i></a></span> ';
return '<span id="disk-' + pk + '" class="label label-primary" style="' + style + '"><i class="fa fa-file"></i> ' + name + '</span> ';
}
var show_all = false;
var in_progress = false;
var activity_hash = 5;
$(function() {
/* do we need to check for new activities */
......@@ -27,6 +28,15 @@ $(function() {
/* save resources */
$('#vm-details-resources-save').click(function() {
var error = false;
$(".cpu-count-input, .ram-input").each(function() {
if(!$(this)[0].checkValidity()) {
error = true;
}
});
if(error) return true;
$('i.fa-floppy-o', this).removeClass("fa-floppy-o").addClass("fa-refresh fa-spin");
var vm = $(this).data("vm");
$.ajax({
......@@ -34,8 +44,12 @@ $(function() {
url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) {
$("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o");
if(data.success) {
$('a[href="#activity"]').trigger("click");
} else {
addMessage(data.messages.join("<br />"), "danger");
}
$("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o");
},
error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o");
......@@ -307,6 +321,10 @@ $(function() {
$("#vm-details-connection-string").focus();
});
$("a.operation-password_reset").click(function() {
if(Boolean($(this).data("disabled"))) return false;
});
});
......@@ -327,6 +345,7 @@ function removePort(data) {
}
});
}
function decideActivityRefresh() {
......@@ -341,17 +360,6 @@ function decideActivityRefresh() {
return check;
}
/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
function unescapeHTML(html) {
return html.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&ndash;/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/&#39;/g, "'").replace(/ /g, '');
}
/* the html page contains some tags that were modified via js (titles for example), we delete these
also some html tags are closed with / */
function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
}
function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2];
......@@ -360,19 +368,24 @@ function checkNewActivity(runs) {
url: '/dashboard/vm/' + instance + '/activity/',
data: {'show_all': show_all},
success: function(data) {
if(show_all) { /* replace on longer string freezes the spinning stuff */
$("#activity-refresh").html(data['activities']);
} else {
a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-refresh").html());
if(a != b)
var new_activity_hash = (data['activities'] + "").hashCode();
if(new_activity_hash != activity_hash) {
$("#activity-refresh").html(data['activities']);
}
activity_hash = new_activity_hash;
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
$("#vm-details-state i").prop("class", "fa " + data['icon']);
/* changing the status text */
var icon = $("#vm-details-state i");
if(data['is_new_state']) {
if(!icon.hasClass("fa-spin"))
icon.prop("class", "fa fa-spinner fa-spin");
} else {
icon.prop("class", "fa " + data['icon']);
}
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
......@@ -380,12 +393,12 @@ function checkNewActivity(runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data['status'] == "STOPPED") {
$(".enabled-when-stopped").prop("disabled", false);
$(".hide-when-stopped").hide();
if(data['status'] == "STOPPED" || data['status'] == "PENDING") {
$(".change-resources-button").prop("disabled", false);
$(".change-resources-help").hide();
} else {
$(".enabled-when-stopped").prop("disabled", true);
$(".hide-when-stopped").show();
$(".change-resources-button").prop("disabled", true);
$(".change-resources-help").show();
}
if(runs > 0 && decideActivityRefresh()) {
......@@ -403,3 +416,14 @@ function checkNewActivity(runs) {
}
});
}
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length == 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
from os.path import splitext
import json
import logging
from urlparse import urljoin
from datetime import datetime
from django.http import Http404
from django.conf import settings
from requests import get, post, codes
from sizefield.utils import filesizeformat
logger = logging.getLogger(__name__)
class StoreApiException(Exception):
pass
class NotOkException(StoreApiException):
def __init__(self, status, *args, **kwargs):
self.status = status
super(NotOkException, self).__init__(*args, **kwargs)
class NoStoreException(StoreApiException):
pass
class Store(object):
def __init__(self, user, default_timeout=0.5):
self.request_args = {'verify': settings.STORE_VERIFY_SSL}
if settings.STORE_SSL_AUTH:
self.request_args['cert'] = (settings.STORE_CLIENT_CERT,
settings.STORE_CLIENT_KEY)
if settings.STORE_BASIC_AUTH:
self.request_args['auth'] = (settings.STORE_CLIENT_USER,
settings.STORE_CLIENT_PASSWORD)
self.username = "u-%d" % user.pk
self.default_timeout = default_timeout
self.store_url = settings.STORE_URL
if not self.store_url:
raise NoStoreException
def _request(self, url, method=get, timeout=None,
raise_status_code=True, **kwargs):
url = urljoin(self.store_url, url)
if timeout is None:
timeout = self.default_timeout
payload = json.dumps(kwargs) if kwargs else None
try:
headers = {'content-type': 'application/json'}
response = method(url, data=payload, headers=headers,
timeout=timeout, **self.request_args)
except Exception:
logger.exception("Error in store %s loading %s",
unicode(method), url)
raise
else:
if raise_status_code and response.status_code != codes.ok:
if response.status_code == 404:
raise Http404()
else:
raise NotOkException(response.status_code)
return response
def _request_cmd(self, cmd, **kwargs):
return self._request(self.username, post, CMD=cmd, **kwargs)
def list(self, path, process=True):
r = self._request_cmd("LIST", PATH=path)
result = r.json()
if process:
return self._process_list(result)
else:
return result
def toplist(self, process=True):
r = self._request_cmd("TOPLIST")
result = r.json()
if process:
return self._process_list(result)
else:
return result
def request_download(self, path):
r = self._request_cmd("DOWNLOAD", PATH=path, timeout=10)
return r.json()['LINK']
def request_upload(self, path):
r = self._request_cmd("UPLOAD", PATH=path)
return r.json()['LINK']
def remove(self, path):
self._request_cmd("REMOVE", PATH=path)
def new_folder(self, path):
self._request_cmd("NEW_FOLDER", PATH=path)
def rename(self, old_path, new_name):
self._request_cmd("RENAME", PATH=old_path, NEW_NAME=new_name)
def get_quota(self): # no CMD? :o
r = self._request(self.username)
quota = r.json()
quota.update({
'readable_used': filesizeformat(float(quota['used'])),
'readable_soft': filesizeformat(float(quota['soft'])),
'readable_hard': filesizeformat(float(quota['hard'])),
})
return quota
def set_quota(self, quota):
self._request("/quota/" + self.username, post, QUOTA=quota)
def user_exist(self):
try:
self._request(self.username)
return True
except NotOkException:
return False
def create_user(self, password, keys, quota):
self._request("/new/" + self.username, method=post,
SMBPASSWD=password, KEYS=keys, QUOTA=quota)
@staticmethod
def _process_list(content):
for d in content:
d['human_readable_date'] = datetime.utcfromtimestamp(float(
d['MTIME']))
delta = (datetime.utcnow() -
d['human_readable_date']).total_seconds()
d['is_new'] = 0 < delta < 5
d['human_readable_size'] = (
"directory" if d['TYPE'] == "D" else
filesizeformat(float(d['SIZE'])))
if d['DIR'] == ".":
d['directory'] = "/"
else:
d['directory'] = "/" + d['DIR'] + "/"
d['path'] = d['directory']
d['path'] += d['NAME']
if d['TYPE'] == "D":
d['path'] += "/"
d['ext'] = splitext(d['path'])[1]
d['icon'] = ("folder-open" if not d['TYPE'] == "F"
else file_icons.get(d['ext'], "file-o"))
return sorted(content, key=lambda k: k['TYPE'])
file_icons = {
'.txt': "file-text-o",
'.pdf': "file-pdf-o",
'.jpg': "file-image-o",
'.jpeg': "file-image-o",
'.png': "file-image-o",
'.gif': "file-image-o",
'.avi': "file-video-o",
'.mkv': "file-video-o",
'.mp4': "file-video-o",
'.mov': "file-video-o",
'.mp3': "file-sound-o",
'.flac': "file-sound-o",
'.wma': "file-sound-o",
'.pptx': "file-powerpoint-o",
'.ppt': "file-powerpoint-o",
'.doc': "file-word-o",
'.docx': "file-word-o",
'.xlsx': "file-excel-o",
'.xls': "file-excel-o",
'.rar': "file-archive-o",
'.zip': "file-archive-o",
'.7z': "file-archive-o",
'.tar': "file-archive-o",
'.gz': "file-archive-o",
'.py': "file-code-o",
'.html': "file-code-o",
'.js': "file-code-o",
'.css': "file-code-o",
'.c': "file-code-o",
'.cpp': "file-code-o",
'.h': "file-code-o",
'.sh': "file-code-o",
}
......@@ -27,43 +27,6 @@ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
class VmListTable(Table):
pk = TemplateColumn(
template_name='dashboard/vm-list/column-id.html',
verbose_name="ID",
attrs={'th': {'class': 'vm-list-table-thin'}},
)
name = TemplateColumn(
template_name="dashboard/vm-list/column-name.html"
)
admin = TemplateColumn(
template_name='dashboard/vm-list/column-admin.html',
attrs={'th': {'class': 'vm-list-table-admin'}},
)
details = TemplateColumn(
template_name='dashboard/vm-list/column-details.html',
attrs={'th': {'class': 'vm-list-table-thin'}},
)
actions = TemplateColumn(
template_name='dashboard/vm-list/column-actions.html',
attrs={'th': {'class': 'vm-list-table-thin'}},
)
time_of_suspend = TemplateColumn(
'{{ record.time_of_suspend|timeuntil }}',
verbose_name=_("Suspend in"))
time_of_delete = TemplateColumn(
'{{ record.time_of_delete|timeuntil }}',
verbose_name=_("Delete in"))
class Meta:
model = Instance
attrs = {'class': ('table table-bordered table-striped table-hover '
'vm-list-table')}
fields = ('pk', 'name', 'state', 'time_of_suspend', 'time_of_delete', )
class NodeListTable(Table):
pk = Column(
......
......@@ -68,6 +68,7 @@
</body>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="{{ STATIC_URL }}dashboard/loopj-jquery-simple-slider/js/simple-slider.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
{% include 'autocomplete_light/static.html' %}
......
{% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %}
<div class="row">
<div class="col-sm-3" style="font-weight: bold;">
<i class="fa fa-trophy"></i> {% trans "CPU priority" %}
</div>
<div class="col-sm-6">
<input type="text" class="vm-slider cpu-priority-slider"
disabled placeholder="{% trans "Enable JS for fancy sliders" %}"
data-slider="true" data-slider-highlight="true" data-slider-range="10, 100"
data-slider-values="0, 10, 30, 80, 100" data-slider-snap="true"/>
</div>
<div class="col-sm-3">
<div class="input-group">
{{ field_priority }}
<span class="input-group-addon input-tags">
<i class="fa fa-question" title="{% trans "The priority of the CPU" %}"></i>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-3" style="font-weight: bold;">
<i class="fa fa-cogs"></i> {% trans "CPU count" %}
</div>
<div class="col-sm-6">
<input type="text" class="vm-slider cpu-count-slider"
disabled placeholder="{% trans "Enable JS for fancy sliders" %}"
data-slider="true" data-slider-highlight="true" data-slider-range="0, 10"
data-slider-step="true" data-slider-snap="true"/>
</div>
<div class="col-sm-3">
<div class="input-group">
{{ field_num_cores }}
<span class="input-group-addon input-tags">
<i class="fa fa-question" title="{% trans "Number of CPU cores" %}"></i>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-3" style="font-weight: bold;">
<i class="fa fa-ticket"></i> {% trans "RAM amount" %}
</div>
<div class="col-sm-6">
<input type="text" class="vm-slider ram-slider"
disabled placeholder="{% trans "Enable JS for fancy sliders" %}"
data-slider="true" data-slider-highlight="true" data-slider-range="0, 8192"
data-slider-step="128" data-slider-snap="true"/>
</div>
<div class="col-sm-3">
<div class="input-group">
{{ field_ram_size }}
<span class="input-group-addon input-tags">
MiB
</span>
<span class="input-group-addon input-tags">
<i title="{% trans "RAM size in mebibytes" %}" class="fa fa-question"></i>
</span>
</div>
</div>
</div>
{% load i18n %}
{% load crispy_forms_tags %}
{% if leases < 1 %}
<form action="" method="POST">
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% csrf_token %}
{{ form.name|as_crispy_field }}
<fieldset class="resources-sliders">
<legend>{% trans "Resource configuration" %}</legend>
{% include "dashboard/_resources-sliders.html" with field_priority=form.priority field_num_cores=form.num_cores field_ram_size=form.ram_size %}
{{ form.max_ram_size|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "Virtual machine settings" %}</legend>
{{ form.arch|as_crispy_field }}
{{ form.access_method|as_crispy_field }}
{{ form.boot_menu|as_crispy_field }}
{{ form.raw_data|as_crispy_field }}
{{ form.req_traits|as_crispy_field }}
{{ form.description|as_crispy_field }}
{{ form.system|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "External resources" %}</legend>
{{ form.networks|as_crispy_field }}
{{ form.lease|as_crispy_field }}
{% if show_lease_create %}
<div class="alert alert-warning">
{% trans "You haven't created any leases yet, but you need one to create a template!" %}
{% trans "You haven't created any leases yet, but you need one to create a template." %}
<a href="{% url "dashboard.views.lease-create" %}">{% trans "Create a new lease now." %}</a>
</div>
{% endif %}
{{ form.tags|as_crispy_field }}
</fieldset>
<input type="submit" value="{% trans "Create new template" %}" class="btn btn-success">
</form>
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% crispy form %}
<style>
fieldset {
......@@ -21,8 +53,3 @@
font-weight: bold;
}
</style>
<script>
$(function() {
$("#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size").hide();
});
</script>
......@@ -58,7 +58,7 @@
</li>
</ul>
<div style="margin-top: 20px; padding: 0 15px; width: 100%">
{% if perms.vm_set_resources %}
{% if perms.vm.set_resources %}
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="fa fa-wrench"></i> {% trans "Customize" %}</a>
{% endif %}
<form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}">
......@@ -76,48 +76,6 @@
</div>
<style>
.row {
margin-bottom: 15px;
}
.vm-create-template {
max-width: 800px;
border: 1px solid black;
border-bottom: none;
}
.vm-create-template-list .vm-create-template:last-child {
border-bottom: 1px solid black;
}
.vm-create-template-summary {
padding: 15px;
cursor: pointer;
}
.vm-create-template:nth-child(odd) .vm-create-template-summary {
background: #F5F5F5;
}
.vm-create-template-list .vm-create-template-summary:hover {
background: #D2D2D2;
}
.vm-create-template-details {
border-top: 1px dashed #D3D3D3;
padding: 15px;
}
.vm-create-template-details ul {
list-style: none;
padding: 0 15px;
}
.vm-create-template-details li {
border-bottom: 1px dotted #aaa;
padding: 5px 0px;
}
.progress {
position: relative;
width: 200px;
......@@ -139,15 +97,3 @@
font-size: 10px;
}
</style>
{% block "extra-js" %}
<script>
$('.progress-bar').each(function() {
var min = $(this).attr('aria-valuemin');
var max = $(this).attr('aria-valuemax');
var now = $(this).attr('aria-valuenow');
var siz = (now-min)*100/(max-min);
$(this).css('width', siz+'%');
});
</script>
{% endblock %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load sizefieldtags %}
{% crispy vm_create_form %}
{% include "display-form-errors.html" with form=vm_create_form %}
<form method="POST">
{% csrf_token %}
<script src="/static/dashboard/vm-create.js"></script>
{{ vm_create_form.template }}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<button id="vm-create-customized-start" class="btn btn-success"
style="float: right; margin-top: 24px;">
<i class="fa fa-play"></i>
{% trans "Start" %}
</button>
<label>{% trans "Name" %}*</label>
{{ vm_create_form.name }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-10">
<div class="form-group">
<label>{% trans "Amount" %}*</label>
{{ vm_create_form.amount }}
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<h2>{% trans "Resources" %}</h2>
</div>
</div>
<!-- resources -->
<div class="resources-sliders" style="max-width: 720px;">
{% include "dashboard/_resources-sliders.html" with field_priority=vm_create_form.cpu_priority field_num_cores=vm_create_form.cpu_count field_ram_size=vm_create_form.ram_size %}
</div>
<div class="row">
<div class="col-sm-4">
<h2>{% trans "Disks" %}</h2>
</div>
<div class="col-sm-8">
<div class="js-hidden">
{{ vm_create_form.disks }}
</div>
<div class="no-js-hidden">
<h3 id="vm-create-disk-list">{% trans "No disks are added." %}</h3>
<div style="clear: both;"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<h2>{% trans "Network" %}</h2>
</div>
<div class="col-sm-8" style="padding-top: 3px;">
<div class="js-hidden" style="padding-top: 15px; max-width: 450px;">
{{ vm_create_form.networks }}
</div>
<div class="no-js-hidden">
<h3 id="vm-create-network-list">
{% trans "Not added to any network." %}
</h3>
<h3 id="vm-create-network-add">
<div class="input-group" style="max-width: 330px;">
<select class="form-control font-awesome-font" id="vm-create-network-add-select">
</select>
<div class="input-group-btn">
<a id="vm-create-network-add-button" class="btn btn-success">
<i class="fa fa-plus-circle"></i>
</a>
</div><!-- .input-group-btn -->
</div><!-- .input-group -->
</h3><!-- #vm-create-network-add -->
</div><!-- .no-js-hidden -->
</div><!-- .col-sm-8 -->
</div><!-- .row -->
</form>
<script>
try {
vmCustomizeLoaded();
} catch(e) {}
</script>
......@@ -5,7 +5,7 @@
{% block extra_link %}
<link rel="stylesheet" href="{{ STATIC_URL }}dashboard/bootstrap-slider/slider.css"/>
<link rel="stylesheet" href="{{ STATIC_URL }}dashboard/loopj-jquery-simple-slider/css/simple-slider.css"/>
<link href="{{ STATIC_URL }}dashboard/bootstrap-tour.min.css" rel="stylesheet">
<link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet">
{% endblock %}
......@@ -18,8 +18,8 @@
{% endblock %}
{% block navbar %}
<ul class="nav navbar-nav pull-right">
{% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav pull-right">
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;"
class="dropdown-toggle" data-toggle="dropdown">
......@@ -32,9 +32,8 @@
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
</ul>
{% if user.is_authenticated and user.pk %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a>
......@@ -48,7 +47,7 @@
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %}
{% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
<a class="navbar-brand pull-right" href="{% url "login" %}?next={{ request.path }}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
{% endif %}
{% endblock %}
......@@ -56,6 +55,5 @@
{% block extra_script %}
<script src="{{ STATIC_URL }}dashboard/js/jquery.knob.js"></script>
<script src="{{ STATIC_URL}}dashboard/bootstrap-slider/bootstrap-slider.js"></script>
<script src="{{ STATIC_URL }}dashboard/dashboard.js"></script>
{% endblock %}
{% load crispy_forms_tags %}
<style>
.row {
margin-bottom: 15px;
}
</style>
<form method="POST" action="{% url "dashboard.views.group-create" %}">
{% csrf_token %}
......
......@@ -7,7 +7,7 @@
<div class="page-header">
<div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button"><i class="fa fa-pencil"></i></a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash"></i></a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash-o"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a>
</div>
<h1>
......
......@@ -23,11 +23,11 @@
</div>
{% endif %}
{% comment %}
{% if not no_store %}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-files.html" %}
{% include "dashboard/store/index-files.html" %}
</div>
{% endcomment %}
{% endif %}
{% if perms.vm.create_template %}
<div class="col-lg-4 col-sm-6">
......
......@@ -82,9 +82,9 @@
</tr>
{% endfor %}
<tr><td><i class="icon-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
<td><input type="text" class="form-control" name="name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
<td><select class="form-control" name="level">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
......
{% load crispy_forms_tags %}
<form method="POST" action="/dashboard/node/create/" id="node-create-form">
{% csrf_token %}
{% crispy formset formset.form.helper %}
</form>
<style>
.row {
margin-bottom: 15px;
#node-create-form .row {
margin-bottom: 10px;
}
</style>
<form method="POST" action="/dashboard/node/create/">
{% csrf_token %}
{% crispy formset formset.form.helper %}
</form>
......@@ -16,7 +16,7 @@
{% endblock %}
{% block extra_js %}
{% if template == "dashboard/_vm-create-1.html" %}
{% if template == "dashboard/_vm-create-1.html" or template == "dashboard/_vm-create-2.html" %}
<script src="{{ STATIC_URL }}dashboard/vm-create.js"></script>
{% endif %}
{% endblock %}
......@@ -64,7 +64,7 @@
<i class="fa fa-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4>
<ul class="dashboard-profile-vm-list">
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_owned %}
<li>
<a href="{{ i.get_absolute_url }}">
......@@ -85,7 +85,7 @@
<i class="fa fa-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4>
<ul class="dashboard-profile-vm-list">
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_with_access %}
<li>
<a href="{{ i.get_absolute_url }}">
......
{% load i18n %}
<div class="list-group">
<div class="list-group-item">
<div class="row">
<div class="col-sm-6">
<a href="{% url "dashboard.views.store-upload"%}?directory={{ current }}"
class="btn btn-info btn-xs js-hidden">
{% trans "Upload" %}
</a>
<form action="" data-action="{% url "dashboard.views.store-upload-url" %}"
method="POST" enctype="multipart/form-data" class="no-js-hidden"
id="store-upload-form">
{% csrf_token %}
<input type="hidden" name="current_dir" value="{{ current }}"/>
<input type="hidden" name="next" value="{{ next_url }}"/>
<div class="input-group" style="max-width: 350px;">
<span class="input-group-btn" id="store-upload-browse">
<span class="btn btn-primary btn-xs">
{% trans "Browse..." %}
</span>
</span>
<input type="text" class="form-control input-tags"
id="store-upload-filename"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary btn-xs" disabled>
<i class="fa fa-cloud-upload"></i> {% trans "Upload" %}
</button>
</span>
</div>
<input id="store-upload-file" name="data" type="file" style="display:none">
</form>
</div><!-- .col-sm-6 upload -->
<div class="col-sm-6">
<a href="{% url "dashboard.views.store-remove" %}?path={{ current }}"
class="btn btn-danger btn-xs pull-right store-action-button"
title="{% trans "Remove directory" %}">
<i class="fa fa-times"></i>
</a>
<a href="{% url "dashboard.views.store-download" %}?path={{ current }}"
class="btn btn-primary btn-xs pull-right store-action-button"
title="{% trans "Download directory" %}">
<i class="fa fa-cloud-download"></i>
</a>
<form method="POST" action="{% url "dashboard.views.store-new-directory" %}">
{% csrf_token %}
<input type="hidden" name="path" value="{{ current }}"/>
<div class="input-group" style="max-width: 300px;">
<span class="input-group-addon input-tags" title="{% trans "New directory" %}">
<i class="fa fa-folder-open"></i>
</span>
<input type="text" class="form-control input-tags" name="name"
placeholder="{% trans "Name "%}" required/>
<span class="input-group-btn">
<input type="submit" class="btn btn-success btn-xs" value="{% trans "Create" %}"/>
</span>
</div>
</form>
</div><!-- .col-sm-6 -->
</div><!-- .row -->
</div><!-- .list-group-item -->
</div><!-- .list-group -->
<div class="list-group" id="store-list-list">
<a href="{% url "dashboard.views.store-list" %}?directory={{ up_url }}"
class="list-group-item store-list-item" data-item-type="D">
{% if current == "/" %}
<div class="store-list-item-icon">
<i class="fa fa-refresh" id="store-list-up-icon"></i>
</div>
{% trans "Refresh" %}
{% else %}
<div class="store-list-item-icon">
<i class="fa fa-reply" id="store-list-up-icon"></i>
</div>
..
{% endif %}
<div class="pull-right">
{{ current }}
</div>
</a>
{% for f in root %}
<a class="list-group-item store-list-item" data-item-type="{{ f.TYPE }}"
href="{% if f.TYPE == "D" %}{% url "dashboard.views.store-list" %}?directory={{ f.path }}{% else %}
{% url "dashboard.views.store-download" %}?path={{ f.path }}{% endif %}"
>
<div class="store-list-item-icon">
<i class="
fa fa-{{ f.icon }}{% if f.TYPE == "D" %} store-list-item-icon-directory{% endif %}"
></i>
</div>
<div class="store-list-item-name">
{{ f.NAME }}
</div>
<div class="store-list-item-new">
{% if f.is_new and f.TYPE == "F" %}
<span class="badge badge-pulse">{% trans "new" %}</span>
{% endif %}
</div>
<div class="store-list-item-size">
{{ f.human_readable_size }}
</div>
<div class="clearfix"></div>
</a>
<div class="store-list-file-infos">
<div class="row">
<div class="col-sm-10">
<dl class="dl-horizontal" style="margin: 0; padding: 0;">
<dt>{% trans "Filename" %}</dt>
<dd>{{ f.NAME }}</dd>
<dt>{% trans "Size" %}</dt>
<dd>{{ f.human_readable_size }}</dd>
<dt>{% trans "Latest modification" %}</dt>
<dd>{{ f.human_readable_date }}</dd>
</dl>
</div>
<div class="col-sm-2" style="text-align: right;">
<a href="{% url "dashboard.views.store-download" %}?path={{ f.path }}"
class="btn btn-primary btn-sm store-download-button">
<i class="fa fa-download"></i>
{% trans "Download" %}
</a>
<a href="{% url "dashboard.views.store-remove" %}?path={{ f.path }}"
class="btn btn-danger btn-xs store-remove-button">
<i class="fa fa-times"></i>
{% trans "Remove" %}
</a>
</div>
</div><!-- .row -->
</div>
{% empty %}
<a class="list-group-item">
{% trans "This folder is empty." %}
</a>
{% endfor %}
</div><!-- closing list-group -->
{% load i18n %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="btn btn-default btn-xs infobtn pull-right store-action-button"
title="{% trans "A list of your most recent files." %}">
<i class="fa fa-info-circle"></i>
</span>
<span class="btn btn-default btn-xs infobtn pull-right"
title="
{% blocktrans with used=files.quota.readable_used soft=files.quota.readable_soft hard=files.quota.readable_hard %}
You are currently using {{ used }}, your soft limit is {{ soft }}, your hard limit is {{ hard }}.
{% endblocktrans %}">
<i class="fa fa-adjust"></i>
</span>
<h3 class="no-margin"><i class="fa fa-briefcase"></i> {% trans "Files" %}
</h3>
</div>
<div class="list-group" id="dashboard-files-toplist">
{% for t in files.toplist %}
{% if t.TYPE == "F" %}
<div class="list-group-item">
<i class="fa fa-{{ t.icon }} dashboard-toplist-icon"></i>
<div class="store-list-item-name">
{{ t.NAME }}
</div>
<a href="{% url "dashboard.views.store-download" %}?path={{ t.path }}"
class="pull-right btn btn-xs" style="color: black;">
<i class="fa fa-cloud-download" title="{% trans "Download" %}"></i>
</a>
<a href="{% url "dashboard.views.store-list" %}?directory={{ t.directory }}"
class="pull-right btn btn-xs" style="color: black;">
<i class="fa fa-folder-open" title="{% trans "Show in directory" %}"></i>
</a>
<div style="clear: both;"></div>
</div>
{% else %}
<a href="{% url "dashboard.views.store-list" %}?directory={{ t.path }}"
class="list-group-item">
<i class="fa fa-{{ t.icon }} dashboard-toplist-icon"></i>
<div class="store-list-item-name">
{{ t.NAME }}
</div>
<div style="clear: both;"></div>
</a>
{% endif %}
{% endfor %}
<div class="list-group-item text-right no-hover">
<form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}">
{% csrf_token %}
<button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/>
<i class="fa fa-refresh"></i>
</button>
</form>
<a href="{% url "dashboard.views.store-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i> {% trans "show my files" %}
</a>
<a href="{% url "dashboard.views.store-upload" %}" class="btn btn-success btn-xs">
<i class="fa fa-cloud-upload"></i> {% trans "upload" %}
</a>
</div>
</div>
</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block title-page %}{% trans "List" %} | {% trans "Store" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div id="store-list-container">
{% include "dashboard/store/_list-box.html" %}
</div>
</div>
</div>
<div style="position: relative;">
<div class="progress" style="width: 100%">
<div class="progress-bar" role="progressbar"
aria-valuenow="{{ quota.used }}" aria-valuemin="0" aria-valuemax="{{ quota.hard }}"
style="width: {% widthratio quota.used quota.hard 100 %}%; min-width: 150px;">
<div style="padding-top: 2px;">
{% blocktrans with used=quota.readable_used %}
{{ used }} used
{% endblocktrans %}
</div>
</div>
<div class="progress-marker" id="progress-marker-hard" data-placement="left"
title="{% trans "Hard limit" %}: {{ quota.readable_hard }}">
</div>
<div class="progress-marker" id="progress-marker-soft" style="background: orange;
left: {% widthratio quota.soft quota.hard 100 %}%"
title="{% trans "Soft limit" %}: {{ quota.readable_soft }}"
data-placement="top">
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/store.js"></script>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-times"></i>
{% if is_dir %}
{% trans "Directory removal confirmation" %}
{% else %}
{% trans "File removal confirmation" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
{% if not is_dir %}
<h4>{% trans "File directory" %}: {{ directory }}</h4>
<h4>{% trans "File name" %}: {{ name }}</h4>
{% blocktrans with path=path %}
Are you sure you want to remove the file at <strong>{{ path }}</strong>?
{% endblocktrans %}
{% else %}
{% blocktrans with directory=directory %}
Are you sure you want to remove the directory <strong>{{ directory }}</strong>?
{% endblocktrans %}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a href="{% url "dashboard.views.store-list" %}?directory={{ directory }}"
class="btn btn-default">{% trans "Cancel" %}</a>
<input type="hidden" name="path" value="{{ path }}"/>
<button class="btn btn-danger">{% trans "Remove" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block title-page %}{% trans "Upload" %} | {% trans "Store" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="btn btn-default pull-right btn-xs"
href="{% url "dashboard.views.store-list" %}">Back</a>
<h3 class="no-margin">
<i class="fa fa-cloud-upload"></i>
{% trans "File upload" %}
</h3>
</div>
<div class="panel-body">
<div style="text-align: center; margin: 0 0 20px 0;">
<div class="label label-info" style="padding: 5px;">
{% trans "Currently uploading to" %}: {{ directory }}
</div>
</div>
<form method="POST" action="{{ action }}" enctype="multipart/form-data">
<input type="hidden" name="next" value="{{ next_url }}"/>
<div>
<input class="btn btn-default btn-sm pull-right"
type="submit" value="{% trans "Upload" %}"/>
<input type="file" name="data" style="padding-top: 5px;" required/>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -15,10 +15,41 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Edit template" %}</h3>
</div>
<div class="panel-body">
<form action="" method="POST">
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% crispy form %}
{% csrf_token %}
{{ form.name|as_crispy_field }}
<fieldset class="resources-sliders">
<legend>{% trans "Resource configuration" %}</legend>
{% include "dashboard/_resources-sliders.html" with field_priority=form.priority field_num_cores=form.num_cores field_ram_size=form.ram_size %}
{{ form.max_ram_size|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "Virtual machine settings" %}</legend>
{{ form.arch|as_crispy_field }}
{{ form.access_method|as_crispy_field }}
{{ form.boot_menu|as_crispy_field }}
{{ form.raw_data|as_crispy_field }}
{{ form.req_traits|as_crispy_field }}
{{ form.description|as_crispy_field }}
{{ form.system|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "External resources" %}</legend>
{{ form.networks|as_crispy_field }}
{{ form.lease|as_crispy_field }}
{{ form.tags|as_crispy_field }}
</fieldset>
<input type="submit" value="{% trans "Save changes" %}" class="btn btn-primary">
</form>
</div>
</div>
</div>
......
......@@ -63,7 +63,11 @@
<div class="col-md-4" id="vm-info-pane">
<div class="big">
<span id="vm-details-state" class="label label-success">
<i class="fa {{ instance.get_status_icon }}"></i>
<i class="fa
{% if is_new_state %}
fa-spinner fa-spin
{% else %}
{{ instance.get_status_icon }}{% endif %}"></i>
<span>{{ instance.get_status_display|upper }}</span>
</span>
</div>
......@@ -75,6 +79,8 @@
<dd>
{% if instance.get_connect_port %}
{{ instance.get_connect_host }}:<strong>{{ instance.get_connect_port }}</strong>
{% elif instance.interface_set.count < 1%}
<strong>{% trans "The VM doesn't have any network interface." %}</strong>
{% else %}
<strong>{% trans "The required port for this protocol is not forwarded." %}</strong>
{% endif %}
......@@ -100,7 +106,7 @@
<dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %}
<a href="{{op.get_url}}" class="btn operation btn-default btn-xs" {% if op.disabled %}disabled{% endif %}>{% trans "Generate new password!" %}</a>
<a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" class="operation operation-{{op.op}}" data-disabled="{% if op.disabled %}true" title="{% trans "Start the VM to change the password." %}"{% else %}false" {% endif %}>{% trans "Generate new password!" %}</a>
{% endif %}{% endwith %}
</div>
</dd>
......@@ -109,8 +115,7 @@
<div class="input-group" id="dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}
{% trans "Connection is not possible." %}{% endif %}"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}{% trans "Connection is not possible." %}{% endif %}"
id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i>
......
......@@ -2,55 +2,26 @@
{% load sizefieldtags %}
{% load crispy_forms_tags %}
<form id="vm-details-resources-form" method="POST" action="">
<form method="POST" action="{{ op.resources_change.get_url }}" id="vm-details-resources-form">
{% csrf_token %}
<p class="row">
<div class="col-sm-3">
<label for="vm-cpu-priority-slider"><i class="fa fa-trophy"></i> {% trans "CPU priority" %}</label>
</div>
<div class="col-sm-9">
<input name="cpu-priority" type="text" id="vm-cpu-priority-slider" class="vm-slider" value="{{ instance.priority }}" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="{{ instance.priority }}" data-slider-orientation="horizontal" data-slider-handle="square" data-slider-tooltip="hide"/>
</div>
</p>
<p class="row">
<div class="col-sm-3">
<label for="cpu-count-slider"><i class="fa fa-cogs"></i> {% trans "CPU count" %}</label>
</div>
<div class="col-sm-9">
<input name="cpu-count" type="text" id="vm-cpu-count-slider" class="vm-slider" value=" {{ instance.num_cores }}" data-slider-min="0" data-slider-max="8" data-slider-step="1" data-slider-value="{{ instance.num_cores }}" data-slider-orientation="horizontal" data-slider-handle="square" data-slider-tooltip="hide"/>
</div>
</p>
<p class="row">
<div class="col-sm-3">
<label for="ram-slider"><i class="fa fa-ticket"></i> {% trans "RAM amount" %}</label>
</div>
<div class="col-sm-9">
<input name="ram-size" type="text" id="vm-ram-size-slider" class="vm-slider" value="{{ instance.ram_size }}" data-slider-min="128" data-slider-max="4096" data-slider-step="128" data-slider-value="{{ instance.ram_size }}" data-slider-orientation="horizontal" data-slider-handle="square" data-slider-tooltip="hide"/> MiB
</div>
</p>
{% include "dashboard/_resources-sliders.html" with field_priority=resources_form.priority field_num_cores=resources_form.num_cores field_ram_size=resources_form.ram_size %}
{% if can_change_resources %}
<p class="row">
<div class="col-sm-12">
<button type="submit" class="btn btn-success btn-sm enabled-when-stopped" id="vm-details-resources-save"
data-vm="{{ instance.pk }}"
{% if not op.resources_change %}disabled{% endif %}>
<button type="submit" class="btn btn-success btn-sm change-resources-button"
id="vm-details-resources-save" data-vm="{{ instance.pk }}"
{% if op.resources_change.disabled %}disabled{% endif %}>
<i class="fa fa-floppy-o"></i> {% trans "Save resources" %}
</button>
<span class="hide-when-stopped"
{% if op.resources_change %}style="display: none;"{% endif %}
<span class="change-resources-help"
{% if not op.resources_change.disabled %}style="display: none;"{% endif %}
>{% trans "Stop your VM to change resources." %}</span>
</div>
</p>
{% endif %}
</form>
<hr />
<div class="row" id="vm-details-resources-disk">
<div class="col-sm-11">
<h3>
......
......@@ -2,8 +2,6 @@
<div class="btn-group">
<button type="button" class="btn btn-xs btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action <i class="fa fa-caret-down"></i></button>
<ul class="nojs-dropdown-toogle dropdown-menu" role="menu">
<li><a href="#"><i class="fa fa-refresh"></i> Reboot</a></li>
<li><a href="#"><i class="fa fa-off"></i> Shutdown</a></li>
<li><a data-vm-pk="{{ record.pk }}" class="real-link vm-delete" href="{% url "dashboard.views.delete-vm" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-times"></i> Discard</a></li>
<li><a class="real-link" href="{%url "dashboard.vm.op.destroy" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-times"></i> Discard</a></li>
</ul>
</div>
......@@ -107,28 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200)
def test_permitted_vm_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 302)
def test_not_permitted_vm_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'operator')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 403)
def test_unpermitted_vm_delete(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 403)
def test_unpermitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user1')
......@@ -304,10 +282,12 @@ class VmDetailTest(LoginMixin, TestCase):
c = Client()
self.login(c, 'superuser')
kwargs = InstanceTemplate.objects.get(id=1).__dict__.copy()
kwargs.update(name='t2', lease=1, disks=1, raw_data='tst2')
kwargs.update(name='t2', lease=1, disks=1,
raw_data='<devices></devices>')
response = c.post('/dashboard/template/1/', kwargs)
self.assertEqual(response.status_code, 302)
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data, 'tst2')
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data,
"<devices></devices>")
def test_permitted_lease_delete_w_template_using_it(self):
c = Client()
......@@ -529,24 +509,25 @@ class VmDetailTest(LoginMixin, TestCase):
def test_permitted_wake_up_wrong_state(self):
c = Client()
self.login(c, "user2")
with patch.object(WakeUpOperation, 'async') as mock_method:
with patch.object(WakeUpOperation, 'async') as mock_method, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1)
mock_method.side_effect = inst.wake_up
inst.status = 'RUNNING'
inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called
assert wro.called
def test_permitted_wake_up(self):
c = Client()
self.login(c, "user2")
with patch.object(Instance, 'select_node', return_value=None):
with patch.object(WakeUpOperation, 'async') as new_wake_up:
with patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa:
with patch.object(Instance, 'select_node', return_value=None), \
patch.object(WakeUpOperation, 'async') as new_wake_up, \
patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up
inst.get_remote_queue_name = Mock(return_value='test')
......@@ -559,6 +540,7 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called
assert wuaa.called
assert not wro.called
def test_unpermitted_wake_up(self):
c = Client()
......@@ -585,7 +567,7 @@ class VmDetailTest(LoginMixin, TestCase):
'amount': 2,
'customized': 1,
'template': 1,
'cpu_priority': 1, 'cpu_count': 1, 'ram_size': 1,
'cpu_priority': 10, 'cpu_count': 1, 'ram_size': 128,
'network': [],
})
......@@ -1796,9 +1778,11 @@ class SshKeyTest(LoginMixin, TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.profile = Profile()
self.u1.save()
self.u2 = User.objects.create(username='user2')
self.u2.set_password('password')
self.u2.profile = Profile()
self.u2.save()
self.k1 = UserKey(key='ssh-rsa AAAAB3NzaC1yc2EC asd', user=self.u1)
self.k1.save()
......
......@@ -28,7 +28,7 @@ from .views import (
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView,
......@@ -39,6 +39,8 @@ from .views import (
get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView,
LeaseAclUpdateView,
......@@ -86,8 +88,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'),
url(r'^vm/delete/(?P<pk>\d+)/$', VmDelete.as_view(),
name="dashboard.views.delete-vm"),
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
......@@ -179,5 +179,21 @@ urlpatterns = patterns(
url(r'^sshkey/create/$',
UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(),
name="dashboard.views.store-list"),
url(r"^store/download/$", store_download,
name="dashboard.views.store-download"),
url(r"^store/upload/url$", store_get_upload_url,
name="dashboard.views.store-upload-url"),
url(r"^store/upload/$", store_upload,
name="dashboard.views.store-upload"),
url(r"^store/remove/$", StoreRemove.as_view(),
name="dashboard.views.store-remove"),
url(r"^store/new_directory/$", store_new_directory,
name="dashboard.views.store-new-directory"),
url(r"^store/refresh_toplist$", store_refresh_toplist,
name="dashboard.views.store-refresh-toplist"),
)
#!/bin/echo Usage: fab --list -f
import contextlib
import datetime
......@@ -9,14 +10,14 @@ from fabric.decorators import roles, parallel
env.roledefs['portal'] = ['localhost']
try:
from vm.models import Node
from storage.models import DataStore
from vm.models import Node as _Node
from storage.models import DataStore as _DataStore
except Exception as e:
print e
else:
env.roledefs['node'] = [unicode(n.host.ipv4)
for n in Node.objects.filter(enabled=True)]
env.roledefs['storage'] = [DataStore.objects.get().hostname]
for n in _Node.objects.filter(enabled=True)]
env.roledefs['storage'] = [_DataStore.objects.get().hostname]
def update_all():
......@@ -131,7 +132,7 @@ def update_storage():
"Update and restart storagedriver"
with _stopped("storage"):
pull("~/storagedriver")
pip("storage", "~/storagedriver/requirements/production.txt")
pip("storagedriver", "~/storagedriver/requirements/production.txt")
@parallel
......
......@@ -98,5 +98,5 @@ def reloadtask(type='Host', timeout=15):
}[type]
logger.info("Reload %s on next periodic iteration applying change to %s.",
", ".join(reload), type)
if all(cache.add("%s_lock" % i, True, 30) for i in reload):
if all([cache.add("%s_lock" % i, 'true', 30) for i in reload]):
reloadtask_worker.apply_async(queue='localhost.man', countdown=5)
[General]
LangCode=hu
MailingList=cloud@ik.bme.hu
PotBaseDir=./
ProjectID=circle-hu
TargetLangCode=hu
This source diff could not be displayed because it is too large. You can view the blob instead.
<!--
Collection name attribute represents the name of the menu, e.g., to use menu "File" use "file" or "Help" use "help". You can add new menus.
If you type a relative script file beware the this script is located in $KDEHOME/share/apps/applicationname/
The following example adds an action with the text "Export..." into the "File" menu
<KrossScripting>
<collection name="file" text="File" comment="File menu">
<script name="export" text="Export..." comment="Export content" file="export.py" />
</collection>
</KrossScripting>
-->
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
import logging
logging.basicConfig(filename='/var/tmp/0015_disk_migration.log',level=logging.INFO)
logger = logging.getLogger()
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
# Note: Don't use "from appname.models import ModelName".
# Use orm.ModelName to refer to models in this application,
# and orm['appname.ModelName'] for models in other applications.
disks = orm.Disk
for disk in disks.objects.all():
if disk.base is None and disk.destroyed is None:
logger.info("Set disk %s (%s) with filename: %s to ready.",
disk.name, disk.pk, disk.filename)
disk.is_ready = True
disk.save()
def backwards(self, orm):
"Write your backwards methods here."
models = {
u'storage.datastore': {
'Meta': {'ordering': "[u'name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
}
}
complete_apps = ['storage']
symmetrical = True
......@@ -27,13 +27,15 @@ from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey)
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField
from .tasks import local_tasks, storage_tasks
from celery.exceptions import TimeoutError
from common.models import WorkerNotFound
from common.models import (
WorkerNotFound, HumanReadableException, humanize_exception
)
logger = logging.getLogger(__name__)
......@@ -104,43 +106,73 @@ class Disk(TimeStampedModel):
('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception):
def __init__(self, type, message=None):
if message is None:
message = ("Operation can't be invoked on a disk of type '%s'."
% type)
Exception.__init__(self, message)
self.type = type
class DiskInUseError(Exception):
def __init__(self, disk, message=None):
if message is None:
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it is in use." %
(disk.name, disk.filename))
Exception.__init__(self, message)
self.disk = disk
class DiskIsNotReady(Exception):
""" Exception for operations that need a deployed disk.
"""
def __init__(self, disk, message=None):
if message is None:
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it has never been"
"deployed." % (disk.name, disk.filename))
Exception.__init__(self, message)
self.disk = disk
class DiskError(HumanReadableException):
admin_message = None
def __init__(self, disk, params=None, level=None, **kwargs):
kwargs.update(params or {})
self.disc = kwargs["disk"] = disk
super(Disk.DiskError, self).__init__(
level, self.message, self.admin_message or self.message,
kwargs)
class WrongDiskTypeError(DiskError):
message = ugettext_noop("Operation can't be invoked on disk "
"'%(name)s' of type '%(type)s'.")
admin_message = ugettext_noop(
"Operation can't be invoked on disk "
"'%(name)s' (%(pk)s) of type '%(type)s'.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.WrongDiskTypeError, self).__init__(
disk, params, type=disk.type, name=disk.name, pk=disk.pk)
class DiskInUseError(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because it is in use.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) because it is in use.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.DiskInUseError, self).__init__(
disk, params, name=disk.name, pk=disk.pk)
class DiskIsNotReady(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because it has never been deployed.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) [%(filename)s] because it has never been"
"deployed.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.DiskIsNotReady, self).__init__(
disk, params, name=disk.name, pk=disk.pk,
filename=disk.filename)
class DiskBaseIsNotReady(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because its base has never been deployed.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) [%(filename)s] because its base "
"'%(b_name)s' (%(b_pk)s) [%(b_filename)s] has never been"
"deployed.")
def __init__(self, disk, params=None, **kwargs):
base = kwargs.get('base')
super(Disk.DiskBaseIsNotReady, self).__init__(
disk, params, name=disk.name, pk=disk.pk,
filename=disk.filename, b_name=base.name,
b_pk=base.pk, b_filename=base.filename)
@property
def path(self):
......@@ -191,7 +223,7 @@ class Disk(TimeStampedModel):
return {
'qcow2-norm': 'virtio',
'qcow2-snap': 'virtio',
'iso': 'scsi',
'iso': 'ide',
'raw-ro': 'virtio',
'raw-rw': 'virtio',
}[self.type]
......@@ -240,7 +272,7 @@ class Disk(TimeStampedModel):
}
if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type)
raise self.WrongDiskTypeError(self)
new_type = type_mapping[self.type]
......@@ -313,6 +345,8 @@ class Disk(TimeStampedModel):
if self.is_ready:
return True
if self.base and not self.base.is_ready:
raise self.DiskBaseIsNotReady(self, base=self.base)
queue_name = self.get_remote_queue_name('storage', priority="fast")
disk_desc = self.get_disk_desc()
if self.base is not None:
......@@ -372,10 +406,11 @@ class Disk(TimeStampedModel):
try:
result = remote.get(timeout=5)
break
except TimeoutError:
except TimeoutError as e:
if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort()
raise Exception("Download aborted by user.")
raise humanize_exception(ugettext_noop(
"Operation aborted by user."), e)
disk.size = result['size']
disk.type = result['type']
disk.is_ready = True
......@@ -417,7 +452,7 @@ class Disk(TimeStampedModel):
'iso': ("iso", self),
}
if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type)
raise self.WrongDiskTypeError(self)
if self.is_in_use:
raise self.DiskInUseError(self)
......@@ -433,7 +468,7 @@ class Disk(TimeStampedModel):
disk = Disk.create(datastore=self.datastore,
base=new_base,
name=self.name, size=self.size,
type=new_type)
type=new_type, dev_num=self.dev_num)
queue_name = self.get_remote_queue_name("storage", priority="slow")
remote = storage_tasks.merge.apply_async(kwargs={
......@@ -445,9 +480,12 @@ class Disk(TimeStampedModel):
try:
remote.get(timeout=5)
break
except TimeoutError:
except TimeoutError as e:
if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort()
disk.destroy()
raise Exception("Save as aborted by use.")
raise humanize_exception(ugettext_noop(
"Operation aborted by user."), e)
disk.is_ready = True
disk.save()
return disk
......@@ -18,6 +18,7 @@
from __future__ import absolute_import, unicode_literals
from datetime import timedelta, datetime
from django.core.validators import MinValueValidator
from django.db.models import Model, CharField, IntegerField, permalink
from django.utils.translation import ugettext_lazy as _
from django.utils.timesince import timeuntil
......@@ -37,16 +38,20 @@ class BaseResourceConfigModel(Model):
"""
num_cores = IntegerField(verbose_name=_('number of cores'),
help_text=_('Number of virtual CPU cores '
'available to the virtual machine.'))
'available to the virtual machine.'),
validators=[MinValueValidator(0)])
ram_size = IntegerField(verbose_name=_('RAM size'),
help_text=_('Mebibytes of memory.'))
help_text=_('Mebibytes of memory.'),
validators=[MinValueValidator(0)])
max_ram_size = IntegerField(verbose_name=_('maximal RAM size'),
help_text=_('Upper memory size limit '
'for balloning.'))
'for balloning.'),
validators=[MinValueValidator(0)])
arch = CharField(max_length=10, verbose_name=_('architecture'),
choices=ARCHITECTURES)
priority = IntegerField(verbose_name=_('priority'),
help_text=_('CPU priority.'))
help_text=_('CPU priority.'),
validators=[MinValueValidator(0)])
class Meta:
abstract = True
......
......@@ -41,7 +41,9 @@ from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager
from acl.models import AclBase
from common.models import create_readable
from common.models import (
create_readable, HumanReadableException, humanize_exception
)
from common.operations import OperatedMixin
from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity,
......@@ -276,28 +278,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
verbose_name = _('instance')
verbose_name_plural = _('instances')
class InstanceDestroyedError(Exception):
class InstanceError(HumanReadableException):
def __init__(self, instance, message=None):
if message is None:
message = ("The instance (%s) has already been destroyed."
% instance)
def __init__(self, instance, params=None, level=None, **kwargs):
kwargs.update(params or {})
self.instance = kwargs["instance"] = instance
super(Instance.InstanceError, self).__init__(
level, self.message, self.message, kwargs)
Exception.__init__(self, message)
class InstanceDestroyedError(InstanceError):
message = ugettext_noop(
"Instance %(instance)s has already been destroyed.")
self.instance = instance
class WrongStateError(InstanceError):
message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is "
"inappropriate for the invoked operation.")
class WrongStateError(Exception):
def __init__(self, instance, message=None):
if message is None:
message = ("The instance's current state (%s) is "
"inappropriate for the invoked operation."
% instance.status)
Exception.__init__(self, message)
self.instance = instance
def __init__(self, instance, params=None, **kwargs):
super(Instance.WrongStateError, self).__init__(
instance, params, state=instance.status)
def __unicode__(self):
parts = (self.name, "(" + str(self.id) + ")")
......@@ -441,9 +441,12 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def vm_state_changed(self, new_state):
# log state change
try:
act = InstanceActivity.create(code_suffix='vm_state_changed',
instance=self,
readable_name="vm state changed")
act = InstanceActivity.create(
code_suffix='vm_state_changed',
readable_name=create_readable(
ugettext_noop("vm state changed to %(state)s"),
state=new_state),
instance=self)
except ActivityInProgressError:
pass # discard state change if another activity is in progress.
else:
......@@ -876,10 +879,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
while True:
try:
return remote.get(timeout=step)
except TimeoutError:
except TimeoutError as e:
if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort()
raise Exception("Shutdown aborted by user.")
raise humanize_exception(ugettext_noop(
"Operation aborted by user."), e)
def suspend_vm(self, timeout=230):
queue_name = self.get_remote_queue_name('vm', 'slow')
......@@ -956,7 +960,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
for a in acts:
if (latest == a.activity_code and
merged_acts[-1].result == a.result and
merged_acts[-1].result_data == a.result_data and
a.finished and merged_acts[-1].finished and
a.user == merged_acts[-1].user and
(merged_acts[-1].finished - a.finished).days < 7 and
......@@ -974,3 +978,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return vm_tasks.screenshot.apply_async(args=[self.vm_name],
queue=queue_name
).get(timeout=timeout)
def get_latest_activity_in_progress(self):
try:
return InstanceActivity.objects.filter(
instance=self, succeeded=None, parent=None).latest("started")
except InstanceActivity.DoesNotExist:
return None
......@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from common.models import create_readable
from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server,
......@@ -25,22 +26,25 @@ from StringIO import StringIO
from tarfile import TarFile, TarInfo
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_noop
from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act, vm):
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup'):
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('restart_networking'):
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password'):
with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw))
with act.sub_activity('set_time'):
with act.sub_activity('set_time', readable_name=ugettext_noop('set time')):
set_time.apply_async(queue=queue, args=(vm, time.time()))
with act.sub_activity('set_hostname'):
with act.sub_activity('set_hostname',
readable_name=ugettext_noop('set hostname')):
set_hostname.apply_async(
queue=queue, args=(vm, instance.primary_host.hostname))
......@@ -73,13 +77,21 @@ def agent_started(vm, version=None):
initialized = InstanceActivity.objects.filter(
instance=instance, activity_code='vm.Instance.agent.cleanup').exists()
with instance_activity(code_suffix='agent', instance=instance) as act:
with act.sub_activity('starting'):
with instance_activity(code_suffix='agent',
readable_name=ugettext_noop('agent'),
instance=instance) as act:
with act.sub_activity('starting',
readable_name=ugettext_noop('starting')):
pass
if version and version != settings.AGENT_VERSION:
try:
with act.sub_activity('update'):
with act.sub_activity(
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION)
):
update.apply_async(
queue=queue,
args=(vm, create_agent_tar())).get(timeout=10)
......@@ -91,7 +103,10 @@ def agent_started(vm, version=None):
measure_boot_time(instance)
send_init_commands(instance, act, vm)
with act.sub_activity('start_access_server'):
with act.sub_activity(
'start_access_server',
readable_name=ugettext_noop('start access server')
):
start_access_server.apply_async(queue=queue, args=(vm, ))
......@@ -122,5 +137,5 @@ def agent_stopped(vm):
qs = InstanceActivity.objects.filter(instance=instance,
activity_code='vm.Instance.agent')
act = qs.latest('id')
with act.sub_activity('stopping'):
with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')):
pass
......@@ -210,7 +210,7 @@ class NodeTestCase(TestCase):
node.enabled = True
node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE")
assert isinstance(Node.get_status_display(node), _("").__class__)
assert isinstance(Node.get_status_display(node), _("x").__class__)
class InstanceActivityTestCase(TestCase):
......
......@@ -21,7 +21,6 @@ kombu==3.0.15
logutils==0.3.3
MarkupSafe==0.21
netaddr==0.7.11
nose==1.3.1
pip-tools==0.3.4
psycopg2==2.5.2
Pygments==1.6
......
......@@ -3,3 +3,5 @@
coverage==3.7.1
factory-boy==2.3.1
mock==1.0.1
django-nose==1.2
nose==1.3.3
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