diff --git a/accounts/migrations/0004_userattributes.py b/accounts/migrations/0004_userattributes.py new file mode 100644 index 0000000..fb32539 --- /dev/null +++ b/accounts/migrations/0004_userattributes.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0003_usersshkey'), + ] + + operations = [ + migrations.CreateModel( + name='UserAttributes', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('max_instances', models.IntegerField(default=0)), + ('max_cpus', models.IntegerField(default=0)), + ('max_memory', models.IntegerField(default=0)), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/accounts/migrations/0005_userattributes_can_clone_instances.py b/accounts/migrations/0005_userattributes_can_clone_instances.py new file mode 100644 index 0000000..4539657 --- /dev/null +++ b/accounts/migrations/0005_userattributes_can_clone_instances.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_userattributes'), + ] + + operations = [ + migrations.AddField( + model_name='userattributes', + name='can_clone_instances', + field=models.BooleanField(default=False), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 15cedee..16f5f6e 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -20,3 +20,13 @@ class UserSSHKey(models.Model): def __unicode__(self): return self.keyname + +class UserAttributes(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + can_clone_instances = models.BooleanField(default=False) + max_instances = models.IntegerField(default=0) + max_cpus = models.IntegerField(default=0) + max_memory = models.IntegerField(default=0) + + def __unicode__(self): + return self.user.username diff --git a/accounts/templates/accounts.html b/accounts/templates/accounts.html index cc03668..41d5448 100644 --- a/accounts/templates/accounts.html +++ b/accounts/templates/accounts.html @@ -83,6 +83,30 @@ <input type="checkbox" name="user_is_superuser" {% if user.is_superuser %}checked{% endif %}> </div> </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Can clone instances" %}</label> + <div class="col-sm-2"> + <input type="checkbox" name="userattributes_can_clone_instances" {% if user.userattributes.can_clone_instances %}checked{% endif %}> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Max instances" %}</label> + <div class="col-sm-6"> + <input type="text" name="userattributes_max_instances" class="form-control" value="{{ user.userattributes.max_instances }}"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Max cpus" %}</label> + <div class="col-sm-6"> + <input type="text" name="userattributes_max_cpus" class="form-control" value="{{ user.userattributes.max_cpus }}"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Max memory" %}</label> + <div class="col-sm-6"> + <input type="text" name="userattributes_max_memory" class="form-control" value="{{ user.userattributes.max_memory }}"> + </div> + </div> </div> <div class="modal-footer"> <button type="submit" class="pull-left btn btn-danger" name="delete"> diff --git a/accounts/views.py b/accounts/views.py index 5b6c4b2..22f6c78 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required -from accounts.models import UserInstance, UserSSHKey +from accounts.models import * from instances.models import Instance from accounts.forms import UserAddForm from django.conf import settings @@ -92,13 +92,23 @@ def accounts(request): if 'edit' in request.POST: user_id = request.POST.get('user_id', '') user_pass = request.POST.get('user_pass', '') - user_is_staff = request.POST.get('user_is_staff', False) - user_is_superuser = request.POST.get('user_is_superuser', False) user_edit = User.objects.get(id=user_id) user_edit.set_password(user_pass) - user_edit.is_staff = user_is_staff - user_edit.is_superuser = user_is_superuser + user_edit.is_staff = request.POST.get('user_is_staff', False) + user_edit.is_superuser = request.POST.get('user_is_superuser', False) user_edit.save() + try: + userattributes = user_edit.userattributes + except UserAttributes.DoesNotExist: + userattributes = UserAttributes(user=user_edit) + userattributes.can_clone_instances = request.POST.get('userattributes_can_clone_instances', False) + userattributes_max_instances = request.POST.get('userattributes_max_instances', 0) + userattributes_max_cpus = request.POST.get('userattributes_max_cpus', 0) + userattributes_max_memory = request.POST.get('userattributes_max_memory', 0) + userattributes.max_instances = userattributes_max_instances if userattributes_max_instances else 0 + userattributes.max_cpus = userattributes_max_cpus if userattributes_max_cpus else 0 + userattributes.max_memory = userattributes_max_memory if userattributes_max_memory else 0 + userattributes.save() return HttpResponseRedirect(request.get_full_path()) if 'block' in request.POST: user_id = request.POST.get('user_id', '') diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 3633b2e..eff8ee9 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -501,11 +501,15 @@ {% trans "Network" %} </a> </li> + {% endif %} + {% if request.user.is_superuser or request.user.userattributes.can_clone_instances %} <li role="presentation"> <a href="#clone" aria-controls="clone" role="tab" data-toggle="tab"> {% trans "Clone" %} </a> </li> + {% endif %} + {% if request.user.is_superuser %} <li role="presentation"> <a href="#migrate" aria-controls="migrate" role="tab" data-toggle="tab"> {% trans "Migrate" %} @@ -690,45 +694,67 @@ </form> <div class="clearfix"></div> </div> + {% endif %} + {% if request.user.is_superuser or request.user.userattributes.can_clone_instances %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="clone"> <p style="font-weight:bold;">{% trans "Create a clone" %}</p> <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} <div class="form-group"> <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Clone Name" %}</label> <div class="col-sm-4"> + {% if request.user.is_superuser %} <input id="clone_name" type="text" class="form-control" name="name" value="{{ vname }}-clone"/> + {% else %} + <select id="select_clone_name" class="form-control" name="name" size="1"/> + {% for name in clone_free_names %} + <option value="{{ name }}">{{ name }}</option> + {% endfor %} + </select> + {% endif %} </div> </div> - <p style="font-weight:bold;">{% trans "Network devices" %}</p> - {% for network in networks %} - <div class="form-group"> - <label class="col-sm-3 control-label" style="font-weight:normal;">eth{{ forloop.counter0 }} ({{ network.nic }})</label> - <div class="col-sm-4"> - <input type="text" class="form-control" name="clone-net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> - </div> - <div class="col-sm-4"> - <button type="button" class="btn btn-sm btn-success pull-left" name="random-mac-{{ forloop.counter0 }}" - onclick="random_mac({{ forloop.counter0 }})" style="margin-top: 2px;">{% trans "Random" %}</button> - <button type="button" class="btn btn-sm btn-success pull-left" name="guess-mac-{{ forloop.counter0 }}" - onclick="guess_mac_address({{ forloop.counter0 }})" style="margin-top: 2px;">{% trans "Guess" %}</button> - </div> - </div> - {% endfor %} - <p style="font-weight:bold;">{% trans "Storage devices" %}</p> - {% for disk in clone_disks %} - <div class="form-group"> - <label class="col-sm-3 control-label" style="font-weight:normal;">{{ disk.dev }} ({{ disk.storage }})</label> - <div class="col-sm-4"> - <input id="disk_name-{{ disk.dev }}" type="text" class="form-control" name="disk-{{ disk.dev }}" value="{{ disk.image }}"/> - </div> - {% ifequal disk.format 'qcow2' %} - <label class="col-sm-2 control-label" style="font-weight:normal;margin-left:-35px;">Metadata</label> - <div class="col-sm-1"> - <input type="checkbox" name="meta-{{ disk.dev }}" value="true" style="margin-top: 10px;"> + {% if request.user.is_superuser %} + <p style="font-weight:bold;">{% trans "Network devices" %}</p> + {% for network in networks %} + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">eth{{ forloop.counter0 }} ({{ network.nic }})</label> + <div class="col-sm-4"> + <input type="text" class="form-control" name="clone-net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> </div> - {% endifequal %} - </div> - {% endfor %} + <div class="col-sm-4"> + <button type="button" class="btn btn-sm btn-success pull-left" name="random-mac-{{ forloop.counter0 }}" + onclick="random_mac({{ forloop.counter0 }})" style="margin-top: 2px;">{% trans "Random" %}</button> + <button type="button" class="btn btn-sm btn-success pull-left" name="guess-mac-{{ forloop.counter0 }}" + onclick="guess_mac_address('#clone_name', {{ forloop.counter0 }})" style="margin-top: 2px;">{% trans "Guess" %}</button> + </div> + </div> + {% endfor %} + {% else %} + {% for network in networks %} + <input type="hidden" class="form-control" name="clone-net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> + {% endfor %} + {% endif %} + {% if request.user.is_superuser %} + <p style="font-weight:bold;">{% trans "Storage devices" %}</p> + {% for disk in clone_disks %} + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{{ disk.dev }} ({{ disk.storage }})</label> + <div class="col-sm-4"> + <input id="disk_name-{{ disk.dev }}" type="text" class="form-control" name="disk-{{ disk.dev }}" value="{{ disk.image }}"/> + </div> + {% ifequal disk.format 'qcow2' %} + <label class="col-sm-2 control-label" style="font-weight:normal;margin-left:-35px;">Metadata</label> + <div class="col-sm-1"> + <input type="checkbox" name="meta-{{ disk.dev }}" value="true" style="margin-top: 10px;"> + </div> + {% endifequal %} + </div> + {% endfor %} + {% else %} + {% for disk in clone_disks %} + <input id="disk_name-{{ disk.dev }}" type="hidden" class="form-control" name="disk-{{ disk.dev }}" value="{{ disk.image }}"/> + {% endfor %} + {% endif %} <div class="form-group"> <label class="col-sm-3 control-label">{% trans "Title" %}</label> <div class="col-sm-6"> @@ -749,6 +775,8 @@ </form> <div class="clearfix"></div> </div> + {% endif %} + {% if request.user.is_superuser %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="migrate"> <p>{% trans "For migration both host servers must have equal settings and OS type" %}</p> <form class="form-horizontal" method="post" role="form">{% csrf_token %} @@ -986,8 +1014,8 @@ } </script> <script> - function guess_mac_address(net) { - new_vname = $('#clone_name').val(); + function guess_mac_address(src_elem, net) { + new_vname = $(src_elem).val(); $.getJSON('/instance/guess_mac_address/' + new_vname + '/', function(data) { $('input[name="clone-net-mac-'+net+'"]').val(data['mac']); }); @@ -1056,6 +1084,16 @@ $("#console_select_type option[value='" + console_type + "']").prop('selected', true); } }); +{% if not request.user.is_superuser %} + $('#select_clone_name').on('change', function () { + update_clone_disk_name($(this).val()); + guess_mac_address('#select_clone_name', 0); + }); + $(document).ready(function () { + update_clone_disk_name($('#select_clone_name').val()); + guess_mac_address('#select_clone_name', 0); + }); +{% endif %} </script> <script> $(function () { diff --git a/instances/views.py b/instances/views.py index 1d2c623..5c7903e 100644 --- a/instances/views.py +++ b/instances/views.py @@ -20,6 +20,7 @@ from vrtManager.connection import connection_manager from libvirt import libvirtError, VIR_DOMAIN_XML_SECURE from webvirtcloud.settings import QEMU_KEYMAPS, QEMU_CONSOLE_TYPES from logs.views import addlogmsg +from django.conf import settings @login_required @@ -204,6 +205,31 @@ def instance(request, compute_id, vname): else: return long(float(size_str)) + def get_clone_free_names(size=10): + prefix = settings.CLONE_INSTANCE_DEFAULT_PREFIX + free_names = [] + existing_names = [i.name for i in Instance.objects.filter(name__startswith=prefix)] + index = 1 + while len(free_names) < size: + new_name = prefix + str(index) + if new_name not in existing_names: + free_names.append(new_name) + index += 1 + return free_names + + def check_user_quota(): + userinstances = UserInstance.objects.filter(user__id=request.user.id) + instances_count = len(userinstances) + cpus_count = instances_count + memory_count = instances_count * 2048 + if request.user.userattributes.max_instances > 0 and instances_count > request.user.userattributes.max_instances: + return "instance" + if request.user.userattributes.max_cpus > 0 and cpus_count > request.user.userattributes.max_cpus: + return "cpu" + if request.user.userattributes.max_memory > 0 and memory_count > request.user.userattributes.max_memory: + return "memory" + return "" + try: conn = wvmInstance(compute.hostname, compute.login, @@ -241,6 +267,7 @@ def instance(request, compute_id, vname): has_managed_save_image = conn.get_managed_save_image() clone_disks = show_clone_disk(disks, vname) console_passwd = conn.get_console_passwd() + clone_free_names = get_clone_free_names() try: instance = Instance.objects.get(compute_id=compute_id, name=vname) @@ -496,23 +523,6 @@ def instance(request, compute_id, vname): addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(reverse('instance', args=[compute_id, vname])) - if 'clone' in request.POST: - clone_data = {} - clone_data['name'] = request.POST.get('name', '') - - check_instance = Instance.objects.filter(name=clone_data['name']) - if check_instance: - msg = _("Instance '%s' already exists!" % clone_data['name']) - error_messages.append(msg) - else: - for post in request.POST: - clone_data[post] = request.POST.get(post, '') - - conn.clone_instance(clone_data) - msg = _("Clone") - addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(reverse('instance', args=[compute_id, clone_data['name']])) - if 'change_network' in request.POST: network_data = {} @@ -539,6 +549,34 @@ def instance(request, compute_id, vname): addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(request.get_full_path() + '#options') + if request.user.is_superuser or request.user.userattributes.can_clone_instances: + if 'clone' in request.POST: + clone_data = {} + clone_data['name'] = request.POST.get('name', '') + + quota_msg = check_user_quota() + if quota_msg: + msg = _("User %s quota reached, cannot create '%s'!" % (quota_msg, clone_data['name'])) + error_messages.append(msg) + + check_instance = Instance.objects.filter(name=clone_data['name']) + if check_instance: + msg = _("Instance '%s' already exists!" % clone_data['name']) + error_messages.append(msg) + else: + for post in request.POST: + clone_data[post] = request.POST.get(post, '') + + new_uuid = conn.clone_instance(clone_data) + new_instance = Instance(compute_id=compute_id, name=clone_data['name'], uuid=new_uuid) + new_instance.save() + userinstance = UserInstance(instance_id=new_instance.id, user_id=request.user.id) + userinstance.save() + + msg = _("Clone of '%s'" % instance.name) + addlogmsg(request.user.username, new_instance.name, msg) + return HttpResponseRedirect(reverse('instance', args=[compute_id, clone_data['name']])) + conn.close() except libvirtError as lib_err: diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 5e2ec68..08e0c39 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -697,9 +697,10 @@ class wvmInstance(wvmConnect): 'description': clone_data.get('clone-description', ''), } self._set_options(tree, options) - self._defineXML(ElementTree.tostring(tree)) + return self.get_instance(clone_data['name']).UUIDString() + def change_network(self, network_data): xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE) tree = ElementTree.fromstring(xml) diff --git a/webvirtcloud/settings.py b/webvirtcloud/settings.py index 471659f..2b70b02 100644 --- a/webvirtcloud/settings.py +++ b/webvirtcloud/settings.py @@ -113,3 +113,4 @@ LIBVIRT_KEEPALIVE_INTERVAL = 5 LIBVIRT_KEEPALIVE_COUNT = 5 ALLOW_INSTANCE_MULTIPLE_OWNER = True +CLONE_INSTANCE_DEFAULT_PREFIX = 'ourea'