diff --git a/.gitignore b/.gitignore index b3ebced..0772847 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ venv .idea .DS_* *.pyc -db.sqlite3 -console/cert.pem +db.sqlite3* +console/cert.pem* tags dhcpd.* +webvirtcloud/settings.py diff --git a/README.md b/README.md index f1b3767..8ec15f5 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,23 @@ sudo service supervisor restart WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported. +### Generate secret key +You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py. + +```python +import random, string +haystack = string.ascii_letters + string.digits + string.punctuation +print(''.join([random.SystemRandom().choice(haystack) for _ in range(50)])) +``` + ### 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 gcc pkg-config git clone https://github.com/retspen/webvirtcloud cd webvirtcloud +cp webvirtcloud/settings.py.template webvirtcloud/settings.py +# now put secret key to webvirtcloud/settings.py sudo cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d cd .. @@ -63,6 +74,8 @@ sudo yum -y install python-virtualenv python-devel libvirt-devel glibc gcc nginx ```bash sudo mkdir /srv && cd /srv sudo git clone https://github.com/retspen/webvirtcloud && cd webvirtcloud +cp webvirtcloud/settings.py.template webvirtcloud/settings.py +# now put secret key to webvirtcloud/settings.py ``` #### Start installation webvirtcloud diff --git a/accounts/backends.py b/accounts/backends.py index 77aa509..e66b94a 100644 --- a/accounts/backends.py +++ b/accounts/backends.py @@ -1,7 +1,13 @@ from django.contrib.auth.backends import RemoteUserBackend +from accounts.models import UserInstance, UserAttributes +from instances.models import Instance class MyRemoteUserBackend(RemoteUserBackend): + + #create_unknown_user = True + def configure_user(self, user): - user.is_superuser = True + #user.is_superuser = True + UserAttributes.configure_user(user) return user diff --git a/accounts/migrations/0009_auto_20171026_0805.py b/accounts/migrations/0009_auto_20171026_0805.py new file mode 100644 index 0000000..7d035c7 --- /dev/null +++ b/accounts/migrations/0009_auto_20171026_0805.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0008_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='userattributes', + name='can_clone_instances', + field=models.BooleanField(default=True), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 06fefee..19e3a20 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,5 +1,6 @@ from django.db import models from django.contrib.auth.models import User +from django.conf import settings from instances.models import Instance @@ -24,11 +25,33 @@ class UserSSHKey(models.Model): class UserAttributes(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - can_clone_instances = models.BooleanField(default=False) + can_clone_instances = models.BooleanField(default=True) 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) + @staticmethod + def create_missing_userattributes(user): + try: + userattributes = user.userattributes + except UserAttributes.DoesNotExist: + userattributes = UserAttributes(user=user) + userattributes.save() + + @staticmethod + def add_default_instances(user): + existing_instances = UserInstance.objects.filter(user=user) + if not existing_instances: + for instance_name in settings.NEW_USER_DEFAULT_INSTANCES: + instance = Instance.objects.get(name=instance_name) + user_instance = UserInstance(user=user, instance=instance) + user_instance.save() + + @staticmethod + def configure_user(user): + UserAttributes.create_missing_userattributes(user) + UserAttributes.add_default_instances(user) + def __unicode__(self): return self.user.username diff --git a/accounts/templates/account.html b/accounts/templates/account.html index c8d8c19..0b39978 100644 --- a/accounts/templates/account.html +++ b/accounts/templates/account.html @@ -13,6 +13,31 @@ {% include 'errors_block.html' %} + {% if request.user.is_superuser and publickeys %} + <div class="row"> + <div class="col-lg-12"> + <div class="table-responsive"> + <table class="table table-bordered table-hover"> + <thead> + <tr> + <th>{% trans "Key name" %}</th> + <th>{% trans "Public key" %}</th> + </tr> + </thead> + <tbody> + {% for publickey in publickeys %} + <tr> + <td>{{ publickey.keyname }}</td> + <td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + {% endif %} + <div class="row"> <div class="col-lg-12"> {% if not user_insts %} @@ -112,4 +137,4 @@ {% endif %} </div> </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/accounts/templates/profile.html b/accounts/templates/profile.html index 3d15393..d2213b4 100644 --- a/accounts/templates/profile.html +++ b/accounts/templates/profile.html @@ -41,6 +41,7 @@ </div> </div> </form> + {% if show_profile_edit_password %} <h3 class="page-header">{% trans "Edit Password" %}</h3> <form class="form-horizontal" method="post" action="" role="form">{% csrf_token %} <div class="form-group"> @@ -67,6 +68,7 @@ </div> </div> </form> + {% endif %} <h3 class="page-header">{% trans "SSH Keys" %}</h3> {% if publickeys %} <div class="col-lg-12"> @@ -93,23 +95,23 @@ {% endif %} <form class="form-horizontal" method="post" action="" role="form">{% csrf_token %} <div class="form-group bridge_name_form_group_dhcp"> - <label class="col-sm-2 control-label">{% trans "Retry" %}</label> + <label class="col-sm-2 control-label">{% trans "Key name" %}</label> <div class="col-sm-4"> <input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}"> </div> </div> <div class="form-group bridge_name_form_group_dhcp"> - <label class="col-sm-2 control-label">{% trans "Retry" %}</label> + <label class="col-sm-2 control-label">{% trans "Public key" %}</label> <div class="col-sm-8"> <textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> - <button type="submit" class="btn btn-primary">{% trans "Create" %}</button> + <button type="submit" class="btn btn-primary">{% trans "Add" %}</button> </div> </div> </form> </div> </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/accounts/templatetags/tags_fingerprint.py b/accounts/templatetags/tags_fingerprint.py index 5688039..31f3952 100644 --- a/accounts/templatetags/tags_fingerprint.py +++ b/accounts/templatetags/tags_fingerprint.py @@ -7,6 +7,9 @@ register = template.Library() @register.simple_tag def ssh_to_fingerprint(line): - key = base64.b64decode(line.strip().split()[1].encode('ascii')) - fp_plain = hashlib.md5(key).hexdigest() - return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])) + try: + key = base64.b64decode(line.strip().split()[1].encode('ascii')) + fp_plain = hashlib.md5(key).hexdigest() + return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2])) + except Exception: + return 'Invalid key' diff --git a/accounts/views.py b/accounts/views.py index 155ecc8..5fdfb4d 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -20,6 +20,7 @@ def profile(request): error_messages = [] user = User.objects.get(id=request.user.id) publickeys = UserSSHKey.objects.filter(user_id=request.user.id) + show_profile_edit_password = settings.SHOW_PROFILE_EDIT_PASSWORD if request.method == 'POST': if 'username' in request.POST: @@ -70,21 +71,11 @@ def accounts(request): :param request: :return: """ - - 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.all().order_by('username') - create_missing_userattributes(users) allow_empty_password = settings.ALLOW_EMPTY_PASSWORD if request.method == 'POST': @@ -98,6 +89,7 @@ def accounts(request): if not error_messages: new_user = User.objects.create_user(data['name'], None, data['password']) new_user.save() + UserAttributes.configure_user(new_user) return HttpResponseRedirect(request.get_full_path()) if 'edit' in request.POST: user_id = request.POST.get('user_id', '') @@ -155,9 +147,7 @@ def account(request, user_id): user = User.objects.get(id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id) instances = Instance.objects.all().order_by('name') - - if user.username == request.user.username: - return HttpResponseRedirect(reverse('profile')) + publickeys = UserSSHKey.objects.filter(user_id=user_id) if request.method == 'POST': if 'delete' in request.POST: diff --git a/console/templates/console-base.html b/console/templates/console-base.html index d0e0e7a..10ef4a3 100644 --- a/console/templates/console-base.html +++ b/console/templates/console-base.html @@ -1,11 +1,12 @@ +{% load staticfiles %} {% load i18n %} <html> <head> <meta charset="utf-8"> - <link rel="shortcut icon" href="{{ STATIC_URL }}img/favicon.ico"> + <link rel="shortcut icon" href="{% static "img/favicon.ico" %}"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <link rel="stylesheet" href="{{ STATIC_URL }}css/bootstrap.min.css"> - <link href="{{ STATIC_URL }}css/webvirtcloud.css" rel="stylesheet"> + <link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}"> + <link href="{% static "css/webvirtcloud.css" %}" rel="stylesheet"> <style> body { @@ -92,8 +93,8 @@ <div id='main_container' class="container"> {% block content %}{% endblock %} </div> -<script src="{{ STATIC_URL }}js/jquery.js"></script> -<script src="{{ STATIC_URL }}js/bootstrap.min.js"></script> +<script src="{% static "js/jquery.js" %}"></script> +<script src="{% static "js/bootstrap.min.js" %}"></script> <script> function log_message(msg,type) { diff --git a/instances/migrations/0003_instance_created.py b/instances/migrations/0003_instance_created.py new file mode 100644 index 0000000..32d4ac9 --- /dev/null +++ b/instances/migrations/0003_instance_created.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('instances', '0002_instance_is_template'), + ] + + operations = [ + migrations.AddField( + model_name='instance', + name='created', + field=models.DateField(default=datetime.datetime(2017, 10, 26, 8, 5, 55, 797326), auto_now_add=True), + preserve_default=False, + ), + ] diff --git a/instances/models.py b/instances/models.py index bf2a1c4..fa1ed40 100644 --- a/instances/models.py +++ b/instances/models.py @@ -7,6 +7,7 @@ class Instance(models.Model): name = models.CharField(max_length=20) uuid = models.CharField(max_length=36) is_template = models.BooleanField(default=False) + created = models.DateField(auto_now_add=True) def __unicode__(self): return self.name diff --git a/instances/templates/add_instance_owner_block.html b/instances/templates/add_instance_owner_block.html new file mode 100644 index 0000000..b7e6df9 --- /dev/null +++ b/instances/templates/add_instance_owner_block.html @@ -0,0 +1,36 @@ +{% load i18n %} +{% if request.user.is_superuser %} + <a href="#addInstanceOwner" type="button" class="btn btn-success pull-right" data-toggle="modal"> + <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> + </a> + + <!-- Modal pool --> + <div class="modal fade" id="addInstanceOwner" tabindex="-1" role="dialog" aria-labelledby="addInstanceOwnerLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">{% trans "Add Instance Owner" %}</h4> + </div> + <div class="modal-body"> + <form class="form-horizontal" method="post" action="" role="form">{% csrf_token %} + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "User" %}</label> + <div class="col-sm-6"> + <select class="form-control" name="user_id"> + {% for user in users %} + <option value="{{ user.id }}">{{ user.username }}</option> + {% endfor %} + </select> + </div> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button> + <button type="submit" class="btn btn-primary" name="add_owner">{% trans "Add" %}</button> + </div> + </form> + </div> <!-- /.modal-content --> + </div> <!-- /.modal-dialog --> + </div> <!-- /.modal --> +{% endif %} diff --git a/instances/templates/instance.html b/instances/templates/instance.html index b77d469..2b73c0a 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -1,48 +1,42 @@ {% extends "base.html" %} +{% load staticfiles %} {% load i18n %} {% block title %}{% trans "Instance" %} - {{ vname }}{% endblock %} {% block content %} {% include 'pleasewaitdialog.html' %} <!-- Page Heading --> <div class="row"> - <table> - <tr> - <td><h3>{{ vname }}</h3></td> - <td> - {% ifequal status 5 %} - <span class="label label-danger">{% trans "Off" %}</span> - {% endifequal %} - {% ifequal status 1 %} - <span class="label label-success">{% trans "Active" %}</span> - {% endifequal %} - {% ifequal status 3 %} - <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%"> - <tr> - <td> - {% if cur_vcpu %} - <h4>{{ cur_vcpu }} {% trans "Vcpu" %}</h4> - {% else %} - <h4>{{ vcpu }} {% trans "Vcpu" %}</h4> - {% endif %} - </td> - <td> - <h4>{{ cur_memory }} {% trans "MB" %} {% trans "Ram" %}</h4> - </td> + <div> + <h3> + {{ vname }}{% if title %} ({{ title }}){% endif %} + </h3> + </div> + <div> + <div> + {% ifequal status 5 %} + <span class="label label-danger">{% trans "Off" %}</span> + {% endifequal %} + {% ifequal status 1 %} + <span class="label label-success">{% trans "Active" %}</span> + {% endifequal %} + {% ifequal status 3 %} + <span class="label label-warning">{% trans "Suspend" %}</span> + {% endifequal %} + | + {% if cur_vcpu %} + {{ cur_vcpu }} {% trans "Vcpu" %} + {% else %} + {{ vcpu }} {% trans "Vcpu" %} + {% endif %} + | + {{ cur_memory }} {% trans "MB" %} {% trans "Ram" %} + | {% for disk in disks %} - <td> - <h4>{{ disk.size|filesizeformat }} {% trans "Disk" %}</h4> - </td> + {{ disk.size|filesizeformat }} {% trans "Disk" %} | {% endfor %} - </tr> - </table> + <a href="{% url 'instance' compute.id vname %}" type="button" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-refresh"></span></a> + </div> + </div> {% if user_quota_msg %} <span class="label label-warning">{{ user_quota_msg|capfirst }} quota reached.</span> {% endif %} @@ -90,8 +84,8 @@ </li> <li role="presentation"> <a href="#graphics" id="chartgraphs" class="action-button" aria-controls="graphics" role="tab" data-toggle="tab"> - <span id="action-block" class="glyphicon glyphicon-signal" aria-hidden="true"></span> - {% trans "Graphs" %} + <span id="action-block" class="glyphicon glyphicon-stats" aria-hidden="true"></span> + {% trans "Stats" %} </a> </li> <li role="presentation"> @@ -233,16 +227,20 @@ {% trans "Console" %} </a> </li> + {% if show_access_root_password %} <li role="presentation"> <a href="#rootpasswd" aria-controls="rootpasswd" role="tab" data-toggle="tab"> {% trans "Root Password" %} </a> </li> + {% endif %} + {% if show_access_ssh_keys %} <li role="presentation"> <a href="#sshkeys" aria-controls="sshkeys" role="tab" data-toggle="tab"> {% trans "SSH Keys" %} </a> </li> + {% endif %} </ul> <!-- Tab panes --> <div class="tab-content"> @@ -255,6 +253,7 @@ {% endifequal %} <div class="clearfix"></div> </div> + {% if show_access_root_password %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="rootpasswd"> <p>{% trans "You need shut down your instance and enter a new root password." %}</p> <form class="form-inline" method="post" role="form">{% csrf_token %} @@ -271,6 +270,8 @@ </form> <div class="clearfix"></div> </div> + {% endif %} + {% if show_access_ssh_keys %} <div role="tabpanel" class="tab-pane tab-pane-bordered" id="sshkeys"> <p>{% trans "You need shut down your instance and choose your public key." %}</p> <form class="form-inline" method="post" role="form">{% csrf_token %} @@ -295,6 +296,7 @@ </form> <div class="clearfix"></div> </div> + {% endif %} </div> </div> </div> @@ -307,11 +309,16 @@ {% trans "Resize Instance" %} </a> </li> + <li role="presentation"> + <a href="#addvolume" aria-controls="addvolume" role="tab" data-toggle="tab"> + {% trans "Add New Volume" %} + </a> + </li> </ul> <!-- Tab panes --> <div class="tab-content"> <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="resizevm"> - {% if request.user.is_superuser or userinstace.is_change %} + {% if request.user.is_superuser or request.user.is_staff or userinstace.is_change %} <form class="form-horizontal" method="post" role="form">{% csrf_token %} <p style="font-weight:bold;">{% trans "Logical host CPUs:" %} {{ vcpu_host }}</p> <div class="form-group"> @@ -388,6 +395,88 @@ {% endif %} <div class="clearfix"></div> </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="addvolume"> + {% if request.user.is_superuser or userinstace.is_change %} + <form class="form-horizontal" method="post" role="form">{% csrf_token %} + <p style="font-weight:bold;">{% trans "Volume parameters" %}</p> + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Storage" %}</label> + <div class="col-sm-4"> + <select name="storage" class="form-control image-format"> + {% for storage in storages %} + <option value="{{ storage }}">{{ storage }}</option> + {% endfor %} + </select> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Name" %}</label> + <div class="col-sm-4"> + <input type="text" class="form-control" name="name" placeholder="{% trans "Name" %}" required pattern="[a-zA-Z0-9\.\-_]+"> + </div> + <div class="col-sm-2"> + <select name="extension" class="form-control image-format"> + {% for format in formats %} + <option value="{{ format }}" {% if format == default_format %}selected{% endif %}>{% trans format %}</option> + {% endfor %} + </select> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Format" %}</label> + <div class="col-sm-4"> + <select name="format" class="form-control image-format"> + {% for format in formats %} + <option value="{{ format }}" {% if format == default_format %}selected{% endif %}>{% trans format %}</option> + {% endfor %} + </select> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Size" %}</label> + <div class="col-sm-4"> + <input type="text" class="form-control" name="size" value="10" maxlength="3" required pattern="[0-9]+"> + </div> + <label class="col-sm-1 control-label">{% trans "GB" %}</label> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Bus" %}</label> + <div class="col-sm-4"> + <select name="bus" class="form-control image-format"> + {% for bus in busses %} + <option value="{{ bus }}" {% if bus == default_bus %}selected{% endif %}>{% trans bus %}</option> + {% endfor %} + </select> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Cache" %}</label> + <div class="col-sm-4"> + <select name="cache" class="form-control image-format"> + {% for mode, name in cache_modes %} + <option value="{{ mode }}" {% if mode == default_cache %}selected{% endif %}>{% trans name %}</option> + {% endfor %} + </select> + </div> + </div> + <div class="form-group meta-prealloc"> + <label class="col-sm-3 control-label" style="font-weight:normal;">{% trans "Metadata" %}</label> + <div class="col-sm-4"> + <input type="checkbox" name="meta_prealloc" value="true"> + </div> + </div> + {% ifequal status 5 %} + <button type="submit" class="btn btn-lg btn-success pull-right" name="addvolume">{% trans "Add volume" %}</button> + {% else %} + <button class="btn btn-lg btn-success pull-right disabled">{% trans "Add volume" %}</button> + {% endifequal %} + </form> + {% else %} + {% trans "You don't have permission for resizing instance" %} + <button class="btn btn-lg btn-success pull-right disabled">{% trans "Add volume" %}</button> + {% endif %} + <div class="clearfix"></div> + </div> </div> </div> </div> @@ -531,11 +620,15 @@ {% trans "XML" %} </a> </li> + {% endif %} + {% if request.user.is_superuser or request.user.userattributes.can_clone_instances %} <li role="presentation"> <a href="#options" aria-controls="options" role="tab" data-toggle="tab"> {% trans "Options" %} </a> </li> + {% endif %} + {% if request.user.is_superuser %} <li role="presentation"> <a href="#users" aria-controls="users" role="tab" data-toggle="tab"> {% trans "Users" %} @@ -546,8 +639,8 @@ <!-- Tab panes --> <div class="tab-content"> <div role="tabpanel" class="tab-pane tab-pane-bordered active" id="media"> - <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} - {% for cd in media %} + {% for cd in media %} + <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} <div class="form-group"> <label class="col-sm-2 control-label">{% trans "CDROM" %} {{ forloop.counter }}</label> {% if not cd.image %} @@ -579,8 +672,8 @@ </div> {% endif %} </div> - {% endfor %} - </form> + </form> + {% endfor %} <div class="clearfix"></div> </div> {% if request.user.is_superuser %} @@ -829,7 +922,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" checked> + <input type="checkbox" name="live_migrate" value="true" id="vm_live_migrate" {% ifequal status 1 %}checked{% endifequal %}> </div> </div> <div class="form-group"> @@ -847,7 +940,7 @@ <div class="form-group"> <label class="col-sm-3 control-label">{% trans "Offline migration" %}</label> <div class="col-sm-6"> - <input type="checkbox" name="offline_migrate" value="true" id="offline_migrate"> + <input type="checkbox" name="offline_migrate" value="true" id="offline_migrate" {% ifequal status 5 %}checked{% endifequal %}> </div> </div> {% if computes_count != 1 %} @@ -877,6 +970,8 @@ </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="options"> <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} <div class="form-group"> @@ -894,7 +989,7 @@ <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 %}> + <input type="checkbox" name="is_template" value="true" id="is_template" {% if instance.is_template %}checked{% endif %} {% if not request.user.is_superuser %}disabled{% endif %}> </div> </div> {% ifequal status 5 %} @@ -905,11 +1000,34 @@ </form> <div class="clearfix"></div> </div> + {% endif %} + {% if request.user.is_superuser %} <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> + <p style="font-weight:bold;"> + {% trans "Instance owners" %} + {% include 'add_instance_owner_block.html' %} + </p> + </div> + <div class="table-responsive"> + <table class="table table-striped sortable-theme-bootstrap" data-sortable> + <tbody class="searchable"> + {% for userinstance in userinstances %} + <tr> + <td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td> + <td style="width:30px;"> + <form action="" method="post" style="height:10px" role="form">{% csrf_token %} + <input type="hidden" name="userinstance" value="{{ userinstance.pk }}"> + <button type="submit" class="btn btn-sm btn-default" name="del_owner" title="{% trans "Delete" %}"> + <i class="fa fa-trash"></i> + </button> + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> <div class="clearfix"></div> </div> {% endif %} @@ -925,6 +1043,11 @@ {% trans "Real Time" %} </a> </li> + <li role="presentation"> + <a href="#logs" aria-controls="logs" role="tab" data-toggle="tab" onclick='update_logs_table("{{ vname }}");'> + {% trans "Logs" %} + </a> + </li> </ul> <!-- Tab panes --> <div class="tab-content"> @@ -971,6 +1094,23 @@ {% endfor %} <div class="clearfix"></div> </div> + <div role="tabpanel" class="tab-pane tab-pane-bordered" id="logs"> + <div class="table-responsive"> + <table class="table table-striped sortable-theme-bootstrap" id="logs_table" data-sortable> + <thead> + <tr> + <th>{% trans "Date" %}</th> + <th>{% trans "User" %}</th> + <th>{% trans "Message" %}</th> + </tr> + </thead> + <tbody class="searchable"> + <tr><td colspan="3"><i>None ...</i></td></tr> + </tbody> + </table> + </div> + <div class="clearfix"></div> + </div> </div> </div> </div> @@ -1016,7 +1156,7 @@ </div> {% endblock %} {% block script %} -<script src="{{ STATIC_URL }}/js/ace.js" type="text/javascript" charset="utf-8"></script> +<script src="{% static "js/ace.js" %}" type="text/javascript" charset="utf-8"></script> <script> var editor = ace.edit("editor"); editor.getSession().setMode("ace/mode/xml"); @@ -1153,7 +1293,7 @@ }); }); </script> -<script src="{{ STATIC_URL }}js/Chart.min.js"></script> +<script src="{% static "js/Chart.min.js" %}"></script> <script> $('#chartgraphs').on('shown.bs.tab', function (event) { var cpuLineData = { @@ -1290,13 +1430,14 @@ }); </script> <script> + backgroundJobRunning = false; window.setInterval(function get_status() { var status = {{ status }}; $.getJSON('{% url 'inst_status' compute_id vname %}', function (data) { - if (data['status'] != status) { + if (data['status'] != status && !backgroundJobRunning) { window.location.reload() } - }) + }); }, 5000); </script> <script> @@ -1348,4 +1489,19 @@ }); } </script> +<script> + function update_logs_table(vname) { + $.getJSON('/logs/vm_logs/'+vname+'/', function(data) { + var logs = ""; + $.each(data, function(id) { + row = data[id]; + console.log(row); + logs += '<tr><td style="width:150px">'+row['date']+'</td>'; + logs += '<td>'+row['user']+'</td>'; + logs += '<td>'+row['message']+'</td></tr>'; + }); + $("#logs_table > tbody").html(logs); + }); + } +</script> {% endblock %} diff --git a/instances/templates/instances.html b/instances/templates/instances.html index 8cee448..43e70c5 100644 --- a/instances/templates/instances.html +++ b/instances/templates/instances.html @@ -43,7 +43,7 @@ <th>Host<br>User</th> <th>Status</th> <th>VCPU</th> - <th>Memory</th> + <th>Memory<br>({% trans "MB" %})</th> <th data-sortable="false" style="width:205px;">Actions</th> </tr> </thead> @@ -64,7 +64,7 @@ {% endifequal %} </td> <td>{{ info.vcpu }}</td> - <td>{{ info.memory }} {% trans "MB" %}</td> + <td>{{ info.memory }}</td> <td><form action="" method="post" role="form">{% csrf_token %} <input type="hidden" name="name" value="{{ vm }}"/> <input type="hidden" name="compute_id" value="{{ host.0 }}"/> diff --git a/instances/urls.py b/instances/urls.py index e8f263b..dfba916 100644 --- a/instances/urls.py +++ b/instances/urls.py @@ -14,4 +14,6 @@ urlpatterns = [ views.guess_clone_name, name='guess_clone_name'), url(r'^check_instance/(?P<vname>[\w\-\.]+)/$', views.check_instance, name='check_instance'), + url(r'^sshkeys/(?P<vname>[\w\-\.]+)/$', + views.sshkeys, name='sshkeys'), ] diff --git a/instances/views.py b/instances/views.py index 44297ce..0d6dfab 100644 --- a/instances/views.py +++ b/instances/views.py @@ -4,7 +4,7 @@ import json import socket import crypt import re -from string import letters, digits +import string from random import choice from bisect import insort from django.http import HttpResponse, HttpResponseRedirect @@ -14,10 +14,12 @@ 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 django.contrib.auth.models import User from accounts.models import UserInstance, UserSSHKey from vrtManager.hostdetails import wvmHostDetails from vrtManager.instance import wvmInstance, wvmInstances from vrtManager.connection import connection_manager +from vrtManager.create import wvmCreate from vrtManager.util import randomPasswd from libvirt import libvirtError, VIR_DOMAIN_XML_SECURE from webvirtcloud.settings import QEMU_KEYMAPS, QEMU_CONSOLE_TYPES @@ -50,13 +52,32 @@ def instances(request): def get_userinstances_info(instance): info = {} uis = UserInstance.objects.filter(instance=instance) - info['count'] = len(uis) - if len(uis) > 0: + info['count'] = uis.count() + if info['count'] > 0: info['first_user'] = uis[0] else: info['first_user'] = None return info + def refresh_instance_database(comp, vm, info): + instances = Instance.objects.filter(name=vm) + if instances.count() > 1: + for i in instances: + user_instances_count = UserInstance.objects.filter(instance=i).count() + if user_instances_count == 0: + addlogmsg(request.user.username, i.name, _("Deleting due to multiple records.")) + i.delete() + + try: + 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() + if not request.user.is_superuser: user_instances = UserInstance.objects.filter(user_id=request.user.id) for usr_inst in user_instances: @@ -73,18 +94,12 @@ def instances(request): if connection_manager.host_is_up(comp.type, comp.hostname): try: conn = wvmHostDetails(comp, comp.login, comp.password, comp.type) - if conn.get_host_instances(): - all_host_vms[comp.id, comp.name] = conn.get_host_instances() - for vm, info in conn.get_host_instances().items(): - try: - 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() + host_instances = conn.get_host_instances() + if host_instances: + all_host_vms[comp.id, comp.name] = host_instances + for vm, info in host_instances.items(): + refresh_instance_database(comp, vm, info) + conn.close() except libvirtError as lib_err: error_messages.append(lib_err) @@ -167,8 +182,9 @@ def instance(request, compute_id, vname): error_messages = [] messages = [] compute = get_object_or_404(Compute, pk=compute_id) - computes = Compute.objects.all() - computes_count = len(computes) + computes = Compute.objects.all().order_by('name') + computes_count = computes.count() + users = User.objects.all().order_by('username') publickeys = UserSSHKey.objects.filter(user_id=request.user.id) keymaps = QEMU_KEYMAPS console_types = QEMU_CONSOLE_TYPES @@ -232,7 +248,7 @@ def instance(request, compute_id, vname): 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) + instance += user_instances.count() for usr_inst in user_instances: if connection_manager.host_is_up(usr_inst.instance.compute.type, usr_inst.instance.compute.hostname): @@ -244,7 +260,8 @@ def instance(request, compute_id, vname): cpu += int(conn.get_vcpu()) memory += int(conn.get_memory()) for disk in conn.get_disk_device(): - disk_size += int(disk['size'])>>30 + if disk['size']: + disk_size += int(disk['size'])>>30 ua = request.user.userattributes msg = "" @@ -266,13 +283,25 @@ def instance(request, compute_id, vname): msg += " (%s > %s)" % (disk_size, ua.max_disk_size) return msg + def get_new_disk_dev(disks, bus): + if bus == "virtio": + dev_base = "vd" + else: + dev_base = "sd" + existing_devs = [ disk['dev'] for disk in disks ] + for l in string.lowercase: + dev = dev_base + l + if dev not in existing_devs: + return dev + raise Exception(_('None available device name')) + try: conn = wvmInstance(compute.hostname, compute.login, compute.password, compute.type, vname) - + status = conn.get_status() autostart = conn.get_autostart() vcpu = conn.get_vcpu() @@ -285,7 +314,10 @@ def instance(request, compute_id, vname): disks = conn.get_disk_device() media = conn.get_media_device() networks = conn.get_net_device() - media_iso = sorted(conn.get_iso_media()) + if len(media) != 0: + media_iso = sorted(conn.get_iso_media()) + else: + media_iso = [] vcpu_range = conn.get_max_cpus() memory_range = [256, 512, 768, 1024, 2048, 4096, 6144, 8192, 16384] if memory not in memory_range: @@ -305,6 +337,16 @@ def instance(request, compute_id, vname): console_passwd = conn.get_console_passwd() clone_free_names = get_clone_free_names() user_quota_msg = check_user_quota(0, 0, 0, 0) + storages = sorted(conn.get_storages()) + cache_modes = sorted(conn.get_cache_modes().items()) + default_cache = settings.INSTANCE_VOLUME_DEFAULT_CACHE + default_format = settings.INSTANCE_VOLUME_DEFAULT_FORMAT + formats = conn.get_image_formats() + default_bus = settings.INSTANCE_VOLUME_DEFAULT_BUS + busses = conn.get_busses() + default_bus = settings.INSTANCE_VOLUME_DEFAULT_BUS + show_access_root_password = settings.SHOW_ACCESS_ROOT_PASSWORD + show_access_ssh_keys = settings.SHOW_ACCESS_SSH_KEYS try: instance = Instance.objects.get(compute_id=compute_id, name=vname) @@ -411,7 +453,7 @@ def instance(request, compute_id, vname): msg = _("Please shutdow down your instance and then try again") error_messages.append(msg) - if 'resize' in request.POST and (request.user.is_superuser or userinstace.is_change): + if 'resize' in request.POST and (request.user.is_superuser or request.user.is_staff or userinstace.is_change): new_vcpu = request.POST.get('vcpu', '') new_cur_vcpu = request.POST.get('cur_vcpu', '') new_memory = request.POST.get('memory', '') @@ -444,6 +486,27 @@ def instance(request, compute_id, vname): addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(request.get_full_path() + '#resize') + if 'addvolume' in request.POST and (request.user.is_superuser or userinstace.is_change): + connCreate = wvmCreate(compute.hostname, + compute.login, + compute.password, + compute.type) + storage = request.POST.get('storage', '') + name = request.POST.get('name', '') + extension = request.POST.get('extension', '') + format = request.POST.get('format', '') + size = request.POST.get('size', 0) + meta_prealloc = request.POST.get('meta_prealloc', False) + bus = request.POST.get('bus', '') + cache = request.POST.get('cache', '') + target = get_new_disk_dev(disks, bus) + + path = connCreate.create_volume(storage, name, size, format, meta_prealloc, extension) + conn.attach_disk(path, target, subdriver=format, cache=cache, targetbus=bus) + msg = _('Attach new disk') + 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', '') dev = request.POST.get('umount_iso', '') @@ -595,6 +658,68 @@ def instance(request, compute_id, vname): addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(request.get_full_path() + '#network') + if 'add_owner' in request.POST: + user_id = int(request.POST.get('user_id', '')) + + if settings.ALLOW_INSTANCE_MULTIPLE_OWNER: + check_inst = UserInstance.objects.filter(instance=instance, user_id=user_id) + else: + check_inst = UserInstance.objects.filter(instance=instance) + + if check_inst: + msg = _("Owner already added") + error_messages.append(msg) + else: + add_user_inst = UserInstance(instance=instance, user_id=user_id) + add_user_inst.save() + msg = _("Added owner %d" % user_id) + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#users') + + if 'del_owner' in request.POST: + userinstance_id = int(request.POST.get('userinstance', '')) + userinstance = UserInstance.objects.get(pk=userinstance_id) + userinstance.delete() + msg = _("Deleted owner %d" % userinstance_id) + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#users') + + + 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', '') + + 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']) + + for post in request.POST: + clone_data[post] = request.POST.get(post, '').strip() + + 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) + elif not re.match(r'^[a-zA-Z0-9-]+$', clone_data['name']): + msg = _("Instance name '%s' contains invalid characters!" % clone_data['name']) + error_messages.append(msg) + elif not re.match(r'^([0-9A-F]{2})(\:?[0-9A-F]{2}){5}$', clone_data['clone-net-mac-0'], re.IGNORECASE): + msg = _("Instance mac '%s' invalid format!" % clone_data['clone-net-mac-0']) + error_messages.append(msg) + else: + 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']])) + if 'change_options' in request.POST: instance.is_template = request.POST.get('is_template', False) instance.save() @@ -609,38 +734,6 @@ 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', '') - - 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) - elif not re.match(r'^[a-zA-Z0-9-]+$', clone_data['name']): - msg = _("Instance name '%s' contains invalid characters!" % 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, 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() except libvirtError as lib_err: @@ -795,7 +888,7 @@ def guess_mac_address(request, vname): if name_found and "hardware ethernet" in line: data['mac'] = line.split(' ')[-1].strip().strip(';') break - return HttpResponse(json.dumps(data)); + return HttpResponse(json.dumps(data)) @login_required def guess_clone_name(request): @@ -811,7 +904,7 @@ def guess_clone_name(request): hostname = fqdn.split('.')[0] if hostname.startswith(prefix) and hostname not in instance_names: return HttpResponse(json.dumps({'name': hostname})) - return HttpResponse(json.dumps({})); + return HttpResponse(json.dumps({})) @login_required def check_instance(request, vname): @@ -819,4 +912,62 @@ def check_instance(request, vname): data = { 'vname': vname, 'exists': False } if check_instance: data['exists'] = True - return HttpResponse(json.dumps(data)); + return HttpResponse(json.dumps(data)) + +def sshkeys(request, vname): + """ + :param request: + :param vm: + :return: + """ + + instance_keys = [] + userinstances = UserInstance.objects.filter(instance__name=vname) + + for ui in userinstances: + keys = UserSSHKey.objects.filter(user=ui.user) + for k in keys: + instance_keys.append(k.keypublic) + if request.GET.get('plain', ''): + response = '\n'.join(instance_keys) + response += '\n' + else: + response = json.dumps(instance_keys) + return HttpResponse(response) + +def delete_instance(instance, delete_disk=False): + compute = instance.compute + instance_name = instance.name + try: + conn = wvmInstance(compute.hostname, + compute.login, + compute.password, + compute.type, + instance.name) + + del_userinstance = UserInstance.objects.filter(instance=instance) + if del_userinstance: + print("Deleting UserInstances") + print(del_userinstance) + del_userinstance.delete() + + if conn.get_status() == 1: + print("Forcing shutdown") + conn.force_shutdown() + if delete_disk: + snapshots = sorted(conn.get_snapshot(), reverse=True) + for snap in snapshots: + print("Deleting snapshot {}".format(snap['name'])) + conn.snapshot_delete(snap['name']) + print("Deleting disks") + conn.delete_disk() + + conn.delete() + instance.delete() + + print("Instance {} on compute {} sucessfully deleted".format(instance_name, compute.hostname)) + + except libvirtError as lib_err: + print("Error removing instance {} on compute {}".format(instance_name, compute.hostname)) + raise lib_err + diff --git a/logs/urls.py b/logs/urls.py index e579387..574405c 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -4,4 +4,5 @@ from . import views urlpatterns = [ url(r'^$', views.showlogs, name='showlogs'), url(r'^(?P<page>[0-9]+)/$', views.showlogs, name='showlogspage'), + url(r'^vm_logs/(?P<vname>[\w\-\.]+)/$', views.vm_logs, name='vm_logs'), ] diff --git a/logs/views.py b/logs/views.py index 830925e..ab6ae1b 100644 --- a/logs/views.py +++ b/logs/views.py @@ -1,8 +1,11 @@ from django.shortcuts import render -from django.http import HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required +from instances.models import Instance from logs.models import Logs from django.conf import settings +import json def addlogmsg(user, instance, message): @@ -14,15 +17,13 @@ def addlogmsg(user, instance, message): add_log_msg.save() +@login_required def showlogs(request, page=1): """ :param request: :return: """ - if not request.user.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - if not request.user.is_superuser: return HttpResponseRedirect(reverse('index')) @@ -34,3 +35,27 @@ def showlogs(request, page=1): # TODO: remove last element from queryset, but do not affect database return render(request, 'showlogs.html', locals()) + +@login_required +def vm_logs(request, vname): + """ + :param request: + :param vm: + :return: + """ + + if not request.user.is_superuser: + return HttpResponseRedirect(reverse('index')) + + vm = Instance.objects.get(name=vname) + logs_ = Logs.objects.filter(instance=vm.name, date__gte=vm.created).order_by('-date') + logs = [] + for l in logs_: + log = {} + log['user'] = l.user + log['instance'] = l.instance + log['message'] = l.message + log['date'] = l.date.strftime('%x %X') + logs.append(log) + + return HttpResponse(json.dumps(logs)) diff --git a/templates/pleasewaitdialog.html b/templates/pleasewaitdialog.html index 49c893b..37dc7db 100644 --- a/templates/pleasewaitdialog.html +++ b/templates/pleasewaitdialog.html @@ -18,8 +18,10 @@ <script> function showPleaseWaitDialog() { $('#pleaseWaitDialog').modal(); + backgroundJobRunning = true; } function hidePleaseWaitDialog() { $('#pleaseWaitDialog').modal('hide'); + backgroundJobRunning = false; } </script> diff --git a/vrtManager/connection.py b/vrtManager/connection.py index c62c6e4..89dd016 100644 --- a/vrtManager/connection.py +++ b/vrtManager/connection.py @@ -380,6 +380,25 @@ class wvmConnect(object): interface.append(inface) return interface + def get_cache_modes(self): + """Get cache available modes""" + return { + 'default': 'Default', + 'none': 'Disabled', + 'writethrough': 'Write through', + 'writeback': 'Write back', + 'directsync': 'Direct sync', # since libvirt 0.9.5 + 'unsafe': 'Unsafe', # since libvirt 0.9.7 + } + + def get_busses(self): + """Get available busses""" + return [ 'ide', 'scsi', 'usb', 'virtio' ] + + def get_image_formats(self): + """Get available image formats""" + return [ 'raw', 'qcow', 'qcow2' ] + def get_iface(self, name): return self.wvm.interfaceLookupByName(name) @@ -424,55 +443,71 @@ class wvmConnect(object): def get_net_device(self): netdevice = [] + def get_info(ctx): + dev_type = util.get_xpath(ctx, '/device/capability/@type') + interface = util.get_xpath(ctx, '/device/capability/interface') + return (dev_type, interface) for dev in self.wvm.listAllDevices(0): xml = dev.XMLDesc(0) - dev_type = util.get_xml_path(xml, '/device/capability/@type') + (dev_type, interface) = util.get_xml_path(xml, func=get_info) if dev_type == 'net': - netdevice.append(util.get_xml_path(xml, '/device/capability/interface')) + netdevice.append(interface) return netdevice def get_host_instances(self): vname = {} - for name in self.get_instances(): - dom = self.get_instance(name) - mem = util.get_xml_path(dom.XMLDesc(0), "/domain/currentMemory") + def get_info(ctx): + mem = util.get_xpath(ctx, "/domain/currentMemory") mem = int(mem) / 1024 - cur_vcpu = util.get_xml_path(dom.XMLDesc(0), "/domain/vcpu/@current") + cur_vcpu = util.get_xpath(ctx, "/domain/vcpu/@current") if cur_vcpu: vcpu = cur_vcpu else: - vcpu = util.get_xml_path(dom.XMLDesc(0), "/domain/vcpu") - title = util.get_xml_path(dom.XMLDesc(0), "/domain/title") - description = util.get_xml_path(dom.XMLDesc(0), "/domain/description") + vcpu = util.get_xpath(ctx, "/domain/vcpu") + title = util.get_xpath(ctx, "/domain/title") + title = title if title else '' + description = util.get_xpath(ctx, "/domain/description") + description = description if description else '' + return (mem, vcpu, title, description) + for name in self.get_instances(): + dom = self.get_instance(name) + xml = dom.XMLDesc(0) + (mem, vcpu, title, description) = util.get_xml_path(xml, func=get_info) vname[dom.name()] = { 'status': dom.info()[0], 'uuid': dom.UUIDString(), 'vcpu': vcpu, 'memory': mem, - 'title': title if title else '', - 'description': description if description else '', + 'title': title, + 'description': description, } return vname def get_user_instances(self, name): dom = self.get_instance(name) - mem = util.get_xml_path(dom.XMLDesc(0), "/domain/currentMemory") - mem = int(mem) / 1024 - cur_vcpu = util.get_xml_path(dom.XMLDesc(0), "/domain/vcpu/@current") - if cur_vcpu: - vcpu = cur_vcpu - else: - vcpu = util.get_xml_path(dom.XMLDesc(0), "/domain/vcpu") - title = util.get_xml_path(dom.XMLDesc(0), "/domain/title") - description = util.get_xml_path(dom.XMLDesc(0), "/domain/description") + xml = dom.XMLDesc(0) + def get_info(ctx): + mem = util.get_xpath(ctx, "/domain/currentMemory") + mem = int(mem) / 1024 + cur_vcpu = util.get_xpath(ctx, "/domain/vcpu/@current") + if cur_vcpu: + vcpu = cur_vcpu + else: + vcpu = util.get_xpath(ctx, "/domain/vcpu") + title = util.get_xpath(ctx, "/domain/title") + title = title if title else '' + description = util.get_xpath(ctx, "/domain/description") + description = description if description else '' + return (mem, vcpu, title, description) + (mem, vcpu, title, description) = util.get_xml_path(xml, func=get_info) 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 '', + 'title': title, + 'description': description, } def close(self): diff --git a/vrtManager/create.py b/vrtManager/create.py index 15f1cd8..9fc8d98 100644 --- a/vrtManager/create.py +++ b/vrtManager/create.py @@ -48,23 +48,12 @@ class wvmCreate(wvmConnect): """Get guest capabilities""" return util.get_xml_path(self.get_cap_xml(), "/capabilities/host/cpu/arch") - def get_cache_modes(self): - """Get cache available modes""" - return { - 'default': 'Default', - 'none': 'Disabled', - 'writethrough': 'Write through', - 'writeback': 'Write back', - 'directsync': 'Direct sync', # since libvirt 0.9.5 - 'unsafe': 'Unsafe', # since libvirt 0.9.7 - } - - def create_volume(self, storage, name, size, format='qcow2', metadata=False): + def create_volume(self, storage, name, size, format='qcow2', metadata=False, image_extension='img'): size = int(size) * 1073741824 stg = self.get_storage(storage) storage_type = util.get_xml_path(stg.XMLDesc(0), "/pool/@type") if storage_type == 'dir': - name += '.img' + name += '.' + image_extension alloc = 0 else: alloc = size diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 0777b35..14feab1 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -340,6 +340,22 @@ class wvmInstance(wvmConnect): xmldom = ElementTree.tostring(tree) self._defineXML(xmldom) + def attach_disk(self, source, target, sourcetype='file', type='disk', driver='qemu', subdriver='raw', cache='none', targetbus='ide'): + tree = ElementTree.fromstring(self._XMLDesc(0)) + xml_disk = """ + <disk type='%s' device='%s'> + <driver name='%s' type='%s' cache='%s'/> + <source file='%s'/> + <target dev='%s' bus='%s'/> + </disk> + """ % (sourcetype, type, driver, subdriver, cache, source, target, targetbus) + if self.get_status() == 5: + devices = tree.find('devices') + elm_disk = ElementTree.fromstring(xml_disk) + devices.append(elm_disk) + xmldom = ElementTree.tostring(tree) + self._defineXML(xmldom) + def cpu_usage(self): cpu_usage = {} if self.get_status() == 1: diff --git a/vrtManager/util.py b/vrtManager/util.py index 23e9498..5f43992 100644 --- a/vrtManager/util.py +++ b/vrtManager/util.py @@ -94,13 +94,7 @@ def get_xml_path(xml, path=None, func=None): ctx = doc.xpathNewContext() if path: - ret = ctx.xpathEval(path) - if ret is not None: - if type(ret) == list: - if len(ret) >= 1: - result = ret[0].content - else: - result = ret + result = get_xpath(ctx, path) elif func: result = func(ctx) @@ -115,6 +109,19 @@ def get_xml_path(xml, path=None, func=None): return result +def get_xpath(ctx, path): + result = None + ret = ctx.xpathEval(path) + if ret is not None: + if type(ret) == list: + if len(ret) >= 1: + result = ret[0].content + else: + result = ret + + return result + + def pretty_mem(val): val = int(val) if val > (10 * 1024 * 1024): diff --git a/webvirtcloud/settings.py b/webvirtcloud/settings.py.template similarity index 79% rename from webvirtcloud/settings.py rename to webvirtcloud/settings.py.template index 866ff36..0effb8e 100644 --- a/webvirtcloud/settings.py +++ b/webvirtcloud/settings.py.template @@ -6,7 +6,7 @@ Django settings for webvirtcloud project. import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -SECRET_KEY = '4y(f4rfqc6f2!i8_vfuu)kav6tdv5#sc=n%o451dm+th0&3uci' +SECRET_KEY = '' DEBUG = True @@ -44,10 +44,10 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) -#AUTHENTICATION_BACKENDS = ( -# 'django.contrib.auth.backends.RemoteUserBackend', -# #'accounts.backends.MyRemoteUserBackend', -#) +AUTHENTICATION_BACKENDS = ( + #'django.contrib.auth.backends.RemoteUserBackend', + #'accounts.backends.MyRemoteUserBackend', +) LOGIN_URL = '/accounts/login' @@ -78,9 +78,13 @@ STATICFILES_DIRS = ( os.path.join(BASE_DIR, "static"), ) -TEMPLATE_DIRS = ( - os.path.join(BASE_DIR, 'templates'), -) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ os.path.join(BASE_DIR, 'templates'), ], + 'APP_DIRS': True, + } +] ## WebVirtCloud settings @@ -113,7 +117,14 @@ LIBVIRT_KEEPALIVE_INTERVAL = 5 LIBVIRT_KEEPALIVE_COUNT = 5 ALLOW_INSTANCE_MULTIPLE_OWNER = True -CLONE_INSTANCE_DEFAULT_PREFIX = 'ourea' +NEW_USER_DEFAULT_INSTANCES = [] +CLONE_INSTANCE_DEFAULT_PREFIX = 'instance' LOGS_PER_PAGE = 100 QUOTA_DEBUG = True ALLOW_EMPTY_PASSWORD = True +SHOW_ACCESS_ROOT_PASSWORD = False +SHOW_ACCESS_SSH_KEYS = False +SHOW_PROFILE_EDIT_PASSWORD = False +INSTANCE_VOLUME_DEFAULT_FORMAT = 'qcow2' +INSTANCE_VOLUME_DEFAULT_BUS = 'virtio' +INSTANCE_VOLUME_DEFAULT_CACHE = 'directsync'