diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 5a43dcd..79cfe41 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -309,6 +309,11 @@ {% 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"> @@ -390,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> diff --git a/instances/views.py b/instances/views.py index 8dd0a3f..d6d5c61 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 @@ -18,6 +18,7 @@ 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 @@ -279,13 +280,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() @@ -318,6 +331,13 @@ 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() try: instance = Instance.objects.get(compute_id=compute_id, name=vname) @@ -457,6 +477,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', '') diff --git a/vrtManager/connection.py b/vrtManager/connection.py index c62c6e4..b9c6a26 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) 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/webvirtcloud/settings.py b/webvirtcloud/settings.py index 724fb1a..1f87d20 100644 --- a/webvirtcloud/settings.py +++ b/webvirtcloud/settings.py @@ -124,3 +124,6 @@ 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'