From 6efe1ef5a1c8783b056e5e5a7cf55e76e340a3d4 Mon Sep 17 00:00:00 2001 From: Őry Máté <orymate@iit.bme.hu> Date: Sat, 9 Feb 2013 18:51:44 +0100 Subject: [PATCH] one: add save as functionality --- cloud/urls.py | 2 ++ one/admin.py | 1 + one/migrations/0009_auto__add_field_instancetype_credit.py | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ one/models.py | 27 ++++++++++++++++++--------- one/static/base.less | 26 ++++++++++++++++++++++++++ one/static/box.less | 21 +++++++++++++++++++++ one/templates/home.html | 3 +++ one/templates/new-template-flow-1.html | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ one/templates/new-template-flow.html | 167 +++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------ one/templates/show.html | 23 +++++++++++++++++++++++ one/views.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- 11 files changed, 493 insertions(+), 145 deletions(-) create mode 100644 one/migrations/0009_auto__add_field_instancetype_credit.py create mode 100644 one/templates/new-template-flow-1.html diff --git a/cloud/urls.py b/cloud/urls.py index d8f653d..491e9a1 100644 --- a/cloud/urls.py +++ b/cloud/urls.py @@ -26,12 +26,14 @@ urlpatterns = patterns('', url(r'^vm/restart/(?P<iid>\d+)/$', 'one.views.vm_restart', name='vm_restart'), url(r'^vm/port_add/(?P<iid>\d+)/$', 'one.views.vm_port_add', name='vm_port_add'), url(r'^vm/port_del/(?P<iid>\d+)/(?P<proto>tcp|udp)/(?P<public>\d+)/$', 'one.views.vm_port_del', name='vm_port_del'), + url(r'^vm/saveas/(?P<vmid>\d+)$', 'one.views.vm_saveas', name='vm_saveas'), url(r'^reload/$', 'firewall.views.reload_firewall', name='reload_firewall'), url(r'^fwapi/$', 'firewall.views.firewall_api', name='firewall_api'), url(r'^store/$', 'store.views.index', name='store_index'), url(r'^store/gui/$', 'store.views.gui', name='store_gui'), url(r'^store/top/$', 'store.views.toplist', name='store_top'), url(r'^ajax/templateWizard$', 'one.views.ajax_template_wizard', name='ajax_template_wizard'), + url(r'^ajax/template_name_unique/(?P<name>.*)$', 'one.views.ajax_template_name_unique', name='ajax_template_name_unique'), url(r'^ajax/store/list$', 'store.views.ajax_listfolder', name='store_ajax_listfolder'), url(r'^ajax/store/download$', 'store.views.ajax_download', name='store_ajax_download'), url(r'^ajax/store/upload$', 'store.views.ajax_upload', name='store_ajax_upload'), diff --git a/one/admin.py b/one/admin.py index 1df5ec4..603fa64 100644 --- a/one/admin.py +++ b/one/admin.py @@ -47,6 +47,7 @@ submit_vm.short_description = _('Submit VM') class TemplateAdmin(contrib.admin.ModelAdmin): model=models.Template + list_display = ('name', 'state', 'owner', 'system') class InstanceAdmin(contrib.admin.ModelAdmin): model=models.Instance diff --git a/one/migrations/0009_auto__add_field_instancetype_credit.py b/one/migrations/0009_auto__add_field_instancetype_credit.py new file mode 100644 index 0000000..e845732 --- /dev/null +++ b/one/migrations/0009_auto__add_field_instancetype_credit.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'InstanceType.credit' + db.add_column('one_instancetype', 'credit', + self.gf('django.db.models.fields.IntegerField')(default=1), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'InstanceType.credit' + db.delete_column('one_instancetype', 'credit') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + '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'}), + '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'}) + }, + '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'}), + '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': "orm['auth.User']"}), + 'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'}) + }, + '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'}), + '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': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'firewall.host': { + 'Meta': {'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'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['firewall.Group']", 'null': 'True', 'blank': 'True'}), + 'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ipv4': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}), + 'ipv6': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', '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'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'pub_ipv4': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + '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': "orm['firewall.Vlan']"}) + }, + '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': "orm['firewall.Domain']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interface': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}), + 'ipv4': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}), + 'ipv6': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}), + 'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}), + 'net4': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}), + 'net6': ('django.db.models.fields.GenericIPAddressField', [], {'unique': 'True', 'max_length': '39'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'prefix4': ('django.db.models.fields.IntegerField', [], {'default': '16'}), + 'prefix6': ('django.db.models.fields.IntegerField', [], {'default': '80'}), + 'reverse_domain': ('django.db.models.fields.TextField', [], {}), + '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': "orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}), + 'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}) + }, + 'one.disk': { + 'Meta': {'ordering': "['name']", 'object_name': 'Disk'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}) + }, + 'one.instance': { + 'Meta': {'object_name': 'Instance'}, + 'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'firewall_host': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['firewall.Host']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'one_id': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'share': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.Share']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'DEPLOYABLE'", 'max_length': '20'}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.Template']"}), + 'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}) + }, + 'one.instancetype': { + 'CPU': ('django.db.models.fields.IntegerField', [], {}), + 'Meta': {'object_name': 'InstanceType'}, + 'RAM': ('django.db.models.fields.IntegerField', [], {}), + 'credit': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}) + }, + 'one.network': { + 'Meta': {'ordering': "['name']", 'object_name': 'Network'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'nat': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'one.share': { + 'Meta': {'object_name': 'Share'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['school.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_limit': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'per_user_limit': ('django.db.models.fields.IntegerField', [], {}), + 'template': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.Template']"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '10'}) + }, + 'one.sshkey': { + 'Meta': {'object_name': 'SshKey'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '2000'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'one.template': { + 'Meta': {'object_name': 'Template'}, + 'access_type': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'disk': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.Disk']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.InstanceType']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'network': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.Network']"}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'state': ('django.db.models.fields.CharField', [], {'default': "'NEW'", 'max_length': '10'}), + 'system': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'one.userclouddetails': { + 'Meta': {'object_name': 'UserCloudDetails'}, + 'disk_quota': ('django.db.models.fields.IntegerField', [], {'default': '2048'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_quota': ('django.db.models.fields.IntegerField', [], {'default': '20'}), + 'share_quota': ('django.db.models.fields.IntegerField', [], {'default': '100'}), + 'smb_password': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'ssh_key': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['one.SshKey']", 'null': 'True'}), + 'ssh_private_key': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'school.course': { + 'Meta': {'object_name': 'Course'}, + 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}), + 'default_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'default_group_of'", 'null': 'True', 'to': "orm['school.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'owners': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['school.Person']", 'null': 'True', 'blank': 'True'}), + 'short_name': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}) + }, + 'school.group': { + 'Meta': {'unique_together': "(('name', 'course', 'semester'),)", 'object_name': 'Group'}, + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['school.Course']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'course_groups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['school.Person']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'owners': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'owned_groups'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['school.Person']"}), + 'semester': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['school.Semester']"}) + }, + 'school.person': { + 'Meta': {'object_name': 'Person'}, + 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'default': "'hu'", 'max_length': '10'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'school.semester': { + 'Meta': {'object_name': 'Semester'}, + 'end': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}), + 'start': ('django.db.models.fields.DateField', [], {}) + } + } + + complete_apps = ['one'] \ No newline at end of file diff --git a/one/models.py b/one/models.py index 4aca64c..b0e08aa 100644 --- a/one/models.py +++ b/one/models.py @@ -122,10 +122,11 @@ class SshKey(models.Model): TEMPLATE_STATES = (("INIT", _('init')), ("PREP", _('perparing')), ("SAVE", _('saving')), ("READY", _('ready'))) -TYPES = {"LAB": {"verbose_name": _('lab'), "suspend": td(hours=5), "delete": td(days=15)}, - "PROJECT": {"verbose_name": _('project'), "suspend": td(weeks=5), "delete": td(days=366/2)}, - "SERVER": {"verbose_name": _('server'), "suspend": td(days=365), "delete": None}, +TYPES = {"LAB": {"verbose_name": _('lab'), "id": "LAB", "suspend": td(hours=5), "delete": td(days=15), "help_text": _('For lab or home work with short life time.')}, + "PROJECT": {"verbose_name": _('project'), "id": "PROJECT", "suspend": td(weeks=5), "delete": td(days=366/2), "help_text": _('For project work.')}, + "SERVER": {"verbose_name": _('server'), "id": "SERVER", "suspend": td(days=365), "delete": None, "help_text": _('For long term server use.')}, } +TYPES_L = sorted(TYPES.values(), key=lambda m: m["suspend"]) TYPES_C = tuple([(i[0], i[1]["verbose_name"]) for i in TYPES.items()]) class Share(models.Model): name = models.CharField(max_length=100, unique=True, verbose_name=_('name')) @@ -222,10 +223,14 @@ class InstanceType(models.Model): verbose_name=_('name')) CPU = models.IntegerField(help_text=_('CPU cores.')) RAM = models.IntegerField(help_text=_('Mebibytes of memory.')) + credit = models.IntegerField(verbose_name=_('credits'), + help_text=_('Price of instance.')) def __unicode__(self): return u"%s" % self.name + class Meta: + ordering = ['credit'] -TEMPLATE_STATES = (('NEW', _('new')), ('PREPARING', _('preparing')), +TEMPLATE_STATES = (('NEW', _('new')), ('SAVING', _('saving')), ('READY', _('ready')), ) """ Virtual machine template specifying OS, disk, type and network. @@ -455,7 +460,8 @@ class Instance(models.Model): proc = subprocess.Popen(["/opt/occi.sh", "compute", "delete", "%d"%self.one_id], stdout=subprocess.PIPE) (out, err) = proc.communicate() - self.firewall_host.delete() + if self.firewall_host: + self.firewall_host.delete() reload_firewall_lock() def _update_vm(self, template): @@ -499,7 +505,10 @@ class Instance(models.Model): imgname = "template-%d-%d" % (self.template.id, self.id) self._update_vm('<DISK id="0"><SAVE_AS name="%s"/></DISK>' % imgname) self._change_state("SHUTDOWN") - def is_save_as_done(self): + t = self.template + t.state = 'SAVING' + t.save() + def check_if_is_save_as_done(self): self.update_state() if self.state != 'DONE': return false @@ -509,9 +518,9 @@ class Instance(models.Model): if len(disks) != 1: return false self.template.disk_id = disks[0].id - self.template.status = 'READY' + self.template.state = 'READY' self.template.save() - self.host.delete() + self.firewall_host.delete() return True @@ -523,7 +532,7 @@ def delete_instance(sender, instance, using, **kwargs): if instance.state != "DONE": instance.one_delete() try: - instance.host.delete() + instance.firewall_host.delete() except: pass post_delete.connect(delete_instance, sender=Instance, dispatch_uid="delete_instance") diff --git a/one/static/base.less b/one/static/base.less index 35e81a9..4f05340 100644 --- a/one/static/base.less +++ b/one/static/base.less @@ -43,6 +43,21 @@ body margin-top:0; padding:10px; } + &.wide { + margin-right: 30px; + } + &.note { + background-color: #ffc; + } + ol { + margin-left: 2em; + &.done { + text-color: #aaa; + } + } +} +.big { + font-size: 2em; } .wm-list{ list-style: none; @@ -75,6 +90,14 @@ ul.messagelist li.error { background-image:url(admin/img/icon_error.gif); } +input.validated { + padding-right: 15px; + &.error + { + background:#fcc url(admin/img/icon_error.gif) right center no-repeat; + padding-right: 15px; + } +} .errornote { @@ -187,3 +210,6 @@ textarea { box-shadow: inset 0 0 10px rgba(0,0,0,0.2), 0 0 10px rgba(255,255,0,0.8); } } +.hilight { + background-color: #ff6; +} diff --git a/one/static/box.less b/one/static/box.less index 7bda568..8ca9cfe 100644 --- a/one/static/box.less +++ b/one/static/box.less @@ -282,6 +282,27 @@ background-image: url(icons/computer--plus.png); } } +#template-wizard { + + .size-summary { + font-size: .8em; + text-align: right; + span { + background-position: left center; + background-repeat: no-repeat; + padding-left: 18px; + } + .cpu{ + background-image: url(icons/processor.png) + } + .memory{ + background-image: url(icons/memory.png) + } + .credit{ + background-image: url(icons/documents-stack.png) /* TODO */ + } + } +} .file-list{ list-style: none; diff --git a/one/templates/home.html b/one/templates/home.html index 934be3f..4e90499 100644 --- a/one/templates/home.html +++ b/one/templates/home.html @@ -73,6 +73,9 @@ <div class="contentblock" id="template"> <h2>{% trans "My templates" %}</h2> <ul class="wm-list"> + {% for t in mytemplates %} + <li class="wm">{{t.name}}</li> + {% endfor %} <li class="wm"> <div class="summary"> <div class="quota"> diff --git a/one/templates/new-template-flow-1.html b/one/templates/new-template-flow-1.html new file mode 100644 index 0000000..f5abce5 --- /dev/null +++ b/one/templates/new-template-flow-1.html @@ -0,0 +1,60 @@ +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} +<form action="/ajax/templateWizard" method="post" id="template-wizard">{% csrf_token %} + <div id="new-template-step-1" class="wizard"> + <div class="progress"> + <div class="bar-container"> + <div class="bar" style="width: 33%"></div> + </div> + <h3>{% blocktrans with step=1 all=3 %}{{step}}/{{all}}{% endblocktrans %}</h3> + </div> + <h2>{% blocktrans with step=1 %}Step {{step}}{% endblocktrans %}</h2> + <p class="help">{% trans "Please choose the base system you want to customize." %}</p> + <div class="container"> + <ul class="tpl-list modal"> + {% if not templates %} + <p>{% trans "There are no available templates." %}</p> + {% endif %} + {% for m in templates %} + <li class="tpl"> + <div class="summary"> + <div class="name tpl os-{{m.os_type}}" title="{{m.description}}"><label><input type="radio" name="base" value="{{m.id}}" /> {{m.name}}</label></div> + <div class="clear"></div> + </div> + </li> + {% endfor %} + </ul> + </div> + <nav> + <a href="#" class="prev">{% trans "« Cancel" %}</a> + <input type="submit" class="next" value="{% trans "Next »" %}" /> + <div class="clear"></div> + </nav> + <script type="text/javascript"> + $(function(){ + $('#template-wizard nav .prev').click(function(){ + $('#modal').hide(); + }) + $('#template-wizard').unbind('submit').submit(function(e){ + if ($("#template-wizard input[type=radio]:checked").length==0) { + $("#template-wizard .help").addClass('hilight'); + } + else { + $.ajax({ + 'type': 'POST', + 'url': '/ajax/templateWizard', + 'data': $('#template-wizard').serialize(), + 'success': function(data) { + $('#modal-container').html(data); + } + }); + } + e.preventDefault(); + e.stopPropagation(); + return false; + }) + }) + </script> + </div> + </div> +</form> diff --git a/one/templates/new-template-flow.html b/one/templates/new-template-flow.html index 645307a..9d686f1 100644 --- a/one/templates/new-template-flow.html +++ b/one/templates/new-template-flow.html @@ -1,88 +1,7 @@ {% load i18n %} {% get_current_language as LANGUAGE_CODE %} -<form action="/" method="post" id="template-wizard"> - <div id="new-template-step-1" class="wizard"> - <div class="progress"> - <div class="bar-container"> - <div class="bar" style="width: 33%"></div> - </div> - <h3>{% blocktrans with step=1 all=3 %}{{step}}/{{all}}{% endblocktrans %}</h3> - </div> - <h2>{% blocktrans with step=1 %}Step {{step}}{% endblocktrans %}</h2> - <p>Leírás, mit is kéne itt ezen az ablakon csinálni, és miért jó, ha azt csinálja, amit.</p> - <div class="container"> - <ul class="wm-list modal"> - {% for m in templates %} - <li class="wm"> - <form method="POST" action="/vm/new/{{m.pk}}/">{% csrf_token %} - <div class="summary"> - <div class="name wm-on">{{m.name}}</div> - <div class="status"> - <input type="submit" value="Indítás"/> - </div> - <div class="clear"></div> - </div> - <div class="details"> - <h3> - {% trans "Details" %} - </h3> - <ul> - <li class="name">{% trans "System" %}: <span class="value">{{m.disk.name}}</span></li> - <li class="type">{% trans "Size" %}: <span class="value">{{m.instance_type.name}}</span></li> - <li class="memory">{% trans "Memory" %}: <span class="value">{{m.instance_type.RAM}} MiB</span></li> - <li class="cpu">{% trans "CPU cores" %}: <span class="value">{{m.instance_type.CPU}}</span></li> - </ul> - </div> - </form> - </li> - {% endfor %} - </ul> - </div> - <nav> - <a href="#" class="prev">{% trans "« Cancel" %}</a> - <a href="#" class="next">{% trans "Next »" %}</a> - <div class="clear"></div> - </nav> - <script type="text/javascript"> - $(function(){ - - console.log('foo'); - $('#modal .wm .summary').each(function(){ - - $(this).next('.details').show(); - console.log($(this).next('.details').css('display'), $(this).next('.details').css('height')); - //this.originalHeight=parseInt($(this).next('.details').css('height')); - }) - $('#modal .wm .summary').click(function(){ - if($(this).next('.details').is(':hidden')){ - $(this).next('.details') - .slideDown(700); - $(this).parent('.wm').addClass('opened'); - } else { - var that=this; - $(this).next('.details') - .removeClass('opened') - .slideUp(700); - } - }); - $('#new-template-step-1 nav .prev').click(function(){ - $('#new-template-step-1').hide(); - $('#new-template-step-1').show(); - }) - $('#new-template-step-1 nav .next').click(function(){ - $('#new-template-step-1').hide(); - $('#new-template-step-3').show(); - $.ajax({ - 'type': 'POST', - 'url': '/ajax/templateWizard', - 'data': $('#template-wizard').serialize() - }) - .done(function(){ console.log('ok')}); - }) - }) - </script> - </div> - <div id="new-template-step-2" class="wizard" style="display: none"> +<form action="/vm/new/{{base.id}}/" method="post" id="template-wizard">{% csrf_token %} + <div id="new-template-step-2" class="wizard"> <div class="progress"> <div class="bar-container"> <div class="bar" style="width: 66%"></div> @@ -90,69 +9,77 @@ <h3>{% blocktrans with step=2 all=3 %}{{step}}/{{all}}{% endblocktrans %}</h3> </div> <h2>{% blocktrans with step=2 %}Step {{step}}{% endblocktrans %}</h2> - <p>Leírás, mit is kéne itt ezen az ablakon csinálni, és miért jó, ha azt csinálja, amit.</p> + <p>{% trans "Change the parameters as needed." %}</p> <ul> <li> <label for="new-template-name">{% trans "Name" %}</label> - <input type="text" placeholder="{% trans "Short name of template" %}" name="name" id="new-template-name" /> + <input type="text" name="name" id="new-template-name" value="{{base.name}}" + class="error validated" title="{% trans "Please choose a different name." %}" /> <div class="clear"></div> </li> - <li> - <label for="new-template-type">{% trans "Type" %}</label> - <ul class="radio"> - <li> - <input type="radio" name="type" value="labor" id="new-template-type-labor" /> - <label for="new-template-type-labor">{% trans "Lab" %}</label> - </li> - <li> - <input type="radio" name="type" value="project" id="new-template-type-project" /> - <label for="new-template-type-project">{% trans "Project" %}</label> - </li> - </ul> - <div class="clear"></div> - </li> - <li> + <li class="new-tpl-size"> <label for="new-template-size">{% trans "Size" %}</label> <ul class="radio"> + {% for s in sizes %} <li> - <input type="radio" name="size" value="small" id="new-template-size-small" /> - <label for="new-template-size-small">Kicsi</label> - </li> - <li> - <input type="radio" name="size" value="medium" id="new-template-size-medium" /> - <label for="new-template-size-medium">Közepes</label> - </li> - <li> - <input type="radio" name="size" value="large" id="new-template-size-large" /> - <label for="new-template-size-large">Nagy</label> + <label> + <input type="radio" name="size" value="{{s.id}}" id="new-template-size-{{s.id}}" + {% if s == base.instance_type %}checked="checked"{% endif %} /> + {{s.name}} + </label> </li> + {% endfor %} </ul> + {% for s in sizes %} + <p id="new-template-size-summary-{{s.id}}" class="size-summary clear" + {% if s != base.instance_type %}style="display:none"{% endif %}> + <span class="cpu">{% blocktrans count n=s.CPU %}{{n}} core{% plural %}{{n}} cores{% endblocktrans %}</span> + <span class="memory">{{s.RAM}} MiB</span> + <span class="credit">{{s.credit}}</span> + </p> + {% endfor %} <div class="clear"></div> </li> <li style="border: none"> <label for="new-template-description">{% trans "Description" %}</label> - <textarea name="description" id="new-template-description" placeholder="{% trans "Comments about the template" %}"></textarea> + <textarea name="description" id="new-template-description" style="text-align: left">{{base.description}}</textarea> <div class="clear"></div> </li> </ul> <nav> <a href="#" class="prev">{% trans "« Cancel" %}</a> - <a href="#" class="next">{% trans "Next »" %}</a> + <input type="submit" value="{% trans "Launch" %}" /> <div class="clear"></div> </nav> <script type="text/javascript"> $(function(){ - $('#new-template-step-1 nav .next').click(function(){ - $('#new-template-step-1').hide(); - $('#new-template-step-2').show(); - }) - $('#new-template-step-1 nav .prev').click(function(){ + $('#new-template-step-2 nav .prev').click(function(){ $('#modal').hide(); }) + $(".new-tpl-size input[name='size']").click(function(e){ + var v = $(".new-tpl-size input[name='size']:checked").val(); + $("p.size-summary").hide(); + $("#new-template-size-summary-" + v).show(); + }); + $("#new-template-name").change(function(e){ + var s = $(this).val(); + $.ajax({ + 'type': 'GET', + 'url': '/ajax/template_name_unique/' + s, + 'success': function(data, b, c) { + if (s != $("#new-template-name").val()) { + return True; + } + if (data == "True") { + $('#new-template-name').removeClass("error"); + } + else { + $('#new-template-name').addClass("error"); + } + } + }); + }); }) </script> </div> - <div id="new-template-step-3" class="wizard" style="display: none"> - - </div> </form> diff --git a/one/templates/show.html b/one/templates/show.html index 0209cf3..8e50bbb 100644 --- a/one/templates/show.html +++ b/one/templates/show.html @@ -21,6 +21,29 @@ {% endblock %} {% block content %} + +{% if i.template.state != "READY" %} +<div class="contentblock wide note big"> + <p>{% blocktrans %}This is a master image for your new template.{% endblocktrans %}</p> + <form action="{% url one.views.vm_saveas id %}" method="POST">{% csrf_token %} + {% if i.template.state == "NEW" %} + <p style="float: right; margin-top:2em;margin-right:1em;"> + <input type="submit" value="{% trans "Save" %}" class="big" style="background-color:rgba(102, 255, 0, 0.4)" /> + </p> + {% endif %} + </form> + <ol> + <li{% if i.template.state == "SAVING" %} class="done"{%endif%}>{% blocktrans %}Connect to the machine.{% endblocktrans %}</li> + <li{% if i.template.state == "SAVING" %} class="done"{%endif%}>{% blocktrans %}Do all the needed installation/customization.{% endblocktrans %}</li> + <li{% if i.template.state == "SAVING" %} class="done"{%endif%}>{% blocktrans %}Log off (keep the machine running).{% endblocktrans %}</li> + <li{% if i.template.state == "SAVING" %} class="done"{%endif%}>{% blocktrans %}Click on the "save" button on the right.{% endblocktrans %}</li> + <li>{% blocktrans %}The machine will be shut down and its disk saved.{% endblocktrans %}</li> + <li>{% blocktrans %}You can share the template with your groups.{% endblocktrans %}</li> + </ol> +</div> +{% endif %} + + <div class="boxes"> <div class="contentblock" id="state"> <h2>{{name}}</h2> diff --git a/one/views.py b/one/views.py index 09344ae..151ff29 100644 --- a/one/views.py +++ b/one/views.py @@ -10,7 +10,6 @@ from django.core.mail import mail_managers, send_mail from django.db import transaction from django.forms import ModelForm, Textarea from django.http import Http404 -#from django_shibboleth.forms import BaseRegisterForm from django.shortcuts import render, render_to_response, get_object_or_404, redirect from django.template import RequestContext from django.template.loader import render_to_string @@ -19,11 +18,14 @@ from django.utils.translation import get_language as lang from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import * from django.views.generic import * +from firewall.tasks import * from one.models import * from school.models import * import django.contrib.auth as auth -from firewall.tasks import * import json +import logging + +logger = logging.getLogger(__name__) class LoginView(View): def get(self, request, *args, **kwargs): @@ -32,12 +34,12 @@ class LoginView(View): nex = request.GET['next'] except: pass - return render_to_response("login.html", RequestContext(request,{'next': nex})) + return render_to_response("login.html", RequestContext(request, {'next': nex})) def post(self, request, *args, **kwargs): if request.POST['pw'] != 'ezmiez': return redirect('/') p, created = User.objects.get_or_create(username=request.POST['neptun']) - if created: + if created: p.set_unusable_password() if not p.email: p.email = "%s@nc.hszk.bme.hu" % p.username @@ -70,29 +72,66 @@ def _list_instances(request): @require_GET @login_required def home(request): - return render_to_response("home.html", RequestContext(request,{ - 'templates': Template.objects.all(), + return render_to_response("home.html", RequestContext(request, { + 'templates': Template.objects.filter(state='READY'), + 'mytemplates': Template.objects.filter(owner=request.user), 'instances': _list_instances(request), 'groups': request.user.person_set.all()[0].owned_groups.all(), 'semesters': Semester.objects.all() })) -@require_GET +def ajax_template_name_unique(request, name): + s = "True" + if Template.objects.filter(name=name).exists(): + s = "False" + return HttpResponse(s) + +class AjaxTemplateWizard(View): + def get(self, request, *args, **kwargs): + return render_to_response('new-template-flow-1.html', RequestContext(request,{ + 'templates': Template.objects.filter(public=True) + # + Template.objects.filter(owner=request.user), + })) + def post(self, request, *args, **kwargs): + base = get_object_or_404(Template, id=request.POST['base']) + if base.owner != request.user and not base.public and not request.user.is_superuser: + raise PermissionDenied() + return render_to_response('new-template-flow.html', RequestContext(request, { + 'sizes': InstanceType.objects.all(), + 'base': base, + })) +ajax_template_wizard = login_required(AjaxTemplateWizard.as_view()) + +@require_POST @login_required -def ajax_template_wizard(request): - return render_to_response('new-template-flow.html', RequestContext(request,{ - 'templates': Template.objects.all(), - })) +def vm_saveas(request, vmid): + inst = get_object_or_404(Instance, pk=vmid) + if inst.owner != request.user and not request.user.is_superuser: + raise PermissionDenied() + inst.save_as() + messages.success(request, _("Template is being saved...")) + return redirect(inst) + @require_POST @login_required def vm_new(request, template): - m = get_object_or_404(Template, pk=template) + base = get_object_or_404(Template, pk=template) + if "name" in request.POST: + if base.owner != request.user and not base.public and not request.user.is_superuser: + raise PermissionDenied() + name = request.POST['name'] + t = Template.objects.create(name=name, disk=base.disk, instance_type_id=request.POST['size'], network=base.network, owner=request.user) + t.access_type = base.access_type + t.description = request.POST['description'] + t.system = base.system + t.save() + base = t try: - i = Instance.submit(m, request.user) + i = Instance.submit(base, request.user, extra="<RECONTEXT>YES</RECONTEXT>") return redirect(i) - except: - raise + except Exception as e: + logger.error('Failed to create virtual machine.' + unicode(e)) messages.error(request, _('Failed to create virtual machine.')) return redirect('/') @@ -111,6 +150,8 @@ vm_list = login_required(VmListView.as_view()) def vm_show(request, iid): inst = get_object_or_404(Instance, id=iid, owner=request.user) inst.update_state() + if inst.template.state == "SAVING": + inst.check_if_is_save_as_done() return render_to_response("show.html", RequestContext(request,{ 'uri': inst.get_connect_uri(), 'state': inst.state, @@ -188,7 +229,7 @@ class VmDeleteView(View): def get(self, request, iid, *args, **kwargs): i = get_object_or_404(Instance, id=iid, owner=request.user) - return render_to_response("confirm_delete.html", RequestContext(request,{ + return render_to_response("confirm_delete.html", RequestContext(request, { 'i': i})) vm_delete = login_required(VmDeleteView.as_view()) -- libgit2 0.26.0