diff --git a/instances/models.py b/instances/models.py index 2946715..978d118 100644 --- a/instances/models.py +++ b/instances/models.py @@ -173,6 +173,10 @@ class Instance(models.Model): def snapshots(self): return sorted(self.proxy.get_snapshot(), reverse=True, key=lambda k: k["date"]) + @cached_property + def external_snapshots(self): + return sorted(self.proxy.get_external_snapshots(), reverse=True, key=lambda k: k["date"]) + @cached_property def inst_xml(self): return self.proxy._XMLDesc(VIR_DOMAIN_XML_SECURE) diff --git a/instances/templates/allinstances.html b/instances/templates/allinstances.html index 095d99c..457d9f6 100644 --- a/instances/templates/allinstances.html +++ b/instances/templates/allinstances.html @@ -25,10 +25,12 @@ {% endif %} {% endfor %}
- {% if app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser or 'instances.view_instances' in perms %} - {% include 'allinstances_index_grouped.html' %} - {% else %} - {% include 'allinstances_index_nongrouped.html' %} + {% if 'instances.view_instances' in perms %} + {% if app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser %} + {% include 'allinstances_index_grouped.html' %} + {% else %} + {% include 'allinstances_index_nongrouped.html' %} + {% endif %} {% endif %}
{% endblock content %} diff --git a/instances/templates/instance.html b/instances/templates/instance.html index bf48bbe..748c0df 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -679,7 +679,7 @@ } }); } - if (~$.inArray(hash, ['#resize', "resizevm_cpu", "resizevm_mem", "resizevm_disk"])) { + if (~$.inArray(hash, ['#resize', "#resizevm_cpu", "#resizevm_mem", "#resizevm_disk"])) { var btnsect = $('#navbtn>li>a'); $(btnsect).each(function () { if ($(this).attr('href') === '#resize') { @@ -702,7 +702,7 @@ } }); } - if (~$.inArray(hash, ['#takesnapshot', '#managesnapshot'])) { + if (~$.inArray(hash, ['#takesnapshot', "#takeextsnapshot", "#managesnapshot"])) { var btnsect = $('#navbtn>li>a'); $(btnsect).each(function () { if ($(this).attr('href') === '#snapshots') { diff --git a/instances/templates/instance_actions.html b/instances/templates/instance_actions.html index f67dc1d..1948b42 100644 --- a/instances/templates/instance_actions.html +++ b/instances/templates/instance_actions.html @@ -7,7 +7,7 @@ {% icon 'clone' %} {% else %} - + {% icon 'play' %} {% endif %} @@ -32,7 +32,7 @@ {% endif %} {% if instance.proxy.instance.info.0 == 1 %} - {% icon 'play' %} + {% icon 'play' %} {% icon 'pause' %} {% icon 'power-off' %} diff --git a/instances/templates/instances/power_tab.html b/instances/templates/instances/power_tab.html index cf4fe1e..3811490 100644 --- a/instances/templates/instances/power_tab.html +++ b/instances/templates/instances/power_tab.html @@ -68,7 +68,7 @@

{% trans "This action forcibly powers off the instance and may cause data corruption." %}

-
+ {% csrf_token %}
@@ -119,7 +119,7 @@

{% trans "Template instance cannot be started." %}

{% else %} - + {% endif %}
diff --git a/instances/templates/instances/snapshots_tab.html b/instances/templates/instances/snapshots_tab.html old mode 100644 new mode 100755 index d6eb88e..5048e7e --- a/instances/templates/instances/snapshots_tab.html +++ b/instances/templates/instances/snapshots_tab.html @@ -5,7 +5,12 @@
- + +
+ {% if instance.status != 5 %} +

{% trans "You can get external snapshots within this tab." %}

+

{% trans "External snapshots are experimental in this stage, use it if you know what you are doing. It may require manual intervention." %}

+ {% else %} +

{% trans "Create an external snapshot" %}

+ {% endif %} +

{% trans "Give your External Snapshot a distinctive description so it wouldn't get mixed with other snapshots." %}

+
+ {% csrf_token %} +
+ + | + + {% if instance.external_snapshots|length > 0 or instance.snapshots|length > 0 %} + + {% else %} + + {% endif %} +
+

{% trans "WebVirtCloud supports only one external snapshot at the moment." %}

+
+
+
- {% if instance.snapshots %} + {% if instance.snapshots or instance.external_snapshots %}

{% trans "Choose a snapshot for restore/delete" %}

- + - {% for snap in instance.snapshots %} - - - - - - - - {% endfor %} + {% if instance.snapshots %} + {% for snap in instance.snapshots %} + + + + + + + + {% endfor %} + {% elif instance.external_snapshots %} + {% for ext_snap in instance.external_snapshots %} + + + + + + + + {% endfor %} + {% endif %}
{% trans "Date" %} {% trans "Name" %}{% trans "Description" %}{% trans "Type - Description" %} {% trans "Action" %}
{{ snap.date|date:"M d H:i:s" }}{{ snap.name }}{{ snap.description }} -
- {% csrf_token %} - - {% if instance.status == 5 %} - - {% else %} - - {% endif %} -
-
-
{% csrf_token %} - - -
-
{{ snap.date|date:"M d H:i:s" }}{{ snap.name }}({% trans "Internal" %}) - {{ snap.description }} +
+ {% csrf_token %} + + {% if instance.status == 5 %} + + {% else %} + + {% endif %} +
+
+
{% csrf_token %} + + +
+
{{ ext_snap.date|date:"M d H:i:s" }}{{ ext_snap.name }}({% trans "External" %}) - {{ ext_snap.description }} +
+ {% csrf_token %} + + + + +
+
+
{% csrf_token %} + + +
+
{% else %}

{% trans "You do not have any snapshots" %}

{% endif %} -
- - + + + diff --git a/instances/urls.py b/instances/urls.py old mode 100644 new mode 100755 index c7a74f9..acb5444 --- a/instances/urls.py +++ b/instances/urls.py @@ -39,6 +39,9 @@ urlpatterns = [ path("/snapshot/", views.snapshot, name="snapshot"), path("/delete_snapshot/", views.delete_snapshot, name="delete_snapshot"), path("/revert_snapshot/", views.revert_snapshot, name="revert_snapshot"), + path("/create_external_snapshot/", views.create_external_snapshot, name="create_external_snapshot"), + path("/revert_external_snapshot/", views.revert_external_snapshot, name="revert_external_snapshot"), + path("/delete_external_snapshot/", views.delete_external_snapshot, name="delete_external_snapshot"), path("/set_vcpu/", views.set_vcpu, name="set_vcpu"), path("/set_vcpu_hotplug/", views.set_vcpu_hotplug, name="set_vcpu_hotplug"), path("/set_autostart/", views.set_autostart, name="set_autostart"), diff --git a/instances/views.py b/instances/views.py old mode 100644 new mode 100755 index 3b018bb..5ba91e9 --- a/instances/views.py +++ b/instances/views.py @@ -20,7 +20,9 @@ from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from libvirt import (VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_NVRAM, +from libvirt import (VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, + VIR_DOMAIN_UNDEFINE_NVRAM, + VIR_DOMAIN_START_PAUSED, libvirtError) from logs.views import addlogmsg from vrtManager import util @@ -146,7 +148,7 @@ def instance(request, pk): instance.drbd = drbd_status(request, pk) instance.save() - return render(request, "instance.html", locals()) + return render(request, "instance.html", locals(),) def status(request, pk): @@ -1015,6 +1017,81 @@ def revert_snapshot(request, pk): return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") +def create_external_snapshot(request, pk): + instance = get_instance(request.user, pk) + allow_admin_or_not_template = ( + request.user.is_superuser or request.user.is_staff or not instance.is_template + ) + + if allow_admin_or_not_template and request.user.has_perm( + "instances.snapshot_instances" + ): + name = request.POST.get("name", "") + desc = request.POST.get("description", "") + instance.proxy.create_external_snapshot("s1." + name, instance, desc=desc) + msg = _("Create external snapshot: %(snap)s") % {"snap": name} + addlogmsg(request.user.username, instance.compute.name, instance.name, msg) + return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") + + +def get_external_snapshots(request, pk): + instance = get_instance(request.user, pk) + allow_admin_or_not_template = ( + request.user.is_superuser or request.user.is_staff or not instance.is_template + ) + + if allow_admin_or_not_template and request.user.has_perm( + "instances.snapshot_instances" + ): + external_snapshots = instance.proxy.get_external_snapshots() + return external_snapshots + + +def revert_external_snapshot(request, pk): + instance = get_instance(request.user, pk) + allow_admin_or_not_template = ( + request.user.is_superuser or request.user.is_staff or not instance.is_template + ) + + if allow_admin_or_not_template and request.user.has_perm( + "instances.snapshot_instances" + ): + instance_state = True if instance.proxy.get_status() != 5 else False + name = request.POST.get("name", "") + date = request.POST.get("date", "") + desc = request.POST.get("desc", "") + instance.proxy.force_shutdown() if instance_state else None + instance.proxy.revert_external_snapshot(name, date, desc) + instance.proxy.start() if instance_state else None + msg = _("Revert external snapshot: %(snap)s") % {"snap": name} + addlogmsg(request.user.username, instance.compute.name, instance.name, msg) + return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") + + +def delete_external_snapshot(request, pk): + instance = get_instance(request.user, pk) + instance_state = True if instance.proxy.get_status() == 5 else False + allow_admin_or_not_template = ( + request.user.is_superuser or request.user.is_staff or not instance.is_template + ) + + if allow_admin_or_not_template and request.user.has_perm( + "instances.snapshot_instances" + ): + name = request.POST.get("name", "") + + instance.proxy.start(VIR_DOMAIN_START_PAUSED) if instance_state else None + + try: + instance.proxy.delete_external_snapshot(name) + msg = _("Delete external snapshot: %(snap)s") % {"snap": name} + addlogmsg(request.user.username, instance.compute.name, instance.name, msg) + finally: + instance.proxy.force_shutdown() if instance_state else None + + return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot") + + @superuser_only def set_vcpu(request, pk): instance = get_instance(request.user, pk) diff --git a/vrtManager/instance.py b/vrtManager/instance.py index e51f8bc..b85f0a0 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -2,6 +2,7 @@ import contextlib import json import os.path import time +import subprocess try: from libvirt import ( @@ -18,6 +19,14 @@ try: VIR_MIGRATE_POSTCOPY, VIR_MIGRATE_UNDEFINE_SOURCE, VIR_MIGRATE_UNSAFE, + VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY, + VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY, + VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL, + VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL, + VIR_DOMAIN_BLOCK_COMMIT_DELETE, + VIR_DOMAIN_BLOCK_COMMIT_ACTIVE, + VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT, + VIR_DOMAIN_START_PAUSED, libvirtError, ) from libvirt_qemu import VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT, qemuAgentCommand @@ -34,7 +43,6 @@ from vrtManager import util from vrtManager.connection import wvmConnect from vrtManager.storage import wvmStorage, wvmStorages - class wvmInstances(wvmConnect): def get_instance_status(self, name): inst = self.get_instance(name) @@ -202,8 +210,8 @@ class wvmInstance(wvmConnect): return info_results - def start(self): - self.instance.create() + def start(self, flags=0): + self.instance.createWithFlags(flags) def shutdown(self): self.instance.shutdown() @@ -449,6 +457,7 @@ class wvmInstance(wvmConnect): disk_format = used_size = disk_size = None disk_cache = disk_io = disk_discard = disk_zeroes = "default" readonly = shareable = serial = None + backing_file = None device = disk.xpath("@device")[0] if device == "disk": @@ -480,6 +489,9 @@ class wvmInstance(wvmConnect): with contextlib.suppress(Exception): disk_zeroes = disk.xpath("driver/@detect_zeroes")[0] + with contextlib.suppress(Exception): + backing_file = disk.xpath("backingStore/source/@file")[0] + readonly = bool(disk.xpath("readonly")) shareable = bool(disk.xpath("shareable")) serial = ( @@ -509,6 +521,7 @@ class wvmInstance(wvmConnect): "storage": storage, "path": src_file, "format": disk_format, + "backing_file": backing_file, "size": disk_size, "used": used_size, "cache": disk_cache, @@ -1283,9 +1296,87 @@ class wvmInstance(wvmConnect): ) self._defineXML(xml_temp) - def get_snapshot(self): + def create_external_snapshot(self, name, date=None, desc=None): + creation_time = time.time() + state = "shutoff" if self.get_status() == 5 else "running" + # + xml = """ + %s + %s + %s + %d + """ % ( + name, + desc, + state, + creation_time, + ) + + self.change_snapshot_xml() + xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE) + xml += """0 + """ + + self._snapshotCreateXML(xml, VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY) + + + def get_external_snapshots(self): + return self.get_snapshot(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL) + + def delete_external_snapshot(self, name): + disk_info = self.get_disk_devices() + for disk in disk_info: + target_dev = disk["dev"] + backing_file = disk["backing_file"] + snap_source_file = disk["path"] + self.instance.blockCommit(target_dev, backing_file, snap_source_file, + flags=VIR_DOMAIN_BLOCK_COMMIT_DELETE| + VIR_DOMAIN_BLOCK_COMMIT_ACTIVE) + while True: + info = self.instance.blockJobInfo(target_dev, 0) + if info.get('cur') == info.get('end'): + self.instance.blockJobAbort(target_dev,flags=VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT) + time.sleep(2) + break + # Check again pool for snapshot delta volume; if it exist, remove it manually + with contextlib.suppress(libvirtError): + vol_snap = self.get_volume_by_path(snap_source_file) + pool = vol_snap.storagePoolLookupByVolume() + pool.refresh(0) + vol_snap.delete(0) + + snap = self.instance.snapshotLookupByName(name, 0) + snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY) + + + def revert_external_snapshot(self, name, date, desc): + pool = None + snap = self.instance.snapshotLookupByName(name, 0) + snap_xml = snap.getXMLDesc(0) + snapXML = ElementTree.fromstring(snap_xml) + disks = snapXML.findall('inactiveDomain/devices/disk') + if not disks: disks = snapXML.findall('domain/devices/disk') + + self.start(flags=VIR_DOMAIN_START_PAUSED) if self.get_status() == 5 else None + + disk_info = self.get_disk_devices() + for disk in disk_info: + vol_snap = self.get_volume_by_path(disk["path"]) + pool = vol_snap.storagePoolLookupByVolume() + pool.refresh(0) + vol_snap.delete(0) + self.force_shutdown() + + snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY) + for disk in disks: + self.instance.updateDeviceFlags(ElementTree.tostring(disk).decode("UTF-8")) + name = name.replace("s1", "s2") + self.create_external_snapshot(name, date, desc) + pool.refresh() if pool else None + + def get_snapshot(self, flag=VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL): snapshots = [] - snapshot_list = self.instance.snapshotListNames(0) + snapshot_list = self.instance.snapshotListNames(flag) for snapshot in snapshot_list: snap = self.instance.snapshotLookupByName(snapshot, 0) snap_description = util.get_xml_path(