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: ...@@ -447,5 +447,16 @@ if graphite_host and graphite_port:
else: else:
GRAPHITE_URL = None 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) ^ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
(getnode() % 983)) & 0xffff) (getnode() % 983)) & 0xffff)
MAX_NODE_RAM = get_env_variable("MAX_NODE_RAM", 1024)
...@@ -70,20 +70,14 @@ SERVER_EMAIL = EMAIL_HOST_USER ...@@ -70,20 +70,14 @@ SERVER_EMAIL = EMAIL_HOST_USER
########## CACHE CONFIGURATION ########## CACHE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#caches # See: https://docs.djangoproject.com/en/dev/ref/settings/#caches
try: from urlparse import urlsplit
CACHES = {
CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': get_env_variable('DJANGO_MEMCACHED'), 'LOCATION': urlsplit(get_env_variable('CACHE_URI')).netloc,
}
}
except ImproperlyConfigured:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': SITE_NAME,
}
} }
}
########## END CACHE CONFIGURATION ########## END CACHE CONFIGURATION
......
...@@ -35,7 +35,11 @@ SOUTH_TESTS_MIGRATE = False ...@@ -35,7 +35,11 @@ SOUTH_TESTS_MIGRATE = False
INSTALLED_APPS += ( INSTALLED_APPS += (
'acl.tests', 'acl.tests',
'django_nose',
) )
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--with-doctest']
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
CACHES = { CACHES = {
'default': { 'default': {
...@@ -52,3 +56,5 @@ LOGGING['handlers']['console'] = {'level': level, ...@@ -52,3 +56,5 @@ LOGGING['handlers']['console'] = {'level': level,
'formatter': 'simple'} 'formatter': 'simple'}
for i in LOCAL_APPS: for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level} LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level}
# Forbid store usage
STORE_URL = ""
...@@ -23,6 +23,7 @@ from logging import getLogger ...@@ -23,6 +23,7 @@ from logging import getLogger
from time import time from time import time
from warnings import warn from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
...@@ -46,17 +47,24 @@ class WorkerNotFound(Exception): ...@@ -46,17 +47,24 @@ class WorkerNotFound(Exception):
def activitycontextimpl(act, on_abort=None, on_commit=None): def activitycontextimpl(act, on_abort=None, on_commit=None):
try: try:
try:
yield act yield act
except HumanReadableException as e:
result = e
raise
except BaseException as e: except BaseException as e:
# BaseException is the common parent of Exception and # BaseException is the common parent of Exception and
# system-exiting exceptions, e.g. KeyboardInterrupt # system-exiting exceptions, e.g. KeyboardInterrupt
handler = None if on_abort is None else lambda a: on_abort(a, e) result = create_readable(
result = create_readable(ugettext_noop("Failure."), ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: " ugettext_noop("Unhandled exception: %(error)s"),
"%(error)s"),
error=unicode(e)) 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) act.finish(succeeded=False, result=result, event_handler=handler)
raise e raise
else: else:
act.finish(succeeded=True, event_handler=on_commit) act.finish(succeeded=True, event_handler=on_commit)
...@@ -70,11 +78,11 @@ activity_code_separator = '.' ...@@ -70,11 +78,11 @@ activity_code_separator = '.'
def has_prefix(activity_code, *prefixes): def has_prefix(activity_code, *prefixes):
"""Determine whether the activity code has the specified prefix. """Determine whether the activity code has the specified prefix.
E.g.: has_prefix('foo.bar.buz', 'foo.bar') == True >>> assert has_prefix('foo.bar.buz', 'foo.bar')
has_prefix('foo.bar.buz', 'foo', 'bar') == True >>> assert has_prefix('foo.bar.buz', 'foo', 'bar')
has_prefix('foo.bar.buz', 'foo.bar', 'buz') == True >>> assert has_prefix('foo.bar.buz', 'foo.bar', 'buz')
has_prefix('foo.bar.buz', 'foo', 'bar', 'buz') == True >>> assert has_prefix('foo.bar.buz', 'foo', 'bar', 'buz')
has_prefix('foo.bar.buz', 'foo', 'buz') == False >>> assert not has_prefix('foo.bar.buz', 'foo', 'buz')
""" """
equal = lambda a, b: a == b equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code) act_code_parts = split_activity_code(activity_code)
...@@ -85,11 +93,11 @@ def has_prefix(activity_code, *prefixes): ...@@ -85,11 +93,11 @@ def has_prefix(activity_code, *prefixes):
def has_suffix(activity_code, *suffixes): def has_suffix(activity_code, *suffixes):
"""Determine whether the activity code has the specified suffix. """Determine whether the activity code has the specified suffix.
E.g.: has_suffix('foo.bar.buz', 'bar.buz') == True >>> assert has_suffix('foo.bar.buz', 'bar.buz')
has_suffix('foo.bar.buz', 'bar', 'buz') == True >>> assert has_suffix('foo.bar.buz', 'bar', 'buz')
has_suffix('foo.bar.buz', 'foo.bar', 'buz') == True >>> assert has_suffix('foo.bar.buz', 'foo.bar', 'buz')
has_suffix('foo.bar.buz', 'foo', 'bar', 'buz') == True >>> assert has_suffix('foo.bar.buz', 'foo', 'bar', 'buz')
has_suffix('foo.bar.buz', 'foo', 'buz') == False >>> assert not has_suffix('foo.bar.buz', 'foo', 'buz')
""" """
equal = lambda a, b: a == b equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code) act_code_parts = split_activity_code(activity_code)
...@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel): ...@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="", value = create_readable(user_text_template="",
admin_text_template=value) 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() self.result_data = None if value is None else value.to_dict()
...@@ -361,8 +373,9 @@ class HumanReadableObject(object): ...@@ -361,8 +373,9 @@ class HumanReadableObject(object):
@classmethod @classmethod
def create(cls, user_text_template, admin_text_template=None, **params): def create(cls, user_text_template, admin_text_template=None, **params):
return cls(user_text_template, return cls(user_text_template=user_text_template,
admin_text_template or user_text_template, params) admin_text_template=(admin_text_template
or user_text_template), params=params)
def set(self, user_text_template, admin_text_template=None, **params): def set(self, user_text_template, admin_text_template=None, **params):
self._set_values(user_text_template, self._set_values(user_text_template,
...@@ -407,10 +420,29 @@ create_readable = HumanReadableObject.create ...@@ -407,10 +420,29 @@ create_readable = HumanReadableObject.create
class HumanReadableException(HumanReadableObject, Exception): class HumanReadableException(HumanReadableObject, Exception):
"""HumanReadableObject that is an Exception so can used in except clause. """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 """Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception. HumanReadableException and the original class with the dict of exception.
...@@ -423,4 +455,7 @@ def humanize_exception(message, exception=None, **params): ...@@ -423,4 +455,7 @@ def humanize_exception(message, exception=None, **params):
Ex = type("HumanReadable" + type(exception).__name__, Ex = type("HumanReadable" + type(exception).__name__,
(HumanReadableException, type(exception)), (HumanReadableException, type(exception)),
exception.__dict__) exception.__dict__)
return Ex.create(message, **params) ex = Ex.create(message, **params)
if level:
ex.level = level
return ex
...@@ -1322,7 +1322,7 @@ ...@@ -1322,7 +1322,7 @@
"user_permissions": [ "user_permissions": [
115 115
], ],
"password": "pbkdf2_sha256$10000$KIoeMs78MiOj$PnVXn3YJMehbOciBO32CMzqL0ZnQrzrdb7+b5dE13os=", "password": "md5$qLN4mQMOrsUJ$f07129fd1a289a0afb4e09f7a6816a4f",
"email": "test@example.org", "email": "test@example.org",
"date_joined": "2013-09-04T15:29:49.914Z" "date_joined": "2013-09-04T15:29:49.914Z"
} }
...@@ -1382,7 +1382,7 @@ ...@@ -1382,7 +1382,7 @@
"pw": "ads", "pw": "ads",
"time_of_suspend": null, "time_of_suspend": null,
"ram_size": 200, "ram_size": 200,
"priority": 4, "priority": 10,
"active_since": null, "active_since": null,
"template": null, "template": null,
"access_method": "nx", "access_method": "nx",
...@@ -1412,7 +1412,7 @@ ...@@ -1412,7 +1412,7 @@
"pw": "ads", "pw": "ads",
"time_of_suspend": null, "time_of_suspend": null,
"ram_size": 200, "ram_size": 200,
"priority": 4, "priority": 10,
"active_since": null, "active_since": null,
"template": null, "template": null,
"access_method": "nx", "access_method": "nx",
...@@ -1518,7 +1518,7 @@ ...@@ -1518,7 +1518,7 @@
"ram_size": 1024, "ram_size": 1024,
"modified": "2014-01-24T00:58:19.654Z", "modified": "2014-01-24T00:58:19.654Z",
"system": "bubuntu", "system": "bubuntu",
"priority": 20, "priority": 10,
"access_method": "ssh", "access_method": "ssh",
"raw_data": "", "raw_data": "",
"arch": "x86_64", "arch": "x86_64",
......
...@@ -97,11 +97,13 @@ class Migration(SchemaMigration): ...@@ -97,11 +97,13 @@ class Migration(SchemaMigration):
}, },
u'dashboard.profile': { u'dashboard.profile': {
'Meta': {'object_name': 'Profile'}, 'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}), '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'}), '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'}), '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'}), 'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
}, },
......
...@@ -98,11 +98,13 @@ class Migration(SchemaMigration): ...@@ -98,11 +98,13 @@ class Migration(SchemaMigration):
}, },
u'dashboard.profile': { u'dashboard.profile': {
'Meta': {'object_name': 'Profile'}, 'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}), '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'}), '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'}), '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'}), 'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
}, },
......
...@@ -93,11 +93,13 @@ class Migration(DataMigration): ...@@ -93,11 +93,13 @@ class Migration(DataMigration):
}, },
u'dashboard.profile': { u'dashboard.profile': {
'Meta': {'object_name': 'Profile'}, 'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}), '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'}), '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'}), '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'}), 'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
}, },
......
...@@ -96,11 +96,13 @@ class Migration(SchemaMigration): ...@@ -96,11 +96,13 @@ class Migration(SchemaMigration):
}, },
u'dashboard.profile': { u'dashboard.profile': {
'Meta': {'object_name': 'Profile'}, 'Meta': {'object_name': 'Profile'},
'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}), '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'}), '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'}), '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'}), 'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
}, },
......
...@@ -29,10 +29,13 @@ from django.db.models import ( ...@@ -29,10 +29,13 @@ from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, BooleanField 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.templatetags.static import static
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField
from jsonfield import JSONField from jsonfield import JSONField
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -44,8 +47,12 @@ from common.models import HumanReadableObject, create_readable, Encoder ...@@ -44,8 +47,12 @@ from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys from vm.tasks.agent_tasks import add_keys, del_keys
from .store_api import Store, NoStoreException, NotOkException
logger = getLogger(__name__) logger = getLogger(__name__)
pwgen = User.objects.make_random_password
class Favourite(Model): class Favourite(Model):
instance = ForeignKey("vm.Instance") instance = ForeignKey("vm.Instance")
...@@ -109,6 +116,18 @@ class Profile(Model): ...@@ -109,6 +116,18 @@ class Profile(Model):
email_notifications = BooleanField( email_notifications = BooleanField(
verbose_name=_("Email notifications"), default=True, verbose_name=_("Email notifications"), default=True,
help_text=_('Whether user wants to get digested email notifications.')) 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, def notify(self, subject, template, context=None, valid_until=None,
**kwargs): **kwargs):
...@@ -201,6 +220,11 @@ def create_profile(sender, user, request, **kwargs): ...@@ -201,6 +220,11 @@ def create_profile(sender, user, request, **kwargs):
if not user.pk: if not user.pk:
return False return False
profile, created = Profile.objects.get_or_create(user=user) 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 return created
user_logged_in.connect(create_profile) user_logged_in.connect(create_profile)
...@@ -268,6 +292,44 @@ else: ...@@ -268,6 +292,44 @@ else:
logger.debug("Do not register save_org_id to djangosaml2 pre_user_save") 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): def add_ssh_keys(sender, **kwargs):
from vm.models import Instance from vm.models import Instance
......
...@@ -186,42 +186,6 @@ html { ...@@ -186,42 +186,6 @@ html {
text-decoration: none !important; 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) { .rule-table tr >:nth-child(1) {
text-align: right; text-align: right;
} }
...@@ -723,6 +687,82 @@ textarea[name="list-new-namelist"] { ...@@ -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 { #group-detail-permissions .filtered {
margin: 2px 0; margin: 2px 0;
padding: 2px 3px; padding: 2px 3px;
...@@ -752,6 +792,74 @@ textarea[name="list-new-namelist"] { ...@@ -752,6 +792,74 @@ textarea[name="list-new-namelist"] {
margin-top: -6px; 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 { #show-all-activities-container {
margin: 20px 0 0 10px; 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 () { ...@@ -112,6 +112,7 @@ $(function () {
/* no js compatibility */ /* no js compatibility */
noJS();
$('.no-js-hidden').show(); $('.no-js-hidden').show();
$('.js-hidden').hide(); $('.js-hidden').hide();
...@@ -349,9 +350,9 @@ $(function () { ...@@ -349,9 +350,9 @@ $(function () {
} }
} }
for(var i=0; i<5 && i<search_result.length; i++) 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) 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); $("#dashboard-group-list").html(html);
// if there is only one result and ENTER is pressed redirect // if there is only one result and ENTER is pressed redirect
...@@ -370,7 +371,10 @@ $(function () { ...@@ -370,7 +371,10 @@ $(function () {
}); });
/* don't close notifications window on missclick */ /* 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; return false;
}); });
...@@ -395,8 +399,8 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { ...@@ -395,8 +399,8 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
'</a>'; '</a>';
} }
function generateGroupHTML(url, name) { function generateGroupHTML(url, name, is_last) {
return '<a href="' + url + '" class="list-group-item real-link">'+ return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+
'<i class="fa fa-users"></i> '+ name + '<i class="fa fa-users"></i> '+ name +
'</a>'; '</a>';
} }
...@@ -431,28 +435,63 @@ function compareVmByFav(a, b) { ...@@ -431,28 +435,63 @@ function compareVmByFav(a, b) {
return a.pk < b.pk ? -1 : 1; 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() { function addSliderMiscs() {
$('.vm-slider').each(function() { // set max values based on inputs
$("<span>").addClass("output").html($(this).val()).insertAfter($(this)); 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() $(".cpu-priority-input").change(function() {
.on('slide', function(e) { var val = $(":selected", $(this)).val();
$(this).val(e.value); $(".cpu-priority-slider").simpleSlider("setValue", val);
$(this).parent('div').nextAll("span").html(e.value)
}); });
refreshSliders(); $(".cpu-count-slider").bind("slider:changed", function (event, data) {
} var value = data.value + 0;
$(".cpu-count-input").val(parseInt(value));
});
// ehhh $(".cpu-count-input").bind("input", function() {
function refreshSliders() { var val = parseInt($(this).val());
$('.vm-slider').each(function() { if(!val) return;
$(this).val($(this).slider().data('slider').getValue()); $(".cpu-count-slider").simpleSlider("setValue", val);
$(this).parent('div').nextAll("span").html($(this).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 /* deletes the VM with the pk
* if dir is true, then redirect to the dashboard landing page * if dir is true, then redirect to the dashboard landing page
* else it adds a success message */ * else it adds a success message */
...@@ -561,3 +600,10 @@ function getCookie(name) { ...@@ -561,3 +600,10 @@ function getCookie(name) {
} }
return cookieValue; 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 @@ ...@@ -53,3 +53,50 @@
border-color: #496805; 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 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/) Licensed under the MIT license (http://mit-license.org/)
*/ */
...@@ -23,8 +26,12 @@ var __slice = [].slice, ...@@ -23,8 +26,12 @@ var __slice = [].slice,
classPrefix: null, classPrefix: null,
classSuffix: null, classSuffix: null,
theme: null, theme: null,
highlight: false highlight: false,
showScale: false
}; };
if(typeof options == 'undefined') {
options = this.loadDataOptions();
}
this.settings = $.extend({}, this.defaultOptions, options); this.settings = $.extend({}, this.defaultOptions, options);
if (this.settings.theme) { if (this.settings.theme) {
this.settings.classSuffix = "-" + this.settings.theme; this.settings.classSuffix = "-" + this.settings.theme;
...@@ -106,6 +113,20 @@ var __slice = [].slice, ...@@ -106,6 +113,20 @@ var __slice = [].slice,
} }
this.setSliderPositionFromValue(this.value); this.setSliderPositionFromValue(this.value);
ratio = this.valueToRatio(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", { this.input.trigger("slider:ready", {
value: this.value, value: this.value,
ratio: ratio, ratio: ratio,
...@@ -114,6 +135,44 @@ var __slice = [].slice, ...@@ -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) { SimpleSlider.prototype.createDivElement = function(classname) {
var item; var item;
item = $("<div>").addClass(classname).css({ item = $("<div>").addClass(classname).css({
...@@ -125,6 +184,12 @@ var __slice = [].slice, ...@@ -125,6 +184,12 @@ var __slice = [].slice,
return item; return item;
}; };
SimpleSlider.prototype.createSpanElement = function(classname, parent) {
var item;
item = $("<span>").addClass(classname).appendTo(parent);
return item;
};
SimpleSlider.prototype.setRatio = function(ratio) { SimpleSlider.prototype.setRatio = function(ratio) {
var value; var value;
ratio = Math.min(1, ratio); ratio = Math.min(1, ratio);
...@@ -322,42 +387,14 @@ var __slice = [].slice, ...@@ -322,42 +387,14 @@ var __slice = [].slice,
}); });
} }
}); });
/*
return $(function() { return $(function() {
return $("[data-slider]").each(function() { return $("[data-slider]").each(function() {
var $el, allowedValues, settings, x; var $el, allowedValues, settings, x;
$el = $(this); $el = $(this);
settings = {}; return $el.simpleSlider();
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);
}); });
}); });
*/
})(this.jQuery || this.Zepto, this); })(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 @@ ...@@ -3,7 +3,7 @@
$(function() { $(function() {
/* vm operations */ /* 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'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({ $.ajax({
......
...@@ -2,13 +2,17 @@ var vlans = []; ...@@ -2,13 +2,17 @@ var vlans = [];
var disks = []; var disks = [];
$(function() { $(function() {
if($(".vm-create-template-list").length) {
vmCreateLoaded();
} else {
vmCustomizeLoaded(); vmCustomizeLoaded();
}
}); });
function vmCreateLoaded() { function vmCreateLoaded() {
$(".vm-create-template-details").hide(); $(".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(); $(this).next(".vm-create-template-details").slideToggle();
}); });
...@@ -64,9 +68,18 @@ function vmCreateLoaded() { ...@@ -64,9 +68,18 @@ function vmCreateLoaded() {
return false; 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() { function vmCustomizeLoaded() {
$("[title]").tooltip();
/* network thingies */ /* network thingies */
/* add network */ /* add network */
...@@ -92,7 +105,7 @@ function vmCustomizeLoaded() { ...@@ -92,7 +105,7 @@ function vmCustomizeLoaded() {
/* add dummy text if no more networks are available */ /* add dummy text if no more networks are available */
if($('#vm-create-network-add-select option').length < 1) { if($('#vm-create-network-add-select option').length < 1) {
$('#vm-create-network-add-button').attr('disabled', true); $('#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; return false;
...@@ -124,7 +137,7 @@ function vmCustomizeLoaded() { ...@@ -124,7 +137,7 @@ function vmCustomizeLoaded() {
/* remove the selection from the multiple select */ /* remove the selection from the multiple select */
$('#vm-create-network-add-vlan option[value="' + vlan_pk + '"]').prop('selected', false); $('#vm-create-network-add-vlan option[value="' + vlan_pk + '"]').prop('selected', false);
if ($('#vm-create-network-list').children('span').length < 1) { 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; return false;
...@@ -155,7 +168,7 @@ function vmCustomizeLoaded() { ...@@ -155,7 +168,7 @@ function vmCustomizeLoaded() {
// if all networks are added add a dummy and disable the add button // if all networks are added add a dummy and disable the add button
if($("#vm-create-network-add-select option").length < 1) { 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); $('#vm-create-network-add-button').attr('disabled', true);
} }
...@@ -170,54 +183,6 @@ function vmCustomizeLoaded() { ...@@ -170,54 +183,6 @@ function vmCustomizeLoaded() {
/* ----- end of networks thingies ----- */ /* ----- 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 */ /* copy disks from hidden select */
$('#vm-create-disk-add-form option').each(function() { $('#vm-create-disk-add-form option').each(function() {
var text = $(this).text(); var text = $(this).text();
...@@ -244,6 +209,14 @@ function vmCustomizeLoaded() { ...@@ -244,6 +209,14 @@ function vmCustomizeLoaded() {
/* start vm button clicks */ /* start vm button clicks */
$('#vm-create-customized-start').click(function() { $('#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({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
...@@ -284,6 +257,7 @@ function vmCustomizeLoaded() { ...@@ -284,6 +257,7 @@ function vmCustomizeLoaded() {
/* for no js stuff */ /* for no js stuff */
$('.no-js-hidden').show(); $('.no-js-hidden').show();
$('.js-hidden').hide(); $('.js-hidden').hide();
} }
...@@ -294,5 +268,5 @@ function vmCreateNetworkLabel(pk, name, managed) { ...@@ -294,5 +268,5 @@ function vmCreateNetworkLabel(pk, name, managed) {
function vmCreateDiskLabel(pk, name) { function vmCreateDiskLabel(pk, name) {
var style = "float: left; margin: 5px 5px 5px 0;"; 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 show_all = false;
var in_progress = false; var in_progress = false;
var activity_hash = 5;
$(function() { $(function() {
/* do we need to check for new activities */ /* do we need to check for new activities */
...@@ -27,6 +28,15 @@ $(function() { ...@@ -27,6 +28,15 @@ $(function() {
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#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"); $('i.fa-floppy-o', this).removeClass("fa-floppy-o").addClass("fa-refresh fa-spin");
var vm = $(this).data("vm"); var vm = $(this).data("vm");
$.ajax({ $.ajax({
...@@ -34,8 +44,12 @@ $(function() { ...@@ -34,8 +44,12 @@ $(function() {
url: "/dashboard/vm/" + vm + "/op/resources_change/", url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(), data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) { 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"); $('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) { error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o"); $("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o");
...@@ -307,6 +321,10 @@ $(function() { ...@@ -307,6 +321,10 @@ $(function() {
$("#vm-details-connection-string").focus(); $("#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) { ...@@ -327,6 +345,7 @@ function removePort(data) {
} }
}); });
} }
function decideActivityRefresh() { function decideActivityRefresh() {
...@@ -341,17 +360,6 @@ function decideActivityRefresh() { ...@@ -341,17 +360,6 @@ function decideActivityRefresh() {
return check; 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) { function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2]; var instance = location.href.split('/'); instance = instance[instance.length - 2];
...@@ -360,19 +368,24 @@ function checkNewActivity(runs) { ...@@ -360,19 +368,24 @@ function checkNewActivity(runs) {
url: '/dashboard/vm/' + instance + '/activity/', url: '/dashboard/vm/' + instance + '/activity/',
data: {'show_all': show_all}, data: {'show_all': show_all},
success: function(data) { success: function(data) {
if(show_all) { /* replace on longer string freezes the spinning stuff */ var new_activity_hash = (data['activities'] + "").hashCode();
$("#activity-refresh").html(data['activities']); if(new_activity_hash != activity_hash) {
} else {
a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-refresh").html());
if(a != b)
$("#activity-refresh").html(data['activities']); $("#activity-refresh").html(data['activities']);
} }
activity_hash = new_activity_hash;
$("#ops").html(data['ops']); $("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']); $("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip(); $("[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()); $("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") { if(data['status'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
...@@ -380,12 +393,12 @@ function checkNewActivity(runs) { ...@@ -380,12 +393,12 @@ function checkNewActivity(runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
} }
if(data['status'] == "STOPPED") { if(data['status'] == "STOPPED" || data['status'] == "PENDING") {
$(".enabled-when-stopped").prop("disabled", false); $(".change-resources-button").prop("disabled", false);
$(".hide-when-stopped").hide(); $(".change-resources-help").hide();
} else { } else {
$(".enabled-when-stopped").prop("disabled", true); $(".change-resources-button").prop("disabled", true);
$(".hide-when-stopped").show(); $(".change-resources-help").show();
} }
if(runs > 0 && decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
...@@ -403,3 +416,14 @@ function checkNewActivity(runs) { ...@@ -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 _ ...@@ -27,43 +27,6 @@ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey 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): class NodeListTable(Table):
pk = Column( pk = Column(
......
...@@ -68,6 +68,7 @@ ...@@ -68,6 +68,7 @@
</body> </body>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <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="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
{% include 'autocomplete_light/static.html' %} {% 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 i18n %}
{% load crispy_forms_tags %} {% 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"> <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> <a href="{% url "dashboard.views.lease-create" %}">{% trans "Create a new lease now." %}</a>
</div> </div>
{% endif %} {% 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> <style>
fieldset { fieldset {
...@@ -21,8 +53,3 @@ ...@@ -21,8 +53,3 @@
font-weight: bold; font-weight: bold;
} }
</style> </style>
<script>
$(function() {
$("#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size").hide();
});
</script>
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
</li> </li>
</ul> </ul>
<div style="margin-top: 20px; padding: 0 15px; width: 100%"> <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> <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 %} {% endif %}
<form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}"> <form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}">
...@@ -76,48 +76,6 @@ ...@@ -76,48 +76,6 @@
</div> </div>
<style> <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 { .progress {
position: relative; position: relative;
width: 200px; width: 200px;
...@@ -139,15 +97,3 @@ ...@@ -139,15 +97,3 @@
font-size: 10px; font-size: 10px;
} }
</style> </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 crispy_forms_tags %}
{% load i18n %}
{% load sizefieldtags %} {% 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 @@ ...@@ -5,7 +5,7 @@
{% block extra_link %} {% 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/bootstrap-tour.min.css" rel="stylesheet">
<link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet"> <link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet">
{% endblock %} {% endblock %}
...@@ -18,8 +18,8 @@ ...@@ -18,8 +18,8 @@
{% endblock %} {% endblock %}
{% block navbar %} {% block navbar %}
{% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav pull-right"> <ul class="nav navbar-nav pull-right">
<li class="dropdown" id="notification-button"> <li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;" <a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;"
class="dropdown-toggle" data-toggle="dropdown"> class="dropdown-toggle" data-toggle="dropdown">
...@@ -32,9 +32,8 @@ ...@@ -32,9 +32,8 @@
<li>{% trans "Loading..." %}</li> <li>{% trans "Loading..." %}</li>
</ul> </ul>
</li> </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;"> <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" %} <i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a> </a>
...@@ -48,7 +47,7 @@ ...@@ -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> <a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %} {% endif %}
{% else %} {% 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 %} {% endif %}
{% endblock %} {% endblock %}
...@@ -56,6 +55,5 @@ ...@@ -56,6 +55,5 @@
{% block extra_script %} {% block extra_script %}
<script src="{{ STATIC_URL }}dashboard/js/jquery.knob.js"></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> <script src="{{ STATIC_URL }}dashboard/dashboard.js"></script>
{% endblock %} {% endblock %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<style>
.row {
margin-bottom: 15px;
}
</style>
<form method="POST" action="{% url "dashboard.views.group-create" %}"> <form method="POST" action="{% url "dashboard.views.group-create" %}">
{% csrf_token %} {% csrf_token %}
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <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 "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> <a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a>
</div> </div>
<h1> <h1>
......
...@@ -23,11 +23,11 @@ ...@@ -23,11 +23,11 @@
</div> </div>
{% endif %} {% endif %}
{% comment %} {% if not no_store %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
{% include "dashboard/index-files.html" %} {% include "dashboard/store/index-files.html" %}
</div> </div>
{% endcomment %} {% endif %}
{% if perms.vm.create_template %} {% if perms.vm.create_template %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
......
...@@ -82,9 +82,9 @@ ...@@ -82,9 +82,9 @@
</tr> </tr>
{% endfor %} {% endfor %}
<tr><td><i class="icon-plus"></i></td> <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> 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 %} {% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option> <option value="{{id}}">{{name}}</option>
{% endfor %} {% endfor %}
......
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<form method="POST" action="/dashboard/node/create/" id="node-create-form">
{% csrf_token %}
{% crispy formset formset.form.helper %}
</form>
<style> <style>
.row { #node-create-form .row {
margin-bottom: 15px; margin-bottom: 10px;
} }
</style> </style>
<form method="POST" action="/dashboard/node/create/">
{% csrf_token %}
{% crispy formset formset.form.helper %}
</form>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% 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> <script src="{{ STATIC_URL }}dashboard/vm-create.js"></script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }}) {% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4> </h4>
<ul class="dashboard-profile-vm-list"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_owned %} {% for i in instances_owned %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <a href="{{ i.get_absolute_url }}">
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }}) {% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4> </h4>
<ul class="dashboard-profile-vm-list"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_with_access %} {% for i in instances_with_access %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <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 @@ ...@@ -15,10 +15,41 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Edit template" %}</h3> <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Edit template" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form action="" method="POST">
{% with form=form %} {% with form=form %}
{% include "display-form-errors.html" %} {% include "display-form-errors.html" %}
{% endwith %} {% 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> </div>
</div> </div>
......
...@@ -63,7 +63,11 @@ ...@@ -63,7 +63,11 @@
<div class="col-md-4" id="vm-info-pane"> <div class="col-md-4" id="vm-info-pane">
<div class="big"> <div class="big">
<span id="vm-details-state" class="label label-success"> <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>{{ instance.get_status_display|upper }}</span>
</span> </span>
</div> </div>
...@@ -75,6 +79,8 @@ ...@@ -75,6 +79,8 @@
<dd> <dd>
{% if instance.get_connect_port %} {% if instance.get_connect_port %}
{{ instance.get_connect_host }}:<strong>{{ instance.get_connect_port }}</strong> {{ 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 %} {% else %}
<strong>{% trans "The required port for this protocol is not forwarded." %}</strong> <strong>{% trans "The required port for this protocol is not forwarded." %}</strong>
{% endif %} {% endif %}
...@@ -100,7 +106,7 @@ ...@@ -100,7 +106,7 @@
<dd style="font-size: 10px; text-align: right; padding-top: 8px;"> <dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<div id="vm-details-pw-reset"> <div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %} {% 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 %} {% endif %}{% endwith %}
</div> </div>
</dd> </dd>
...@@ -109,8 +115,7 @@ ...@@ -109,8 +115,7 @@
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group" id="dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span> <span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" <input type="text" spellcheck="false"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %} value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}{% trans "Connection is not possible." %}{% endif %}"
{% trans "Connection is not possible." %}{% endif %}"
id="vm-details-connection-string" class="form-control input-tags" /> id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy"> <span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i> <i class="fa fa-copy" title="{% trans "Select all" %}"></i>
......
...@@ -2,55 +2,26 @@ ...@@ -2,55 +2,26 @@
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %} {% 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 %} {% csrf_token %}
<p class="row"> {% 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 %}
<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>
{% if can_change_resources %} {% if can_change_resources %}
<p class="row"> <button type="submit" class="btn btn-success btn-sm change-resources-button"
<div class="col-sm-12"> id="vm-details-resources-save" data-vm="{{ instance.pk }}"
<button type="submit" class="btn btn-success btn-sm enabled-when-stopped" id="vm-details-resources-save" {% if op.resources_change.disabled %}disabled{% endif %}>
data-vm="{{ instance.pk }}"
{% if not op.resources_change %}disabled{% endif %}>
<i class="fa fa-floppy-o"></i> {% trans "Save resources" %} <i class="fa fa-floppy-o"></i> {% trans "Save resources" %}
</button> </button>
<span class="hide-when-stopped" <span class="change-resources-help"
{% if op.resources_change %}style="display: none;"{% endif %} {% if not op.resources_change.disabled %}style="display: none;"{% endif %}
>{% trans "Stop your VM to change resources." %}</span> >{% trans "Stop your VM to change resources." %}</span>
</div>
</p>
{% endif %} {% endif %}
</form> </form>
<hr /> <hr />
<div class="row" id="vm-details-resources-disk"> <div class="row" id="vm-details-resources-disk">
<div class="col-sm-11"> <div class="col-sm-11">
<h3> <h3>
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
<div class="btn-group"> <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> <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"> <ul class="nojs-dropdown-toogle dropdown-menu" role="menu">
<li><a href="#"><i class="fa fa-refresh"></i> Reboot</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>
<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>
</ul> </ul>
</div> </div>
...@@ -107,28 +107,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -107,28 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/') response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200) 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): def test_unpermitted_vm_mass_delete(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -304,10 +282,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -304,10 +282,12 @@ class VmDetailTest(LoginMixin, TestCase):
c = Client() c = Client()
self.login(c, 'superuser') self.login(c, 'superuser')
kwargs = InstanceTemplate.objects.get(id=1).__dict__.copy() 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) response = c.post('/dashboard/template/1/', kwargs)
self.assertEqual(response.status_code, 302) 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): def test_permitted_lease_delete_w_template_using_it(self):
c = Client() c = Client()
...@@ -529,24 +509,25 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -529,24 +509,25 @@ class VmDetailTest(LoginMixin, TestCase):
def test_permitted_wake_up_wrong_state(self): def test_permitted_wake_up_wrong_state(self):
c = Client() c = Client()
self.login(c, "user2") 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) inst = Instance.objects.get(pk=1)
mock_method.side_effect = inst.wake_up mock_method.side_effect = inst.wake_up
inst.status = 'RUNNING' inst.status = 'RUNNING'
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
c.post("/dashboard/vm/1/op/wake_up/") c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called assert mock_method.called
assert wro.called
def test_permitted_wake_up(self): def test_permitted_wake_up(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
with patch.object(Instance, 'select_node', return_value=None): with patch.object(Instance, 'select_node', return_value=None), \
with patch.object(WakeUpOperation, 'async') as new_wake_up: patch.object(WakeUpOperation, 'async') as new_wake_up, \
with patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa: 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) inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up new_wake_up.side_effect = inst.wake_up
inst.get_remote_queue_name = Mock(return_value='test') inst.get_remote_queue_name = Mock(return_value='test')
...@@ -559,6 +540,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -559,6 +540,7 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(inst.status, 'RUNNING') self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called assert new_wake_up.called
assert wuaa.called assert wuaa.called
assert not wro.called
def test_unpermitted_wake_up(self): def test_unpermitted_wake_up(self):
c = Client() c = Client()
...@@ -585,7 +567,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -585,7 +567,7 @@ class VmDetailTest(LoginMixin, TestCase):
'amount': 2, 'amount': 2,
'customized': 1, 'customized': 1,
'template': 1, 'template': 1,
'cpu_priority': 1, 'cpu_count': 1, 'ram_size': 1, 'cpu_priority': 10, 'cpu_count': 1, 'ram_size': 128,
'network': [], 'network': [],
}) })
...@@ -1796,9 +1778,11 @@ class SshKeyTest(LoginMixin, TestCase): ...@@ -1796,9 +1778,11 @@ class SshKeyTest(LoginMixin, TestCase):
def setUp(self): def setUp(self):
self.u1 = User.objects.create(username='user1') self.u1 = User.objects.create(username='user1')
self.u1.set_password('password') self.u1.set_password('password')
self.u1.profile = Profile()
self.u1.save() self.u1.save()
self.u2 = User.objects.create(username='user2') self.u2 = User.objects.create(username='user2')
self.u2.set_password('password') self.u2.set_password('password')
self.u2.profile = Profile()
self.u2.save() self.u2.save()
self.k1 = UserKey(key='ssh-rsa AAAAB3NzaC1yc2EC asd', user=self.u1) self.k1 = UserKey(key='ssh-rsa AAAAB3NzaC1yc2EC asd', user=self.u1)
self.k1.save() self.k1.save()
......
...@@ -28,7 +28,7 @@ from .views import ( ...@@ -28,7 +28,7 @@ from .views import (
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus, NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView, GroupRemoveUserView,
...@@ -39,6 +39,8 @@ from .views import ( ...@@ -39,6 +39,8 @@ from .views import (
get_vm_screenshot, get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView, ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate, UserKeyDelete, UserKeyDetail, UserKeyCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate, VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView, GroupPermissionsView,
LeaseAclUpdateView, LeaseAclUpdateView,
...@@ -86,8 +88,6 @@ urlpatterns = patterns( ...@@ -86,8 +88,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(), url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'), 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(), url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'), name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
...@@ -179,5 +179,21 @@ urlpatterns = patterns( ...@@ -179,5 +179,21 @@ urlpatterns = patterns(
url(r'^sshkey/create/$', url(r'^sshkey/create/$',
UserKeyCreate.as_view(), UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"), name="dashboard.views.userkey-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')), 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 contextlib
import datetime import datetime
...@@ -9,14 +10,14 @@ from fabric.decorators import roles, parallel ...@@ -9,14 +10,14 @@ from fabric.decorators import roles, parallel
env.roledefs['portal'] = ['localhost'] env.roledefs['portal'] = ['localhost']
try: try:
from vm.models import Node from vm.models import Node as _Node
from storage.models import DataStore from storage.models import DataStore as _DataStore
except Exception as e: except Exception as e:
print e print e
else: else:
env.roledefs['node'] = [unicode(n.host.ipv4) env.roledefs['node'] = [unicode(n.host.ipv4)
for n in Node.objects.filter(enabled=True)] for n in _Node.objects.filter(enabled=True)]
env.roledefs['storage'] = [DataStore.objects.get().hostname] env.roledefs['storage'] = [_DataStore.objects.get().hostname]
def update_all(): def update_all():
...@@ -131,7 +132,7 @@ def update_storage(): ...@@ -131,7 +132,7 @@ def update_storage():
"Update and restart storagedriver" "Update and restart storagedriver"
with _stopped("storage"): with _stopped("storage"):
pull("~/storagedriver") pull("~/storagedriver")
pip("storage", "~/storagedriver/requirements/production.txt") pip("storagedriver", "~/storagedriver/requirements/production.txt")
@parallel @parallel
......
...@@ -98,5 +98,5 @@ def reloadtask(type='Host', timeout=15): ...@@ -98,5 +98,5 @@ def reloadtask(type='Host', timeout=15):
}[type] }[type]
logger.info("Reload %s on next periodic iteration applying change to %s.", logger.info("Reload %s on next periodic iteration applying change to %s.",
", ".join(reload), type) ", ".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) 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 ...@@ -27,13 +27,15 @@ from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField, from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey) ForeignKey)
from django.utils import timezone 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 model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from .tasks import local_tasks, storage_tasks from .tasks import local_tasks, storage_tasks
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from common.models import WorkerNotFound from common.models import (
WorkerNotFound, HumanReadableException, humanize_exception
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -104,43 +106,73 @@ class Disk(TimeStampedModel): ...@@ -104,43 +106,73 @@ class Disk(TimeStampedModel):
('create_empty_disk', _('Can create an empty disk.')), ('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.'))) ('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception): class DiskError(HumanReadableException):
admin_message = None
def __init__(self, type, message=None):
if message is None: def __init__(self, disk, params=None, level=None, **kwargs):
message = ("Operation can't be invoked on a disk of type '%s'." kwargs.update(params or {})
% type) self.disc = kwargs["disk"] = disk
super(Disk.DiskError, self).__init__(
Exception.__init__(self, message) level, self.message, self.admin_message or self.message,
kwargs)
self.type = type
class WrongDiskTypeError(DiskError):
class DiskInUseError(Exception): message = ugettext_noop("Operation can't be invoked on disk "
"'%(name)s' of type '%(type)s'.")
def __init__(self, disk, message=None):
if message is None: admin_message = ugettext_noop(
message = ("The requested operation can't be performed on " "Operation can't be invoked on disk "
"disk '%s (%s)' because it is in use." % "'%(name)s' (%(pk)s) of type '%(type)s'.")
(disk.name, disk.filename))
def __init__(self, disk, params=None, **kwargs):
Exception.__init__(self, message) super(Disk.WrongDiskTypeError, self).__init__(
disk, params, type=disk.type, name=disk.name, pk=disk.pk)
self.disk = disk
class DiskInUseError(DiskError):
class DiskIsNotReady(Exception): message = ugettext_noop(
"The requested operation can't be performed on "
""" Exception for operations that need a deployed disk. "disk '%(name)s' because it is in use.")
"""
admin_message = ugettext_noop(
def __init__(self, disk, message=None): "The requested operation can't be performed on "
if message is None: "disk '%(name)s' (%(pk)s) because it is in use.")
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it has never been" def __init__(self, disk, params=None, **kwargs):
"deployed." % (disk.name, disk.filename)) super(Disk.DiskInUseError, self).__init__(
disk, params, name=disk.name, pk=disk.pk)
Exception.__init__(self, message)
class DiskIsNotReady(DiskError):
self.disk = disk 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 @property
def path(self): def path(self):
...@@ -191,7 +223,7 @@ class Disk(TimeStampedModel): ...@@ -191,7 +223,7 @@ class Disk(TimeStampedModel):
return { return {
'qcow2-norm': 'virtio', 'qcow2-norm': 'virtio',
'qcow2-snap': 'virtio', 'qcow2-snap': 'virtio',
'iso': 'scsi', 'iso': 'ide',
'raw-ro': 'virtio', 'raw-ro': 'virtio',
'raw-rw': 'virtio', 'raw-rw': 'virtio',
}[self.type] }[self.type]
...@@ -240,7 +272,7 @@ class Disk(TimeStampedModel): ...@@ -240,7 +272,7 @@ class Disk(TimeStampedModel):
} }
if self.type not in type_mapping.keys(): if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self)
new_type = type_mapping[self.type] new_type = type_mapping[self.type]
...@@ -313,6 +345,8 @@ class Disk(TimeStampedModel): ...@@ -313,6 +345,8 @@ class Disk(TimeStampedModel):
if self.is_ready: if self.is_ready:
return True 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") queue_name = self.get_remote_queue_name('storage', priority="fast")
disk_desc = self.get_disk_desc() disk_desc = self.get_disk_desc()
if self.base is not None: if self.base is not None:
...@@ -372,10 +406,11 @@ class Disk(TimeStampedModel): ...@@ -372,10 +406,11 @@ class Disk(TimeStampedModel):
try: try:
result = remote.get(timeout=5) result = remote.get(timeout=5)
break break
except TimeoutError: except TimeoutError as e:
if task is not None and task.is_aborted(): if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort() 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.size = result['size']
disk.type = result['type'] disk.type = result['type']
disk.is_ready = True disk.is_ready = True
...@@ -417,7 +452,7 @@ class Disk(TimeStampedModel): ...@@ -417,7 +452,7 @@ class Disk(TimeStampedModel):
'iso': ("iso", self), 'iso': ("iso", self),
} }
if self.type not in mapping.keys(): if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self)
if self.is_in_use: if self.is_in_use:
raise self.DiskInUseError(self) raise self.DiskInUseError(self)
...@@ -433,7 +468,7 @@ class Disk(TimeStampedModel): ...@@ -433,7 +468,7 @@ class Disk(TimeStampedModel):
disk = Disk.create(datastore=self.datastore, disk = Disk.create(datastore=self.datastore,
base=new_base, base=new_base,
name=self.name, size=self.size, 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") queue_name = self.get_remote_queue_name("storage", priority="slow")
remote = storage_tasks.merge.apply_async(kwargs={ remote = storage_tasks.merge.apply_async(kwargs={
...@@ -445,9 +480,12 @@ class Disk(TimeStampedModel): ...@@ -445,9 +480,12 @@ class Disk(TimeStampedModel):
try: try:
remote.get(timeout=5) remote.get(timeout=5)
break break
except TimeoutError: except TimeoutError as e:
if task is not None and task.is_aborted(): if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort() AbortableAsyncResult(remote.id).abort()
disk.destroy() 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 return disk
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from datetime import timedelta, datetime from datetime import timedelta, datetime
from django.core.validators import MinValueValidator
from django.db.models import Model, CharField, IntegerField, permalink from django.db.models import Model, CharField, IntegerField, permalink
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.timesince import timeuntil from django.utils.timesince import timeuntil
...@@ -37,16 +38,20 @@ class BaseResourceConfigModel(Model): ...@@ -37,16 +38,20 @@ class BaseResourceConfigModel(Model):
""" """
num_cores = IntegerField(verbose_name=_('number of cores'), num_cores = IntegerField(verbose_name=_('number of cores'),
help_text=_('Number of virtual CPU 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'), 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'), max_ram_size = IntegerField(verbose_name=_('maximal RAM size'),
help_text=_('Upper memory size limit ' help_text=_('Upper memory size limit '
'for balloning.')) 'for balloning.'),
validators=[MinValueValidator(0)])
arch = CharField(max_length=10, verbose_name=_('architecture'), arch = CharField(max_length=10, verbose_name=_('architecture'),
choices=ARCHITECTURES) choices=ARCHITECTURES)
priority = IntegerField(verbose_name=_('priority'), priority = IntegerField(verbose_name=_('priority'),
help_text=_('CPU priority.')) help_text=_('CPU priority.'),
validators=[MinValueValidator(0)])
class Meta: class Meta:
abstract = True abstract = True
......
...@@ -41,7 +41,9 @@ from model_utils.models import TimeStampedModel, StatusModel ...@@ -41,7 +41,9 @@ from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from acl.models import AclBase 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 common.operations import OperatedMixin
from ..tasks import vm_tasks, agent_tasks from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity, from .activity import (ActivityInProgressError, instance_activity,
...@@ -276,28 +278,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -276,28 +278,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
verbose_name = _('instance') verbose_name = _('instance')
verbose_name_plural = _('instances') verbose_name_plural = _('instances')
class InstanceDestroyedError(Exception): class InstanceError(HumanReadableException):
def __init__(self, instance, message=None): def __init__(self, instance, params=None, level=None, **kwargs):
if message is None: kwargs.update(params or {})
message = ("The instance (%s) has already been destroyed." self.instance = kwargs["instance"] = 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, params=None, **kwargs):
super(Instance.WrongStateError, self).__init__(
def __init__(self, instance, message=None): instance, params, state=instance.status)
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 __unicode__(self): def __unicode__(self):
parts = (self.name, "(" + str(self.id) + ")") parts = (self.name, "(" + str(self.id) + ")")
...@@ -441,9 +441,12 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -441,9 +441,12 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def vm_state_changed(self, new_state): def vm_state_changed(self, new_state):
# log state change # log state change
try: try:
act = InstanceActivity.create(code_suffix='vm_state_changed', act = InstanceActivity.create(
instance=self, code_suffix='vm_state_changed',
readable_name="vm state changed") readable_name=create_readable(
ugettext_noop("vm state changed to %(state)s"),
state=new_state),
instance=self)
except ActivityInProgressError: except ActivityInProgressError:
pass # discard state change if another activity is in progress. pass # discard state change if another activity is in progress.
else: else:
...@@ -876,10 +879,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -876,10 +879,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
while True: while True:
try: try:
return remote.get(timeout=step) return remote.get(timeout=step)
except TimeoutError: except TimeoutError as e:
if task is not None and task.is_aborted(): if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort() 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): def suspend_vm(self, timeout=230):
queue_name = self.get_remote_queue_name('vm', 'slow') queue_name = self.get_remote_queue_name('vm', 'slow')
...@@ -956,7 +960,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -956,7 +960,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
for a in acts: for a in acts:
if (latest == a.activity_code and 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.finished and merged_acts[-1].finished and
a.user == merged_acts[-1].user and a.user == merged_acts[-1].user and
(merged_acts[-1].finished - a.finished).days < 7 and (merged_acts[-1].finished - a.finished).days < 7 and
...@@ -974,3 +978,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -974,3 +978,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return vm_tasks.screenshot.apply_async(args=[self.vm_name], return vm_tasks.screenshot.apply_async(args=[self.vm_name],
queue=queue_name queue=queue_name
).get(timeout=timeout) ).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 @@ ...@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from common.models import create_readable
from manager.mancelery import celery from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password, from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server, set_time, set_hostname, start_access_server,
...@@ -25,22 +26,25 @@ from StringIO import StringIO ...@@ -25,22 +26,25 @@ from StringIO import StringIO
from tarfile import TarFile, TarInfo from tarfile import TarFile, TarInfo
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_noop
from celery.result import TimeoutError from celery.result import TimeoutError
from monitor.client import Client from monitor.client import Client
def send_init_commands(instance, act, vm): def send_init_commands(instance, act, vm):
queue = instance.get_remote_queue_name("agent") queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
with act.sub_activity('cleanup'):
cleanup.apply_async(queue=queue, args=(vm, )) 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, )) 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)) 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())) 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( set_hostname.apply_async(
queue=queue, args=(vm, instance.primary_host.hostname)) queue=queue, args=(vm, instance.primary_host.hostname))
...@@ -73,13 +77,21 @@ def agent_started(vm, version=None): ...@@ -73,13 +77,21 @@ def agent_started(vm, version=None):
initialized = InstanceActivity.objects.filter( initialized = InstanceActivity.objects.filter(
instance=instance, activity_code='vm.Instance.agent.cleanup').exists() instance=instance, activity_code='vm.Instance.agent.cleanup').exists()
with instance_activity(code_suffix='agent', instance=instance) as act: with instance_activity(code_suffix='agent',
with act.sub_activity('starting'): readable_name=ugettext_noop('agent'),
instance=instance) as act:
with act.sub_activity('starting',
readable_name=ugettext_noop('starting')):
pass pass
if version and version != settings.AGENT_VERSION: if version and version != settings.AGENT_VERSION:
try: 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( update.apply_async(
queue=queue, queue=queue,
args=(vm, create_agent_tar())).get(timeout=10) args=(vm, create_agent_tar())).get(timeout=10)
...@@ -91,7 +103,10 @@ def agent_started(vm, version=None): ...@@ -91,7 +103,10 @@ def agent_started(vm, version=None):
measure_boot_time(instance) measure_boot_time(instance)
send_init_commands(instance, act, vm) 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, )) start_access_server.apply_async(queue=queue, args=(vm, ))
...@@ -122,5 +137,5 @@ def agent_stopped(vm): ...@@ -122,5 +137,5 @@ def agent_stopped(vm):
qs = InstanceActivity.objects.filter(instance=instance, qs = InstanceActivity.objects.filter(instance=instance,
activity_code='vm.Instance.agent') activity_code='vm.Instance.agent')
act = qs.latest('id') act = qs.latest('id')
with act.sub_activity('stopping'): with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')):
pass pass
...@@ -210,7 +210,7 @@ class NodeTestCase(TestCase): ...@@ -210,7 +210,7 @@ class NodeTestCase(TestCase):
node.enabled = True node.enabled = True
node.STATES = Node.STATES node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE") 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): class InstanceActivityTestCase(TestCase):
......
...@@ -21,7 +21,6 @@ kombu==3.0.15 ...@@ -21,7 +21,6 @@ kombu==3.0.15
logutils==0.3.3 logutils==0.3.3
MarkupSafe==0.21 MarkupSafe==0.21
netaddr==0.7.11 netaddr==0.7.11
nose==1.3.1
pip-tools==0.3.4 pip-tools==0.3.4
psycopg2==2.5.2 psycopg2==2.5.2
Pygments==1.6 Pygments==1.6
......
...@@ -3,3 +3,5 @@ ...@@ -3,3 +3,5 @@
coverage==3.7.1 coverage==3.7.1
factory-boy==2.3.1 factory-boy==2.3.1
mock==1.0.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