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 = (
'dashboard',
'manager',
'acl',
'monitor',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......
......@@ -20,7 +20,7 @@ from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
logger = getLogger(__name__)
......@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations.
"""
async_queue = 'localhost.man'
required_perms = ()
required_perms = None
do_not_call_in_templates = True
abortable = False
has_percentage = False
......@@ -141,6 +141,9 @@ class Operation(object):
pass
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):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
......
......@@ -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,
"model": "auth.group",
"fields": {
......
......@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
)
from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
......@@ -39,13 +40,16 @@ from django.template import Context
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey
from firewall.models import Vlan, Host
from storage.models import Disk
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 circle.settings.base import LANGUAGES
from django.utils.translation import string_concat
......@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True)
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:
self.instance.priority = 20
self.instance.ram_size = 512
......@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk)
return self.user
def clean_raw_data(self):
# if raw_data has changed and the user is not superuser
if "raw_data" in self.changed_data and not self.user.is_superuser:
old_raw_data = InstanceTemplate.objects.get(
pk=self.instance.pk).raw_data
return old_raw_data
else:
return self.cleaned_data['raw_data']
def _clean_fields(self):
try:
old = InstanceTemplate.objects.get(pk=self.instance.pk)
except InstanceTemplate.DoesNotExist:
old = None
for name, field in self.fields.items():
if name in self.allowed_fields:
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):
data = self.cleaned_data
......@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm):
networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True)
for m in data['networks']:
if not m.has_level(self.user, "user"):
raise PermissionDenied()
if m.pk not in networks:
InterfaceTemplate(vlan=m, managed=m.managed,
template=self.instance).save()
......@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm):
@property
def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper()
helper.layout = Layout(
Field("name"),
......@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm):
_("Virtual machine settings"),
Field('access_method'),
Field('boot_menu'),
Field('raw_data', **kwargs_raw_data),
Field('raw_data'),
Field('req_traits'),
Field('description'),
Field("parent", type="hidden"),
......@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form):
@property
def helper(self):
helper = FormHelper(self)
helper.add_input(Submit("submit", _("Create"),
css_class="btn btn-success"))
helper.form_tag = False
return helper
......@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm):
if self.user:
self.instance.user = self.user
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):
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):
ACL_LEVELS = (
('operator', _('operator')),
......@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g))
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', [])
for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]):
......
......@@ -192,6 +192,9 @@
},
mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') {
......
......@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] {
}
<<<<<<< HEAD
#store-list-list {
list-style: none;
}
......@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] {
.no-hover:hover {
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) {
$('body').animate({scrollTop: 0});
div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>';
$('.messagelist').html('').append(div);
$('.messagelist div').fadeIn();
var div = $('.messagelist div').fadeIn();
setTimeout(function() {
$(div).fadeOut();
}, 9000);
}
......
......@@ -30,4 +30,56 @@ $(function() {
});
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() {
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0);
checkNewActivity(false, 1);
});
/* save resources */
$('#vm-details-resources-save').click(function() {
$('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin");
var vm = $(this).data("vm");
$.ajax({
type: 'POST',
url: location.href,
url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) {
addMessage(data['message'], 'success');
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
$('a[href="#activity"]').trigger("click");
},
error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
......@@ -328,6 +329,17 @@ function decideActivityRefresh() {
return check;
}
/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
function unescapeHTML(html) {
return html.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&ndash;/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/&#39;/g, "'").replace(/ /g, '');
}
/* the html page contains some tags that were modified via js (titles for example), we delete these
also some html tags are closed with / */
function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
}
function checkNewActivity(only_status, runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
......@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) {
data: {'only_status': only_status},
success: function(data) {
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']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
}
......@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data['status'] == "STOPPED") {
$(".enabled-when-stopped").prop("disabled", false);
$(".hide-when-stopped").hide();
} else {
$(".enabled-when-stopped").prop("disabled", true);
$(".hide-when-stopped").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_status, runs + 1)},
......
......@@ -9,8 +9,7 @@
<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-migrate-1.2.1.min.js"></script>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<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>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
......
......@@ -14,7 +14,12 @@
</h3>
</div>
<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>
{% endblock %}
......@@ -3,7 +3,11 @@
<div class="modal-dialog">
<div class="modal-content">
<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>
<div class="clearfix"></div>
......
......@@ -16,10 +16,12 @@
<div class="clearfix"></div>
</div>
{% endfor %}
{% if perms.vm.create_base_template %}
<div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="base_vm"/>
{% trans "Create a new base VM without disk" %}
</div>
{% endif %}
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></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 @@
{% crispy group_profile_form %}
</form>
<hr />
<h3>{% trans "User list"|capfirst %}
{% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a>
......@@ -71,23 +73,37 @@
</td>
</tr>
{% 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>
<td><i class="icon-plus"></i></td>
<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>
</tr>
</tbody>
</table>
<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">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
<h3 id="group-detail-perm-header">{% trans "Permissions"|capfirst %}</h3>
<hr />
<h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table">
<thead>
......@@ -158,11 +174,25 @@
</div>
</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>
<script src="{{ STATIC_URL}}dashboard/group-details.js"></script>
{% endblock %}
......@@ -3,8 +3,8 @@
{% block question %}
<p>
{% blocktrans with obj=object op=op.name %}
Do you want to do the following operation on {{obj}}:
{% blocktrans with obj=object url=object.get_absolute_url op=op.name %}
Do you want to do the following operation on <a href="{{url}}">{{obj}}</a>:
<strong>{{op}}</strong>?
{% endblocktrans %}
</p>
......@@ -19,6 +19,8 @@ Do you want to do the following operation on {{obj}}:
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}"
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>
</form>
......@@ -80,9 +80,9 @@
{% endif %}
</dd>
{% if instance.ipv6 %}
{% if instance.ipv6 and instance.get_connect_port %}
<dt>{% trans "Host (IPv6)" %}</dt>
<dd>{{ ipv6_host }}:<strong>{{ instance.ipv6_port }}</strong></dd>
<dd>{{ ipv6_host }}:<strong>{{ ipv6_port }}</strong></dd>
{% endif %}
<dt>{% trans "Username" %}</dt>
......@@ -90,7 +90,8 @@
<dt>{% trans "Password" %}</dt>
<dd>
<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">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span>
......@@ -112,7 +113,7 @@
<div class="input-group" id="dashboard-vm-details-connect-command">
<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 %}
{% trans "Connection is not possible." %}{% endif %}"
id="vm-details-connection-string" class="form-control input-tags" />
......
......@@ -6,16 +6,18 @@
</span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% 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 %}
{% if a.has_percent %}
- {{ a.percentage }}%
{% endif %}
</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 %}">
{% include "dashboard/_display-name.html" with user=a.user show_org=True %}
</a>
{% endif %}
{% if a.has_percent %}
{{ a.percentage }}%
{% endif %}
{% if a.is_abortable_for_user %}
<form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right">
{% 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 @@
{% for op in ops %}
{% 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>
<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>
{% endif %}
{% endif %}
{% endfor %}
{% load i18n %}
<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="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>
</div>
{% if perms.vm.access_console %}
<div class="alert alert-info" id="noVNC_status">
</div>
{% endif %}
<div id="vm-console-screenshot">
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
......@@ -14,6 +18,7 @@
<hr />
</div>
{% if perms.vm.access_console %}
<canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported.
</canvas>
......@@ -22,3 +27,4 @@
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
</script>
{% endif %}
......@@ -47,7 +47,12 @@
<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 }}
{% 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 }}">
{% trans "remove" %}
</a>
......@@ -63,6 +68,8 @@
<dd>
{% for g in i.host.groups.all %}
{{ g }}{% if not forloop.last %},{% endif %}
{% empty %}
-
{% endfor %}
</dd>
</dl>
......@@ -92,7 +99,7 @@
{% if l.ipv4 %}
<tr>
<td>
{% display_portforward l %}
{% display_portforward4 l %}
</td>
<td><i class="icon-long-arrow-right"></i></td>
<td>
......@@ -124,7 +131,7 @@
{% if l.ipv6 %}
<tr>
<td>
{% display_portforward l %}
{% display_portforward6 l %}
</td>
<td><i class="icon-long-arrow-right"></i></td>
<td>
......
......@@ -33,11 +33,20 @@
</div>
</p>
{% if can_change_resources %}
<p class="row">
<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>
</p>
{% endif %}
</form>
<hr />
......@@ -47,18 +56,9 @@
<h3>
{% trans "Disks" %}
<div class="pull-right">
{% if op.download_disk %}
<a href="{{op.download_disk.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.download_disk.op}} btn btn-default">
<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 id="disk-ops">
{% include "dashboard/vm-detail/_disk-operations.html" %}
</div>
</div>
</h3>
......@@ -77,6 +77,32 @@
</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 %}
<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>
</a>
<a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
......
......@@ -6,10 +6,18 @@ register = template.Library()
LINKABLE_PORTS = {80: "http", 8080: "http", 443: "https", 21: "ftp"}
@register.simple_tag(name="display_portforward")
def display_pf(ports):
is_ipv6 = "ipv6" in ports
data = ports["ipv6" if is_ipv6 else "ipv4"]
@register.simple_tag(name="display_portforward4")
def display_pf4(ports):
return display_pf(ports, '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():
href = "%s:%d" % (data['host'], data['port'])
......
......@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1})
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \
......@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert msg.error.called
def test_migrate_template(self):
request = FakeRequestFactory()
request = FakeRequestFactory(superuser=True)
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go:
......@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render().status_code, 200)
def test_save_as_wo_name(self):
request = FakeRequestFactory(POST={})
request = FakeRequestFactory(POST={}, has_perms_mock=True)
view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go, \
......@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called
def test_save_as_template(self):
request = FakeRequestFactory()
request = FakeRequestFactory(has_perms_mock=True)
view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go:
......@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs):
user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False)
if kwargs.get('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True)
request = HttpRequest()
request.user = user
......
......@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase):
self.g1.user_set.add(self.u1)
self.g1.user_set.add(self.u2)
self.g1.save()
self.u1.user_permissions.add(Permission.objects.get(
codename='create_vm'))
settings["default_vlangroup"] = 'public'
VlanGroup.objects.create(name='public')
......@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'operator')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200)
......@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'user')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403)
......
......@@ -28,9 +28,10 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate,
TemplateChoose,
UserCreationView,
......@@ -38,7 +39,9 @@ from .views import (
ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate,
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(
......@@ -83,14 +86,16 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
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(),
name='dashboard.views.vm-renew'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_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/(?P<pk>\d+)/$', NodeDetailView.as_view(),
......@@ -156,11 +161,17 @@ urlpatterns = patterns(
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(),
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(),
name='dashboard.views.group-create'),
url(r'^group/(?P<group_pk>\d+)/create/$',
UserCreationView.as_view(),
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+)/$',
UserKeyDelete.as_view(),
......
......@@ -18,6 +18,7 @@
from __future__ import unicode_literals, absolute_import
from collections import OrderedDict
from itertools import chain
from os import getenv
import os
......@@ -67,6 +68,7 @@ from .forms import (
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, UserKeyForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm
)
from .tables import (
......@@ -79,7 +81,7 @@ from vm.models import (
)
from storage.models import Disk
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
......@@ -259,6 +261,8 @@ class VmDetailVncTokenView(CheckedDetailView):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if not request.user.has_perm('vm.access_console'):
raise PermissionDenied()
if self.object.node:
with instance_activity(code_suffix='console-accessed',
instance=self.object, user=request.user,
......@@ -289,7 +293,8 @@ class VmDetailView(CheckedDetailView):
})
# 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(
'user', self.request.user
......@@ -303,13 +308,19 @@ class VmDetailView(CheckedDetailView):
# ipv6 infos
context['ipv6_host'] = instance.get_connect_host(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
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 = {
'change_password': self.__change_password,
'new_name': self.__set_name,
......@@ -337,33 +348,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail",
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):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
......@@ -515,10 +499,27 @@ class VmDetailView(CheckedDetailView):
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):
template_name = 'dashboard/operate.html'
show_in_toolbar = True
effect = None
@property
def name(self):
......@@ -528,6 +529,9 @@ class OperationView(DetailView):
def description(self):
return self.get_op().description
def is_preferred(self):
return self.get_op().is_preferred()
@classmethod
def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op
......@@ -535,11 +539,11 @@ class OperationView(DetailView):
def get_url(self):
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():
return 'dashboard/_modal.html'
return ['dashboard/_modal.html']
else:
return 'dashboard/_base.html'
return ['dashboard/_base.html']
@classmethod
def get_op_by_object(cls, obj):
......@@ -553,16 +557,14 @@ class OperationView(DetailView):
def get_context_data(self, **kwargs):
ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op()
ctx['opview'] = self
ctx['url'] = self.request.path
ctx['template'] = super(OperationView, self).get_template_names()[0]
return ctx
def get(self, request, *args, **kwargs):
self.get_op().check_auth(request.user)
response = super(OperationView, self).get(request, *args, **kwargs)
response.render()
response.content = render_to_string(self.get_wrapper_template_name(),
{'body': response.content})
return response
return super(OperationView, self).get(request, *args, **kwargs)
def post(self, request, extra=None, *args, **kwargs):
self.object = self.get_object()
......@@ -576,14 +578,16 @@ class OperationView(DetailView):
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
def factory(cls, op, icon='cog'):
def factory(cls, op, icon='cog', effect='info'):
return type(str(cls.__name__ + op),
(cls, ), {'op': op, 'icon': icon})
(cls, ), {'op': op, 'icon': icon, 'effect': effect})
@classmethod
def bind_to_object(cls, instance):
def bind_to_object(cls, instance, **kwargs):
v = cls()
v.get_object = lambda: instance
for key, value in kwargs.iteritems():
setattr(v, key, value)
return v
......@@ -592,6 +596,20 @@ class VmOperationView(OperationView):
model = Instance
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):
......@@ -611,8 +629,15 @@ class FormOperationMixin(object):
form = self.form_class(self.request.POST)
if form.is_valid():
extra.update(form.cleaned_data)
return super(FormOperationMixin, self).post(
resp = super(FormOperationMixin, self).post(
request, extra, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({'success': True}),
content_type="application=json"
)
else:
return resp
else:
return self.get(request)
......@@ -623,6 +648,7 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView):
form_class = VmCreateDiskForm
show_in_toolbar = False
icon = 'hdd'
is_disk_operation = True
class VmDownloadDiskView(FormOperationMixin, VmOperationView):
......@@ -631,12 +657,14 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
form_class = VmDownloadDiskForm
show_in_toolbar = False
icon = 'download'
is_disk_operation = True
class VmMigrateView(VmOperationView):
op = 'migrate'
icon = 'truck'
effect = 'info'
template_name = 'dashboard/_vm-migrate.html'
def get_context_data(self, **kwargs):
......@@ -659,22 +687,56 @@ class VmSaveView(FormOperationMixin, VmOperationView):
op = 'save_as_template'
icon = 'save'
effect = 'info'
form_class = VmSaveForm
vm_ops = {
'reset': VmOperationView.factory(op='reset', icon='bolt'),
'deploy': VmOperationView.factory(op='deploy', icon='play'),
'migrate': VmMigrateView,
'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
'save_as_template': VmSaveView,
'destroy': VmOperationView.factory(op='destroy', icon='remove'),
'sleep': VmOperationView.factory(op='sleep', icon='moon'),
'wake_up': VmOperationView.factory(op='wake_up', icon='sun'),
'create_disk': VmCreateDiskView,
'download_disk': VmDownloadDiskView,
}
class VmResourcesChangeView(VmOperationView):
op = 'resources_change'
icon = "save"
show_in_toolbar = False
def post(self, request, extra=None, *args, **kwargs):
if extra is None:
extra = {}
resources = {
'num_cores': "cpu-count",
'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):
......@@ -684,9 +746,11 @@ def get_operations(instance, user):
op = v.get_op_by_object(instance)
op.check_auth(user)
op.check_precond()
except Exception as e:
except PermissionDenied as e:
logger.debug('Not showing operation %s for %s: %s',
k, instance, unicode(e))
except Exception:
ops.append(v.bind_to_object(instance, disabled=True))
else:
ops.append(v.bind_to_object(instance))
return ops
......@@ -773,15 +837,23 @@ class GroupDetailView(CheckedDetailView):
context = super(GroupDetailView, self).get_context_data(**kwargs)
context['group'] = self.object
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['group_profile_form'] = GroupProfileUpdate.get_form_object(
self.request, self.object.profile)
if self.request.user.is_superuser:
context['group_perm_form'] = GroupPermissionForm(
instance=self.object)
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.get_has_level()(request.user, 'operator'):
raise PermissionDenied()
if request.POST.get('new_name'):
return self.__set_name(request)
if request.POST.get('list-new-name'):
......@@ -803,10 +875,14 @@ class GroupDetailView(CheckedDetailView):
if not name:
return
try:
entity = User.objects.get(username=name)
entity = search_user(name)
self.object.user_set.add(entity)
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):
if not self.get_has_level()(request.user, 'operator'):
......@@ -839,6 +915,17 @@ class GroupDetailView(CheckedDetailView):
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):
def post(self, request, *args, **kwargs):
......@@ -892,7 +979,7 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
if not name:
return
try:
entity = User.objects.get(username=name)
entity = search_user(name)
except User.DoesNotExist:
entity = None
try:
......@@ -956,7 +1043,7 @@ class GroupAclUpdateView(AclUpdateView):
kwargs=self.kwargs))
class TemplateChoose(TemplateView):
class TemplateChoose(LoginRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
......@@ -989,6 +1076,9 @@ class TemplateChoose(TemplateView):
else:
template = get_object_or_404(InstanceTemplate, pk=template)
if not template.has_level(request.user, "user"):
raise PermissionDenied()
instance = Instance.create_from_template(
template=template, owner=request.user, is_base=True)
......@@ -1016,7 +1106,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return context
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()
return super(TemplateCreate, self).get(*args, **kwargs)
......@@ -1027,7 +1117,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return 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()
form = self.form_class(request.POST, user=request.user)
......@@ -1049,8 +1139,6 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return redirect("%s#resources" % inst.get_absolute_url())
return super(TemplateCreate, self).post(self, request, args, kwargs)
def __create_networks(self, vlans, user):
networks = []
for v in vlans:
......@@ -1111,12 +1199,6 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
template = self.get_object()
if not template.has_level(request.user, 'owner'):
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)
def get_form_kwargs(self):
......@@ -1325,6 +1407,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
slug_field = 'pk'
slug_url_kwarg = 'group_pk'
read_level = 'operator'
member_key = 'member_pk'
def get_has_level(self):
return self.object.profile.has_level
......@@ -1366,7 +1449,7 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
object = self.get_object()
if not object.profile.has_level(request.user, 'operator'):
raise PermissionDenied()
self.remove_member(kwargs["member_pk"])
self.remove_member(kwargs[self.member_key])
success_url = self.get_success_url()
success_message = self.get_success_message()
if request.is_ajax():
......@@ -1379,6 +1462,31 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
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):
def remove_member(self, pk):
......@@ -1464,6 +1572,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
return ['dashboard/nojs-wrapper.html']
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
template = (form.template.pk if form_error
else request.GET.get("template"))
......@@ -1569,6 +1680,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
def post(self, request, *args, **kwargs):
user = request.user
if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied()
# limit chekcs
try:
limit = user.profile.instance_limit
......@@ -2110,7 +2224,7 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
@require_GET
def vm_activity(request, 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()
response = {}
......@@ -2122,7 +2236,7 @@ def vm_activity(request, pk):
if only_status == "false": # instance activity
context = {
'instance': instance,
'activities': instance.get_activities(request.user),
'activities': instance.get_merged_activities(request.user),
'ops': get_operations(instance, request.user),
}
......@@ -2134,6 +2248,10 @@ def vm_activity(request, pk):
"dashboard/vm-detail/_operations.html",
RequestContext(request, context),
)
response['disk_ops'] = render_to_string(
"dashboard/vm-detail/_disk-operations.html",
RequestContext(request, context),
)
return HttpResponse(
json.dumps(response),
......@@ -2811,8 +2929,7 @@ class InterfaceDeleteView(DeleteView):
def get_vm_screenshot(request, pk):
instance = get_object_or_404(Instance, pk=pk)
try:
image = instance.screenshot(instance=instance,
user=request.user).getvalue()
image = instance.screenshot(user=request.user).getvalue()
except:
# TODO handle this better
raise Http404()
......
......@@ -28,6 +28,7 @@ import re
alfanum_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]+)$')
reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
......@@ -216,12 +217,23 @@ def is_valid_domain(value):
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):
"""Validate whether the parameter is a valid domin name."""
if not is_valid_domain(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):
"""Check whether the parameter is a valid reverse domain name."""
return reverse_domain_re.match(value) is not None
......
......@@ -27,6 +27,7 @@ from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
val_ipv6_template, val_domain, val_ipv4,
val_domain_wildcard,
val_ipv6, val_mx, convert_ipv4_to_ipv6,
IPNetworkField, IPAddressField)
from django.core.validators import MinValueValidator, MaxValueValidator
......@@ -695,8 +696,7 @@ class Host(models.Model):
:param private: Port number of host in subject.
"""
self.rules.filter(owner=self.owner, proto=proto, host=self,
dport=private).delete()
self.rules.filter(proto=proto, dport=private).delete()
def get_hostname(self, proto, public=True):
"""
......@@ -728,7 +728,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set.
"""
retval = []
for rule in self.rules.filter(owner=self.owner):
for rule in self.rules.all():
forward = {
'proto': rule.proto,
'private': rule.dport,
......@@ -770,9 +770,7 @@ class Host(models.Model):
if public_port else
None)
# IPv6
blocked = self.incoming_rules.exclude(
action='accept').filter(dport=port, proto=protocol).exists()
endpoints['ipv6'] = (self.ipv6, port) if not blocked else None
endpoints['ipv6'] = (self.ipv6, port) if public_port else None
return endpoints
@models.permalink
......@@ -821,7 +819,7 @@ class Domain(models.Model):
class Record(models.Model):
CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'),
('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'))
domain = models.ForeignKey('Domain', verbose_name=_('domain'))
host = models.ForeignKey('Host', blank=True, null=True,
......
......@@ -30,7 +30,9 @@ celery = Celery('manager',
'vm.tasks.local_agent_tasks',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', ])
'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
......@@ -69,6 +71,24 @@ celery.conf.update(
'schedule': timedelta(hours=24),
'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):
ordering = ['name']
verbose_name = _('disk')
verbose_name_plural = _('disks')
permissions = (
('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception):
......@@ -131,6 +134,7 @@ class Disk(AclBase, TimeStampedModel):
self.disk = disk
class DiskIsNotReady(Exception):
""" Exception for operations that need a deployed disk.
"""
......@@ -380,13 +384,18 @@ class Disk(AclBase, TimeStampedModel):
self.save()
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.
"""
# TODO
pass
def save_as(self, user=None, task_uuid=None, timeout=300):
queue_name = self.datastore.get_remote_queue_name(
'storage', priority='slow')
logger.info("Image: %s at Datastore: %s recovered from trash." %
(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.
Based on disk type:
......@@ -421,10 +430,18 @@ class Disk(AclBase, TimeStampedModel):
type=new_type)
queue_name = self.get_remote_queue_name("storage", priority="slow")
storage_tasks.merge.apply_async(args=[self.get_disk_desc(),
disk.get_disk_desc()],
queue=queue_name
).get() # Timeout
disk.is_ready = True
disk.save()
remote = storage_tasks.merge.apply_async(kwargs={
"old_json": self.get_disk_desc(),
"new_json": disk.get_disk_desc()},
queue=queue_name
) # Timeout
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
......@@ -73,6 +73,11 @@ def move_to_trash(datastore, disk_path):
pass
@celery.task(name='storagedriver.recover_from_trash')
def recover_from_trash(datastore, disk_path):
pass
@celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path):
pass
......@@ -11,8 +11,14 @@ class Migration(SchemaMigration):
# Removing unique constraint on 'InstanceTemplate', fields ['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):
# 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']
db.create_unique(u'vm_instancetemplate', ['name'])
......@@ -131,6 +137,7 @@ class Migration(SchemaMigration):
'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'}),
......@@ -147,6 +154,7 @@ class Migration(SchemaMigration):
'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'}),
......@@ -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):
ordering = ('name', )
permissions = (
('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_plural = _('templates')
......@@ -263,7 +267,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('access_console', _('Can access the graphical console of a VM.')),
('change_resources', _('Can change resources of a running VM.')),
('set_resources', _('Can change resources of a new VM.')),
('create_vm', _('Can create a new VM.')),
('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')),
)
verbose_name = _('instance')
verbose_name_plural = _('instances')
......@@ -574,11 +580,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def get_connect_host(self, use_ipv6=False):
"""Get public hostname.
"""
if not self.interface_set.exclude(host=None):
return _('None')
if not self.primary_host:
return None
proto = 'ipv6' if use_ipv6 else 'ipv4'
return self.interface_set.exclude(host=None)[0].host.get_hostname(
proto=proto)
return self.primary_host.get_hostname(proto=proto)
def get_connect_command(self, use_ipv6=False):
"""Returns a formatted connect string.
......@@ -648,11 +653,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
raise Node.DoesNotExist()
def _is_notified_about_expiration(self):
renews = self.activity_log.filter(activity_code__endswith='renew')
cond = {'activity_code__endswith': 'notification_about_expiration'}
if len(renews) > 0:
cond['finished__gt'] = renews[0].started
return self.activity_log.filter(**cond).exists()
last_activity = self.activity_log.latest('pk')
return (last_activity.activity_code ==
'vm.Instance.notification_about_expiration')
def notify_owners_about_expiration(self, again=False):
"""Notify owners about vm expiring soon if they aren't already.
......@@ -825,7 +828,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def migrate_vm(self, to_node, timeout=120):
queue_name = self.get_remote_queue_name('vm', 'slow')
return vm_tasks.migrate.apply_async(args=[self.vm_name,
to_node.host.hostname],
to_node.host.hostname,
True],
queue=queue_name
).get(timeout=timeout)
......@@ -862,7 +866,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
AbortableAsyncResult(remote.id).abort()
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')
return vm_tasks.sleep.apply_async(args=[self.vm_name,
self.mem_dump['path']],
......@@ -929,8 +933,29 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
user=user)
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):
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],
queue=queue_name
).get(timeout=timeout)
......@@ -148,10 +148,14 @@ class Node(OperatedMixin, TimeStampedModel):
self.enabled = False
self.save()
def enable(self, user=None):
def enable(self, user=None, base_activity=None):
''' Enable the node. '''
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.save()
self.get_info(invalidate_cache=True)
......
......@@ -42,6 +42,7 @@ class InstanceOperation(Operation):
acl_level = 'owner'
async_operation = abortable_async_instance_operation
host_cls = Instance
concurrency_check = True
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -73,7 +74,12 @@ class InstanceOperation(Operation):
else:
return InstanceActivity.create(
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):
......@@ -82,6 +88,7 @@ class AddInterfaceOperation(InstanceOperation):
name = _("add interface")
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
required_perms = ()
def _operation(self, activity, user, system, vlan, managed=None):
if managed is None:
......@@ -104,6 +111,7 @@ class CreateDiskOperation(InstanceOperation):
id = 'create_disk'
name = _("create disk")
description = _("Create empty disk for the VM.")
required_perms = ('storage.create_empty_disk', )
def check_precond(self):
super(CreateDiskOperation, self).check_precond()
......@@ -118,6 +126,7 @@ class CreateDiskOperation(InstanceOperation):
if not name:
name = "new disk"
disk = Disk.create(size=size, name=name, type="qcow2-norm")
disk.full_clean()
self.instance.disks.add(disk)
register_operation(CreateDiskOperation)
......@@ -130,6 +139,7 @@ class DownloadDiskOperation(InstanceOperation):
description = _("Download disk for the VM.")
abortable = True
has_percentage = True
required_perms = ('storage.download_disk', )
def check_precond(self):
super(DownloadDiskOperation, self).check_precond()
......@@ -143,6 +153,7 @@ class DownloadDiskOperation(InstanceOperation):
from storage.models import Disk
disk = Disk.download(url=url, name=name, task=task)
disk.full_clean()
self.instance.disks.add(disk)
register_operation(DownloadDiskOperation)
......@@ -153,6 +164,17 @@ class DeployOperation(InstanceOperation):
id = 'deploy'
name = _("deploy")
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):
activity.resultant_state = 'RUNNING'
......@@ -189,17 +211,19 @@ class DestroyOperation(InstanceOperation):
id = 'destroy'
name = _("destroy")
description = _("Destroy virtual machine and its networks.")
required_perms = ()
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
def _operation(self, activity):
if self.instance.node:
# Destroy networks
with activity.sub_activity('destroying_net'):
# Destroy networks
with activity.sub_activity('destroying_net'):
if self.instance.node:
self.instance.shutdown_net()
self.instance.destroy_net()
self.instance.destroy_net()
if self.instance.node:
# Delete virtual machine
with activity.sub_activity('destroying_vm'):
self.instance.delete_vm()
......@@ -230,21 +254,29 @@ class MigrateOperation(InstanceOperation):
id = 'migrate'
name = _("migrate")
description = _("Live migrate running VM to another node.")
required_perms = ()
def rollback(self, activity):
with activity.sub_activity('rollback_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):
if not to_node:
with activity.sub_activity('scheduling') as sa:
to_node = self.instance.select_node()
sa.result = to_node
# Shutdown networks
with activity.sub_activity('shutdown_net'):
self.instance.shutdown_net()
try:
with activity.sub_activity('migrate_vm'):
self.instance.migrate_vm(to_node=to_node, timeout=timeout)
......@@ -253,6 +285,10 @@ class MigrateOperation(InstanceOperation):
self.rollback(activity)
raise
# Shutdown networks
with activity.sub_activity('shutdown_net'):
self.instance.shutdown_net()
# Refresh node information
self.instance.node = to_node
self.instance.save()
......@@ -269,6 +305,12 @@ class RebootOperation(InstanceOperation):
id = 'reboot'
name = _("reboot")
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):
self.instance.reboot_vm(timeout=timeout)
......@@ -282,6 +324,7 @@ class RemoveInterfaceOperation(InstanceOperation):
id = 'remove_interface'
name = _("remove interface")
description = _("Remove the specified network interface from the VM.")
required_perms = ()
def _operation(self, activity, user, system, interface):
if self.instance.is_running:
......@@ -299,6 +342,7 @@ class RemoveDiskOperation(InstanceOperation):
id = 'remove_disk'
name = _("remove disk")
description = _("Remove the specified disk from the VM.")
required_perms = ()
def check_precond(self):
super(RemoveDiskOperation, self).check_precond()
......@@ -319,6 +363,12 @@ class ResetOperation(InstanceOperation):
id = 'reset'
name = _("reset")
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):
self.instance.reset_vm(timeout=timeout)
......@@ -336,6 +386,11 @@ class SaveAsTemplateOperation(InstanceOperation):
Users can instantiate Virtual Machines from Templates.
""")
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
def _rename(name):
......@@ -348,10 +403,15 @@ class SaveAsTemplateOperation(InstanceOperation):
return "%s v%d" % (name, v)
def on_abort(self, activity, error):
if getattr(self, 'disks'):
if hasattr(self, 'disks'):
for disk in self.disks:
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,
with_shutdown=True, task=None, **kwargs):
if with_shutdown:
......@@ -385,7 +445,7 @@ class SaveAsTemplateOperation(InstanceOperation):
def __try_save_disk(disk):
try:
return disk.save_as()
return disk.save_as(task)
except Disk.WrongDiskTypeError:
return disk
......@@ -422,6 +482,7 @@ class ShutdownOperation(InstanceOperation):
name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.")
abortable = True
required_perms = ()
def check_precond(self):
super(ShutdownOperation, self).check_precond()
......@@ -445,6 +506,12 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off'
name = _("shut off")
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):
activity.resultant_state = 'STOPPED'
......@@ -471,6 +538,11 @@ class SleepOperation(InstanceOperation):
id = 'sleep'
name = _("sleep")
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):
super(SleepOperation, self).check_precond()
......@@ -486,7 +558,7 @@ class SleepOperation(InstanceOperation):
def on_commit(self, activity):
activity.resultant_state = 'SUSPENDED'
def _operation(self, activity, timeout=60):
def _operation(self, activity, timeout=240):
# Destroy networks
with activity.sub_activity('shutdown_net'):
self.instance.shutdown_net()
......@@ -510,6 +582,11 @@ class WakeUpOperation(InstanceOperation):
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):
super(WakeUpOperation, self).check_precond()
......@@ -572,8 +649,22 @@ class FlushOperation(NodeOperation):
id = 'flush'
name = _("flush")
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):
self.node_enabled = self.node.enabled
self.node.disable(user, activity)
for i in self.node.instance_set.all():
with activity.sub_activity('migrate_instance_%d' % i.pk):
......@@ -589,14 +680,70 @@ class ScreenshotOperation(InstanceOperation):
name = _("screenshot")
description = _("Get screenshot")
acl_level = "owner"
required_perms = ()
def check_precond(self):
super(ScreenshotOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, instance, user):
def _operation(self):
return self.instance.get_screenshot(timeout=20)
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
from StringIO import StringIO
from tarfile import TarFile, TarInfo
from django.conf import settings
from django.utils import timezone
from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act, vm):
......@@ -86,12 +88,33 @@ def agent_started(vm, version=None):
pass
if not initialized:
measure_boot_time(instance)
send_init_commands(instance, act, vm)
with act.sub_activity('start_access_server'):
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
def agent_stopped(vm):
from vm.models import Instance, InstanceActivity
......
......@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr:
act = MagicMock()
......@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase):
inst = MagicMock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr:
inst.select_node.side_effect = AssertionError
......@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
e = Exception('abc')
setattr(e, 'libvirtError', '')
inst.migrate_vm.side_effect = e
......@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase):
node = MagicMock(spec=Node, enabled=True)
node.instance_set.all.return_value = insts
user = MagicMock(spec=User)
user.is_superuser = MagicMock(return_value=True)
flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act:
......@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase):
node.disable.assert_called_with(user, act)
for i in insts:
i.migrate.assert_called()
user.is_superuser.assert_called()
def test_flush_disabled_wo_user(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()),
......
......@@ -31,3 +31,4 @@ simplejson==3.4.0
six==1.6.1
South==0.8.4
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