diff --git a/circle/circle/settings/production.py b/circle/circle/settings/production.py index ab28f52..3221246 100644 --- a/circle/circle/settings/production.py +++ b/circle/circle/settings/production.py @@ -20,9 +20,12 @@ from os import environ +from sys import argv from base import * # noqa +if 'runserver' in argv: + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') ########## HOST CONFIGURATION # See: https://docs.djangoproject.com/en/1.5/releases/1.5/ diff --git a/circle/dashboard/admin.py b/circle/dashboard/admin.py index 8f20a88..ac798ca 100644 --- a/circle/dashboard/admin.py +++ b/circle/dashboard/admin.py @@ -21,18 +21,22 @@ from django import contrib from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.models import User, Group -from dashboard.models import Profile, GroupProfile +from dashboard.models import Profile, GroupProfile, ConnectCommand class ProfileInline(contrib.admin.TabularInline): model = Profile +class CommandInline(contrib.admin.TabularInline): + model = ConnectCommand + + class GroupProfileInline(contrib.admin.TabularInline): model = GroupProfile -UserAdmin.inlines = (ProfileInline, ) +UserAdmin.inlines = (ProfileInline, CommandInline, ) GroupAdmin.inlines = (GroupProfileInline, ) contrib.admin.site.unregister(User) diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index cc3f504..c30ac45 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -54,7 +54,9 @@ from .models import Profile, GroupProfile from circle.settings.base import LANGUAGES, MAX_NODE_RAM from django.utils.translation import string_concat -from .virtvalidator import domain_validator +from .validators import domain_validator + +from dashboard.models import ConnectCommand LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) for l in LANGUAGES) @@ -176,7 +178,14 @@ class GroupCreateForm(forms.ModelForm): self.fields['org_id'] = forms.ChoiceField( # TRANSLATORS: directory like in LDAP choices=choices, required=False, label=_('Directory identifier')) - if not new_groups: + if new_groups: + self.fields['org_id'].help_text = _( + "If you select an item here, the members of this directory " + "group will be automatically added to the group at the time " + "they log in. Please note that other users (those with " + "permissions like yours) may also automatically become a " + "group co-owner).") + else: self.fields['org_id'].widget = HiddenInput() def save(self, commit=True): @@ -1057,6 +1066,22 @@ class UserKeyForm(forms.ModelForm): return super(UserKeyForm, self).clean() +class ConnectCommandForm(forms.ModelForm): + class Meta: + fields = ('name', 'access_method', 'template') + model = ConnectCommand + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + super(ConnectCommandForm, self).__init__(*args, **kwargs) + + def clean(self): + if self.user: + self.instance.user = self.user + + return super(ConnectCommandForm, self).clean() + + class TraitsForm(forms.ModelForm): class Meta: diff --git a/circle/dashboard/migrations/0015_auto__add_connectcommand.py b/circle/dashboard/migrations/0015_auto__add_connectcommand.py new file mode 100644 index 0000000..9c01637 --- /dev/null +++ b/circle/dashboard/migrations/0015_auto__add_connectcommand.py @@ -0,0 +1,278 @@ +# -*- 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 'ConnectCommand' + db.create_table(u'dashboard_connectcommand', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='command_set', to=orm['auth.User'])), + ('access_method', self.gf('django.db.models.fields.CharField')(max_length=10)), + ('application', self.gf('django.db.models.fields.CharField')(max_length='128')), + ('template', self.gf('django.db.models.fields.CharField')(max_length=256, null=True, blank=True)), + )) + db.send_create_signal(u'dashboard', ['ConnectCommand']) + + + def backwards(self, orm): + # Deleting model 'ConnectCommand' + db.delete_table(u'dashboard_connectcommand') + + + 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.connectcommand': { + 'Meta': {'object_name': 'ConnectCommand'}, + 'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'application': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'template': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'command_set'", 'to': u"orm['auth.User']"}) + }, + 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_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}), + '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_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}), + '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'}, + 'disk_quota': ('sizefield.models.FileSizeField', [], {'default': '2147483648'}), + 'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}), + 'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}), + 'smb_password': ('django.db.models.fields.CharField', [], {'default': "u'uUmt7R9peX'", 'max_length': '20'}), + 'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + 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 diff --git a/circle/dashboard/migrations/0016_auto__del_field_connectcommand_application__add_field_connectcommand_n.py b/circle/dashboard/migrations/0016_auto__del_field_connectcommand_application__add_field_connectcommand_n.py new file mode 100644 index 0000000..d43b315 --- /dev/null +++ b/circle/dashboard/migrations/0016_auto__del_field_connectcommand_application__add_field_connectcommand_n.py @@ -0,0 +1,281 @@ +# -*- 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): + # Deleting field 'ConnectCommand.application' + db.delete_column(u'dashboard_connectcommand', 'application') + + # Adding field 'ConnectCommand.name' + db.add_column(u'dashboard_connectcommand', 'name', + self.gf('django.db.models.fields.CharField')(default='szia megint', max_length='128'), + keep_default=False) + + + def backwards(self, orm): + # Adding field 'ConnectCommand.application' + db.add_column(u'dashboard_connectcommand', 'application', + self.gf('django.db.models.fields.CharField')(default='szia', max_length='128'), + keep_default=False) + + # Deleting field 'ConnectCommand.name' + db.delete_column(u'dashboard_connectcommand', 'name') + + + 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.connectcommand': { + 'Meta': {'object_name': 'ConnectCommand'}, + 'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}), + 'template': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'command_set'", 'to': u"orm['auth.User']"}) + }, + 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_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}), + '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_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}), + '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'}, + 'disk_quota': ('sizefield.models.FileSizeField', [], {'default': '2147483648'}), + 'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}), + 'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}), + 'smb_password': ('django.db.models.fields.CharField', [], {'default': "u'NRERukxe3Z'", 'max_length': '20'}), + 'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + 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 diff --git a/circle/dashboard/models.py b/circle/dashboard/models.py index 044f0cd..5e9ec2f 100644 --- a/circle/dashboard/models.py +++ b/circle/dashboard/models.py @@ -46,8 +46,10 @@ from acl.models import AclBase from common.models import HumanReadableObject, create_readable, Encoder from vm.tasks.agent_tasks import add_keys, del_keys +from vm.models.instance import ACCESS_METHODS from .store_api import Store, NoStoreException, NotOkException +from .validators import connect_command_template_validator logger = getLogger(__name__) @@ -100,6 +102,25 @@ class Notification(TimeStampedModel): self.message_data = None if value is None else value.to_dict() +class ConnectCommand(Model): + user = ForeignKey(User, related_name='command_set') + access_method = CharField(max_length=10, choices=ACCESS_METHODS, + verbose_name=_('access method'), + help_text=_('Type of the remote access method.')) + name = CharField(max_length="128", verbose_name=_('name'), blank=False, + help_text=_("Name of your custom command.")) + template = CharField(blank=True, null=True, max_length=256, + verbose_name=_('command template'), + help_text=_('Template for connection command string. ' + 'Available parameters are: ' + 'username, password, ' + 'host, port.'), + validators=[connect_command_template_validator]) + + def __unicode__(self): + return self.template + + class Profile(Model): user = OneToOneField(User) preferred_language = CharField(verbose_name=_('preferred language'), @@ -129,6 +150,25 @@ class Profile(Model): default=2048 * 1024 * 1024, help_text=_('Disk quota in mebibytes.')) + def get_connect_commands(self, instance, use_ipv6=False): + """ Generate connection command based on template.""" + single_command = instance.get_connect_command(use_ipv6) + if single_command: # can we even connect to that VM + commands = self.user.command_set.filter( + access_method=instance.access_method) + if commands.count() < 1: + return [single_command] + else: + return [ + command.template % { + 'port': instance.get_connect_port(use_ipv6=use_ipv6), + 'host': instance.get_connect_host(use_ipv6=use_ipv6), + 'password': instance.pw, + 'username': 'cloud', + } for command in commands] + else: + return [] + def notify(self, subject, template, context=None, valid_until=None, **kwargs): if context is not None: diff --git a/circle/dashboard/static/dashboard/dashboard.css b/circle/dashboard/static/dashboard/dashboard.css index 489cad8..d33d8ce 100644 --- a/circle/dashboard/static/dashboard/dashboard.css +++ b/circle/dashboard/static/dashboard/dashboard.css @@ -654,7 +654,8 @@ textarea[name="list-new-namelist"] { width: 130px; } -#vm-details-connection-string-copy { +.vm-details-connection-string-copy, +#vm-details-pw-show { cursor: pointer; } @@ -681,10 +682,9 @@ textarea[name="list-new-namelist"] { max-width: 200px; } -#dashboard-vm-details-connect-command { +.dashboard-vm-details-connect-command { /* for mobile view */ margin-bottom: 20px; - } #store-list-list { @@ -868,6 +868,12 @@ textarea[name="list-new-namelist"] { padding: 5px 0px; } +#profile-key-list-table td:last-child, #profile-key-list-table th:last-child, +#profile-command-list-table td:last-child, #profile-command-list-table th:last-child, +#profile-command-list-table td:nth-child(2), #profile-command-list-table th:nth-child(2) { + text-align: center; + vertical-align: middle; +} #vm-list-table .migrating-icon { -webkit-animation: passing 2s linear infinite; diff --git a/circle/dashboard/static/dashboard/vm-details.js b/circle/dashboard/static/dashboard/vm-details.js index 475e038..3312ad2 100644 --- a/circle/dashboard/static/dashboard/vm-details.js +++ b/circle/dashboard/static/dashboard/vm-details.js @@ -105,19 +105,20 @@ $(function() { $("#vm-details-pw-show").click(function() { var input = $(this).parent("div").children("input"); var eye = $(this).children("#vm-details-pw-eye"); + var span = $(this); - eye.tooltip("destroy") + span.tooltip("destroy") if(eye.hasClass("fa-eye")) { eye.removeClass("fa-eye").addClass("fa-eye-slash"); input.prop("type", "text"); - input.focus(); - eye.prop("title", "Hide password"); + input.select(); + span.prop("title", gettext("Hide password")); } else { eye.removeClass("fa-eye-slash").addClass("fa-eye"); input.prop("type", "password"); - eye.prop("title", "Show password"); + span.prop("title", gettext("Show password")); } - eye.tooltip(); + span.tooltip(); }); /* change password confirmation */ @@ -198,7 +199,7 @@ $(function() { $("#vm-details-h1-name, .vm-details-rename-button").click(function() { $("#vm-details-h1-name").hide(); $("#vm-details-rename").css('display', 'inline'); - $("#vm-details-rename-name").focus(); + $("#vm-details-rename-name").select(); return false; }); @@ -206,7 +207,7 @@ $(function() { $(".vm-details-home-edit-name-click").click(function() { $(".vm-details-home-edit-name-click").hide(); $("#vm-details-home-rename").show(); - $("input", $("#vm-details-home-rename")).focus(); + $("input", $("#vm-details-home-rename")).select(); return false; }); @@ -306,8 +307,8 @@ $(function() { }); // select connection string - $("#vm-details-connection-string-copy").click(function() { - $("#vm-details-connection-string").focus(); + $(".vm-details-connection-string-copy").click(function() { + $(this).parent("div").find("input").select(); }); $("a.operation-password_reset").click(function() { diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py index 88e9c8f..e078a4a 100644 --- a/circle/dashboard/tables.py +++ b/circle/dashboard/tables.py @@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, from vm.models import Node, InstanceTemplate, Lease from django.utils.translation import ugettext_lazy as _ from django_sshkey.models import UserKey +from dashboard.models import ConnectCommand class NodeListTable(Table): @@ -249,5 +250,41 @@ class UserKeyListTable(Table): class Meta: model = UserKey - attrs = {'class': ('table table-bordered table-striped table-hover')} + attrs = {'class': ('table table-bordered table-striped table-hover'), + 'id': "profile-key-list-table"} fields = ('name', 'fingerprint', 'created', 'actions') + prefix = "key-" + empty_text = _("You haven't added any public keys yet.") + + +class ConnectCommandListTable(Table): + name = LinkColumn( + 'dashboard.views.connect-command-detail', + args=[A('pk')], + attrs={'th': {'data-sort': "string"}} + ) + access_method = Column( + verbose_name=_("Access method"), + attrs={'th': {'data-sort': "string"}} + ) + template = Column( + verbose_name=_("Template"), + attrs={'th': {'data-sort': "string"}} + ) + actions = TemplateColumn( + verbose_name=_("Actions"), + template_name=("dashboard/connect-command-list/column-command" + "-actions.html"), + orderable=False, + ) + + class Meta: + model = ConnectCommand + attrs = {'class': ('table table-bordered table-striped table-hover'), + 'id': "profile-command-list-table"} + fields = ('name', 'access_method', 'template', 'actions') + prefix = "cmd-" + empty_text = _( + "You don't have any custom connection commands yet. You can " + "specify commands to be displayed on VM detail pages instead of " + "the defaults.") diff --git a/circle/dashboard/templates/dashboard/connect-command-create.html b/circle/dashboard/templates/dashboard/connect-command-create.html new file mode 100644 index 0000000..ebbb883 --- /dev/null +++ b/circle/dashboard/templates/dashboard/connect-command-create.html @@ -0,0 +1,44 @@ +{% extends "dashboard/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title-page %}{% trans "Create command template" %}{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a> + <h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Create new command template" %}</h3> + </div> + <div class="panel-body"> + <form method="POST"> + {% csrf_token %} + {{ form.name|as_crispy_field }} + {{ form.access_method|as_crispy_field }} + {{ form.template|as_crispy_field }} + <p class="text-muted"> + {% trans "Examples" %} + </p> + <p> + <strong>SSH:</strong> + <span class="text-muted"> + sshpass -p %(password)s ssh -o StrictHostKeyChecking=no cloud@%(host)s -p %(port)d + </span> + </p> + <p> + <strong>RDP:</strong> + <span class="text-muted"> + rdesktop %(host)s:%(port)d -u cloud -p %(password)s + </span> + </p> + <input type="submit" class="btn btn-primary" value="{% trans "Save" %}"> + </form> + </div> + </div> + </div> +</div> + +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/connect-command-edit.html b/circle/dashboard/templates/dashboard/connect-command-edit.html new file mode 100644 index 0000000..25c014d --- /dev/null +++ b/circle/dashboard/templates/dashboard/connect-command-edit.html @@ -0,0 +1,44 @@ +{% extends "dashboard/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title-page %}{% trans "Edit command template" %}{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a> + <h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Edit command template" %}</h3> + </div> + <div class="panel-body"> + <form method="POST"> + {% csrf_token %} + {{ form.name|as_crispy_field }} + {{ form.access_method|as_crispy_field }} + {{ form.template|as_crispy_field }} + <p class="text-muted"> + {% trans "Examples" %} + </p> + <p> + <strong>SSH:</strong> + <span class="text-muted"> + sshpass -p %(password)s ssh -o StrictHostKeyChecking=no cloud@%(host)s -p %(port)d + </span> + </p> + <p> + <strong>RDP:</strong> + <span class="text-muted"> + rdesktop %(host)s:%(port)d -u cloud -p %(password)s + </span> + </p> + <input type="submit" class="btn btn-primary" value="{% trans "Save" %}"> + </form> + </div> + </div> + </div> +</div> + +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/connect-command-list/column-command-actions.html b/circle/dashboard/templates/dashboard/connect-command-list/column-command-actions.html new file mode 100644 index 0000000..8ed8cb6 --- /dev/null +++ b/circle/dashboard/templates/dashboard/connect-command-list/column-command-actions.html @@ -0,0 +1,7 @@ +{% load i18n %} +<a href="{% url "dashboard.views.connect-command-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}"> + <i class="fa fa-edit"></i> +</a> +<a data-template-pk="{{ record.pk }}" href="{% url "dashboard.views.connect-command-delete" pk=record.pk %}" class="btn btn-danger btn-xs template-delete" title="{% trans "Delete" %}"> + <i class="fa fa-times"></i> +</a> diff --git a/circle/dashboard/templates/dashboard/group-create.html b/circle/dashboard/templates/dashboard/group-create.html index 35e47a9..1574e36 100644 --- a/circle/dashboard/templates/dashboard/group-create.html +++ b/circle/dashboard/templates/dashboard/group-create.html @@ -1,4 +1,10 @@ {% load crispy_forms_tags %} +{% load i18n %} + +<p class="text-muted"> +{% trans "User groups allow sharing templates or other resources with multiple users at once." %} +</p> + <form method="POST" action="{% url "dashboard.views.group-create" %}"> {% csrf_token %} diff --git a/circle/dashboard/templates/dashboard/profile_form.html b/circle/dashboard/templates/dashboard/profile_form.html index beb4b47..4f3a543 100644 --- a/circle/dashboard/templates/dashboard/profile_form.html +++ b/circle/dashboard/templates/dashboard/profile_form.html @@ -66,4 +66,20 @@ </div> </div> +<div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <a href="{% url "dashboard.views.connect-command-create" %}" + class="pull-right btn btn-success btn-xs" style="margin-right: 10px;"> + <i class="fa fa-plus"></i> {% trans "add command template" %} + </a> + <h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Command templates" %}</h3> + </div> + <div class="panel-body"> + {% render_table connectcommand_table %} + </div> + </div> + </div> +</div> {% endblock %} diff --git a/circle/dashboard/templates/dashboard/vm-detail.html b/circle/dashboard/templates/dashboard/vm-detail.html index 43f747a..6131a59 100644 --- a/circle/dashboard/templates/dashboard/vm-detail.html +++ b/circle/dashboard/templates/dashboard/vm-detail.html @@ -98,8 +98,9 @@ <div class="input-group"> <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="fa fa-eye" id="vm-details-pw-eye" title="Show password"></i> + <span class="input-group-addon input-tags" id="vm-details-pw-show" + title="{% trans "Show password" %}" data-container="body"> + <i class="fa fa-eye" id="vm-details-pw-eye"></i> </span> </div> </dd> @@ -111,16 +112,29 @@ </div> </dd> </dl> - - <div class="input-group" id="dashboard-vm-details-connect-command"> + {% for c in connect_commands %} + <div class="input-group dashboard-vm-details-connect-command"> <span class="input-group-addon input-tags">{% trans "Command" %}</span> <input type="text" spellcheck="false" - value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}{% trans "Connection is not possible." %}{% endif %}" + value="{{ c }}" + id="vm-details-connection-string" class="form-control input-tags" /> + <span class="input-group-addon input-tags vm-details-connection-string-copy" + title="{% trans "Select all" %}" data-container="body"> + <i class="fa fa-copy"></i> + </span> + </div> + + {% empty %} + <div class="input-group dashboard-vm-details-connect-command"> + <span class="input-group-addon input-tags">{% trans "Command" %}</span> + <input type="text" spellcheck="false" value="{% trans "Connection is not possible." %}" id="vm-details-connection-string" class="form-control input-tags" /> <span class="input-group-addon input-tags" id="vm-details-connection-string-copy"> <i class="fa fa-copy" title="{% trans "Select all" %}"></i> </span> </div> + + {% endfor %} </div> <div class="col-md-8" id="vm-detail-pane"> <div class="panel panel-default" id="vm-detail-panel"> diff --git a/circle/dashboard/templates/dashboard/vm-detail/home.html b/circle/dashboard/templates/dashboard/vm-detail/home.html index 1ec326f..4c946bf 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/home.html +++ b/circle/dashboard/templates/dashboard/vm-detail/home.html @@ -94,7 +94,13 @@ <dt>{% trans "Template" %}:</dt> <dd> {% if instance.template %} - {{ instance.template.name }} + {% if can_link_template %} + <a href="{{ instance.template.get_absolute_url }}"> + {{ instance.template.name }} + </a> + {% else %} + {{ instance.template.name }} + {% endif %} {% else %} - {% endif %} diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index 6c7113b..00f5f69 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -39,6 +39,7 @@ from .views import ( get_vm_screenshot, ProfileView, toggle_use_gravatar, UnsubscribeFormView, UserKeyDelete, UserKeyDetail, UserKeyCreate, + ConnectCommandDelete, ConnectCommandDetail, ConnectCommandCreate, StoreList, store_download, store_upload, store_get_upload_url, StoreRemove, store_new_directory, store_refresh_toplist, VmTraitsUpdate, VmRawDataUpdate, @@ -177,6 +178,16 @@ urlpatterns = patterns( UserKeyCreate.as_view(), name="dashboard.views.userkey-create"), + url(r'^conncmd/delete/(?P<pk>\d+)/$', + ConnectCommandDelete.as_view(), + name="dashboard.views.connect-command-delete"), + url(r'^conncmd/(?P<pk>\d+)/$', + ConnectCommandDetail.as_view(), + name="dashboard.views.connect-command-detail"), + url(r'^conncmd/create/$', + ConnectCommandCreate.as_view(), + name="dashboard.views.connect-command-create"), + url(r'^autocomplete/', include('autocomplete_light.urls')), url(r"^store/list/$", StoreList.as_view(), diff --git a/circle/dashboard/virtvalidator.py b/circle/dashboard/validators.py similarity index 71% rename from circle/dashboard/virtvalidator.py rename to circle/dashboard/validators.py index fa68f99..c0acc9a 100644 --- a/circle/dashboard/virtvalidator.py +++ b/circle/dashboard/validators.py @@ -1,4 +1,6 @@ from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + from lxml import etree as ET import logging @@ -29,3 +31,27 @@ def domain_validator(value): relaxng.assertValid(parsed_xml) except Exception as e: raise ValidationError(e.message) + + +def connect_command_template_validator(value): + """Validate value as a connect command template. + + >>> try: connect_command_template_validator("%(host)s") + ... except ValidationError as e: print e + ... + >>> connect_command_template_validator("%(host)s") + >>> try: connect_command_template_validator("%(host)s %s") + ... except ValidationError as e: print e + ... + [u'Invalid template string.'] + """ + + try: + value % { + 'username': "uname", + 'password': "pw", + 'host': "111.111.111.111", + 'port': 12345, + } + except (KeyError, TypeError, ValueError): + raise ValidationError(_("Invalid template string.")) diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index ba3304f..949e3ad 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -71,12 +71,12 @@ from .forms import ( CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm, VmResourcesForm, VmAddInterfaceForm, VmListSearchForm, - TemplateListSearchForm, + TemplateListSearchForm, ConnectCommandForm ) from .tables import ( NodeListTable, TemplateListTable, LeaseListTable, - GroupListTable, UserKeyListTable + GroupListTable, UserKeyListTable, ConnectCommandListTable, ) from common.models import ( HumanReadableObject, HumanReadableException, fetch_human_exception, @@ -88,7 +88,8 @@ from vm.models import ( ) from storage.models import Disk from firewall.models import Vlan, Host, Rule -from .models import Favourite, Profile, GroupProfile, FutureMember +from .models import (Favourite, Profile, GroupProfile, FutureMember, + ConnectCommand) from .store_api import Store, NoStoreException, NotOkException @@ -366,6 +367,7 @@ class VmDetailView(CheckedDetailView): kwargs={'pk': self.object.pk}), 'ops': ops, 'op': {i.op: i for i in ops}, + 'connect_commands': user.profile.get_connect_commands(instance) }) # activity data @@ -396,7 +398,7 @@ class VmDetailView(CheckedDetailView): # resources forms can_edit = ( - instance in Instance.get_objects_with_level("owner", user) + instance.has_level(user, "owner") and self.request.user.has_perm("vm.change_resources")) context['resources_form'] = VmResourcesForm( can_edit=can_edit, instance=instance) @@ -409,6 +411,11 @@ class VmDetailView(CheckedDetailView): context['can_change_resources'] = self.request.user.has_perm( "vm.change_resources") + # can link template + context['can_link_template'] = ( + instance.template and instance.template.has_level(user, "operator") + ) + return context def post(self, request, *args, **kwargs): @@ -1538,7 +1545,7 @@ class TemplateChoose(LoginRequiredMixin, TemplateView): self.request.user) context.update({ 'box_title': _('Choose template'), - 'ajax_title': False, + 'ajax_title': True, 'template': "dashboard/_template-choose.html", 'templates': templates.all(), }) @@ -2093,7 +2100,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): context.update({ 'template': 'dashboard/_vm-create-2.html', 'box_title': _('Customize VM'), - 'ajax_title': False, + 'ajax_title': True, 'vm_create_form': form, 'template_o': templates.get(pk=template), }) @@ -2101,7 +2108,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): context.update({ 'template': 'dashboard/_vm-create-1.html', 'box_title': _('Create a VM'), - 'ajax_title': False, + 'ajax_title': True, 'templates': templates.all(), }) return self.render_to_response(context) @@ -2291,9 +2298,9 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): context = self.get_context_data(**kwargs) context.update({ 'template': 'dashboard/group-create.html', - 'box_title': 'Create a Group', + 'box_title': _('Create a Group'), 'form': form, - + 'ajax_title': True, }) return self.render_to_response(context) @@ -2961,11 +2968,16 @@ class MyPreferencesView(UpdateView): user=self.request.user), 'change_language': MyProfileForm(instance=self.get_object()), } - table = UserKeyListTable( + key_table = UserKeyListTable( UserKey.objects.filter(user=self.request.user), request=self.request) - table.page = None - context['userkey_table'] = table + key_table.page = None + context['userkey_table'] = key_table + cmd_table = ConnectCommandListTable( + self.request.user.command_set.all(), + request=self.request) + cmd_table.page = None + context['connectcommand_table'] = cmd_table return context def get_object(self, queryset=None): @@ -3370,6 +3382,82 @@ class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): return kwargs +class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin, + UpdateView): + model = ConnectCommand + template_name = "dashboard/connect-command-edit.html" + form_class = ConnectCommandForm + success_message = _("Successfully modified command template.") + + def get(self, request, *args, **kwargs): + object = self.get_object() + if object.user != request.user: + raise PermissionDenied() + return super(ConnectCommandDetail, self).get(request, *args, **kwargs) + + def get_success_url(self): + return reverse_lazy("dashboard.views.connect-command-detail", + kwargs=self.kwargs) + + def post(self, request, *args, **kwargs): + object = self.get_object() + if object.user != request.user: + raise PermissionDenied() + return super(ConnectCommandDetail, self).post(request, args, kwargs) + + def get_form_kwargs(self): + kwargs = super(ConnectCommandDetail, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + +class ConnectCommandDelete(LoginRequiredMixin, DeleteView): + model = ConnectCommand + + def get_success_url(self): + return reverse("dashboard.views.profile-preferences") + + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/confirm/ajax-delete.html'] + else: + return ['dashboard/confirm/base-delete.html'] + + def delete(self, request, *args, **kwargs): + object = self.get_object() + if object.user != request.user: + raise PermissionDenied() + + object.delete() + success_url = self.get_success_url() + success_message = _("Command template successfully deleted.") + + if request.is_ajax(): + return HttpResponse( + json.dumps({'message': success_message}), + content_type="application/json", + ) + else: + messages.success(request, success_message) + return HttpResponseRedirect(success_url) + + +class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin, + CreateView): + model = ConnectCommand + form_class = ConnectCommandForm + template_name = "dashboard/connect-command-create.html" + success_message = _("Successfully created a new command template.") + + def get_success_url(self): + return reverse_lazy("dashboard.views.profile-preferences") + + def get_form_kwargs(self): + kwargs = super(ConnectCommandCreate, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + class HelpView(TemplateView): def get_context_data(self, *args, **kwargs): diff --git a/circle/fabfile.py b/circle/fabfile.py index b41813a..88244a7 100755 --- a/circle/fabfile.py +++ b/circle/fabfile.py @@ -101,7 +101,7 @@ def pull(dir="~/circle/circle"): @roles('portal') def update_portal(test=False): "Update and restart portal+manager" - with _stopped("portal", "mancelery"): + with _stopped("portal", "manager"): pull() pip("circle", "~/circle/requirements.txt") migrate() @@ -113,7 +113,7 @@ def update_portal(test=False): @roles('portal') def stop_portal(test=False): "Stop portal and manager" - _stop_services("portal", "mancelery") + _stop_services("portal", "manager") @roles('node') diff --git a/circle/firewall/models.py b/circle/firewall/models.py index 5081059..882ba3f 100644 --- a/circle/firewall/models.py +++ b/circle/firewall/models.py @@ -215,7 +215,7 @@ class Rule(models.Model): dst = None if host: - ip = (host.ipv4, host.ipv6_with_prefixlen) + ip = (host.ipv4, host.ipv6_with_host_prefixlen) if self.direction == 'in': dst = ip else: @@ -530,14 +530,30 @@ class Host(models.Model): def incoming_rules(self): return self.rules.filter(direction='in') - @property - def ipv6_with_prefixlen(self): + @staticmethod + def create_ipnetwork(ip, prefixlen): try: - net = IPNetwork(self.ipv6) - net.prefixlen = self.vlan.host_ipv6_prefixlen - return net + net = IPNetwork(ip) + net.prefixlen = prefixlen except TypeError: return None + else: + return net + + @property + def ipv4_with_vlan_prefixlen(self): + return Host.create_ipnetwork( + self.ipv4, self.vlan.network4.prefixlen) + + @property + def ipv6_with_vlan_prefixlen(self): + return Host.create_ipnetwork( + self.ipv6, self.vlan.network6.prefixlen) + + @property + def ipv6_with_host_prefixlen(self): + return Host.create_ipnetwork( + self.ipv6, self.vlan.host_ipv6_prefixlen) def get_external_ipv4(self): return self.external_ipv4 if self.external_ipv4 else self.ipv4 @@ -600,6 +616,19 @@ class Host(models.Model): description='created by host.save()', type='AAAA').save() + def get_network_config(self): + interface = {'addresses': []} + + if self.ipv4 and self.vlan.network4: + interface['addresses'].append(str(self.ipv4_with_vlan_prefixlen)) + interface['gw4'] = str(self.vlan.network4.ip) + + if self.ipv6 and self.vlan.network6: + interface['addresses'].append(str(self.ipv6_with_vlan_prefixlen)) + interface['gw6'] = str(self.vlan.network6.ip) + + return interface + def enable_net(self): for i in settings.get('default_host_groups', []): self.groups.add(Group.objects.get(name=i)) diff --git a/circle/manager/mancelery.py b/circle/manager/mancelery.py index 71e01e6..3268fd9 100755 --- a/circle/manager/mancelery.py +++ b/circle/manager/mancelery.py @@ -31,7 +31,6 @@ celery = Celery('manager', 'storage.tasks.local_tasks', 'storage.tasks.periodic_tasks', 'firewall.tasks.local_tasks', - 'monitor.tasks.local_periodic_tasks', 'dashboard.tasks.local_periodic_tasks', ]) @@ -42,20 +41,8 @@ celery.conf.update( CELERY_QUEUES=( Queue(HOSTNAME + '.man', Exchange('manager', type='direct'), routing_key="manager"), - Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'), - routing_key="monitor"), ), CELERYBEAT_SCHEDULE={ - 'vm.update_domain_states': { - 'task': 'vm.tasks.local_periodic_tasks.update_domain_states', - 'schedule': timedelta(seconds=10), - 'options': {'queue': 'localhost.man'} - }, - 'vm.garbage_collector': { - 'task': 'vm.tasks.local_periodic_tasks.garbage_collector', - 'schedule': timedelta(minutes=10), - 'options': {'queue': 'localhost.man'} - }, 'storage.periodic_tasks': { 'task': 'storage.tasks.periodic_tasks.garbage_collector', 'schedule': timedelta(hours=1), @@ -67,24 +54,6 @@ 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'} - }, } ) diff --git a/circle/manager/moncelery.py b/circle/manager/moncelery.py new file mode 100755 index 0000000..9b1f1a0 --- /dev/null +++ b/circle/manager/moncelery.py @@ -0,0 +1,66 @@ +# Copyright 2014 Budapest University of Technology and Economics (BME IK) +# +# This file is part of CIRCLE Cloud. +# +# CIRCLE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along +# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. + +from celery import Celery +from datetime import timedelta +from kombu import Queue, Exchange +from os import getenv + +HOSTNAME = "localhost" +CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/") + +celery = Celery('monitor', + broker=getenv("AMQP_URI"), + include=['vm.tasks.local_periodic_tasks', + 'monitor.tasks.local_periodic_tasks', + ]) + +celery.conf.update( + CELERY_RESULT_BACKEND='cache', + CELERY_CACHE_BACKEND=CACHE_URI, + CELERY_TASK_RESULT_EXPIRES=300, + CELERY_QUEUES=( + Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'), + routing_key="monitor"), + ), + CELERYBEAT_SCHEDULE={ + 'vm.update_domain_states': { + 'task': 'vm.tasks.local_periodic_tasks.update_domain_states', + 'schedule': timedelta(seconds=10), + 'options': {'queue': 'localhost.monitor'} + }, + 'monitor.measure_response_time': { + 'task': 'monitor.tasks.local_periodic_tasks.' + 'measure_response_time', + 'schedule': timedelta(seconds=30), + 'options': {'queue': 'localhost.monitor'} + }, + 'monitor.check_celery_queues': { + 'task': 'monitor.tasks.local_periodic_tasks.' + 'check_celery_queues', + 'schedule': timedelta(seconds=60), + 'options': {'queue': 'localhost.monitor'} + }, + 'monitor.instance_per_template': { + 'task': 'monitor.tasks.local_periodic_tasks.' + 'instance_per_template', + 'schedule': timedelta(seconds=30), + 'options': {'queue': 'localhost.monitor'} + }, + } + +) diff --git a/circle/manager/slowcelery.py b/circle/manager/slowcelery.py new file mode 100755 index 0000000..ee8eba1 --- /dev/null +++ b/circle/manager/slowcelery.py @@ -0,0 +1,50 @@ +# Copyright 2014 Budapest University of Technology and Economics (BME IK) +# +# This file is part of CIRCLE Cloud. +# +# CIRCLE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along +# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. + +from celery import Celery +from datetime import timedelta +from kombu import Queue, Exchange +from os import getenv + +HOSTNAME = "localhost" +CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/") + +celery = Celery('manager.slow', + broker=getenv("AMQP_URI"), + include=['vm.tasks.local_tasks', + 'vm.tasks.local_periodic_tasks', + 'storage.tasks.local_tasks', + 'storage.tasks.periodic_tasks', + ]) + +celery.conf.update( + CELERY_RESULT_BACKEND='cache', + CELERY_CACHE_BACKEND=CACHE_URI, + CELERY_TASK_RESULT_EXPIRES=300, + CELERY_QUEUES=( + Queue(HOSTNAME + '.man.slow', Exchange('manager.slow', type='direct'), + routing_key="manager.slow"), + ), + CELERYBEAT_SCHEDULE={ + 'vm.garbage_collector': { + 'task': 'vm.tasks.local_periodic_tasks.garbage_collector', + 'schedule': timedelta(minutes=10), + 'options': {'queue': 'localhost.man.slow'} + }, + } + +) diff --git a/circle/storage/models.py b/circle/storage/models.py index 54a1eb8..f9df81c 100644 --- a/circle/storage/models.py +++ b/circle/storage/models.py @@ -278,7 +278,7 @@ class Disk(TimeStampedModel): return Disk.create(base=self, datastore=self.datastore, name=self.name, size=self.size, - type=new_type) + type=new_type, dev_num=self.dev_num) def get_vmdisk_desc(self): """Serialize disk object to the vmdriver. diff --git a/circle/vm/operations.py b/circle/vm/operations.py index 8155707..b14895c 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -41,7 +41,7 @@ from .models import ( Instance, InstanceActivity, InstanceTemplate, Interface, Node, NodeActivity, pwgen ) -from .tasks import agent_tasks +from .tasks import agent_tasks, local_agent_tasks from dashboard.store_api import Store, NoStoreException @@ -153,6 +153,7 @@ class AddInterfaceOperation(InstanceOperation): self.rollback(net, activity) raise net.deploy() + local_agent_tasks.send_networking_commands(self.instance, activity) def get_activity_name(self, kwargs): return create_readable(ugettext_noop("add %(vlan)s interface"), @@ -218,6 +219,7 @@ class DownloadDiskOperation(InstanceOperation): has_percentage = True required_perms = ('storage.download_disk', ) accept_states = ('STOPPED', 'PENDING', 'RUNNING') + async_queue = "localhost.man.slow" def _operation(self, user, url, task, activity, name=None): activity.result = url @@ -368,6 +370,7 @@ class MigrateOperation(InstanceOperation): required_perms = () superuser_required = True accept_states = ('RUNNING', ) + async_queue = "localhost.man.slow" def rollback(self, activity): with activity.sub_activity( @@ -512,6 +515,7 @@ class SaveAsTemplateOperation(InstanceOperation): abortable = True required_perms = ('vm.create_template', ) accept_states = ('RUNNING', 'PENDING', 'STOPPED') + async_queue = "localhost.man.slow" def is_preferred(self): return (self.instance.is_base and @@ -667,6 +671,7 @@ class SleepOperation(InstanceOperation): required_perms = () accept_states = ('RUNNING', ) resultant_state = 'SUSPENDED' + async_queue = "localhost.man.slow" def is_preferred(self): return (not self.instance.is_base and @@ -839,6 +844,7 @@ class FlushOperation(NodeOperation): description = _("Disable node and move all instances to other ones.") required_perms = () superuser_required = True + async_queue = "localhost.man.slow" def on_abort(self, activity, error): from manager.scheduler import TraitsUnsatisfiableException @@ -998,12 +1004,12 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation): except NoStoreException: raise PermissionDenied # not show the button at all - def _operation(self): + def _operation(self, user): inst = self.instance queue = self.instance.get_remote_queue_name("agent") host = urlsplit(settings.STORE_URL).hostname - username = Store(inst.owner).username - password = inst.owner.profile.smb_password + username = Store(user).username + password = user.profile.smb_password agent_tasks.mount_store.apply_async( queue=queue, args=(inst.vm_name, host, username, password)) diff --git a/circle/vm/tasks/agent_tasks.py b/circle/vm/tasks/agent_tasks.py index 13af817..5528735 100644 --- a/circle/vm/tasks/agent_tasks.py +++ b/circle/vm/tasks/agent_tasks.py @@ -76,3 +76,8 @@ def get_keys(vm): @celery.task(name='agent.send_expiration') def send_expiration(vm, url): pass + + +@celery.task(name='agent.change_ip') +def change_ip(vm, interfaces, dns): + pass diff --git a/circle/vm/tasks/local_agent_tasks.py b/circle/vm/tasks/local_agent_tasks.py index 27b471b..2e54510 100644 --- a/circle/vm/tasks/local_agent_tasks.py +++ b/circle/vm/tasks/local_agent_tasks.py @@ -19,7 +19,9 @@ from common.models import create_readable from manager.mancelery import celery from vm.tasks.agent_tasks import (restart_networking, change_password, set_time, set_hostname, start_access_server, - cleanup, update) + cleanup, update, change_ip) +from firewall.models import Host + import time from base64 import encodestring from StringIO import StringIO @@ -31,13 +33,11 @@ from celery.result import TimeoutError from monitor.client import Client -def send_init_commands(instance, act, vm): +def send_init_commands(instance, act): + vm = instance.vm_name queue = instance.get_remote_queue_name("agent") with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')): cleanup.apply_async(queue=queue, args=(vm, )) - with act.sub_activity('restart_networking', - readable_name=ugettext_noop('restart networking')): - restart_networking.apply_async(queue=queue, args=(vm, )) with act.sub_activity('change_password', readable_name=ugettext_noop('change password')): change_password.apply_async(queue=queue, args=(vm, instance.pw)) @@ -49,6 +49,17 @@ def send_init_commands(instance, act, vm): queue=queue, args=(vm, instance.primary_host.hostname)) +def send_networking_commands(instance, act): + queue = instance.get_remote_queue_name("agent") + with act.sub_activity('change_ip', + readable_name=ugettext_noop('change ip')): + change_ip.apply_async(queue=queue, args=( + instance.vm_name, ) + get_network_configs(instance)) + with act.sub_activity('restart_networking', + readable_name=ugettext_noop('restart networking')): + restart_networking.apply_async(queue=queue, args=(instance.vm_name, )) + + def create_agent_tar(): def exclude(tarinfo): if tarinfo.name.startswith('./.git'): @@ -94,8 +105,9 @@ def agent_started(vm, version=None): if not initialized: measure_boot_time(instance) - send_init_commands(instance, act, vm) + send_init_commands(instance, act) + send_networking_commands(instance, act) with act.sub_activity( 'start_access_server', readable_name=ugettext_noop('start access server') @@ -134,6 +146,13 @@ def agent_stopped(vm): pass +def get_network_configs(instance): + interfaces = {} + for host in Host.objects.filter(interface__instance=instance): + interfaces[str(host.mac)] = host.get_network_config() + return (interfaces, settings.FIREWALL_SETTINGS['rdns_ip']) + + def update_agent(instance, act=None): if act: act = act.sub_activity( diff --git a/miscellaneous/manager.conf b/miscellaneous/manager.conf new file mode 100644 index 0000000..291d323 --- /dev/null +++ b/miscellaneous/manager.conf @@ -0,0 +1,16 @@ +description "CIRCLE manager" + +start on runlevel [2345] +stop on runlevel [!2345] + +pre-start script + start moncelery + start mancelery + start slowcelery +end script + +post-stop script + stop moncelery + stop mancelery + stop slowcelery +end script diff --git a/miscellaneous/mancelery.conf b/miscellaneous/mancelery.conf index 6972593..a6f63c6 100644 --- a/miscellaneous/mancelery.conf +++ b/miscellaneous/mancelery.conf @@ -1,10 +1,8 @@ -description "CIRCLE mancelery" - -start on runlevel [2345] -stop on runlevel [!2345] +description "CIRCLE mancelery for common jobs" respawn respawn limit 30 30 + setgid cloud setuid cloud @@ -12,6 +10,5 @@ script cd /home/cloud/circle/circle . /home/cloud/.virtualenvs/circle/bin/activate . /home/cloud/.virtualenvs/circle/bin/postactivate - exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1 + exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10 end script - diff --git a/miscellaneous/moncelery.conf b/miscellaneous/moncelery.conf new file mode 100644 index 0000000..ca00325 --- /dev/null +++ b/miscellaneous/moncelery.conf @@ -0,0 +1,14 @@ +description "CIRCLE moncelery for monitoring jobs" + +respawn +respawn limit 30 30 + +setgid cloud +setuid cloud + +script + cd /home/cloud/circle/circle + . /home/cloud/.virtualenvs/circle/bin/activate + . /home/cloud/.virtualenvs/circle/bin/postactivate + exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3 +end script diff --git a/miscellaneous/portal-uwsgi.conf b/miscellaneous/portal-uwsgi.conf index bc2cbeb..3d0663b 100644 --- a/miscellaneous/portal-uwsgi.conf +++ b/miscellaneous/portal-uwsgi.conf @@ -12,4 +12,3 @@ script . /home/cloud/.virtualenvs/circle/bin/postactivate exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666 end script - diff --git a/miscellaneous/portal.conf b/miscellaneous/portal.conf index fcc2a0d..ce1d2c9 100644 --- a/miscellaneous/portal.conf +++ b/miscellaneous/portal.conf @@ -14,4 +14,3 @@ script . /home/cloud/.virtualenvs/circle/bin/postactivate exec ./manage.py runserver '[::]:8080' end script - diff --git a/miscellaneous/slowcelery.conf b/miscellaneous/slowcelery.conf new file mode 100644 index 0000000..b4fdc75 --- /dev/null +++ b/miscellaneous/slowcelery.conf @@ -0,0 +1,14 @@ +description "CIRCLE mancelery for slow jobs" + +respawn +respawn limit 30 30 + +setgid cloud +setuid cloud + +script + cd /home/cloud/circle/circle + . /home/cloud/.virtualenvs/circle/bin/activate + . /home/cloud/.virtualenvs/circle/bin/postactivate + exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5 +end script