diff --git a/.gitignore b/.gitignore index 7d921d2..b3ebced 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ venv .DS_* *.pyc db.sqlite3 +console/cert.pem +tags +dhcpd.* diff --git a/README.md b/README.md index 1d140ff..f1b3767 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ WebVirtCloud is a virtualization web interface for admins and users. It can dele ### Install WebVirtCloud panel (Ubuntu) ```bash -sudo apt-get -y install git python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev nginx supervisor libsasl2-modules +sudo apt-get -y install git python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev nginx supervisor libsasl2-modules gcc pkg-config git clone https://github.com/retspen/webvirtcloud cd webvirtcloud sudo cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d @@ -185,6 +185,12 @@ webvirtcloud RUNNING pid 24185, uptime 2:59:14 ``` +#### Apache mod_wsgi configuration +``` +WSGIDaemonProcess webvirtcloud threads=2 maximum-requests=1000 display-name=webvirtcloud +WSGIScriptAlias / /srv/webvirtcloud/webvirtcloud/wsgi.py +``` + #### Install final required packages for libvirtd and others on Host Server ```bash wget -O - https://clck.ru/9V9fH | sudo sh diff --git a/accounts/backends.py b/accounts/backends.py new file mode 100644 index 0000000..77aa509 --- /dev/null +++ b/accounts/backends.py @@ -0,0 +1,7 @@ +from django.contrib.auth.backends import RemoteUserBackend + +class MyRemoteUserBackend(RemoteUserBackend): + def configure_user(self, user): + user.is_superuser = True + return user + diff --git a/accounts/forms.py b/accounts/forms.py index 55d5c29..4127ac4 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -2,13 +2,14 @@ import re from django import forms from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User +from django.conf import settings class UserAddForm(forms.Form): name = forms.CharField(label="Name", error_messages={'required': _('No User name has been entered')}, max_length=20) - password = forms.CharField(required=True, error_messages={'required': _('No password has been entered')},) + password = forms.CharField(required=not settings.ALLOW_EMPTY_PASSWORD, error_messages={'required': _('No password has been entered')},) def clean_name(self): name = self.cleaned_data['name'] 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/migrations/0006_userattributes_max_disk_size.py b/accounts/migrations/0006_userattributes_max_disk_size.py new file mode 100644 index 0000000..3d21f5f --- /dev/null +++ b/accounts/migrations/0006_userattributes_max_disk_size.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', '0005_userattributes_can_clone_instances'), + ] + + operations = [ + migrations.AddField( + model_name='userattributes', + name='max_disk_size', + field=models.IntegerField(default=0), + ), + ] diff --git a/accounts/migrations/0007_auto_20160426_0635.py b/accounts/migrations/0007_auto_20160426_0635.py new file mode 100644 index 0000000..2f92aba --- /dev/null +++ b/accounts/migrations/0007_auto_20160426_0635.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0006_userattributes_max_disk_size'), + ] + + operations = [ + migrations.AlterField( + model_name='userattributes', + name='max_cpus', + field=models.IntegerField(default=1), + ), + migrations.AlterField( + model_name='userattributes', + name='max_disk_size', + field=models.IntegerField(default=20), + ), + migrations.AlterField( + model_name='userattributes', + name='max_instances', + field=models.IntegerField(default=1), + ), + migrations.AlterField( + model_name='userattributes', + name='max_memory', + field=models.IntegerField(default=2048), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 20efc6f..06fefee 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -21,3 +21,14 @@ 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=1) + max_cpus = models.IntegerField(default=1) + max_memory = models.IntegerField(default=2048) + max_disk_size = models.IntegerField(default=20) + + def __unicode__(self): + return self.user.username diff --git a/accounts/templates/accounts.html b/accounts/templates/accounts.html index 9ebff0b..4fdfc8a 100644 --- a/accounts/templates/accounts.html +++ b/accounts/templates/accounts.html @@ -71,6 +71,48 @@ <input type="password" name="user_pass" class="form-control" value=""> </div> </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Is staff" %}</label> + <div class="col-sm-2"> + <input type="checkbox" name="user_is_staff" {% if user.is_staff %}checked{% endif %}> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Is superuser" %}</label> + <div class="col-sm-2"> + <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 (MB)" %}</label> + <div class="col-sm-6"> + <input type="text" name="userattributes_max_memory" class="form-control" value="{{ user.userattributes.max_memory }}"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Max disk size (GB)" %}</label> + <div class="col-sm-6"> + <input type="text" name="userattributes_max_disk_size" class="form-control" value="{{ user.userattributes.max_disk_size }}"> + </div> + </div> </div> <div class="modal-footer"> <button type="submit" class="pull-left btn btn-danger" name="delete"> @@ -99,4 +141,4 @@ {% endfor %} {% endif %} </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/accounts/templates/create_user_block.html b/accounts/templates/create_user_block.html index 7752f56..f4b4679 100644 --- a/accounts/templates/create_user_block.html +++ b/accounts/templates/create_user_block.html @@ -23,7 +23,7 @@ <div class="form-group"> <label class="col-sm-4 control-label">{% trans "Password" %}</label> <div class="col-sm-6"> - <input type="password" class="form-control" name="password" placeholder="*******" required> + <input type="password" class="form-control" name="password" placeholder="*******" {% if not allow_empty_password %}required{% endif %}> </div> </div> </div> @@ -35,4 +35,4 @@ </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> -{% endif %} \ No newline at end of file +{% endif %} diff --git a/accounts/views.py b/accounts/views.py index 3b6a63e..155ecc8 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -3,18 +3,19 @@ from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User -from accounts.models import UserInstance, UserSSHKey +from django.contrib.auth.decorators import login_required +from accounts.models import * from instances.models import Instance from accounts.forms import UserAddForm +from django.conf import settings +@login_required def profile(request): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) error_messages = [] user = User.objects.get(id=request.user.id) @@ -63,21 +64,28 @@ def profile(request): return HttpResponseRedirect(request.get_full_path()) return render(request, 'profile.html', locals()) - +@login_required def accounts(request): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) + def create_missing_userattributes(users): + for user in users: + try: + userattributes = user.userattributes + except UserAttributes.DoesNotExist: + userattributes = UserAttributes(user=user) + userattributes.save() if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) error_messages = [] - users = User.objects.filter(is_staff=False, is_superuser=False) + users = User.objects.all().order_by('username') + create_missing_userattributes(users) + allow_empty_password = settings.ALLOW_EMPTY_PASSWORD if request.method == 'POST': if 'create' in request.POST: @@ -96,7 +104,17 @@ def accounts(request): user_pass = request.POST.get('user_pass', '') user_edit = User.objects.get(id=user_id) user_edit.set_password(user_pass) + 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() + + userattributes = user_edit.userattributes + 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_disk_size = request.POST.get('userattributes_max_disk_size', 0) + userattributes.save() return HttpResponseRedirect(request.get_full_path()) if 'block' in request.POST: user_id = request.POST.get('user_id', '') @@ -123,22 +141,20 @@ def accounts(request): return render(request, 'accounts.html', locals()) +@login_required def account(request, user_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) error_messages = [] user = User.objects.get(id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id) - instances = Instance.objects.all() + instances = Instance.objects.all().order_by('name') if user.username == request.user.username: return HttpResponseRedirect(reverse('profile')) @@ -162,12 +178,17 @@ def account(request, user_id): return HttpResponseRedirect(request.get_full_path()) if 'add' in request.POST: inst_id = request.POST.get('inst_id', '') - try: - check_inst = UserInstance.objects.get(instance_id=int(inst_id)) + + if settings.ALLOW_INSTANCE_MULTIPLE_OWNER: + check_inst = UserInstance.objects.filter(instance_id=int(inst_id), user_id=int(user_id)) + else: + check_inst = UserInstance.objects.filter(instance_id=int(inst_id)) + + if check_inst: msg = _("Instance already added") error_messages.append(msg) - except UserInstance.DoesNotExist: - add_user_inst = UserInstance(instance_id=int(inst_id), user_id=user_id) + else: + add_user_inst = UserInstance(instance_id=int(inst_id), user_id=int(user_id)) add_user_inst.save() return HttpResponseRedirect(request.get_full_path()) diff --git a/computes/forms.py b/computes/forms.py index a626106..7dfcbe6 100644 --- a/computes/forms.py +++ b/computes/forms.py @@ -149,6 +149,8 @@ class ComputeEditHostForm(forms.Form): class ComputeAddSocketForm(forms.Form): name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, max_length=20) + details = forms.CharField(error_messages={'required': _('No details has been entred')}, + max_length=50) def clean_name(self): name = self.cleaned_data['name'] diff --git a/computes/migrations/0002_compute_details.py b/computes/migrations/0002_compute_details.py new file mode 100644 index 0000000..1e0fdf5 --- /dev/null +++ b/computes/migrations/0002_compute_details.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +class Migration(migrations.Migration): + + dependencies = [ + ('computes', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='Compute', + name='details', + field=models.CharField(max_length=50, null=True, blank=True), + ), + ] diff --git a/computes/models.py b/computes/models.py index 6ee7de8..df9bf02 100644 --- a/computes/models.py +++ b/computes/models.py @@ -6,6 +6,7 @@ class Compute(models.Model): hostname = models.CharField(max_length=20) login = models.CharField(max_length=20) password = models.CharField(max_length=14, blank=True, null=True) + details = models.CharField(max_length=50, null=True, blank=True) type = models.IntegerField() def __unicode__(self): diff --git a/computes/templates/computes.html b/computes/templates/computes.html index 7c1c28f..2ffc6f4 100644 --- a/computes/templates/computes.html +++ b/computes/templates/computes.html @@ -45,6 +45,11 @@ {% else %} <p>{% trans "Not Connected" %}</p> {% endif %} + {% if compute.details %} + <p>{% trans compute.details %}</p> + {% else %} + <p>{% trans "No details available" %}</p> + {% endif %} </div> </div> diff --git a/computes/templates/create_comp_block.html b/computes/templates/create_comp_block.html index 57e327a..9e9a965 100644 --- a/computes/templates/create_comp_block.html +++ b/computes/templates/create_comp_block.html @@ -141,6 +141,14 @@ <input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+"> </div> </div> + + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Details" %}</label> + <div class="col-sm-6"> + <input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}"> + </div> + </div> + </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal"> @@ -156,4 +164,4 @@ </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div><!-- /.modal --> -{% endif %} \ No newline at end of file +{% endif %} diff --git a/computes/templates/overview.html b/computes/templates/overview.html index 09d5d83..8bfad6e 100644 --- a/computes/templates/overview.html +++ b/computes/templates/overview.html @@ -40,6 +40,7 @@ <p>{% trans "Logical CPUs" %}</p> <p>{% trans "Processor" %}</p> <p>{% trans "Connection" %}</p> + <p>{% trans "Details" %}</p> </div> <div class="col-xs-8 col-sm-7"> <p>{{ hostname }}</p> @@ -49,6 +50,7 @@ <p>{{ logical_cpu }}</p> <p>{{ model_cpu }}</p> <p>{{ uri_conn }}</p> + <p>{{ compute.details }}</p> </div> </div> <div class="row"> diff --git a/computes/views.py b/computes/views.py index 39af4d0..13fb70d 100644 --- a/computes/views.py +++ b/computes/views.py @@ -3,6 +3,7 @@ import json from django.http import HttpResponse, HttpResponseRedirect from django.core.urlresolvers import reverse from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.decorators import login_required from computes.models import Compute from instances.models import Instance from accounts.models import UserInstance @@ -12,15 +13,13 @@ from vrtManager.connection import CONN_SSH, CONN_TCP, CONN_TLS, CONN_SOCKET, con from libvirt import libvirtError +@login_required def computes(request): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) @@ -36,14 +35,15 @@ def computes(request): 'status': connection_manager.host_is_up(compute.type, compute.hostname), 'type': compute.type, 'login': compute.login, - 'password': compute.password + 'password': compute.password, + 'details': compute.details }) return compute_data error_messages = [] - computes = Compute.objects.filter() + computes = Compute.objects.filter().order_by('name') computes_info = get_hosts_status(computes) - + if request.method == 'POST': if 'host_del' in request.POST: compute_id = request.POST.get('host_id', '') @@ -104,6 +104,7 @@ def computes(request): if form.is_valid(): data = form.cleaned_data new_socket_host = Compute(name=data['name'], + details=data['details'], hostname='localhost', type=CONN_SOCKET, login='', @@ -122,6 +123,7 @@ def computes(request): compute_edit.hostname = data['hostname'] compute_edit.login = data['login'] compute_edit.password = data['password'] + compute.edit_details = data['details'] compute_edit.save() return HttpResponseRedirect(request.get_full_path()) else: @@ -130,15 +132,13 @@ def computes(request): return render(request, 'computes.html', locals()) +@login_required def overview(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) @@ -160,15 +160,13 @@ def overview(request, compute_id): return render(request, 'overview.html', locals()) +@login_required def compute_graph(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('login')) - points = 5 datasets = {} cookies = {} diff --git a/console/views.py b/console/views.py index a123064..4651c87 100644 --- a/console/views.py +++ b/console/views.py @@ -2,6 +2,7 @@ import re from django.shortcuts import render from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from instances.models import Instance from vrtManager.instance import wvmInstance from webvirtcloud.settings import WS_PORT @@ -9,15 +10,13 @@ from webvirtcloud.settings import WS_PUBLIC_HOST from libvirt import libvirtError +@login_required def console(request): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('login')) - if request.method == 'GET': token = request.GET.get('token', '') diff --git a/create/templates/create_instance.html b/create/templates/create_instance.html index 9df7326..d2be478 100644 --- a/create/templates/create_instance.html +++ b/create/templates/create_instance.html @@ -239,7 +239,7 @@ </div> {% else %} <div class="col-lg-12"> - <h3 class="page-header">{% trans "Create from flover" %}</h3> + <h3 class="page-header">{% trans "Create from flavor" %}</h3> <div class="table-responsive"> <table class="table table-bordered table-hover"> <thead> diff --git a/create/views.py b/create/views.py index c3e1139..6c223da 100644 --- a/create/views.py +++ b/create/views.py @@ -2,6 +2,7 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from computes.models import Compute from create.models import Flavor from create.forms import FlavorAddForm, NewVMForm @@ -11,15 +12,13 @@ from vrtManager import util from libvirt import libvirtError +@login_required def create_instance(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) diff --git a/instances/migrations/0002_instance_is_template.py b/instances/migrations/0002_instance_is_template.py new file mode 100644 index 0000000..cbf2cdd --- /dev/null +++ b/instances/migrations/0002_instance_is_template.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('instances', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='instance', + name='is_template', + field=models.BooleanField(default=False), + ), + ] diff --git a/instances/models.py b/instances/models.py index 4345cc1..bf2a1c4 100644 --- a/instances/models.py +++ b/instances/models.py @@ -6,6 +6,7 @@ class Instance(models.Model): compute = models.ForeignKey(Compute) name = models.CharField(max_length=20) uuid = models.CharField(max_length=36) + is_template = models.BooleanField(default=False) def __unicode__(self): return self.name diff --git a/instances/templates/create_inst_block.html b/instances/templates/create_inst_block.html index 36ec37d..264aa75 100644 --- a/instances/templates/create_inst_block.html +++ b/instances/templates/create_inst_block.html @@ -35,16 +35,16 @@ {% trans "Close" %} </button> {% if computes %} - <button type="submit" class="btn btn-primary" name="chose" onclick='goto_compute()'> - {% trans "Chose" %} + <button type="submit" class="btn btn-primary" name="choose" onclick='goto_compute()'> + {% trans "Choose" %} </button> {% else %} <button class="btn btn-primary disabled"> - {% trans "Chose" %} + {% trans "Choose" %} </button> {% endif %} </div> </div> <!-- /.modal-content --> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> -{% endif %} \ No newline at end of file +{% endif %} diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 36e7afa..e7ca557 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -2,6 +2,7 @@ {% load i18n %} {% block title %}{% trans "Instance" %} - {{ vname }}{% endblock %} {% block content %} + {% include 'pleasewaitdialog.html' %} <!-- Page Heading --> <div class="row"> <table> @@ -18,6 +19,9 @@ <span class="label label-warning">{% trans "Suspend" %}</span> {% endifequal %} </td> + <td> + <a href="{% url 'instance' compute.id vname %}" type="button" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-refresh"></span></a> + </td> </tr> </table> <table width="65%"> @@ -39,6 +43,9 @@ {% endfor %} </tr> </table> + {% if user_quota_msg %} + <span class="label label-warning">{{ user_quota_msg|capfirst }} quota reached.</span> + {% endif %} <hr> </div> @@ -204,7 +211,12 @@ <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="boot"> <p>{% trans "Click on Boot button to start this instance." %}</p> <form action="" method="post" role="form">{% csrf_token %} - <input type="submit" name="poweron" class="btn btn-lg btn-success pull-right" value="{% trans "Power On" %}"> + {% if instance.is_template %} + <p>{% trans "Template instance cannot be started." %}</p> + <input type="submit" name="poweron" class="btn btn-lg btn-success pull-right disabled" value="{% trans "Power On" %}"> + {% else %} + <input type="submit" name="poweron" class="btn btn-lg btn-success pull-right" value="{% trans "Power On" %}"> + {% endif %} <div class="clearfix"></div> </form> </div> @@ -355,6 +367,15 @@ <small><input type="checkbox" class="js-custom__checkbox" /> {% trans "Custom value" %}</small> </div> </div> + <p style="font-weight:bold;">{% trans "Disk allocation (B):" %}</p> + {% for disk in disks %} + <div class="form-group"> + <label class="col-sm-4 control-label" style="font-weight:normal;">{% trans "Current allocation" %} ({{ disk.dev }})</label> + <div class="col-sm-4 js-custom__container"> + <input type="text" name="disk_size_{{ disk.dev }}" class="form-control" value="{{ disk.size|filesizeformat }}" /> + </div> + </div> + {% endfor %} {% ifequal status 5 %} <button type="submit" class="btn btn-lg btn-success pull-right" name="resize">{% trans "Resize" %}</button> {% else %} @@ -486,11 +507,20 @@ </li> {% endif %} {% if request.user.is_superuser %} + <li role="presentation"> + <a href="#network" aria-controls="network" role="tab" data-toggle="tab"> + {% 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" %} @@ -501,6 +531,16 @@ {% trans "XML" %} </a> </li> + <li role="presentation"> + <a href="#options" aria-controls="options" role="tab" data-toggle="tab"> + {% trans "Options" %} + </a> + </li> + <li role="presentation"> + <a href="#users" aria-controls="users" role="tab" data-toggle="tab"> + {% trans "Users" %} + </a> + </li> {% endif %} </ul> <!-- Tab panes --> @@ -651,51 +691,118 @@ </div> {% endif %} {% if request.user.is_superuser %} + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="network"> + <p>{% trans "Assign network device to bridge" %}</p> + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} + <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 }}</label> + <div class="col-sm-4"> + <input type="text" class="form-control" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> + </div> + <div class="col-sm-3"> + <input type="text" class="form-control" name="net-source-{{ forloop.counter0 }}" value="{{ network.nic }}"/> + </div> + </div> + {% endfor %} + {% ifequal status 5 %} + <button type="submit" class="btn btn-lg btn-success pull-right" name="change_network">{% trans "Change" %}</button> + {% else %} + <button class="btn btn-lg btn-success pull-right disabled" name="change_network">{% trans "Change" %}</button> + {% endifequal %} + </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-3"> - <input type="text" class="form-control" name="name" value="{{ vname }}-clone"/> + {% if request.user.is_superuser %} + <div class="col-sm-4"> + <input id="clone_name" type="text" class="form-control" name="name" value="{{ vname }}-clone"/> + </div> + <div class="col-sm-4"> + <button type="button" class="btn btn-sm btn-success pull-left" name="guess-clone-name" + onclick="guess_clone_name()" style="margin-top: 2px;">{% trans "Guess" %}</button> + </div> + {% else %} + <div class="col-sm-4"> + <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> + </div> + {% endif %} + </div> + {% 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> + <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"> + <input type="text" name="clone-title" class="form-control"> </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-3"> - <input type="text" class="form-control" name="net-{{ forloop.counter0 }}" value="{{ network.mac }}"/> - </div> - <div class="col-sm-1"> - <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> - </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Description" %}</label> + <div class="col-sm-6"> + <textarea name="clone-description" class="form-control"></textarea> </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-3"> - <input 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 %} + </div> {% ifequal status 5 %} - <button type="submit" class="btn btn-lg btn-success pull-right" name="clone">{% trans "Clone" %}</button> + <button type="submit" class="btn btn-lg btn-success pull-right" name="clone" onclick="showPleaseWaitDialog();">{% trans "Clone" %}</button> {% else %} <button class="btn btn-lg btn-success pull-right disabled" name="clone">{% trans "Clone" %}</button> {% endifequal %} </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 %} @@ -722,7 +829,7 @@ <div class="form-group"> <label class="col-sm-3 control-label">{% trans "Live migration" %}</label> <div class="col-sm-6"> - <input type="checkbox" name="live_migrate" value="true" id="vm_live_migrate"> + <input type="checkbox" name="live_migrate" value="true" id="vm_live_migrate" checked> </div> </div> <div class="form-group"> @@ -734,11 +841,11 @@ <div class="form-group"> <label class="col-sm-3 control-label">{% trans "Delete original" %}</label> <div class="col-sm-6"> - <input type="checkbox" name="xml_delete" value="true" id="xml_delete"> + <input type="checkbox" name="xml_delete" value="true" id="xml_delete" checked> </div> </div> {% if computes_count != 1 %} - <button type="submit" class="btn btn-lg btn-success pull-right" name="migrate">{% trans "Migrate" %}</button> + <button type="submit" class="btn btn-lg btn-success pull-right" name="migrate" onclick="showPleaseWaitDialog();">{% trans "Migrate" %}</button> {% else %} <button class="btn btn-lg btn-success pull-right disabled">{% trans "Migrate" %}</button> {% endif %} @@ -764,6 +871,41 @@ </form> <div class="clearfix"></div> </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="options"> + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Title" %}</label> + <div class="col-sm-6"> + <input type="text" name="title" class="form-control" value="{{ title }}"> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Description" %}</label> + <div class="col-sm-6"> + <textarea name="description" class="form-control">{{ description }}</textarea> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label">{% trans "Is template" %}</label> + <div class="col-sm-6"> + <input type="checkbox" name="is_template" value="true" id="is_template" {% if instance.is_template %}checked{% endif %}> + </div> + </div> + {% ifequal status 5 %} + <button type="submit" class="btn btn-lg btn-success pull-right" name="change_options">{% trans "Change" %}</button> + {% else %} + <button class="btn btn-lg btn-success pull-right disabled" name="change_options">{% trans "Change" %}</button> + {% endifequal %} + </form> + <div class="clearfix"></div> + </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="users"> + <p style="font-weight:bold;">{% trans "Instance owners" %}</p> + {% for userinstance in userinstances %} + <p><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></p> + {% endfor %} + <div class="clearfix"></div> + </div> {% endif %} </div> </div> @@ -847,7 +989,7 @@ <form class="form-group" method="post" role="form">{% csrf_token %} <div class="checkbox" style="margin-left: 8px;"> <label> - <input type="checkbox" name="delete_disk" value="true"> + <input type="checkbox" name="delete_disk" value="true" checked> <strong>{% trans "Remove Instance's data" %}</strong> </label> </div> @@ -892,7 +1034,7 @@ macAddress+=hexDigits.charAt(Math.round(Math.random()*16)); if (i != 2) macAddress+=":"; } - $('input[name="net-'+net+'"]').val(macAddress); + $('input[name="clone-net-mac-'+net+'"]').val(macAddress); }; </script> <script> @@ -904,6 +1046,46 @@ } } </script> +<script> + 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']); + }); + } +</script> +<script> + function guess_clone_name() { + $.getJSON('/instance/guess_clone_name/', function(data) { + guessed_name = data['name'].split(".")[0]; + $('#clone_name').val(guessed_name); + update_clone_disk_name(guessed_name); + guess_mac_address('#clone_name', 0); + }); + } +</script> +<script> + function update_clone_disk_name(new_vname) { + vname = '{{ vname }}-clone'; + {% for disk in clone_disks %} + disk_name = '{{ disk.image }}'; + disk_minus = disk_name.split('-'); + disk_minus_suffix = disk_minus[disk_minus.length-1]; + disk_minus.pop(); + disk_minus_name = disk_minus.join('-'); + disk_dot = disk_name.split('.') + disk_dot_suffix = disk_dot[disk_dot.length-1]; + if (disk_name.lastIndexOf('-') > -1 && disk_minus_name == vname) { + image = new_vname + "-" + disk_minus_suffix; + } else if (disk_name.lastIndexOf('.') > -1 && disk_dot_suffix.length <= 7) { + image = new_vname + "." + disk_dot_suffix + } else { + image = new_vname + '-clone'; + } + $('#disk_name-{{ disk.dev }}').val(image); + {% endfor %} + } +</script> <script> $(document).on('change', '#console_passwd_gen', function () { if ($(this).prop('checked')) { @@ -928,6 +1110,9 @@ $('#console_keymap_selection').show(); } }); + $('#clone_name').on('input', function () { + update_clone_disk_name($(this).val()); + }); $(document).ready(function () { // set current console keymap or fall back to default var keymap = "{{ console_keymap }}" @@ -942,6 +1127,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 () { @@ -1116,7 +1311,7 @@ } }); } - if (~$.inArray(hash, ['#media', '#clone', '#autostart', '#xmledit', '#vncsettings', '#migrate'])) { + if (~$.inArray(hash, ['#media', '#network', '#clone', '#autostart', '#xmledit', '#vncsettings', '#migrate', '#options', '#users'])) { var btnsect = $('#navbtn>li>a'); $(btnsect).each(function () { if ($(this).attr('href') === '#settings') { diff --git a/instances/templates/instances.html b/instances/templates/instances.html index 144a5cd..8cee448 100644 --- a/instances/templates/instances.html +++ b/instances/templates/instances.html @@ -39,8 +39,8 @@ <table class="table table-hover table-striped sortable-theme-bootstrap" data-sortable> <thead> <tr> - <th>Name</th> - <th>Host</th> + <th>Name<br>Description</th> + <th>Host<br>User</th> <th>Status</th> <th>VCPU</th> <th>Memory</th> @@ -51,8 +51,8 @@ {% for host, inst in all_host_vms.items %} {% for vm, info in inst.items %} <tr> - <td><a href="{% url 'instance' host.0 vm %}">{{ vm }}</a></td> - <td><a href="{% url 'overview' host.0 %}">{{ host.1 }}</a></td> + <td><a href="{% url 'instance' host.0 vm %}">{{ vm }}</a><br><small><em>{{ info.title }}</em></small></td> + <td><a href="{% url 'overview' host.0 %}">{{ host.1 }}</a><br><small><em>{% if info.userinstances.count > 0 %}{{ info.userinstances.first_user.user.username }}{% if info.userinstances.count > 1 %} (+{{ info.userinstances.count|add:"-1" }}){% endif %}{% endif %}</em></small></td> <td>{% ifequal info.status 1 %} <span class="text-success">{% trans "Active" %}</span> {% endifequal %} @@ -69,9 +69,15 @@ <input type="hidden" name="name" value="{{ vm }}"/> <input type="hidden" name="compute_id" value="{{ host.0 }}"/> {% ifequal info.status 5 %} - <button class="btn btn-sm btn-default" type="submit" name="poweron" title="{% trans "Power On" %}"> - <span class="glyphicon glyphicon-play"></span> - </button> + {% if info.is_template %} + <button class="btn btn-sm btn-default" type="button" name="clone" title="{% trans "Clone" %}" onclick="goto_instance_clone({{ host.0 }}, '{{ vm }}');"> + <span class="glyphicon glyphicon-duplicate"></span> + </button> + {% else %} + <button class="btn btn-sm btn-default" type="submit" name="poweron" title="{% trans "Power On" %}"> + <span class="glyphicon glyphicon-play"></span> + </button> + {% endif %} <button class="btn btn-sm btn-default disabled" title="{% trans "Suspend" %}"> <span class="glyphicon glyphicon-pause"></span> </button> @@ -149,7 +155,7 @@ <tbody class="searchable"> {% for inst, vm in all_user_vms.items %} <tr> - <td><a href="{% url 'instance' vm.compute_id vm.name %}">{{ vm.name }}</a></td> + <td><a href="{% url 'instance' vm.compute_id vm.name %}">{{ vm.name }}</a><br><small><em>{{ vm.title }}</em></small></td> <td>{% ifequal vm.status 1 %} <span class="text-success">{% trans "Active" %}</span> {% endifequal %} @@ -166,9 +172,15 @@ <input type="hidden" name="name" value="{{ vm.name }}"/> <input type="hidden" name="compute_id" value="{{ vm.compute_id }}"/> {% ifequal vm.status 5 %} - <button class="btn btn-sm btn-default" type="submit" name="poweron" title="Power On"> - <span class="glyphicon glyphicon-play"></span> - </button> + {% if inst.instance.is_template %} + <button class="btn btn-sm btn-default" type="button" name="clone" title="{% trans "Clone" %}" onclick="goto_instance_clone({{ vm.compute_id }}, '{{ vm.name }}');"> + <span class="glyphicon glyphicon-duplicate"></span> + </button> + {% else %} + <button class="btn btn-sm btn-default" type="submit" name="poweron" title="{% trans "Power On" %}"> + <span class="glyphicon glyphicon-play"></span> + </button> + {% endif %} <button class="btn btn-sm btn-default disabled" title="{% trans "Power Off" %}"> <span class="glyphicon glyphicon-off"></span> </button> @@ -227,18 +239,30 @@ } </script> <script> + function filter_table() { + var rex = new RegExp($(this).val(), 'i'); + $('.searchable tr').hide(); + $('.searchable tr').filter(function () { + return rex.test($(this).text()); + }).show(); + Cookies.set("instances_filter", $(this).val(), { expires: 1 }); + } $(document).ready(function () { + instances_filter_cookie = Cookies.get("instances_filter"); + if (instances_filter_cookie) { + $('#filter').val(instances_filter_cookie); + $('#filter').each(filter_table); + } (function ($) { - $('#filter').keyup(function () { - var rex = new RegExp($(this).val(), 'i'); - $('.searchable tr').hide(); - $('.searchable tr').filter(function () { - return rex.test($(this).text()); - }).show(); - }) + $('#filter').keyup(filter_table) }(jQuery)); }); </script> +<script> + function goto_instance_clone(compute, instance) { + window.location = "/instance/" + compute + "/" + instance + "/#clone"; + } +</script> {% if request.user.is_superuser %} <script> function goto_compute() { diff --git a/instances/urls.py b/instances/urls.py index ce53b89..e8f263b 100644 --- a/instances/urls.py +++ b/instances/urls.py @@ -8,4 +8,10 @@ urlpatterns = [ views.inst_graph, name='inst_graph'), url(r'^status/(?P<compute_id>[0-9]+)/(?P<vname>[\w\-\.]+)/$', views.inst_status, name='inst_status'), + url(r'^guess_mac_address/(?P<vname>[\w\-\.]+)/$', + views.guess_mac_address, name='guess_mac_address'), + url(r'^guess_clone_name/$', + views.guess_clone_name, name='guess_clone_name'), + url(r'^check_instance/(?P<vname>[\w\-\.]+)/$', + views.check_instance, name='check_instance'), ] diff --git a/instances/views.py b/instances/views.py index 4d29df8..89f6765 100644 --- a/instances/views.py +++ b/instances/views.py @@ -1,3 +1,4 @@ +import os import time import json import socket @@ -9,6 +10,7 @@ from django.http import HttpResponse, HttpResponseRedirect from django.core.urlresolvers import reverse from django.shortcuts import render, get_object_or_404 from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.decorators import login_required from computes.models import Compute from instances.models import Instance from accounts.models import UserInstance, UserSSHKey @@ -19,34 +21,41 @@ from vrtManager.util import randomPasswd 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 def index(request): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('login')) - else: - return HttpResponseRedirect(reverse('instances')) + return HttpResponseRedirect(reverse('instances')) +@login_required def instances(request): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - error_messages = [] all_host_vms = {} all_user_vms = {} computes = Compute.objects.all() + def get_userinstances_info(instance): + info = {} + uis = UserInstance.objects.filter(instance=instance) + info['count'] = len(uis) + if len(uis) > 0: + info['first_user'] = uis[0] + else: + info['first_user'] = None + return info + if not request.user.is_superuser: user_instances = UserInstance.objects.filter(user_id=request.user.id) for usr_inst in user_instances: @@ -70,6 +79,8 @@ def instances(request): check_uuid = Instance.objects.get(compute_id=comp.id, name=vm) if check_uuid.uuid != info['uuid']: check_uuid.save() + all_host_vms[comp.id, comp.name][vm]['is_template'] = check_uuid.is_template + all_host_vms[comp.id, comp.name][vm]['userinstances'] = get_userinstances_info(check_uuid) except Instance.DoesNotExist: check_uuid = Instance(compute_id=comp.id, name=vm, uuid=info['uuid']) check_uuid.save() @@ -145,15 +156,13 @@ def instances(request): return render(request, 'instances.html', locals()) +@login_required def instance(request, compute_id, vname): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - error_messages = [] messages = [] compute = get_object_or_404(Compute, pk=compute_id) @@ -173,12 +182,15 @@ def instance(request, compute_id, vname): if not userinstace: return HttpResponseRedirect(reverse('index')) - def show_clone_disk(disks): + def show_clone_disk(disks, vname=''): clone_disk = [] for disk in disks: if disk['image'] is None: continue - if disk['image'].count(".") and len(disk['image'].rsplit(".", 1)[1]) <= 7: + if disk['image'].count("-") and disk['image'].rsplit("-", 1)[0] == vname: + name, suffix = disk['image'].rsplit("-", 1) + image = name + "-clone" + "-" + suffix + elif disk['image'].count(".") and len(disk['image'].rsplit(".", 1)[1]) <= 7: name, suffix = disk['image'].rsplit(".", 1) image = name + "-clone" + "." + suffix else: @@ -187,6 +199,71 @@ def instance(request, compute_id, vname): {'dev': disk['dev'], 'storage': disk['storage'], 'image': image, 'format': disk['format']}) return clone_disk + + def filesizefstr(size_str): + if size_str == '': + return 0 + size_str = size_str.encode('ascii', 'ignore').upper().translate(None, " B") + if 'K' == size_str[-1]: + return long(float(size_str[:-1]))<<10 + elif 'M' == size_str[-1]: + return long(float(size_str[:-1]))<<20 + elif 'G' == size_str[-1]: + return long(float(size_str[:-1]))<<30 + elif 'T' == size_str[-1]: + return long(float(size_str[:-1]))<<40 + elif 'P' == size_str[-1]: + return long(float(size_str[:-1]))<<50 + 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(instance, cpu, memory, disk_size): + user_instances = UserInstance.objects.filter(user_id=request.user.id, instance__is_template=False) + instance += len(user_instances) + for usr_inst in user_instances: + if connection_manager.host_is_up(usr_inst.instance.compute.type, + usr_inst.instance.compute.hostname): + conn = wvmInstance(usr_inst.instance.compute, + usr_inst.instance.compute.login, + usr_inst.instance.compute.password, + usr_inst.instance.compute.type, + usr_inst.instance.name) + cpu += int(conn.get_vcpu()) + memory += int(conn.get_memory()) + for disk in conn.get_disk_device(): + disk_size += int(disk['size'])>>30 + + ua = request.user.userattributes + msg = "" + if ua.max_instances > 0 and instance > ua.max_instances: + msg = "instance" + if settings.QUOTA_DEBUG: + msg += " (%s > %s)" % (instance, ua.max_instances) + if ua.max_cpus > 0 and cpu > ua.max_cpus: + msg = "cpu" + if settings.QUOTA_DEBUG: + msg += " (%s > %s)" % (cpu, ua.max_cpus) + if ua.max_memory > 0 and memory > ua.max_memory: + msg = "memory" + if settings.QUOTA_DEBUG: + msg += " (%s > %s)" % (memory, ua.max_memory) + if ua.max_disk_size > 0 and disk_size > ua.max_disk_size: + msg = "disk" + if settings.QUOTA_DEBUG: + msg += " (%s > %s)" % (disk_size, ua.max_disk_size) + return msg try: conn = wvmInstance(compute.hostname, @@ -202,6 +279,7 @@ def instance(request, compute_id, vname): uuid = conn.get_uuid() memory = conn.get_memory() cur_memory = conn.get_cur_memory() + title = conn.get_title() description = conn.get_description() disks = conn.get_disk_device() media = conn.get_media_device() @@ -222,8 +300,10 @@ def instance(request, compute_id, vname): snapshots = sorted(conn.get_snapshot(), reverse=True) inst_xml = conn._XMLDesc(VIR_DOMAIN_XML_SECURE) has_managed_save_image = conn.get_managed_save_image() - clone_disks = show_clone_disk(disks) + clone_disks = show_clone_disk(disks, vname) console_passwd = conn.get_console_passwd() + clone_free_names = get_clone_free_names() + user_quota_msg = check_user_quota(0, 0, 0, 0) try: instance = Instance.objects.get(compute_id=compute_id, name=vname) @@ -234,6 +314,8 @@ def instance(request, compute_id, vname): instance = Instance(compute_id=compute_id, name=vname, uuid=uuid) instance.save() + userinstances = UserInstance.objects.filter(instance=instance).order_by('user__username') + if request.method == 'POST': if 'poweron' in request.POST: conn.start() @@ -271,15 +353,11 @@ def instance(request, compute_id, vname): instance_name = instance.name instance.delete() - if not request.user.is_superuser: - del_userinstance = UserInstance.objects.get(id=userinstace.id) + try: + del_userinstance = UserInstance.objects.filter(instance__compute_id=compute_id, instance__name=vname) del_userinstance.delete() - else: - try: - del_userinstance = UserInstance.objects.filter(instance__compute_id=compute_id, instance__name=vname) - del_userinstance.delete() - except UserInstance.DoesNotExist: - pass + except UserInstance.DoesNotExist: + pass msg = _("Destroy") addlogmsg(request.user.username, instance_name, msg) @@ -331,20 +409,37 @@ def instance(request, compute_id, vname): error_messages.append(msg) if 'resize' in request.POST and (request.user.is_superuser or userinstace.is_change): - vcpu = request.POST.get('vcpu', '') - cur_vcpu = request.POST.get('cur_vcpu', '') - memory = request.POST.get('memory', '') - memory_custom = request.POST.get('memory_custom', '') - if memory_custom: - memory = memory_custom - cur_memory = request.POST.get('cur_memory', '') - cur_memory_custom = request.POST.get('cur_memory_custom', '') - if cur_memory_custom: - cur_memory = cur_memory_custom - conn.resize(cur_memory, memory, cur_vcpu, vcpu) - msg = _("Resize") - addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#resize') + new_vcpu = request.POST.get('vcpu', '') + new_cur_vcpu = request.POST.get('cur_vcpu', '') + new_memory = request.POST.get('memory', '') + new_memory_custom = request.POST.get('memory_custom', '') + if new_memory_custom: + new_memory = new_memory_custom + new_cur_memory = request.POST.get('cur_memory', '') + new_cur_memory_custom = request.POST.get('cur_memory_custom', '') + if new_cur_memory_custom: + new_cur_memory = new_cur_memory_custom + disks_new = [] + for disk in disks: + input_disk_size = filesizefstr(request.POST.get('disk_size_' + disk['dev'], '')) + if input_disk_size > disk['size']+(64<<20): + disk['size_new'] = input_disk_size + disks_new.append(disk) + disk_sum = sum([disk['size']>>30 for disk in disks_new]) + disk_new_sum = sum([disk['size_new']>>30 for disk in disks_new]) + quota_msg = check_user_quota(0, int(new_vcpu)-vcpu, int(new_memory)-memory, disk_new_sum-disk_sum) + if not request.user.is_superuser and quota_msg: + msg = _("User %s quota reached, cannot resize '%s'!" % (quota_msg, instance.name)) + error_messages.append(msg) + else: + cur_memory = new_cur_memory + memory = new_memory + cur_vcpu = new_cur_vcpu + vcpu = new_vcpu + conn.resize(cur_memory, memory, cur_vcpu, vcpu, disks_new) + msg = _("Resize") + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#resize') if 'umount_iso' in request.POST: image = request.POST.get('path', '') @@ -470,23 +565,67 @@ def instance(request, compute_id, vname): new_compute.type) conn_migrate.moveto(conn, vname, live, unsafe, xml_del) conn_migrate.define_move(vname) + instance.compute = new_compute + instance.save() conn_migrate.close() msg = _("Migrate") addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(reverse('instance', args=[compute_id, vname])) + if 'change_network' in request.POST: + network_data = {} + + for post in request.POST: + if post.startswith('net-'): + network_data[post] = request.POST.get(post, '') + + conn.change_network(network_data) + msg = _("Edit network") + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#network') + + if 'change_options' in request.POST: + instance.is_template = request.POST.get('is_template', False) + instance.save() + + options = {} + for post in request.POST: + if post in ['title', 'description']: + options[post] = request.POST.get(post, '') + conn.set_options(options) + + msg = _("Edit options") + 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', '') - for post in request.POST: - if 'disk' or 'meta' in post: + disk_sum = sum([disk['size']>>30 for disk in disks]) + quota_msg = check_user_quota(1, vcpu, memory, disk_sum) + check_instance = Instance.objects.filter(name=clone_data['name']) + + if not request.user.is_superuser and quota_msg: + msg = _("User %s quota reached, cannot create '%s'!" % (quota_msg, clone_data['name'])) + error_messages.append(msg) + elif 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']])) + 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, is_delete=True) + 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() @@ -497,15 +636,13 @@ def instance(request, compute_id, vname): return render(request, 'instance.html', locals()) +@login_required def inst_status(request, compute_id, vname): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('login')) - compute = get_object_or_404(Compute, pk=compute_id) response = HttpResponse() response['Content-Type'] = "text/javascript" @@ -524,15 +661,13 @@ def inst_status(request, compute_id, vname): return response +@login_required def inst_graph(request, compute_id, vname): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('login')) - datasets = {} json_blk = [] datasets_blk = {} @@ -632,3 +767,42 @@ def inst_graph(request, compute_id, vname): response.write(data) return response + +@login_required +def guess_mac_address(request, vname): + dhcp_file = '/srv/webvirtcloud/dhcpd.conf' + data = { 'vname': vname, 'mac': '52:54:00:' } + if os.path.isfile(dhcp_file): + with open(dhcp_file, 'r') as f: + name_found = False + for line in f: + if "host %s." % vname in line: + name_found = True + if name_found and "hardware ethernet" in line: + data['mac'] = line.split(' ')[-1].strip().strip(';') + break + return HttpResponse(json.dumps(data)); + +@login_required +def guess_clone_name(request): + dhcp_file = '/srv/webvirtcloud/dhcpd.conf' + prefix = settings.CLONE_INSTANCE_DEFAULT_PREFIX + if os.path.isfile(dhcp_file): + instance_names = [i.name for i in Instance.objects.filter(name__startswith=prefix)] + with open(dhcp_file, 'r') as f: + for line in f: + line = line.strip() + if "host %s" % prefix in line: + fqdn = line.split(' ')[1] + hostname = fqdn.split('.')[0] + if hostname.startswith(prefix) and hostname not in instance_names: + return HttpResponse(json.dumps({'name': hostname})) + return HttpResponse(json.dumps({})); + +@login_required +def check_instance(request, vname): + check_instance = Instance.objects.filter(name=vname) + data = { 'vname': vname, 'exists': False } + if check_instance: + data['exists'] = True + return HttpResponse(json.dumps(data)); diff --git a/interfaces/views.py b/interfaces/views.py index a889b27..921d0b8 100644 --- a/interfaces/views.py +++ b/interfaces/views.py @@ -1,21 +1,20 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from computes.models import Compute from interfaces.forms import AddInterface from vrtManager.interface import wvmInterface, wvmInterfaces from libvirt import libvirtError +@login_required def interfaces(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) @@ -57,15 +56,13 @@ def interfaces(request, compute_id): return render(request, 'interfaces.html', locals()) +@login_required def interface(request, compute_id, iface): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) diff --git a/logs/templates/paging.html b/logs/templates/paging.html new file mode 100644 index 0000000..2cf8f48 --- /dev/null +++ b/logs/templates/paging.html @@ -0,0 +1,12 @@ +<center> + {% if page > 1 %} + <a href="{% url 'showlogspage' page|add:"-1" %}">←</a> + {% else %} + + {% endif %} + {% if has_next_page %} + <a href="{% url 'showlogspage' page|add:"1" %}">→</a> + {% else %} + + {% endif %} +</center> diff --git a/logs/templates/showlogs.html b/logs/templates/showlogs.html index 1aa98da..13d0e34 100644 --- a/logs/templates/showlogs.html +++ b/logs/templates/showlogs.html @@ -22,31 +22,33 @@ </div> </div> {% else %} + {% include "paging.html" %} <div class="table-responsive"> <table class="table table-bordered table-hover"> <thead> <tr> <th>#</th> + <th>{% trans "Date" %}</th> <th>{% trans "User" %}</th> <th>{% trans "Instance" %}</th> <th>{% trans "Message" %}</th> - <th>{% trans "Date" %}</th> </tr> </thead> <tbody> {% for log in logs %} <tr> - <td>{{ forloop.counter }}</td> + <td>{{ log.id }}</td> + <td style="width:130px;">{{ log.date|date:"M d H:i:s" }}</td> <td>{{ log.user }}</a></td> <td>{{ log.instance }}</a></td> <td>{{ log.message }}</td> - <td style="width:130px;">{{ log.date|date:"M d H:i:s" }}</td> </tr> {% endfor %} </tbody> </table> </div> + {% include "paging.html" %} {% endif %} </div> </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/logs/urls.py b/logs/urls.py new file mode 100644 index 0000000..e579387 --- /dev/null +++ b/logs/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from . import views + +urlpatterns = [ + url(r'^$', views.showlogs, name='showlogs'), + url(r'^(?P<page>[0-9]+)/$', views.showlogs, name='showlogspage'), +] diff --git a/logs/views.py b/logs/views.py index cb408ca..830925e 100644 --- a/logs/views.py +++ b/logs/views.py @@ -2,6 +2,7 @@ from django.shortcuts import render from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from logs.models import Logs +from django.conf import settings def addlogmsg(user, instance, message): @@ -13,7 +14,7 @@ def addlogmsg(user, instance, message): add_log_msg.save() -def showlogs(request): +def showlogs(request, page=1): """ :param request: :return: @@ -25,6 +26,11 @@ def showlogs(request): if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) - logs = Logs.objects.all() + page = int(page) + limit_from = (page-1)*settings.LOGS_PER_PAGE + limit_to = page*settings.LOGS_PER_PAGE + logs = Logs.objects.all().order_by('-date')[limit_from:limit_to+1] + has_next_page = logs.count() > settings.LOGS_PER_PAGE + # TODO: remove last element from queryset, but do not affect database return render(request, 'showlogs.html', locals()) diff --git a/networks/views.py b/networks/views.py index 01347db..24d6bc6 100644 --- a/networks/views.py +++ b/networks/views.py @@ -2,6 +2,7 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from computes.models import Compute from networks.forms import AddNetPool from vrtManager.network import wvmNetwork, wvmNetworks @@ -9,15 +10,13 @@ from vrtManager.network import network_size from libvirt import libvirtError +@login_required def networks(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) @@ -60,15 +59,13 @@ def networks(request, compute_id): return render(request, 'networks.html', locals()) +@login_required def network(request, compute_id, pool): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) diff --git a/secrets/views.py b/secrets/views.py index 5e6bf0e..90e5e36 100644 --- a/secrets/views.py +++ b/secrets/views.py @@ -1,21 +1,20 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from computes.models import Compute from secrets.forms import AddSecret from vrtManager.secrets import wvmSecrets from libvirt import libvirtError +@login_required def secrets(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) diff --git a/static/css/webvirtcloud.css b/static/css/webvirtcloud.css index d7382f1..542e8b9 100644 --- a/static/css/webvirtcloud.css +++ b/static/css/webvirtcloud.css @@ -12,7 +12,7 @@ body { } .container { - max-width: 768px; + max-width: 900px; } .page-header { @@ -132,4 +132,4 @@ p { .keyselect { display: inline; min-width: 250px; -} \ No newline at end of file +} diff --git a/static/js/js.cookie.js b/static/js/js.cookie.js new file mode 100644 index 0000000..d4232b9 --- /dev/null +++ b/static/js/js.cookie.js @@ -0,0 +1,151 @@ +/*! + * JavaScript Cookie v2.1.1 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ +;(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + var OldCookies = window.Cookies; + var api = window.Cookies = factory(); + api.noConflict = function () { + window.Cookies = OldCookies; + return api; + }; + } +}(function () { + function extend () { + var i = 0; + var result = {}; + for (; i < arguments.length; i++) { + var attributes = arguments[ i ]; + for (var key in attributes) { + result[key] = attributes[key]; + } + } + return result; + } + + function init (converter) { + function api (key, value, attributes) { + var result; + if (typeof document === 'undefined') { + return; + } + + // Write + + if (arguments.length > 1) { + attributes = extend({ + path: '/' + }, api.defaults, attributes); + + if (typeof attributes.expires === 'number') { + var expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); + attributes.expires = expires; + } + + try { + result = JSON.stringify(value); + if (/^[\{\[]/.test(result)) { + value = result; + } + } catch (e) {} + + if (!converter.write) { + value = encodeURIComponent(String(value)) + .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); + } else { + value = converter.write(value, key); + } + + key = encodeURIComponent(String(key)); + key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); + key = key.replace(/[\(\)]/g, escape); + + return (document.cookie = [ + key, '=', value, + attributes.expires && '; expires=' + attributes.expires.toUTCString(), // use expires attribute, max-age is not supported by IE + attributes.path && '; path=' + attributes.path, + attributes.domain && '; domain=' + attributes.domain, + attributes.secure ? '; secure' : '' + ].join('')); + } + + // Read + + if (!key) { + result = {}; + } + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling "get()" + var cookies = document.cookie ? document.cookie.split('; ') : []; + var rdecode = /(%[0-9A-Z]{2})+/g; + var i = 0; + + for (; i < cookies.length; i++) { + var parts = cookies[i].split('='); + var name = parts[0].replace(rdecode, decodeURIComponent); + var cookie = parts.slice(1).join('='); + + if (cookie.charAt(0) === '"') { + cookie = cookie.slice(1, -1); + } + + try { + cookie = converter.read ? + converter.read(cookie, name) : converter(cookie, name) || + cookie.replace(rdecode, decodeURIComponent); + + if (this.json) { + try { + cookie = JSON.parse(cookie); + } catch (e) {} + } + + if (key === name) { + result = cookie; + break; + } + + if (!key) { + result[name] = cookie; + } + } catch (e) {} + } + + return result; + } + + api.set = api; + api.get = function (key) { + return api(key); + }; + api.getJSON = function () { + return api.apply({ + json: true + }, [].slice.call(arguments)); + }; + api.defaults = {}; + + api.remove = function (key, attributes) { + api(key, '', extend(attributes, { + expires: -1 + })); + }; + + api.withConverter = init; + + return api; + } + + return init(function () {}); +})); diff --git a/storages/views.py b/storages/views.py index 499d8b3..d3f1965 100644 --- a/storages/views.py +++ b/storages/views.py @@ -2,21 +2,20 @@ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from computes.models import Compute from storages.forms import AddStgPool, AddImage, CloneImage from vrtManager.storage import wvmStorage, wvmStorages from libvirt import libvirtError +@login_required def storages(request, compute_id): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) @@ -68,15 +67,13 @@ def storages(request, compute_id): return render(request, 'storages.html', locals()) +@login_required def storage(request, compute_id, pool): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) diff --git a/templates/base.html b/templates/base.html index e9f4505..cbdd75a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,6 +48,8 @@ <script src="{% static "js/jquery.js" %}"></script> <!-- Bootstrap Core JavaScript --> <script src="{% static "js/bootstrap.min.js" %}"></script> + <!-- JavaScript Cookie --> + <script src="{% static "js/js.cookie.js" %}"></script> {% block script %}{% endblock %} </body> -</html> \ No newline at end of file +</html> diff --git a/templates/pleasewaitdialog.html b/templates/pleasewaitdialog.html new file mode 100644 index 0000000..49c893b --- /dev/null +++ b/templates/pleasewaitdialog.html @@ -0,0 +1,25 @@ +{% load i18n %} + <!-- Please wait dialog --> + <div class="modal fade" id="pleaseWaitDialog" tabindex="-1" role="dialog" aria-labelledby="pleaseWaitDialogLabel" data-backdrop="static" data-keyboard="false"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">{% trans "Processing" %}...</h4> + </div> + <div class="modal-body"> + <div class="progress"> + <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="10" aria-valuemin="0" aria-valuemax="10" style="width:100%"> + </div> + </div> + </div> + </div> + </div> + </div> +<script> +function showPleaseWaitDialog() { + $('#pleaseWaitDialog').modal(); +} +function hidePleaseWaitDialog() { + $('#pleaseWaitDialog').modal('hide'); +} +</script> diff --git a/vrtManager/connection.py b/vrtManager/connection.py index fd9fb80..c62c6e4 100644 --- a/vrtManager/connection.py +++ b/vrtManager/connection.py @@ -442,7 +442,16 @@ class wvmConnect(object): vcpu = cur_vcpu else: vcpu = util.get_xml_path(dom.XMLDesc(0), "/domain/vcpu") - vname[dom.name()] = {'status': dom.info()[0], 'uuid': dom.UUIDString(), 'vcpu': vcpu, 'memory': mem} + title = util.get_xml_path(dom.XMLDesc(0), "/domain/title") + description = util.get_xml_path(dom.XMLDesc(0), "/domain/description") + vname[dom.name()] = { + 'status': dom.info()[0], + 'uuid': dom.UUIDString(), + 'vcpu': vcpu, + 'memory': mem, + 'title': title if title else '', + 'description': description if description else '', + } return vname def get_user_instances(self, name): @@ -454,7 +463,17 @@ class wvmConnect(object): vcpu = cur_vcpu else: vcpu = util.get_xml_path(dom.XMLDesc(0), "/domain/vcpu") - return {'name': dom.name(), 'status': dom.info()[0], 'uuid': dom.UUIDString(), 'vcpu': vcpu, 'memory': mem} + title = util.get_xml_path(dom.XMLDesc(0), "/domain/title") + description = util.get_xml_path(dom.XMLDesc(0), "/domain/description") + return { + 'name': dom.name(), + 'status': dom.info()[0], + 'uuid': dom.UUIDString(), + 'vcpu': vcpu, + 'memory': mem, + 'title': title if title else '', + 'description': description if description else '', + } def close(self): """Close connection""" diff --git a/vrtManager/create.py b/vrtManager/create.py index 1d6d9e7..15f1cd8 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -220,7 +220,8 @@ class wvmCreate(wvmConnect): xml += """<interface type='network'>""" if mac: xml += """<mac address='%s'/>""" % mac - xml += """<source network='%s'/>""" % net + xml += """<source network='%s'/> + <filterref filter='clean-traffic'/>""" % net if virtio: xml += """<model type='virtio'/>""" xml += """</interface>""" diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 0f3b258..3c287e0 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -8,6 +8,7 @@ from vrtManager import util from xml.etree import ElementTree from datetime import datetime from vrtManager.connection import wvmConnect +from vrtManager.storage import wvmStorage from webvirtcloud.settings import QEMU_CONSOLE_TYPES @@ -184,8 +185,13 @@ class wvmInstance(wvmConnect): mem = util.get_xml_path(self._XMLDesc(0), "/domain/currentMemory") return int(mem) / 1024 + def get_title(self): + title = util.get_xml_path(self._XMLDesc(0), "/domain/title") + return title if title else '' + def get_description(self): - return util.get_xml_path(self._XMLDesc(0), "/domain/description") + description = util.get_xml_path(self._XMLDesc(0), "/domain/description") + return description if description else '' def get_max_memory(self): return self.wvm.getInfo()[1] * 1048576 @@ -523,7 +529,7 @@ class wvmInstance(wvmConnect): return util.get_xml_path(self._XMLDesc(VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@keymap") or '' - def resize(self, cur_memory, memory, cur_vcpu, vcpu): + def resize(self, cur_memory, memory, cur_vcpu, vcpu, disks=[]): """ Function change ram and cpu on vds. """ @@ -541,6 +547,11 @@ class wvmInstance(wvmConnect): set_vcpu.text = vcpu set_vcpu.set('current', cur_vcpu) + for disk in disks: + source_dev = disk['path'] + vol = self.get_volume_by_path(source_dev) + vol.resize(disk['size_new']) + new_xml = ElementTree.tostring(tree) self._defineXML(new_xml) @@ -598,6 +609,22 @@ class wvmInstance(wvmConnect): def get_managed_save_image(self): return self.instance.hasManagedSaveImage(0) + def get_wvmStorage(self, pool): + storage = wvmStorage(self.host, + self.login, + self.passwd, + self.conn, + pool) + return storage + + def fix_mac(self, mac): + if ":" in mac: + return mac + # if mac does not contain ":", try to split into tuples and join with ":" + n = 2 + mac_tuples = [mac[i:i+n] for i in range(0, len(mac), n)] + return ':'.join(mac_tuples) + def clone_instance(self, clone_data): clone_dev_path = [] @@ -610,7 +637,8 @@ class wvmInstance(wvmConnect): for num, net in enumerate(tree.findall('devices/interface')): elm = net.find('mac') - elm.set('address', clone_data['net-' + str(num)]) + mac_address = self.fix_mac(clone_data['clone-net-mac-' + str(num)]) + elm.set('address', mac_address) for disk in tree.findall('devices/disk'): if disk.get('device') == 'disk': @@ -649,5 +677,65 @@ class wvmInstance(wvmConnect): </volume>""" % (target_file, vol_format) stg = vol.storagePoolLookupByVolume() stg.createXMLFrom(vol_clone_xml, vol, meta_prealloc) + + source_dev = elm.get('dev') + if source_dev: + clone_path = os.path.join(os.path.dirname(source_dev), target_file) + elm.set('dev', clone_path) + + vol = self.get_volume_by_path(source_dev) + stg = vol.storagePoolLookupByVolume() + + vol_name = util.get_xml_path(vol.XMLDesc(0), "/volume/name") + pool_name = util.get_xml_path(stg.XMLDesc(0), "/pool/name") + + storage = self.get_wvmStorage(pool_name) + storage.clone_volume(vol_name, target_file) + options = { + 'title': clone_data.get('clone-title', ''), + '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) + + for num, interface in enumerate(tree.findall('devices/interface')): + if interface.get('type') == 'bridge': + source = interface.find('mac') + source.set('address', network_data['net-mac-' + str(num)]) + source = interface.find('source') + source.set('bridge', network_data['net-source-' + str(num)]) + + new_xml = ElementTree.tostring(tree) + self._defineXML(new_xml) + + def _set_options(self, tree, options): + for o in ['title', 'description']: + option = tree.find(o) + option_value = options.get(o, '').strip() + if not option_value: + if not option is None: + tree.remove(option) + else: + if option is None: + option = ElementTree.SubElement(tree, o) + option.text = option_value + + def set_options(self, options): + """ + Function change description, title + """ + xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE) + tree = ElementTree.fromstring(xml) + + self._set_options(tree, options) + + new_xml = ElementTree.tostring(tree) + self._defineXML(new_xml) + diff --git a/webvirtcloud/settings.py b/webvirtcloud/settings.py index a163fa3..0c31433 100644 --- a/webvirtcloud/settings.py +++ b/webvirtcloud/settings.py @@ -8,7 +8,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__)) SECRET_KEY = '4y(f4rfqc6f2!i8_vfuu)kav6tdv5#sc=n%o451dm+th0&3uci' -DEBUG = False +DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -38,11 +38,19 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.RemoteUserMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.RemoteUserBackend', + #'accounts.backends.MyRemoteUserBackend', +) + +LOGIN_URL = '/accounts/login' + ROOT_URLCONF = 'webvirtcloud.urls' WSGI_APPLICATION = 'webvirtcloud.wsgi.application' @@ -103,3 +111,9 @@ QEMU_KEYMAPS = ['ar', 'da', 'de', 'de-ch', 'en-gb', 'en-us', 'es', 'et', 'fi', # keepalive interval and count for libvirt connections LIBVIRT_KEEPALIVE_INTERVAL = 5 LIBVIRT_KEEPALIVE_COUNT = 5 + +ALLOW_INSTANCE_MULTIPLE_OWNER = True +CLONE_INSTANCE_DEFAULT_PREFIX = 'ourea' +LOGS_PER_PAGE = 100 +QUOTA_DEBUG = True +ALLOW_EMPTY_PASSWORD = True diff --git a/webvirtcloud/urls.py b/webvirtcloud/urls.py index 0eff1a5..8cfa9d9 100644 --- a/webvirtcloud/urls.py +++ b/webvirtcloud/urls.py @@ -8,6 +8,7 @@ urlpatterns = patterns('', url(r'^instance/', include('instances.urls')), url(r'^accounts/', include('accounts.urls')), url(r'^computes/', include('computes.urls')), + url(r'^logs/', include('logs.urls')), url(r'^compute/(?P<compute_id>[0-9]+)/storages/$', 'storages.views.storages', name='storages'), @@ -27,6 +28,5 @@ urlpatterns = patterns('', 'create.views.create_instance', name='create_instance'), url(r'^console/$', 'console.views.console', name='console'), - url(r'^logs/$', 'logs.views.showlogs', name='showlogs'), # (r'^admin/', include(admin.site.urls)), ) diff --git a/webvirtcloud/wsgi.py b/webvirtcloud/wsgi.py index 35ceb9b..a9bf44c 100644 --- a/webvirtcloud/wsgi.py +++ b/webvirtcloud/wsgi.py @@ -7,7 +7,10 @@ For more information on this file, see https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ -import os +execfile('/srv/webvirtcloud/venv/bin/activate_this.py', dict(__file__='/srv/webvirtcloud/venv/bin/activate_this.py')) + +import os, sys +sys.path.append('/srv/webvirtcloud') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webvirtcloud.settings") from django.core.wsgi import get_wsgi_application