Commit 482c59f5 by Kálmán Viktor

Merge branch 'master' into feature-store

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
parents 9fabdf4b 61b90140
# register a signal do update permissions every migration.
# This is based on app django_extensions update_permissions command
from south.signals import post_migrate
def update_permissions_after_migration(app, **kwargs):
"""
Update app permission just after every migration.
This is based on app django_extensions update_permissions
management command.
"""
from django.conf import settings
from django.db.models import get_app, get_models
from django.contrib.auth.management import create_permissions
create_permissions(get_app(app), get_models(), 2 if settings.DEBUG else 0)
post_migrate.connect(update_permissions_after_migration)
...@@ -271,6 +271,7 @@ LOCAL_APPS = ( ...@@ -271,6 +271,7 @@ LOCAL_APPS = (
'dashboard', 'dashboard',
'manager', 'manager',
'acl', 'acl',
'monitor',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......
...@@ -20,7 +20,7 @@ from logging import getLogger ...@@ -20,7 +20,7 @@ from logging import getLogger
from .models import activity_context, has_suffix from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ImproperlyConfigured
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -30,7 +30,7 @@ class Operation(object): ...@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations. """Base class for VM operations.
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = () required_perms = None
do_not_call_in_templates = True do_not_call_in_templates = True
abortable = False abortable = False
has_percentage = False has_percentage = False
...@@ -141,6 +141,9 @@ class Operation(object): ...@@ -141,6 +141,9 @@ class Operation(object):
pass pass
def check_auth(self, user): def check_auth(self, user):
if self.required_perms is None:
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms): if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions." raise PermissionDenied("%s doesn't have the required permissions."
% user) % user)
......
...@@ -1240,6 +1240,24 @@ ...@@ -1240,6 +1240,24 @@
} }
}, },
{ {
"pk": 1367,
"model": "auth.permission",
"fields": {
"codename": "create_vm",
"name": "Can create a new VM.",
"content_type": 28
}
},
{
"pk": 1368,
"model": "auth.permission",
"fields": {
"codename": "access_console",
"name": "Can access the graphical console of a VM.",
"content_type": 28
}
},
{
"pk": 1, "pk": 1,
"model": "auth.group", "model": "auth.group",
"fields": { "fields": {
......
...@@ -25,6 +25,7 @@ from django.contrib.auth.forms import ( ...@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
) )
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
...@@ -39,13 +40,16 @@ from django.template import Context ...@@ -39,13 +40,16 @@ from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk from storage.models import Disk
from vm.models import ( from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission
from .models import Profile, GroupProfile from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES from circle.settings.base import LANGUAGES
from django.utils.translation import string_concat from django.utils.translation import string_concat
...@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm): ...@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True) n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n self.initial['networks'] = n
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags')
if self.user.has_perm('vm.change_template_resources'):
self.allowed_fields += tuple(set(self.fields.keys()) -
set(['raw_data']))
if self.user.is_superuser:
self.allowed_fields += ('raw_data', )
for name, field in self.fields.items():
if name not in self.allowed_fields:
field.widget.attrs['disabled'] = 'disabled'
if not self.instance.pk and len(self.errors) < 1: if not self.instance.pk and len(self.errors) < 1:
self.instance.priority = 20 self.instance.priority = 20
self.instance.ram_size = 512 self.instance.ram_size = 512
...@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm): ...@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk) return User.objects.get(pk=self.instance.owner.pk)
return self.user return self.user
def clean_raw_data(self): def _clean_fields(self):
# if raw_data has changed and the user is not superuser try:
if "raw_data" in self.changed_data and not self.user.is_superuser: old = InstanceTemplate.objects.get(pk=self.instance.pk)
old_raw_data = InstanceTemplate.objects.get( except InstanceTemplate.DoesNotExist:
pk=self.instance.pk).raw_data old = None
return old_raw_data for name, field in self.fields.items():
else: if name in self.allowed_fields:
return self.cleaned_data['raw_data'] value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, forms.FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
elif old:
if name == 'networks':
self.cleaned_data[name] = [
i.vlan for i in self.instance.interface_set.all()]
else:
self.cleaned_data[name] = getattr(old, name)
def save(self, commit=True): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
...@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm): ...@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm):
networks = InterfaceTemplate.objects.filter( networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True) template=self.instance).values_list("vlan", flat=True)
for m in data['networks']: for m in data['networks']:
if not m.has_level(self.user, "user"):
raise PermissionDenied()
if m.pk not in networks: if m.pk not in networks:
InterfaceTemplate(vlan=m, managed=m.managed, InterfaceTemplate(vlan=m, managed=m.managed,
template=self.instance).save() template=self.instance).save()
...@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm): ...@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm):
@property @property
def helper(self): def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm): ...@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm):
_("Virtual machine settings"), _("Virtual machine settings"),
Field('access_method'), Field('access_method'),
Field('boot_menu'), Field('boot_menu'),
Field('raw_data', **kwargs_raw_data), Field('raw_data'),
Field('req_traits'), Field('req_traits'),
Field('description'), Field('description'),
Field("parent", type="hidden"), Field("parent", type="hidden"),
...@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form): ...@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form):
@property @property
def helper(self): def helper(self):
helper = FormHelper(self) helper = FormHelper(self)
helper.add_input(Submit("submit", _("Create"),
css_class="btn btn-success"))
helper.form_tag = False helper.form_tag = False
return helper return helper
...@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm): ...@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm):
if self.user: if self.user:
self.instance.user = self.user self.instance.user = self.user
return super(UserKeyForm, self).clean() return super(UserKeyForm, self).clean()
class TraitsForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('req_traits', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy("dashboard.views.vm-traits",
kwargs={'pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class RawDataForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('raw_data', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy("dashboard.views.vm-raw-data",
kwargs={'pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success",
css_id="submit-password-button"))
return helper
permissions_filtered = Permission.objects.exclude(
codename__startswith="add_").exclude(
codename__startswith="delete_").exclude(
codename__startswith="change_")
class GroupPermissionForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField(
queryset=permissions_filtered,
widget=FilteredSelectMultiple(_("permissions"), is_stacked=False)
)
class Meta:
model = Group
fields = ('permissions', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy(
"dashboard.views.group-permissions",
kwargs={'group_pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'FutureMember'
db.create_table(u'dashboard_futuremember', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('org_id', self.gf('django.db.models.fields.CharField')(max_length=64)),
('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'])),
))
db.send_create_signal(u'dashboard', ['FutureMember'])
# Adding unique constraint on 'FutureMember', fields ['org_id', 'group']
db.create_unique(u'dashboard_futuremember', ['org_id', 'group_id'])
def backwards(self, orm):
# Removing unique constraint on 'FutureMember', fields ['org_id', 'group']
db.delete_unique(u'dashboard_futuremember', ['org_id', 'group_id'])
# Deleting model 'FutureMember'
db.delete_table(u'dashboard_futuremember')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'dashboard.favourite': {
'Meta': {'object_name': 'Favourite'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Instance']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'dashboard.futuremember': {
'Meta': {'unique_together': "(('org_id', 'group'),)", 'object_name': 'FutureMember'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
u'dashboard.groupprofile': {
'Meta': {'object_name': 'GroupProfile'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'dashboard.notification': {
'Meta': {'ordering': "['-created']", 'object_name': 'Notification'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message': ('django.db.models.fields.TextField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'status': ('model_utils.fields.StatusField', [], {'default': "'new'", 'max_length': '100', u'no_check_for_status': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'valid_until': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'})
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'ordering': "('normalized_hostname', 'vlan')", 'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'normalized_hostname': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '80', 'monitor': "'hostname'", 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
'host_ipv6_prefixlen': ('django.db.models.fields.IntegerField', [], {'default': '112'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'default': "'portforward'", 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
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'})
},
u'vm.instance': {
'Meta': {'ordering': "(u'pk',)", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'status': ('model_utils.fields.StatusField', [], {'default': "u'NOSTATE'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'system': ('django.db.models.fields.TextField', [], {}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "(u'name',)", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.node': {
'Meta': {'ordering': "(u'-enabled', u'normalized_name')", 'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'normalized_name': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '100', 'monitor': "u'name'", 'blank': 'True'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['dashboard']
\ No newline at end of file
...@@ -140,6 +140,18 @@ class Profile(Model): ...@@ -140,6 +140,18 @@ class Profile(Model):
return self.get_display_name() return self.get_display_name()
class FutureMember(Model):
org_id = CharField(max_length=64, help_text=_(
'Unique identifier of the person, e.g. a student number.'))
group = ForeignKey(Group)
class Meta:
unique_together = ('org_id', 'group')
def __unicode__(self):
return u"%s (%s)" % (self.org_id, self.group)
class GroupProfile(AclBase): class GroupProfile(AclBase):
ACL_LEVELS = ( ACL_LEVELS = (
('operator', _('operator')), ('operator', _('operator')),
...@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g)) group, unicode(g))
g.user_set.add(sender) g.user_set.add(sender)
for i in FutureMember.objects.filter(org_id=value):
i.group.user_set.add(sender)
i.delete()
owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', []) owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i] for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]): for i in owneratrs if i in attributes]):
......
...@@ -192,6 +192,9 @@ ...@@ -192,6 +192,9 @@
}, },
mousedown: function(ev) { mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event: // Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') { if (this.touchCapable && ev.type === 'touchstart') {
......
...@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] { ...@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] {
} }
<<<<<<< HEAD
#store-list-list { #store-list-list {
list-style: none; list-style: none;
} }
...@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] { ...@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] {
.no-hover:hover { .no-hover:hover {
background: none !important; background: none !important;
} }
#group-detail-permissions .filtered {
margin: 2px 0;
padding: 2px 3px;
vertical-align: middle;
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
font-weight: normal;
font-size: 11px;
border: 1px solid #ccc;
}
#group-detail-permissions .selector-available h2,
#group-detail-permissions .selector-chosen h2 {
margin: 0;
padding: 5px 8px 5px 8px;
font-size: 12px;
text-align: left;
font-weight: bold;
background: #7CA0C7;
color: white;
}
#group-detail-user-table {
margin-top: 20px;
}
#group-detail-permissions input[type="submit"]{
margin-top: -6px;
}
...@@ -512,7 +512,10 @@ function addMessage(text, type) { ...@@ -512,7 +512,10 @@ function addMessage(text, type) {
$('body').animate({scrollTop: 0}); $('body').animate({scrollTop: 0});
div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>'; div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>';
$('.messagelist').html('').append(div); $('.messagelist').html('').append(div);
$('.messagelist div').fadeIn(); var div = $('.messagelist div').fadeIn();
setTimeout(function() {
$(div).fadeOut();
}, 9000);
} }
......
...@@ -30,4 +30,56 @@ $(function() { ...@@ -30,4 +30,56 @@ $(function() {
}); });
return false; return false;
}); });
/* if the operation fails show the modal again */
$("body").on("click", "#op-form-send", function() {
var url = $(this).closest("form").prop("action");
$.ajax({
url: url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: $(this).closest('form').serialize(),
success: function(data, textStatus, xhr) {
/* hide the modal we just submitted */
$('#confirmation-modal').modal("hide");
/* if it was successful trigger a click event on activity, this will
* - go to that tab
* - starts refreshing the activity
*/
if(data.success) {
$('a[href="#activity"]').trigger("click");
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
addMessage(data.messages.join("<br />"), "danger");
}
}
else {
/* if the post was not successful wait for the modal to disappear
* then append the new modal
*/
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
});
}
},
error: function(xhr, textStatus, error) {
$('#confirmation-modal').modal("hide");
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
return false;
});
}); });
...@@ -5,19 +5,20 @@ $(function() { ...@@ -5,19 +5,20 @@ $(function() {
} }
$('a[href="#activity"]').click(function(){ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin'); $('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0); checkNewActivity(false, 1);
}); });
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function() {
$('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin"); $('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin");
var vm = $(this).data("vm");
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: location.href, 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) {
addMessage(data['message'], 'success');
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save"); $("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
$('a[href="#activity"]').trigger("click");
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save"); $("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
...@@ -328,6 +329,17 @@ function decideActivityRefresh() { ...@@ -328,6 +329,17 @@ 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(only_status, runs) { function checkNewActivity(only_status, runs) {
// set default only_status to false // set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false; only_status = typeof only_status !== 'undefined' ? only_status : false;
...@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) { ...@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) {
data: {'only_status': only_status}, data: {'only_status': only_status},
success: function(data) { success: function(data) {
if(!only_status) { if(!only_status) {
$("#activity-timeline").html(data['activities']); a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html());
if(a != b)
$("#activity-timeline").html(data['activities']);
$("#ops").html(data['ops']); $("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip(); $("[title]").tooltip();
} }
...@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) { ...@@ -352,6 +368,14 @@ function checkNewActivity(only_status, 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") {
$(".enabled-when-stopped").prop("disabled", false);
$(".hide-when-stopped").hide();
} else {
$(".enabled-when-stopped").prop("disabled", true);
$(".hide-when-stopped").show();
}
if(runs > 0 && decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
setTimeout( setTimeout(
function() {checkNewActivity(only_status, runs + 1)}, function() {checkNewActivity(only_status, runs + 1)},
......
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
<title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title> <title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title>
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<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>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
......
...@@ -14,7 +14,12 @@ ...@@ -14,7 +14,12 @@
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{ body|safe|default:"(body missing from context.)" }} {% if template %}
{% include template %}
{% else %}
{{ body|safe|default:"(body missing from context.)" }}
{% endif %}
</div> </div>
</div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
{{ body|safe|default:"(body missing from context.)" }} {% if template %}
{% include template %}
{% else %}
{{ body|safe|default:"(body missing from context.)" }}
{% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
......
...@@ -16,10 +16,12 @@ ...@@ -16,10 +16,12 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
{% endfor %} {% endfor %}
{% if perms.vm.create_base_template %}
<div class="panel panel-default template-choose-list-element"> <div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="base_vm"/> <input type="radio" name="parent" value="base_vm"/>
{% trans "Create a new base VM without disk" %} {% trans "Create a new base VM without disk" %}
</div> </div>
{% endif %}
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button> <button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
......
{% extends "base.html" %}
{% load i18n %}
{% block title-site %}Dashboard | CIRCLE{% endblock %}
{% block content %}
{% blocktrans with group=object member=member %}
Do you really want to remove {{member}} from {{group}}?
{% endblocktrans %}
<form action="" method="POST">{% csrf_token %}
<input type="submit" value="{% trans "Remove" %}" />
</form>
{% endblock %}
...@@ -48,6 +48,8 @@ ...@@ -48,6 +48,8 @@
{% crispy group_profile_form %} {% crispy group_profile_form %}
</form> </form>
<hr />
<h3>{% trans "User list"|capfirst %} <h3>{% trans "User list"|capfirst %}
{% if perms.auth.add_user %} {% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a> <a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a>
...@@ -71,23 +73,37 @@ ...@@ -71,23 +73,37 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
{% for i in future_users %}
<tr>
<td>
<i class="icon-user text-muted"></i>
</td>
<td> {{ i.org_id }} </td>
<td>
<a href="{% url "dashboard.views.remove-future-user" member_org_id=i.org_id group_pk=group.pk %}"
class="real-link btn-link btn-xs">
<i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a>
</td>
</tr>
{% endfor %}
<tr> <tr>
<td><i class="icon-plus"></i></td> <td><i class="icon-plus"></i></td>
<td colspan="2"> <td colspan="2">
<input type="text" class="form-control" name="list-new-name"placeholder="{% trans "Name of user" %}"> <input type="text" class="form-control" name="list-new-name"
placeholder="{% trans "Name of user" %}">
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<textarea name="list-new-namelist" class="form-control" <textarea name="list-new-namelist" class="form-control"
placeholder="{% trans "List of usernames (one per line)." %}"></textarea> placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button> <button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div> </div>
</form> </form>
<hr />
<h3 id="group-detail-perm-header">{% trans "Permissions"|capfirst %}</h3> <h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} <form action="{{acl.url}}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table"> <table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table">
<thead> <thead>
...@@ -158,11 +174,25 @@ ...@@ -158,11 +174,25 @@
</div> </div>
</form> </form>
{% if user.is_superuser %}
<hr />
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
{{ group_perm_form.media }}
<h3>{% trans "Group permissions" %}</h3>
<div id="group-detail-permissions">
{% crispy group_perm_form %}
</div>
<link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" />
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="{{ STATIC_URL}}dashboard/group-details.js"></script>
{% endblock %} {% endblock %}
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
{% block question %} {% block question %}
<p> <p>
{% blocktrans with obj=object op=op.name %} {% blocktrans with obj=object url=object.get_absolute_url op=op.name %}
Do you want to do the following operation on {{obj}}: Do you want to do the following operation on <a href="{{url}}">{{obj}}</a>:
<strong>{{op}}</strong>? <strong>{{op}}</strong>?
{% endblocktrans %} {% endblocktrans %}
</p> </p>
...@@ -19,6 +19,8 @@ Do you want to do the following operation on {{obj}}: ...@@ -19,6 +19,8 @@ Do you want to do the following operation on {{obj}}:
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" <a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a> data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-danger" type="submit">{% if op.icon %}<i class="icon-{{op.icon}}"></i> {% endif %}{{ op|capfirst }}</button> <button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send">
{% if opview.icon %}<i class="icon-{{opview.icon}}"></i> {% endif %}{{ op|capfirst }}
</button>
</div> </div>
</form> </form>
...@@ -80,9 +80,9 @@ ...@@ -80,9 +80,9 @@
{% endif %} {% endif %}
</dd> </dd>
{% if instance.ipv6 %} {% if instance.ipv6 and instance.get_connect_port %}
<dt>{% trans "Host (IPv6)" %}</dt> <dt>{% trans "Host (IPv6)" %}</dt>
<dd>{{ ipv6_host }}:<strong>{{ instance.ipv6_port }}</strong></dd> <dd>{{ ipv6_host }}:<strong>{{ ipv6_port }}</strong></dd>
{% endif %} {% endif %}
<dt>{% trans "Username" %}</dt> <dt>{% trans "Username" %}</dt>
...@@ -90,7 +90,8 @@ ...@@ -90,7 +90,8 @@
<dt>{% trans "Password" %}</dt> <dt>{% trans "Password" %}</dt>
<dd> <dd>
<div class="input-group"> <div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/> <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"> <span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i> <i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span> </span>
...@@ -112,7 +113,7 @@ ...@@ -112,7 +113,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" <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" />
......
...@@ -6,16 +6,18 @@ ...@@ -6,16 +6,18 @@
</span> </span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}> <strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %} {% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %}
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %} {{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}
{% if a.has_percent %}
- {{ a.percentage }}%
{% endif %}
</strong> </strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {% if a.times < 2%}{{ a.started|date:"Y-m-d H:i" }}{% endif %}{% if a.user %},
<a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}"> <a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}">
{% include "dashboard/_display-name.html" with user=a.user show_org=True %} {% include "dashboard/_display-name.html" with user=a.user show_org=True %}
</a> </a>
{% endif %} {% endif %}
{% if a.has_percent %}
{{ a.percentage }}%
{% endif %}
{% if a.is_abortable_for_user %} {% if a.is_abortable_for_user %}
<form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right"> <form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right">
{% csrf_token %} {% csrf_token %}
......
{% load i18n %}
{% for op in ops %}
{% if op.is_disk_operation %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.op}} btn btn-default">
<i class="icon-{{op.icon}}"></i>
{{op.name}} </a>
{% endif %}
{% endfor %}
...@@ -2,10 +2,20 @@ ...@@ -2,10 +2,20 @@
{% for op in ops %} {% for op in ops %}
{% if op.show_in_toolbar %} {% if op.show_in_toolbar %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn btn-default btn-xs"
title="{{op.name}}: {{op.description}}"> {% if op.disabled %}
<span class="operation operation-{{op.op}} btn btn-default disabled btn-xs">
{% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn
btn-{{op.effect}} btn-xs" title="{{op.name}}: {{op.description}}">
{% endif %}
<i class="icon-{{op.icon}}"></i> <i class="icon-{{op.icon}}"></i>
<span class="sr-only">{{op.name}}</span> <span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
{% if op.disabled %}
</span>
{% else %}
</a> </a>
{% endif %} {% endif %}
{% endif %}
{% endfor %} {% endfor %}
{% load i18n %} {% load i18n %}
<div class="btn-toolbar"> <div class="btn-toolbar">
{% if perms.vm.access_console %}
<button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button> <button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button>
<button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button> <button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button>
{% endif %}
<button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="icon-picture"></i> {% trans "Screenshot" %}</button> <button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="icon-picture"></i> {% trans "Screenshot" %}</button>
</div> </div>
{% if perms.vm.access_console %}
<div class="alert alert-info" id="noVNC_status"> <div class="alert alert-info" id="noVNC_status">
</div> </div>
{% endif %}
<div id="vm-console-screenshot"> <div id="vm-console-screenshot">
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button> <button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
...@@ -14,6 +18,7 @@ ...@@ -14,6 +18,7 @@
<hr /> <hr />
</div> </div>
{% if perms.vm.access_console %}
<canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported. <canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported.
</canvas> </canvas>
...@@ -22,3 +27,4 @@ ...@@ -22,3 +27,4 @@
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/'; var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}"; var VNC_URL = "{{ vnc_url }}";
</script> </script>
{% endif %}
...@@ -47,7 +47,12 @@ ...@@ -47,7 +47,12 @@
<h3 class="list-group-item-heading dashboard-vm-details-network-h3"> <h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }} <i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host%}({% trans "unmanaged" %}){% endif %} {% if not i.host%}({% trans "unmanaged" %}){% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" class="btn btn-danger btn-xs interface-remove" {% if user.is_superuser %}
<a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}"
class="btn btn-danger btn-xs interface-remove"
data-interface-pk="{{ i.pk }}"> data-interface-pk="{{ i.pk }}">
{% trans "remove" %} {% trans "remove" %}
</a> </a>
...@@ -63,6 +68,8 @@ ...@@ -63,6 +68,8 @@
<dd> <dd>
{% for g in i.host.groups.all %} {% for g in i.host.groups.all %}
{{ g }}{% if not forloop.last %},{% endif %} {{ g }}{% if not forloop.last %},{% endif %}
{% empty %}
-
{% endfor %} {% endfor %}
</dd> </dd>
</dl> </dl>
...@@ -92,7 +99,7 @@ ...@@ -92,7 +99,7 @@
{% if l.ipv4 %} {% if l.ipv4 %}
<tr> <tr>
<td> <td>
{% display_portforward l %} {% display_portforward4 l %}
</td> </td>
<td><i class="icon-long-arrow-right"></i></td> <td><i class="icon-long-arrow-right"></i></td>
<td> <td>
...@@ -124,7 +131,7 @@ ...@@ -124,7 +131,7 @@
{% if l.ipv6 %} {% if l.ipv6 %}
<tr> <tr>
<td> <td>
{% display_portforward l %} {% display_portforward6 l %}
</td> </td>
<td><i class="icon-long-arrow-right"></i></td> <td><i class="icon-long-arrow-right"></i></td>
<td> <td>
......
...@@ -33,11 +33,20 @@ ...@@ -33,11 +33,20 @@
</div> </div>
</p> </p>
{% if can_change_resources %}
<p class="row"> <p class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-success btn-sm" id="vm-details-resources-save"><i class="icon-save"></i> {% trans "Save resources" %}</button> <button type="submit" class="btn btn-success btn-sm enabled-when-stopped" id="vm-details-resources-save"
data-vm="{{ instance.pk }}"
{% if not op.resources_change %}disabled{% endif %}>
<i class="icon-save"></i> {% trans "Save resources" %}
</button>
<span class="hide-when-stopped"
{% if op.resources_change %}style="display: none;"{% endif %}
>{% trans "Stop your VM to change resources." %}</span>
</div> </div>
</p> </p>
{% endif %}
</form> </form>
<hr /> <hr />
...@@ -47,18 +56,9 @@ ...@@ -47,18 +56,9 @@
<h3> <h3>
{% trans "Disks" %} {% trans "Disks" %}
<div class="pull-right"> <div class="pull-right">
{% if op.download_disk %} <div id="disk-ops">
<a href="{{op.download_disk.get_url}}" class="btn btn-success btn-xs {% include "dashboard/vm-detail/_disk-operations.html" %}
operation operation-{{op.download_disk.op}} btn btn-default"> </div>
<i class="icon-{{op.download_disk.icon}}"></i>
{{op.download_disk.name}} </a>
{% endif %}
{% if op.create_disk %}
<a href="{{op.create_disk.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.create_disk.op}} btn btn-default">
<i class="icon-{{op.create_disk.icon}}"></i>
{{op.create_disk.name}} </a>
{% endif %}
</div> </div>
</h3> </h3>
...@@ -77,6 +77,32 @@ ...@@ -77,6 +77,32 @@
</div> </div>
</div> </div>
{% if user.is_superuser %}
<hr/>
<div class="row" id="">
<div class="col-sm-12">
<h3>
{% trans "Required traits" %}
</h3>
{% crispy traits_form %}
</div>
</div>
<hr/>
<div class="row" id="">
<div class="col-sm-12">
<h3>
{% trans "Raw data" %}
</h3>
{% crispy raw_data_form %}
</div>
</div>
{% endif %}
{% block extra_js %} {% block extra_js %}
<style> <style>
......
<a href="{% url "dashboard.views.vm-migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate"> <a href="{% url "dashboard.vm.op.migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate">
<i class="icon-truck"></i> <i class="icon-truck"></i>
</a> </a>
<a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename"> <a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
......
...@@ -6,10 +6,18 @@ register = template.Library() ...@@ -6,10 +6,18 @@ register = template.Library()
LINKABLE_PORTS = {80: "http", 8080: "http", 443: "https", 21: "ftp"} LINKABLE_PORTS = {80: "http", 8080: "http", 443: "https", 21: "ftp"}
@register.simple_tag(name="display_portforward") @register.simple_tag(name="display_portforward4")
def display_pf(ports): def display_pf4(ports):
is_ipv6 = "ipv6" in ports return display_pf(ports, 'ipv4')
data = ports["ipv6" if is_ipv6 else "ipv4"]
@register.simple_tag(name="display_portforward6")
def display_pf6(ports):
return display_pf(ports, 'ipv6')
def display_pf(ports, proto):
data = ports[proto]
if ports['private'] in LINKABLE_PORTS.keys(): if ports['private'] in LINKABLE_PORTS.keys():
href = "%s:%d" % (data['host'], data['port']) href = "%s:%d" % (data['host'], data['port'])
......
...@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called assert not msg.error.called
def test_migrate_failed(self): def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}) request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
...@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert msg.error.called assert msg.error.called
def test_migrate_template(self): def test_migrate_template(self):
request = FakeRequestFactory() request = FakeRequestFactory(superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
with patch.object(view, 'get_object') as go: with patch.object(view, 'get_object') as go:
...@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render().status_code, 200) view.as_view()(request, pk=1234).render().status_code, 200)
def test_save_as_wo_name(self): def test_save_as_wo_name(self):
request = FakeRequestFactory(POST={}) request = FakeRequestFactory(POST={}, has_perms_mock=True)
view = vm_ops['save_as_template'] view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
...@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called assert not msg.error.called
def test_save_as_template(self): def test_save_as_template(self):
request = FakeRequestFactory() request = FakeRequestFactory(has_perms_mock=True)
view = vm_ops['save_as_template'] view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go: with patch.object(view, 'get_object') as go:
...@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs): ...@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs):
user = UserFactory() user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True) user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False) user.is_superuser = kwargs.get('superuser', False)
if kwargs.get('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True)
request = HttpRequest() request = HttpRequest()
request.user = user request.user = user
......
...@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase):
self.g1.user_set.add(self.u1) self.g1.user_set.add(self.u1)
self.g1.user_set.add(self.u2) self.g1.user_set.add(self.u2)
self.g1.save() self.g1.save()
self.u1.user_permissions.add(Permission.objects.get(
codename='create_vm'))
settings["default_vlangroup"] = 'public' settings["default_vlangroup"] = 'public'
VlanGroup.objects.create(name='public') VlanGroup.objects.create(name='public')
...@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase): ...@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0] inst.node = Node.objects.all()[0]
inst.save() inst.save()
inst.set_level(self.u1, 'operator') inst.set_level(self.u1, 'operator')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/') response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase): ...@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0] inst.node = Node.objects.all()[0]
inst.save() inst.save()
inst.set_level(self.u1, 'user') inst.set_level(self.u1, 'user')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/') response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
......
...@@ -28,9 +28,10 @@ from .views import ( ...@@ -28,9 +28,10 @@ from .views import (
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, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView, VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView, GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate, GroupCreate, GroupProfileUpdate,
TemplateChoose, TemplateChoose,
UserCreationView, UserCreationView,
...@@ -38,7 +39,9 @@ from .views import ( ...@@ -38,7 +39,9 @@ from .views import (
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, StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -83,14 +86,16 @@ urlpatterns = patterns( ...@@ -83,14 +86,16 @@ urlpatterns = patterns(
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),
url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(),
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(), url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'), name='dashboard.views.vm-renew'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(), url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'), name='dashboard.views.vm-activity'),
url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot, url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot,
name='dashboard.views.vm-get-screenshot'), name='dashboard.views.vm-get-screenshot'),
url(r'^vm/(?P<pk>\d+)/traits/$', VmTraitsUpdate.as_view(),
name='dashboard.views.vm-traits'),
url(r'^vm/(?P<pk>\d+)/raw_data/$', VmRawDataUpdate.as_view(),
name='dashboard.views.vm-raw-data'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
...@@ -156,11 +161,17 @@ urlpatterns = patterns( ...@@ -156,11 +161,17 @@ urlpatterns = patterns(
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$', url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(), GroupRemoveUserView.as_view(),
name="dashboard.views.remove-user"), name="dashboard.views.remove-user"),
url(r'^group/(?P<group_pk>\d+)/remove/futureuser/(?P<member_org_id>.+)/$',
GroupRemoveFutureUserView.as_view(),
name="dashboard.views.remove-future-user"),
url(r'^group/create/$', GroupCreate.as_view(), url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'), name='dashboard.views.group-create'),
url(r'^group/(?P<group_pk>\d+)/create/$', url(r'^group/(?P<group_pk>\d+)/create/$',
UserCreationView.as_view(), UserCreationView.as_view(),
name="dashboard.views.create-user"), name="dashboard.views.create-user"),
url(r'^group/(?P<group_pk>\d+)/permissions/$',
GroupPermissionsView.as_view(),
name="dashboard.views.group-permissions"),
url(r'^sshkey/delete/(?P<pk>\d+)/$', url(r'^sshkey/delete/(?P<pk>\d+)/$',
UserKeyDelete.as_view(), UserKeyDelete.as_view(),
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from collections import OrderedDict
from itertools import chain from itertools import chain
from os import getenv from os import getenv
import os import os
...@@ -67,6 +68,7 @@ from .forms import ( ...@@ -67,6 +68,7 @@ from .forms import (
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm, UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, UserKeyForm, VmSaveForm, UserKeyForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm
) )
from .tables import ( from .tables import (
...@@ -79,7 +81,7 @@ from vm.models import ( ...@@ -79,7 +81,7 @@ from vm.models import (
) )
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from .models import Favourite, Profile, GroupProfile from .models import Favourite, Profile, GroupProfile, FutureMember
from dashboard import store_api from dashboard import store_api
...@@ -259,6 +261,8 @@ class VmDetailVncTokenView(CheckedDetailView): ...@@ -259,6 +261,8 @@ class VmDetailVncTokenView(CheckedDetailView):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'): if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied() raise PermissionDenied()
if not request.user.has_perm('vm.access_console'):
raise PermissionDenied()
if self.object.node: if self.object.node:
with instance_activity(code_suffix='console-accessed', with instance_activity(code_suffix='console-accessed',
instance=self.object, user=request.user, instance=self.object, user=request.user,
...@@ -289,7 +293,8 @@ class VmDetailView(CheckedDetailView): ...@@ -289,7 +293,8 @@ class VmDetailView(CheckedDetailView):
}) })
# activity data # activity data
context['activities'] = self.object.get_activities(self.request.user) context['activities'] = self.object.get_merged_activities(
self.request.user)
context['vlans'] = Vlan.get_objects_with_level( context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user 'user', self.request.user
...@@ -303,13 +308,19 @@ class VmDetailView(CheckedDetailView): ...@@ -303,13 +308,19 @@ class VmDetailView(CheckedDetailView):
# ipv6 infos # ipv6 infos
context['ipv6_host'] = instance.get_connect_host(use_ipv6=True) context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
context['ipv6_port'] = instance.get_connect_port(use_ipv6=True) context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
# resources forms
if self.request.user.is_superuser:
context['traits_form'] = TraitsForm(instance=instance)
context['raw_data_form'] = RawDataForm(instance=instance)
# resources change perm
context['can_change_resources'] = self.request.user.has_perm(
"vm.change_resources")
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if (request.POST.get('ram-size') and request.POST.get('cpu-count')
and request.POST.get('cpu-priority')):
return self.__set_resources(request)
options = { options = {
'change_password': self.__change_password, 'change_password': self.__change_password,
'new_name': self.__set_name, 'new_name': self.__set_name,
...@@ -337,33 +348,6 @@ class VmDetailView(CheckedDetailView): ...@@ -337,33 +348,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail", return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk})) kwargs={'pk': self.object.pk}))
def __set_resources(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
if not request.user.has_perm('vm.change_resources'):
raise PermissionDenied()
resources = {
'num_cores': request.POST.get('cpu-count'),
'ram_size': request.POST.get('ram-size'),
'max_ram_size': request.POST.get('ram-size'), # TODO: max_ram
'priority': request.POST.get('cpu-priority')
}
Instance.objects.filter(pk=self.object.pk).update(**resources)
success_message = _("Resources successfully updated.")
if request.is_ajax():
response = {'message': success_message}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
def __set_name(self, request): def __set_name(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, 'owner'):
...@@ -515,10 +499,27 @@ class VmDetailView(CheckedDetailView): ...@@ -515,10 +499,27 @@ class VmDetailView(CheckedDetailView):
return redirect("%s#activity" % self.object.get_absolute_url()) return redirect("%s#activity" % self.object.get_absolute_url())
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
form_class = TraitsForm
model = Instance
def get_success_url(self):
return self.get_object().get_absolute_url() + "#resources"
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
form_class = RawDataForm
model = Instance
def get_success_url(self):
return self.get_object().get_absolute_url() + "#resources"
class OperationView(DetailView): class OperationView(DetailView):
template_name = 'dashboard/operate.html' template_name = 'dashboard/operate.html'
show_in_toolbar = True show_in_toolbar = True
effect = None
@property @property
def name(self): def name(self):
...@@ -528,6 +529,9 @@ class OperationView(DetailView): ...@@ -528,6 +529,9 @@ class OperationView(DetailView):
def description(self): def description(self):
return self.get_op().description return self.get_op().description
def is_preferred(self):
return self.get_op().is_preferred()
@classmethod @classmethod
def get_urlname(cls): def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op return 'dashboard.vm.op.%s' % cls.op
...@@ -535,11 +539,11 @@ class OperationView(DetailView): ...@@ -535,11 +539,11 @@ class OperationView(DetailView):
def get_url(self): def get_url(self):
return reverse(self.get_urlname(), args=(self.get_object().pk, )) return reverse(self.get_urlname(), args=(self.get_object().pk, ))
def get_wrapper_template_name(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return 'dashboard/_modal.html' return ['dashboard/_modal.html']
else: else:
return 'dashboard/_base.html' return ['dashboard/_base.html']
@classmethod @classmethod
def get_op_by_object(cls, obj): def get_op_by_object(cls, obj):
...@@ -553,16 +557,14 @@ class OperationView(DetailView): ...@@ -553,16 +557,14 @@ class OperationView(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super(OperationView, self).get_context_data(**kwargs) ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op() ctx['op'] = self.get_op()
ctx['opview'] = self
ctx['url'] = self.request.path ctx['url'] = self.request.path
ctx['template'] = super(OperationView, self).get_template_names()[0]
return ctx return ctx
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.get_op().check_auth(request.user) self.get_op().check_auth(request.user)
response = super(OperationView, self).get(request, *args, **kwargs) return super(OperationView, self).get(request, *args, **kwargs)
response.render()
response.content = render_to_string(self.get_wrapper_template_name(),
{'body': response.content})
return response
def post(self, request, extra=None, *args, **kwargs): def post(self, request, extra=None, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
...@@ -576,14 +578,16 @@ class OperationView(DetailView): ...@@ -576,14 +578,16 @@ class OperationView(DetailView):
return redirect("%s#activity" % self.object.get_absolute_url()) return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod @classmethod
def factory(cls, op, icon='cog'): def factory(cls, op, icon='cog', effect='info'):
return type(str(cls.__name__ + op), return type(str(cls.__name__ + op),
(cls, ), {'op': op, 'icon': icon}) (cls, ), {'op': op, 'icon': icon, 'effect': effect})
@classmethod @classmethod
def bind_to_object(cls, instance): def bind_to_object(cls, instance, **kwargs):
v = cls() v = cls()
v.get_object = lambda: instance v.get_object = lambda: instance
for key, value in kwargs.iteritems():
setattr(v, key, value)
return v return v
...@@ -592,6 +596,20 @@ class VmOperationView(OperationView): ...@@ -592,6 +596,20 @@ class VmOperationView(OperationView):
model = Instance model = Instance
context_object_name = 'instance' # much simpler to mock object context_object_name = 'instance' # much simpler to mock object
def post(self, request, extra=None, *args, **kwargs):
resp = super(VmOperationView, self).post(request, extra, *args,
**kwargs)
if request.is_ajax():
store = messages.get_messages(request)
store.used = True
return HttpResponse(
json.dumps({'success': True,
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
else:
return resp
class FormOperationMixin(object): class FormOperationMixin(object):
...@@ -611,8 +629,15 @@ class FormOperationMixin(object): ...@@ -611,8 +629,15 @@ class FormOperationMixin(object):
form = self.form_class(self.request.POST) form = self.form_class(self.request.POST)
if form.is_valid(): if form.is_valid():
extra.update(form.cleaned_data) extra.update(form.cleaned_data)
return super(FormOperationMixin, self).post( resp = super(FormOperationMixin, self).post(
request, extra, *args, **kwargs) request, extra, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({'success': True}),
content_type="application=json"
)
else:
return resp
else: else:
return self.get(request) return self.get(request)
...@@ -623,6 +648,7 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView): ...@@ -623,6 +648,7 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView):
form_class = VmCreateDiskForm form_class = VmCreateDiskForm
show_in_toolbar = False show_in_toolbar = False
icon = 'hdd' icon = 'hdd'
is_disk_operation = True
class VmDownloadDiskView(FormOperationMixin, VmOperationView): class VmDownloadDiskView(FormOperationMixin, VmOperationView):
...@@ -631,12 +657,14 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView): ...@@ -631,12 +657,14 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
form_class = VmDownloadDiskForm form_class = VmDownloadDiskForm
show_in_toolbar = False show_in_toolbar = False
icon = 'download' icon = 'download'
is_disk_operation = True
class VmMigrateView(VmOperationView): class VmMigrateView(VmOperationView):
op = 'migrate' op = 'migrate'
icon = 'truck' icon = 'truck'
effect = 'info'
template_name = 'dashboard/_vm-migrate.html' template_name = 'dashboard/_vm-migrate.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
...@@ -659,22 +687,56 @@ class VmSaveView(FormOperationMixin, VmOperationView): ...@@ -659,22 +687,56 @@ class VmSaveView(FormOperationMixin, VmOperationView):
op = 'save_as_template' op = 'save_as_template'
icon = 'save' icon = 'save'
effect = 'info'
form_class = VmSaveForm form_class = VmSaveForm
vm_ops = {
'reset': VmOperationView.factory(op='reset', icon='bolt'), class VmResourcesChangeView(VmOperationView):
'deploy': VmOperationView.factory(op='deploy', icon='play'), op = 'resources_change'
'migrate': VmMigrateView, icon = "save"
'reboot': VmOperationView.factory(op='reboot', icon='refresh'), show_in_toolbar = False
'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
'shutdown': VmOperationView.factory(op='shutdown', icon='off'), def post(self, request, extra=None, *args, **kwargs):
'save_as_template': VmSaveView, if extra is None:
'destroy': VmOperationView.factory(op='destroy', icon='remove'), extra = {}
'sleep': VmOperationView.factory(op='sleep', icon='moon'),
'wake_up': VmOperationView.factory(op='wake_up', icon='sun'), resources = {
'create_disk': VmCreateDiskView, 'num_cores': "cpu-count",
'download_disk': VmDownloadDiskView, 'priority': "cpu-priority",
} 'ram_size': "ram-size",
"max_ram_size": "ram-size", # TODO
}
for k, v in resources.iteritems():
extra[k] = request.POST.get(v)
return super(VmResourcesChangeView, self).post(request, extra,
*args, **kwargs)
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
op='deploy', icon='play', effect='success')),
('wake_up', VmOperationView.factory(
op='wake_up', icon='sun', effect='success')),
('sleep', VmOperationView.factory(
op='sleep', icon='moon', effect='info')),
('migrate', VmMigrateView),
('save_as_template', VmSaveView),
('reboot', VmOperationView.factory(
op='reboot', icon='refresh', effect='warning')),
('reset', VmOperationView.factory(
op='reset', icon='bolt', effect='warning')),
('shutdown', VmOperationView.factory(
op='shutdown', icon='off', effect='warning')),
('shut_off', VmOperationView.factory(
op='shut_off', icon='ban-circle', effect='warning')),
('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')),
('destroy', VmOperationView.factory(
op='destroy', icon='remove', effect='danger')),
('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView),
])
def get_operations(instance, user): def get_operations(instance, user):
...@@ -684,9 +746,11 @@ def get_operations(instance, user): ...@@ -684,9 +746,11 @@ def get_operations(instance, user):
op = v.get_op_by_object(instance) op = v.get_op_by_object(instance)
op.check_auth(user) op.check_auth(user)
op.check_precond() op.check_precond()
except Exception as e: except PermissionDenied as e:
logger.debug('Not showing operation %s for %s: %s', logger.debug('Not showing operation %s for %s: %s',
k, instance, unicode(e)) k, instance, unicode(e))
except Exception:
ops.append(v.bind_to_object(instance, disabled=True))
else: else:
ops.append(v.bind_to_object(instance)) ops.append(v.bind_to_object(instance))
return ops return ops
...@@ -773,15 +837,23 @@ class GroupDetailView(CheckedDetailView): ...@@ -773,15 +837,23 @@ class GroupDetailView(CheckedDetailView):
context = super(GroupDetailView, self).get_context_data(**kwargs) context = super(GroupDetailView, self).get_context_data(**kwargs)
context['group'] = self.object context['group'] = self.object
context['users'] = self.object.user_set.all() context['users'] = self.object.user_set.all()
context['future_users'] = FutureMember.objects.filter(
group=self.object)
context['acl'] = get_group_acl_data(self.object) context['acl'] = get_group_acl_data(self.object)
context['group_profile_form'] = GroupProfileUpdate.get_form_object( context['group_profile_form'] = GroupProfileUpdate.get_form_object(
self.request, self.object.profile) self.request, self.object.profile)
if self.request.user.is_superuser:
context['group_perm_form'] = GroupPermissionForm(
instance=self.object)
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
if not self.get_has_level()(request.user, 'operator'): if not self.get_has_level()(request.user, 'operator'):
raise PermissionDenied() raise PermissionDenied()
if request.POST.get('new_name'): if request.POST.get('new_name'):
return self.__set_name(request) return self.__set_name(request)
if request.POST.get('list-new-name'): if request.POST.get('list-new-name'):
...@@ -803,10 +875,14 @@ class GroupDetailView(CheckedDetailView): ...@@ -803,10 +875,14 @@ class GroupDetailView(CheckedDetailView):
if not name: if not name:
return return
try: try:
entity = User.objects.get(username=name) entity = search_user(name)
self.object.user_set.add(entity) self.object.user_set.add(entity)
except User.DoesNotExist: except User.DoesNotExist:
warning(request, _('User "%s" not found.') % name) if saml_available:
FutureMember.objects.get_or_create(org_id=name,
group=self.object)
else:
warning(request, _('User "%s" not found.') % name)
def __add_list(self, request): def __add_list(self, request):
if not self.get_has_level()(request.user, 'operator'): if not self.get_has_level()(request.user, 'operator'):
...@@ -839,6 +915,17 @@ class GroupDetailView(CheckedDetailView): ...@@ -839,6 +915,17 @@ class GroupDetailView(CheckedDetailView):
kwargs={'pk': self.object.pk})) kwargs={'pk': self.object.pk}))
class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
model = Group
form_class = GroupPermissionForm
slug_field = "pk"
slug_url_kwarg = "group_pk"
def get_success_url(self):
return "%s#group-detail-permissions" % (
self.get_object().groupprofile.get_absolute_url())
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin): class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
...@@ -892,7 +979,7 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin): ...@@ -892,7 +979,7 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
if not name: if not name:
return return
try: try:
entity = User.objects.get(username=name) entity = search_user(name)
except User.DoesNotExist: except User.DoesNotExist:
entity = None entity = None
try: try:
...@@ -956,7 +1043,7 @@ class GroupAclUpdateView(AclUpdateView): ...@@ -956,7 +1043,7 @@ class GroupAclUpdateView(AclUpdateView):
kwargs=self.kwargs)) kwargs=self.kwargs))
class TemplateChoose(TemplateView): class TemplateChoose(LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
...@@ -989,6 +1076,9 @@ class TemplateChoose(TemplateView): ...@@ -989,6 +1076,9 @@ class TemplateChoose(TemplateView):
else: else:
template = get_object_or_404(InstanceTemplate, pk=template) template = get_object_or_404(InstanceTemplate, pk=template)
if not template.has_level(request.user, "user"):
raise PermissionDenied()
instance = Instance.create_from_template( instance = Instance.create_from_template(
template=template, owner=request.user, is_base=True) template=template, owner=request.user, is_base=True)
...@@ -1016,7 +1106,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView): ...@@ -1016,7 +1106,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
if not self.request.user.has_perm('vm.create_template'): if not self.request.user.has_perm('vm.create_base_template'):
raise PermissionDenied() raise PermissionDenied()
return super(TemplateCreate, self).get(*args, **kwargs) return super(TemplateCreate, self).get(*args, **kwargs)
...@@ -1027,7 +1117,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView): ...@@ -1027,7 +1117,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return kwargs return kwargs
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not self.request.user.has_perm('vm.create_template'): if not self.request.user.has_perm('vm.create_base_template'):
raise PermissionDenied() raise PermissionDenied()
form = self.form_class(request.POST, user=request.user) form = self.form_class(request.POST, user=request.user)
...@@ -1049,8 +1139,6 @@ class TemplateCreate(SuccessMessageMixin, CreateView): ...@@ -1049,8 +1139,6 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return redirect("%s#resources" % inst.get_absolute_url()) return redirect("%s#resources" % inst.get_absolute_url())
return super(TemplateCreate, self).post(self, request, args, kwargs)
def __create_networks(self, vlans, user): def __create_networks(self, vlans, user):
networks = [] networks = []
for v in vlans: for v in vlans:
...@@ -1111,12 +1199,6 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -1111,12 +1199,6 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
template = self.get_object() template = self.get_object()
if not template.has_level(request.user, 'owner'): if not template.has_level(request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
for disk in self.get_object().disks.all():
if not disk.has_level(request.user, 'user'):
raise PermissionDenied()
for network in self.get_object().interface_set.all():
if not network.vlan.has_level(request.user, "user"):
raise PermissionDenied()
return super(TemplateDetail, self).post(self, request, args, kwargs) return super(TemplateDetail, self).post(self, request, args, kwargs)
def get_form_kwargs(self): def get_form_kwargs(self):
...@@ -1325,6 +1407,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView): ...@@ -1325,6 +1407,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
slug_field = 'pk' slug_field = 'pk'
slug_url_kwarg = 'group_pk' slug_url_kwarg = 'group_pk'
read_level = 'operator' read_level = 'operator'
member_key = 'member_pk'
def get_has_level(self): def get_has_level(self):
return self.object.profile.has_level return self.object.profile.has_level
...@@ -1366,7 +1449,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView): ...@@ -1366,7 +1449,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
object = self.get_object() object = self.get_object()
if not object.profile.has_level(request.user, 'operator'): if not object.profile.has_level(request.user, 'operator'):
raise PermissionDenied() raise PermissionDenied()
self.remove_member(kwargs["member_pk"]) self.remove_member(kwargs[self.member_key])
success_url = self.get_success_url() success_url = self.get_success_url()
success_message = self.get_success_message() success_message = self.get_success_message()
if request.is_ajax(): if request.is_ajax():
...@@ -1379,6 +1462,31 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView): ...@@ -1379,6 +1462,31 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
class GroupRemoveFutureUserView(GroupRemoveUserView):
member_key = 'member_org_id'
def get(self, request, member_org_id, *args, **kwargs):
self.member_org_id = member_org_id
return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
try:
context['member'] = FutureMember.objects.get(
org_id=self.member_org_id, group=self.get_object())
except FutureMember.DoesNotExist:
raise Http404()
return context
def remove_member(self, org_id):
FutureMember.objects.filter(org_id=org_id,
group=self.get_object()).delete()
def get_success_message(self):
return _("Future user successfully removed from group.")
class GroupRemoveAclUserView(GroupRemoveUserView): class GroupRemoveAclUserView(GroupRemoveUserView):
def remove_member(self, pk): def remove_member(self, pk):
...@@ -1464,6 +1572,9 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1464,6 +1572,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs): def get(self, request, form=None, *args, **kwargs):
if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied()
form_error = form is not None form_error = form is not None
template = (form.template.pk if form_error template = (form.template.pk if form_error
else request.GET.get("template")) else request.GET.get("template"))
...@@ -1569,6 +1680,9 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1569,6 +1680,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
user = request.user user = request.user
if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied()
# limit chekcs # limit chekcs
try: try:
limit = user.profile.instance_limit limit = user.profile.instance_limit
...@@ -2110,7 +2224,7 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -2110,7 +2224,7 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
@require_GET @require_GET
def vm_activity(request, pk): def vm_activity(request, pk):
instance = Instance.objects.get(pk=pk) instance = Instance.objects.get(pk=pk)
if not instance.has_level(request.user, 'owner'): if not instance.has_level(request.user, 'user'):
raise PermissionDenied() raise PermissionDenied()
response = {} response = {}
...@@ -2122,7 +2236,7 @@ def vm_activity(request, pk): ...@@ -2122,7 +2236,7 @@ def vm_activity(request, pk):
if only_status == "false": # instance activity if only_status == "false": # instance activity
context = { context = {
'instance': instance, 'instance': instance,
'activities': instance.get_activities(request.user), 'activities': instance.get_merged_activities(request.user),
'ops': get_operations(instance, request.user), 'ops': get_operations(instance, request.user),
} }
...@@ -2134,6 +2248,10 @@ def vm_activity(request, pk): ...@@ -2134,6 +2248,10 @@ def vm_activity(request, pk):
"dashboard/vm-detail/_operations.html", "dashboard/vm-detail/_operations.html",
RequestContext(request, context), RequestContext(request, context),
) )
response['disk_ops'] = render_to_string(
"dashboard/vm-detail/_disk-operations.html",
RequestContext(request, context),
)
return HttpResponse( return HttpResponse(
json.dumps(response), json.dumps(response),
...@@ -2811,8 +2929,7 @@ class InterfaceDeleteView(DeleteView): ...@@ -2811,8 +2929,7 @@ class InterfaceDeleteView(DeleteView):
def get_vm_screenshot(request, pk): def get_vm_screenshot(request, pk):
instance = get_object_or_404(Instance, pk=pk) instance = get_object_or_404(Instance, pk=pk)
try: try:
image = instance.screenshot(instance=instance, image = instance.screenshot(user=request.user).getvalue()
user=request.user).getvalue()
except: except:
# TODO handle this better # TODO handle this better
raise Http404() raise Http404()
......
...@@ -28,6 +28,7 @@ import re ...@@ -28,6 +28,7 @@ import re
alfanum_re = re.compile(r'^[A-Za-z0-9_-]+$') alfanum_re = re.compile(r'^[A-Za-z0-9_-]+$')
domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$') domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$')
domain_wildcard_re = re.compile(r'^(\*\.)?([A-Za-z0-9_-]\.?)+$')
ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$') ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$')
reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$') reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$') ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
...@@ -216,12 +217,23 @@ def is_valid_domain(value): ...@@ -216,12 +217,23 @@ def is_valid_domain(value):
return domain_re.match(value) is not None return domain_re.match(value) is not None
def is_valid_domain_wildcard(value):
"""Check whether the parameter is a valid domain name."""
return domain_wildcard_re.match(value) is not None
def val_domain(value): def val_domain(value):
"""Validate whether the parameter is a valid domin name.""" """Validate whether the parameter is a valid domin name."""
if not is_valid_domain(value): if not is_valid_domain(value):
raise ValidationError(_(u'%s - invalid domain name') % value) raise ValidationError(_(u'%s - invalid domain name') % value)
def val_domain_wildcard(value):
"""Validate whether the parameter is a valid domin name."""
if not is_valid_domain_wildcard(value):
raise ValidationError(_(u'%s - invalid domain name') % value)
def is_valid_reverse_domain(value): def is_valid_reverse_domain(value):
"""Check whether the parameter is a valid reverse domain name.""" """Check whether the parameter is a valid reverse domain name."""
return reverse_domain_re.match(value) is not None return reverse_domain_re.match(value) is not None
......
...@@ -27,6 +27,7 @@ from django.forms import ValidationError ...@@ -27,6 +27,7 @@ from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain, from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
val_ipv6_template, val_domain, val_ipv4, val_ipv6_template, val_domain, val_ipv4,
val_domain_wildcard,
val_ipv6, val_mx, convert_ipv4_to_ipv6, val_ipv6, val_mx, convert_ipv4_to_ipv6,
IPNetworkField, IPAddressField) IPNetworkField, IPAddressField)
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
...@@ -695,8 +696,7 @@ class Host(models.Model): ...@@ -695,8 +696,7 @@ class Host(models.Model):
:param private: Port number of host in subject. :param private: Port number of host in subject.
""" """
self.rules.filter(owner=self.owner, proto=proto, host=self, self.rules.filter(proto=proto, dport=private).delete()
dport=private).delete()
def get_hostname(self, proto, public=True): def get_hostname(self, proto, public=True):
""" """
...@@ -728,7 +728,7 @@ class Host(models.Model): ...@@ -728,7 +728,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set. Return a list of ports with forwarding rules set.
""" """
retval = [] retval = []
for rule in self.rules.filter(owner=self.owner): for rule in self.rules.all():
forward = { forward = {
'proto': rule.proto, 'proto': rule.proto,
'private': rule.dport, 'private': rule.dport,
...@@ -770,9 +770,7 @@ class Host(models.Model): ...@@ -770,9 +770,7 @@ class Host(models.Model):
if public_port else if public_port else
None) None)
# IPv6 # IPv6
blocked = self.incoming_rules.exclude( endpoints['ipv6'] = (self.ipv6, port) if public_port else None
action='accept').filter(dport=port, proto=protocol).exists()
endpoints['ipv6'] = (self.ipv6, port) if not blocked else None
return endpoints return endpoints
@models.permalink @models.permalink
...@@ -821,7 +819,7 @@ class Domain(models.Model): ...@@ -821,7 +819,7 @@ class Domain(models.Model):
class Record(models.Model): class Record(models.Model):
CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'), CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'),
('MX', 'MX'), ('NS', 'NS'), ('PTR', 'PTR'), ('TXT', 'TXT')) ('MX', 'MX'), ('NS', 'NS'), ('PTR', 'PTR'), ('TXT', 'TXT'))
name = models.CharField(max_length=40, validators=[val_domain], name = models.CharField(max_length=40, validators=[val_domain_wildcard],
blank=True, null=True, verbose_name=_('name')) blank=True, null=True, verbose_name=_('name'))
domain = models.ForeignKey('Domain', verbose_name=_('domain')) domain = models.ForeignKey('Domain', verbose_name=_('domain'))
host = models.ForeignKey('Host', blank=True, null=True, host = models.ForeignKey('Host', blank=True, null=True,
......
...@@ -30,7 +30,9 @@ celery = Celery('manager', ...@@ -30,7 +30,9 @@ celery = Celery('manager',
'vm.tasks.local_agent_tasks', 'vm.tasks.local_agent_tasks',
'storage.tasks.local_tasks', 'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks', 'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', ]) 'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks',
])
celery.conf.update( celery.conf.update(
CELERY_RESULT_BACKEND='cache', CELERY_RESULT_BACKEND='cache',
...@@ -69,6 +71,24 @@ celery.conf.update( ...@@ -69,6 +71,24 @@ celery.conf.update(
'schedule': timedelta(hours=24), 'schedule': timedelta(hours=24),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.man'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
} }
) )
from itertools import islice
from socket import gethostname
import logging
import os
import pika
logger = logging.getLogger(__name__)
class Client:
env_config = {
"server_address": "GRAPHITE_HOST",
"server_port": "GRAPHITE_AMQP_PORT",
"amqp_user": "GRAPHITE_AMQP_USER",
"amqp_pass": "GRAPHITE_AMQP_PASSWORD",
"amqp_queue": "GRAPHITE_AMQP_QUEUE",
"amqp_vhost": "GRAPHITE_AMQP_VHOST",
}
def __init__(self):
"""
Constructor of the client class that is responsible for handling the
communication between the graphite server and the data source. In
order to initialize a client you must have the following
environmental varriables:
- GRAPHITE_SERVER_ADDRESS:
- GRAPHITE_SERVER_PORT:
- GRAPHITE_AMQP_USER:
- GRAPHITE_AMQP_PASSWORD:
- GRAPHITE_AMQP_QUEUE:
- GRAPHITE_AMQP_VHOST:
Missing only one of these variables will cause the client not to work.
"""
self.name = 'circle.%s' % gethostname()
for var, env_var in self.env_config.items():
value = os.getenv(env_var, "")
if value:
setattr(self, var, value)
else:
raise RuntimeError('%s environment variable missing' % env_var)
def connect(self):
"""
This method creates the connection to the queue of the graphite
server using the environmental variables given in the constructor.
"""
try:
credentials = pika.PlainCredentials(self.amqp_user, self.amqp_pass)
params = pika.ConnectionParameters(host=self.server_address,
port=int(self.server_port),
virtual_host=self.amqp_vhost,
credentials=credentials)
self.connection = pika.BlockingConnection(params)
self.channel = self.connection.channel()
logger.info('Connection established to %s.', self.server_address)
except RuntimeError:
logger.error('Cannot connect to the server. '
'Parameters may be wrong.')
logger.error("An error has occured while connecting to the server")
raise
except: # FIXME
logger.error('Cannot connect to the server. There is no one '
'listening on the other side.')
raise
def disconnect(self):
"""
Break up the connection to the graphite server. If something went
wrong while disconnecting it simply cut the connection up.
"""
try:
self.channel.close()
self.connection.close()
except RuntimeError as e:
logger.error('An error has occured while disconnecting. %s',
unicode(e))
raise
def _send(self, message):
"""
Send the message given in the parameters given in the message
parameter. This function expects that the graphite server want the
metric name given in the message body. (This option must be enabled
on the server. Otherwise it can't parse the data sent.)
"""
body = "\n".join(message)
try:
self.channel.basic_publish(exchange=self.amqp_queue,
routing_key='', body=body)
except:
logger.error('An error has occured while sending metrics (%dB).',
len(body))
raise
@staticmethod
def _chunker(seq, size):
"""Yield seq in size-long chunks.
"""
for pos in xrange(0, len(seq), size):
yield islice(seq, pos, pos + size)
def send(self, message):
self.connect()
try:
for chunk in self._chunker(message, 100):
self._send(chunk)
finally:
self.disconnect()
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import logging
import requests
from time import time
from django.conf import settings
from manager.mancelery import celery
from vm.tasks.vm_tasks import check_queue
from vm.models import Node, InstanceTemplate
from storage.models import DataStore
from monitor.client import Client
logger = logging.getLogger(__name__)
@celery.task(ignore_result=True)
def measure_response_time():
try:
r = requests.get(settings.DJANGO_URL, verify=False,
timeout=0.5)
except requests.exceptions.Timeout:
return
total_miliseconds = (
r.elapsed.seconds * 10**6 +
r.elapsed.microseconds) / 1000
Client().send([
"%(name)s %(val)d %(time)s" % {
'name': "portal.response_time",
'val': total_miliseconds,
'time': time(),
}
])
@celery.task(ignore_result=True)
def check_celery_queues():
graphite_string = lambda component, hostname, celery, is_alive, time: (
"%s.%s.celery-queues.%s %d %s" % (
component, hostname, celery, 1 if is_alive else 0, time)
)
metrics = []
for n in Node.objects.all(): # disabled, offline nodes?
for s in ["fast", "slow"]:
is_queue_alive = check_queue(n.host.hostname, "vm", s)
metrics.append(graphite_string("circle", n.host.hostname,
"vm-" + s, is_queue_alive, time()))
is_net_queue_alive = check_queue(n.host.hostname, "net", "fast")
metrics.append(graphite_string("circle", n.host.hostname,
"net-fast", is_net_queue_alive, time()))
is_agent_queue_alive = check_queue(n.host.hostname, "agent")
metrics.append(graphite_string("circle", n.host.hostname, "agent",
is_agent_queue_alive, time()))
for ds in DataStore.objects.all():
for s in ["fast", "slow"]:
is_queue_alive = check_queue(ds.hostname, "vm", s)
metrics.append(graphite_string("storage", ds.hostname,
"storage-" + s, is_queue_alive,
time()))
Client().send(metrics)
@celery.task(ignore_result=True)
def instance_per_template():
graphite_string = lambda pk, state, val, time: (
"template.%d.instances.%s %d %s" % (
pk, state, val, time)
)
metrics = []
for t in InstanceTemplate.objects.all():
base = t.instance_set.filter(destroyed_at=None)
running = base.filter(status="RUNNING").count()
not_running = base.exclude(status="RUNNING").count()
metrics.append(graphite_string(t.pk, "running", running, time()))
metrics.append(graphite_string(t.pk, "not_running", not_running,
time()))
Client().send(metrics)
...@@ -106,6 +106,9 @@ class Disk(AclBase, TimeStampedModel): ...@@ -106,6 +106,9 @@ class Disk(AclBase, TimeStampedModel):
ordering = ['name'] ordering = ['name']
verbose_name = _('disk') verbose_name = _('disk')
verbose_name_plural = _('disks') verbose_name_plural = _('disks')
permissions = (
('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception): class WrongDiskTypeError(Exception):
...@@ -131,6 +134,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -131,6 +134,7 @@ class Disk(AclBase, TimeStampedModel):
self.disk = disk self.disk = disk
class DiskIsNotReady(Exception): class DiskIsNotReady(Exception):
""" Exception for operations that need a deployed disk. """ Exception for operations that need a deployed disk.
""" """
...@@ -380,13 +384,18 @@ class Disk(AclBase, TimeStampedModel): ...@@ -380,13 +384,18 @@ class Disk(AclBase, TimeStampedModel):
self.save() self.save()
return True return True
def restore(self, user=None, task_uuid=None): def restore(self, user=None, task_uuid=None, timeout=15):
"""Recover destroyed disk from trash if possible. """Recover destroyed disk from trash if possible.
""" """
# TODO queue_name = self.datastore.get_remote_queue_name(
pass 'storage', priority='slow')
logger.info("Image: %s at Datastore: %s recovered from trash." %
def save_as(self, user=None, task_uuid=None, timeout=300): (self.filename, self.datastore.path))
storage_tasks.recover_from_trash.apply_async(
args=[self.datastore.path, self.filename],
queue=queue_name).get(timeout=timeout)
def save_as(self, task=None, user=None, task_uuid=None, timeout=300):
"""Save VM as template. """Save VM as template.
Based on disk type: Based on disk type:
...@@ -421,10 +430,18 @@ class Disk(AclBase, TimeStampedModel): ...@@ -421,10 +430,18 @@ class Disk(AclBase, TimeStampedModel):
type=new_type) type=new_type)
queue_name = self.get_remote_queue_name("storage", priority="slow") queue_name = self.get_remote_queue_name("storage", priority="slow")
storage_tasks.merge.apply_async(args=[self.get_disk_desc(), remote = storage_tasks.merge.apply_async(kwargs={
disk.get_disk_desc()], "old_json": self.get_disk_desc(),
queue=queue_name "new_json": disk.get_disk_desc()},
).get() # Timeout queue=queue_name
disk.is_ready = True ) # Timeout
disk.save() while True:
try:
remote.get(timeout=5)
break
except TimeoutError:
if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort()
disk.destroy()
raise Exception("Save as aborted by use.")
return disk return disk
...@@ -73,6 +73,11 @@ def move_to_trash(datastore, disk_path): ...@@ -73,6 +73,11 @@ def move_to_trash(datastore, disk_path):
pass pass
@celery.task(name='storagedriver.recover_from_trash')
def recover_from_trash(datastore, disk_path):
pass
@celery.task(name='storagedriver.get_storage_stat') @celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path): def get_storage_stat(path):
pass pass
...@@ -11,8 +11,14 @@ class Migration(SchemaMigration): ...@@ -11,8 +11,14 @@ class Migration(SchemaMigration):
# Removing unique constraint on 'InstanceTemplate', fields ['name'] # Removing unique constraint on 'InstanceTemplate', fields ['name']
db.delete_unique(u'vm_instancetemplate', ['name']) db.delete_unique(u'vm_instancetemplate', ['name'])
# Changing field 'InstanceTemplate.parent'
db.alter_column(u'vm_instancetemplate', 'parent_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vm.InstanceTemplate'], null=True, on_delete=models.SET_NULL))
def backwards(self, orm): def backwards(self, orm):
# Changing field 'InstanceTemplate.parent'
db.alter_column(u'vm_instancetemplate', 'parent_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vm.InstanceTemplate'], null=True))
# Adding unique constraint on 'InstanceTemplate', fields ['name'] # Adding unique constraint on 'InstanceTemplate', fields ['name']
db.create_unique(u'vm_instancetemplate', ['name']) db.create_unique(u'vm_instancetemplate', ['name'])
...@@ -131,6 +137,7 @@ class Migration(SchemaMigration): ...@@ -131,6 +137,7 @@ class Migration(SchemaMigration):
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}), 'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 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'}), 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}), 'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
...@@ -147,6 +154,7 @@ class Migration(SchemaMigration): ...@@ -147,6 +154,7 @@ class Migration(SchemaMigration):
'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}), 'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}), 'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}), 'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
...@@ -273,4 +281,4 @@ class Migration(SchemaMigration): ...@@ -273,4 +281,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['vm'] complete_apps = ['vm']
\ No newline at end of file
...@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
ordering = ('name', ) ordering = ('name', )
permissions = ( permissions = (
('create_template', _('Can create an instance template.')), ('create_template', _('Can create an instance template.')),
('create_base_template',
_('Can create an instance template (base).')),
('change_template_resources',
_('Can change resources of a template.')),
) )
verbose_name = _('template') verbose_name = _('template')
verbose_name_plural = _('templates') verbose_name_plural = _('templates')
...@@ -263,7 +267,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -263,7 +267,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('access_console', _('Can access the graphical console of a VM.')), ('access_console', _('Can access the graphical console of a VM.')),
('change_resources', _('Can change resources of a running VM.')), ('change_resources', _('Can change resources of a running VM.')),
('set_resources', _('Can change resources of a new VM.')), ('set_resources', _('Can change resources of a new VM.')),
('create_vm', _('Can create a new VM.')),
('config_ports', _('Can configure port forwards.')), ('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')),
) )
verbose_name = _('instance') verbose_name = _('instance')
verbose_name_plural = _('instances') verbose_name_plural = _('instances')
...@@ -574,11 +580,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -574,11 +580,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def get_connect_host(self, use_ipv6=False): def get_connect_host(self, use_ipv6=False):
"""Get public hostname. """Get public hostname.
""" """
if not self.interface_set.exclude(host=None): if not self.primary_host:
return _('None') return None
proto = 'ipv6' if use_ipv6 else 'ipv4' proto = 'ipv6' if use_ipv6 else 'ipv4'
return self.interface_set.exclude(host=None)[0].host.get_hostname( return self.primary_host.get_hostname(proto=proto)
proto=proto)
def get_connect_command(self, use_ipv6=False): def get_connect_command(self, use_ipv6=False):
"""Returns a formatted connect string. """Returns a formatted connect string.
...@@ -648,11 +653,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -648,11 +653,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
raise Node.DoesNotExist() raise Node.DoesNotExist()
def _is_notified_about_expiration(self): def _is_notified_about_expiration(self):
renews = self.activity_log.filter(activity_code__endswith='renew') last_activity = self.activity_log.latest('pk')
cond = {'activity_code__endswith': 'notification_about_expiration'} return (last_activity.activity_code ==
if len(renews) > 0: 'vm.Instance.notification_about_expiration')
cond['finished__gt'] = renews[0].started
return self.activity_log.filter(**cond).exists()
def notify_owners_about_expiration(self, again=False): def notify_owners_about_expiration(self, again=False):
"""Notify owners about vm expiring soon if they aren't already. """Notify owners about vm expiring soon if they aren't already.
...@@ -825,7 +828,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -825,7 +828,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def migrate_vm(self, to_node, timeout=120): def migrate_vm(self, to_node, timeout=120):
queue_name = self.get_remote_queue_name('vm', 'slow') queue_name = self.get_remote_queue_name('vm', 'slow')
return vm_tasks.migrate.apply_async(args=[self.vm_name, return vm_tasks.migrate.apply_async(args=[self.vm_name,
to_node.host.hostname], to_node.host.hostname,
True],
queue=queue_name queue=queue_name
).get(timeout=timeout) ).get(timeout=timeout)
...@@ -862,7 +866,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -862,7 +866,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
AbortableAsyncResult(remote.id).abort() AbortableAsyncResult(remote.id).abort()
raise Exception("Shutdown aborted by user.") raise Exception("Shutdown aborted by user.")
def suspend_vm(self, timeout=60): def suspend_vm(self, timeout=230):
queue_name = self.get_remote_queue_name('vm', 'slow') queue_name = self.get_remote_queue_name('vm', 'slow')
return vm_tasks.sleep.apply_async(args=[self.vm_name, return vm_tasks.sleep.apply_async(args=[self.vm_name,
self.mem_dump['path']], self.mem_dump['path']],
...@@ -929,8 +933,29 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -929,8 +933,29 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
user=user) user=user)
return acts return acts
def get_merged_activities(self, user=None):
whitelist = ("create_disk", "download_disk")
acts = self.get_activities(user)
merged_acts = []
latest = None
for a in acts:
if (latest == a.activity_code and
merged_acts[-1].result == a.result and
a.finished and merged_acts[-1].finished and
a.user == merged_acts[-1].user and
(merged_acts[-1].finished - a.finished).days < 7 and
not a.activity_code.endswith(whitelist)):
merged_acts[-1].times += 1
else:
merged_acts.append(a)
merged_acts[-1].times = 1
latest = a.activity_code
return merged_acts
def get_screenshot(self, timeout=5): def get_screenshot(self, timeout=5):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name("vm", "fast")
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)
...@@ -148,10 +148,14 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -148,10 +148,14 @@ class Node(OperatedMixin, TimeStampedModel):
self.enabled = False self.enabled = False
self.save() self.save()
def enable(self, user=None): def enable(self, user=None, base_activity=None):
''' Enable the node. ''' ''' Enable the node. '''
if self.enabled is not True: if self.enabled is not True:
with node_activity(code_suffix='enable', node=self, user=user): if base_activity:
act_ctx = base_activity.sub_activity('enable')
else:
act_ctx = node_activity('enable', node=self, user=user)
with act_ctx:
self.enabled = True self.enabled = True
self.save() self.save()
self.get_info(invalidate_cache=True) self.get_info(invalidate_cache=True)
......
...@@ -42,6 +42,7 @@ class InstanceOperation(Operation): ...@@ -42,6 +42,7 @@ class InstanceOperation(Operation):
acl_level = 'owner' acl_level = 'owner'
async_operation = abortable_async_instance_operation async_operation = abortable_async_instance_operation
host_cls = Instance host_cls = Instance
concurrency_check = True
def __init__(self, instance): def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance) super(InstanceOperation, self).__init__(subject=instance)
...@@ -73,7 +74,12 @@ class InstanceOperation(Operation): ...@@ -73,7 +74,12 @@ class InstanceOperation(Operation):
else: else:
return InstanceActivity.create( return InstanceActivity.create(
code_suffix=self.activity_code_suffix, instance=self.instance, code_suffix=self.activity_code_suffix, instance=self.instance,
user=user) user=user, concurrency_check=self.concurrency_check)
def is_preferred(self):
"""If this is the recommended op in the current state of the instance.
"""
return False
class AddInterfaceOperation(InstanceOperation): class AddInterfaceOperation(InstanceOperation):
...@@ -82,6 +88,7 @@ class AddInterfaceOperation(InstanceOperation): ...@@ -82,6 +88,7 @@ class AddInterfaceOperation(InstanceOperation):
name = _("add interface") name = _("add interface")
description = _("Add a new network interface for the specified VLAN to " description = _("Add a new network interface for the specified VLAN to "
"the VM.") "the VM.")
required_perms = ()
def _operation(self, activity, user, system, vlan, managed=None): def _operation(self, activity, user, system, vlan, managed=None):
if managed is None: if managed is None:
...@@ -104,6 +111,7 @@ class CreateDiskOperation(InstanceOperation): ...@@ -104,6 +111,7 @@ class CreateDiskOperation(InstanceOperation):
id = 'create_disk' id = 'create_disk'
name = _("create disk") name = _("create disk")
description = _("Create empty disk for the VM.") description = _("Create empty disk for the VM.")
required_perms = ('storage.create_empty_disk', )
def check_precond(self): def check_precond(self):
super(CreateDiskOperation, self).check_precond() super(CreateDiskOperation, self).check_precond()
...@@ -118,6 +126,7 @@ class CreateDiskOperation(InstanceOperation): ...@@ -118,6 +126,7 @@ class CreateDiskOperation(InstanceOperation):
if not name: if not name:
name = "new disk" name = "new disk"
disk = Disk.create(size=size, name=name, type="qcow2-norm") disk = Disk.create(size=size, name=name, type="qcow2-norm")
disk.full_clean()
self.instance.disks.add(disk) self.instance.disks.add(disk)
register_operation(CreateDiskOperation) register_operation(CreateDiskOperation)
...@@ -130,6 +139,7 @@ class DownloadDiskOperation(InstanceOperation): ...@@ -130,6 +139,7 @@ class DownloadDiskOperation(InstanceOperation):
description = _("Download disk for the VM.") description = _("Download disk for the VM.")
abortable = True abortable = True
has_percentage = True has_percentage = True
required_perms = ('storage.download_disk', )
def check_precond(self): def check_precond(self):
super(DownloadDiskOperation, self).check_precond() super(DownloadDiskOperation, self).check_precond()
...@@ -143,6 +153,7 @@ class DownloadDiskOperation(InstanceOperation): ...@@ -143,6 +153,7 @@ class DownloadDiskOperation(InstanceOperation):
from storage.models import Disk from storage.models import Disk
disk = Disk.download(url=url, name=name, task=task) disk = Disk.download(url=url, name=name, task=task)
disk.full_clean()
self.instance.disks.add(disk) self.instance.disks.add(disk)
register_operation(DownloadDiskOperation) register_operation(DownloadDiskOperation)
...@@ -153,6 +164,17 @@ class DeployOperation(InstanceOperation): ...@@ -153,6 +164,17 @@ class DeployOperation(InstanceOperation):
id = 'deploy' id = 'deploy'
name = _("deploy") name = _("deploy")
description = _("Deploy new virtual machine with network.") description = _("Deploy new virtual machine with network.")
required_perms = ()
def check_precond(self):
super(DeployOperation, self).check_precond()
if self.instance.status in ['RUNNING', 'SUSPENDED']:
raise self.instance.WrongStateError(self.instance)
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
self.instance.STATUS.PENDING,
self.instance.STATUS.ERROR)
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'RUNNING' activity.resultant_state = 'RUNNING'
...@@ -189,17 +211,19 @@ class DestroyOperation(InstanceOperation): ...@@ -189,17 +211,19 @@ class DestroyOperation(InstanceOperation):
id = 'destroy' id = 'destroy'
name = _("destroy") name = _("destroy")
description = _("Destroy virtual machine and its networks.") description = _("Destroy virtual machine and its networks.")
required_perms = ()
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'DESTROYED' activity.resultant_state = 'DESTROYED'
def _operation(self, activity): def _operation(self, activity):
if self.instance.node: # Destroy networks
# Destroy networks with activity.sub_activity('destroying_net'):
with activity.sub_activity('destroying_net'): if self.instance.node:
self.instance.shutdown_net() self.instance.shutdown_net()
self.instance.destroy_net() self.instance.destroy_net()
if self.instance.node:
# Delete virtual machine # Delete virtual machine
with activity.sub_activity('destroying_vm'): with activity.sub_activity('destroying_vm'):
self.instance.delete_vm() self.instance.delete_vm()
...@@ -230,21 +254,29 @@ class MigrateOperation(InstanceOperation): ...@@ -230,21 +254,29 @@ class MigrateOperation(InstanceOperation):
id = 'migrate' id = 'migrate'
name = _("migrate") name = _("migrate")
description = _("Live migrate running VM to another node.") description = _("Live migrate running VM to another node.")
required_perms = ()
def rollback(self, activity): def rollback(self, activity):
with activity.sub_activity('rollback_net'): with activity.sub_activity('rollback_net'):
self.instance.deploy_net() self.instance.deploy_net()
def check_precond(self):
super(MigrateOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
super(MigrateOperation, self).check_auth(user=user)
def _operation(self, activity, to_node=None, timeout=120): def _operation(self, activity, to_node=None, timeout=120):
if not to_node: if not to_node:
with activity.sub_activity('scheduling') as sa: with activity.sub_activity('scheduling') as sa:
to_node = self.instance.select_node() to_node = self.instance.select_node()
sa.result = to_node sa.result = to_node
# Shutdown networks
with activity.sub_activity('shutdown_net'):
self.instance.shutdown_net()
try: try:
with activity.sub_activity('migrate_vm'): with activity.sub_activity('migrate_vm'):
self.instance.migrate_vm(to_node=to_node, timeout=timeout) self.instance.migrate_vm(to_node=to_node, timeout=timeout)
...@@ -253,6 +285,10 @@ class MigrateOperation(InstanceOperation): ...@@ -253,6 +285,10 @@ class MigrateOperation(InstanceOperation):
self.rollback(activity) self.rollback(activity)
raise raise
# Shutdown networks
with activity.sub_activity('shutdown_net'):
self.instance.shutdown_net()
# Refresh node information # Refresh node information
self.instance.node = to_node self.instance.node = to_node
self.instance.save() self.instance.save()
...@@ -269,6 +305,12 @@ class RebootOperation(InstanceOperation): ...@@ -269,6 +305,12 @@ class RebootOperation(InstanceOperation):
id = 'reboot' id = 'reboot'
name = _("reboot") name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.") description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
required_perms = ()
def check_precond(self):
super(RebootOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, timeout=5): def _operation(self, timeout=5):
self.instance.reboot_vm(timeout=timeout) self.instance.reboot_vm(timeout=timeout)
...@@ -282,6 +324,7 @@ class RemoveInterfaceOperation(InstanceOperation): ...@@ -282,6 +324,7 @@ class RemoveInterfaceOperation(InstanceOperation):
id = 'remove_interface' id = 'remove_interface'
name = _("remove interface") name = _("remove interface")
description = _("Remove the specified network interface from the VM.") description = _("Remove the specified network interface from the VM.")
required_perms = ()
def _operation(self, activity, user, system, interface): def _operation(self, activity, user, system, interface):
if self.instance.is_running: if self.instance.is_running:
...@@ -299,6 +342,7 @@ class RemoveDiskOperation(InstanceOperation): ...@@ -299,6 +342,7 @@ class RemoveDiskOperation(InstanceOperation):
id = 'remove_disk' id = 'remove_disk'
name = _("remove disk") name = _("remove disk")
description = _("Remove the specified disk from the VM.") description = _("Remove the specified disk from the VM.")
required_perms = ()
def check_precond(self): def check_precond(self):
super(RemoveDiskOperation, self).check_precond() super(RemoveDiskOperation, self).check_precond()
...@@ -319,6 +363,12 @@ class ResetOperation(InstanceOperation): ...@@ -319,6 +363,12 @@ class ResetOperation(InstanceOperation):
id = 'reset' id = 'reset'
name = _("reset") name = _("reset")
description = _("Reset virtual machine (reset button).") description = _("Reset virtual machine (reset button).")
required_perms = ()
def check_precond(self):
super(ResetOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, timeout=5): def _operation(self, timeout=5):
self.instance.reset_vm(timeout=timeout) self.instance.reset_vm(timeout=timeout)
...@@ -336,6 +386,11 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -336,6 +386,11 @@ class SaveAsTemplateOperation(InstanceOperation):
Users can instantiate Virtual Machines from Templates. Users can instantiate Virtual Machines from Templates.
""") """)
abortable = True abortable = True
required_perms = ('vm.create_template', )
def is_preferred(self):
return (self.instance.is_base and
self.instance.status == self.instance.STATUS.RUNNING)
@staticmethod @staticmethod
def _rename(name): def _rename(name):
...@@ -348,10 +403,15 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -348,10 +403,15 @@ class SaveAsTemplateOperation(InstanceOperation):
return "%s v%d" % (name, v) return "%s v%d" % (name, v)
def on_abort(self, activity, error): def on_abort(self, activity, error):
if getattr(self, 'disks'): if hasattr(self, 'disks'):
for disk in self.disks: for disk in self.disks:
disk.destroy() disk.destroy()
def check_precond(self):
super(SaveAsTemplateOperation, self).check_precond()
if self.instance.status not in ['RUNNING', 'PENDING', 'STOPPED']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, activity, user, system, timeout=300, name=None, def _operation(self, activity, user, system, timeout=300, name=None,
with_shutdown=True, task=None, **kwargs): with_shutdown=True, task=None, **kwargs):
if with_shutdown: if with_shutdown:
...@@ -385,7 +445,7 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -385,7 +445,7 @@ class SaveAsTemplateOperation(InstanceOperation):
def __try_save_disk(disk): def __try_save_disk(disk):
try: try:
return disk.save_as() return disk.save_as(task)
except Disk.WrongDiskTypeError: except Disk.WrongDiskTypeError:
return disk return disk
...@@ -422,6 +482,7 @@ class ShutdownOperation(InstanceOperation): ...@@ -422,6 +482,7 @@ class ShutdownOperation(InstanceOperation):
name = _("shutdown") name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.") description = _("Shutdown virtual machine with ACPI signal.")
abortable = True abortable = True
required_perms = ()
def check_precond(self): def check_precond(self):
super(ShutdownOperation, self).check_precond() super(ShutdownOperation, self).check_precond()
...@@ -445,6 +506,12 @@ class ShutOffOperation(InstanceOperation): ...@@ -445,6 +506,12 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off' id = 'shut_off'
name = _("shut off") name = _("shut off")
description = _("Shut off VM (plug-out).") description = _("Shut off VM (plug-out).")
required_perms = ()
def check_precond(self):
super(ShutOffOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'STOPPED' activity.resultant_state = 'STOPPED'
...@@ -471,6 +538,11 @@ class SleepOperation(InstanceOperation): ...@@ -471,6 +538,11 @@ class SleepOperation(InstanceOperation):
id = 'sleep' id = 'sleep'
name = _("sleep") name = _("sleep")
description = _("Suspend virtual machine with memory dump.") description = _("Suspend virtual machine with memory dump.")
required_perms = ()
def is_preferred(self):
return (not self.instance.is_base and
self.instance.status == self.instance.STATUS.RUNNING)
def check_precond(self): def check_precond(self):
super(SleepOperation, self).check_precond() super(SleepOperation, self).check_precond()
...@@ -486,7 +558,7 @@ class SleepOperation(InstanceOperation): ...@@ -486,7 +558,7 @@ class SleepOperation(InstanceOperation):
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'SUSPENDED' activity.resultant_state = 'SUSPENDED'
def _operation(self, activity, timeout=60): def _operation(self, activity, timeout=240):
# Destroy networks # Destroy networks
with activity.sub_activity('shutdown_net'): with activity.sub_activity('shutdown_net'):
self.instance.shutdown_net() self.instance.shutdown_net()
...@@ -510,6 +582,11 @@ class WakeUpOperation(InstanceOperation): ...@@ -510,6 +582,11 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump. Power on Virtual Machine and load its memory from dump.
""") """)
required_perms = ()
def is_preferred(self):
return (self.instance.is_base and
self.instance.status == self.instance.STATUS.SUSPENDED)
def check_precond(self): def check_precond(self):
super(WakeUpOperation, self).check_precond() super(WakeUpOperation, self).check_precond()
...@@ -572,8 +649,22 @@ class FlushOperation(NodeOperation): ...@@ -572,8 +649,22 @@ class FlushOperation(NodeOperation):
id = 'flush' id = 'flush'
name = _("flush") name = _("flush")
description = _("Disable node and move all instances to other ones.") description = _("Disable node and move all instances to other ones.")
required_perms = ()
def on_abort(self, activity, error):
from manager.scheduler import TraitsUnsatisfiableException
if isinstance(error, TraitsUnsatisfiableException):
if self.node_enabled:
self.node.enable(activity.user, activity)
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
super(FlushOperation, self).check_auth(user=user)
def _operation(self, activity, user): def _operation(self, activity, user):
self.node_enabled = self.node.enabled
self.node.disable(user, activity) self.node.disable(user, activity)
for i in self.node.instance_set.all(): for i in self.node.instance_set.all():
with activity.sub_activity('migrate_instance_%d' % i.pk): with activity.sub_activity('migrate_instance_%d' % i.pk):
...@@ -589,14 +680,70 @@ class ScreenshotOperation(InstanceOperation): ...@@ -589,14 +680,70 @@ class ScreenshotOperation(InstanceOperation):
name = _("screenshot") name = _("screenshot")
description = _("Get screenshot") description = _("Get screenshot")
acl_level = "owner" acl_level = "owner"
required_perms = ()
def check_precond(self): def check_precond(self):
super(ScreenshotOperation, self).check_precond() super(ScreenshotOperation, self).check_precond()
if self.instance.status not in ['RUNNING']: if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance) raise self.instance.WrongStateError(self.instance)
def _operation(self, instance, user): def _operation(self):
return self.instance.get_screenshot(timeout=20) return self.instance.get_screenshot(timeout=20)
register_operation(ScreenshotOperation) register_operation(ScreenshotOperation)
class RecoverOperation(InstanceOperation):
activity_code_suffix = 'recover'
id = 'recover'
name = _("recover")
description = _("Recover virtual machine from destroyed state.")
acl_level = "owner"
required_perms = ('vm.recover', )
def check_precond(self):
if not self.instance.destroyed_at:
raise self.instance.WrongStateError(self.instance)
def on_commit(self, activity):
activity.resultant_state = 'PENDING'
def _operation(self):
for disk in self.instance.disks.all():
disk.destroyed = None
disk.restore()
disk.save()
self.instance.destroyed_at = None
self.instance.save()
register_operation(RecoverOperation)
class ResourcesOperation(InstanceOperation):
activity_code_suffix = 'Resources change'
id = 'resources_change'
name = _("resources change")
description = _("Change resources")
acl_level = "owner"
concurrency_check = False
required_perms = ('vm.change_resources', )
def check_precond(self):
super(ResourcesOperation, self).check_precond()
if self.instance.status not in ["STOPPED", "PENDING"]:
raise self.instance.WrongStateError(self.instance)
def _operation(self, user, num_cores, ram_size, max_ram_size, priority):
self.instance.num_cores = num_cores
self.instance.ram_size = ram_size
self.instance.max_ram_size = max_ram_size
self.instance.priority = priority
self.instance.full_clean()
self.instance.save()
register_operation(ResourcesOperation)
...@@ -24,7 +24,9 @@ from base64 import encodestring ...@@ -24,7 +24,9 @@ from base64 import encodestring
from StringIO import StringIO 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 celery.result import TimeoutError from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act, vm): def send_init_commands(instance, act, vm):
...@@ -86,12 +88,33 @@ def agent_started(vm, version=None): ...@@ -86,12 +88,33 @@ def agent_started(vm, version=None):
pass pass
if not initialized: if not initialized:
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'):
start_access_server.apply_async(queue=queue, args=(vm, )) start_access_server.apply_async(queue=queue, args=(vm, ))
def measure_boot_time(instance):
if not instance.template:
return
from vm.models import InstanceActivity
deploy_time = InstanceActivity.objects.filter(
instance=instance, activity_code="vm.Instance.deploy"
).latest("finished").finished
total_boot_time = (timezone.now() - deploy_time).total_seconds()
Client().send([
"template.%(pk)d.boot_time %(val)f %(time)s" % {
'pk': instance.template.pk,
'val': total_boot_time,
'time': time.time(),
}
])
@celery.task @celery.task
def agent_stopped(vm): def agent_stopped(vm):
from vm.models import Instance, InstanceActivity from vm.models import Instance, InstanceActivity
......
...@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase): ...@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance) inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = [] inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.models.instance.vm_tasks.migrate') as migr:
act = MagicMock() act = MagicMock()
...@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase): ...@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase):
inst = MagicMock(destroyed_at=None, spec=Instance) inst = MagicMock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = [] inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.models.instance.vm_tasks.migrate') as migr:
inst.select_node.side_effect = AssertionError inst.select_node.side_effect = AssertionError
...@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase): ...@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance) inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = [] inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
e = Exception('abc') e = Exception('abc')
setattr(e, 'libvirtError', '') setattr(e, 'libvirtError', '')
inst.migrate_vm.side_effect = e inst.migrate_vm.side_effect = e
...@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase): ...@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase):
node = MagicMock(spec=Node, enabled=True) node = MagicMock(spec=Node, enabled=True)
node.instance_set.all.return_value = insts node.instance_set.all.return_value = insts
user = MagicMock(spec=User) user = MagicMock(spec=User)
user.is_superuser = MagicMock(return_value=True)
flush_op = FlushOperation(node) flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act: with patch.object(FlushOperation, 'create_activity') as create_act:
...@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase): ...@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase):
node.disable.assert_called_with(user, act) node.disable.assert_called_with(user, act)
for i in insts: for i in insts:
i.migrate.assert_called() i.migrate.assert_called()
user.is_superuser.assert_called()
def test_flush_disabled_wo_user(self): def test_flush_disabled_wo_user(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()), insts = [MagicMock(spec=Instance, migrate=MagicMock()),
......
...@@ -31,3 +31,4 @@ simplejson==3.4.0 ...@@ -31,3 +31,4 @@ simplejson==3.4.0
six==1.6.1 six==1.6.1
South==0.8.4 South==0.8.4
sqlparse==0.1.11 sqlparse==0.1.11
pika==0.9.13
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