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." %}
-
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." %}
+
+
+
- {% if instance.snapshots %}
+ {% if instance.snapshots or instance.external_snapshots %}
{% trans "Choose a snapshot for restore/delete" %}
| {% trans "Date" %} |
{% trans "Name" %} |
- {% trans "Description" %} |
+ {% trans "Type - Description" %} |
{% trans "Action" %} |
- {% for snap in instance.snapshots %}
-
- | {{ snap.date|date:"M d H:i:s" }} |
- {{ snap.name }} |
- {{ snap.description }} |
-
-
- |
-
-
- |
-
- {% endfor %}
+ {% if instance.snapshots %}
+ {% for snap in instance.snapshots %}
+
+ | {{ snap.date|date:"M d H:i:s" }} |
+ {{ snap.name }} |
+ ({% trans "Internal" %}) - {{ snap.description }} |
+
+
+ |
+
+
+ |
+
+ {% endfor %}
+ {% elif instance.external_snapshots %}
+ {% for ext_snap in instance.external_snapshots %}
+
+ | {{ ext_snap.date|date:"M d H:i:s" }} |
+ {{ ext_snap.name }} |
+ ({% trans "External" %}) - {{ ext_snap.description }} |
+
+
+ |
+
+
+ |
+
+ {% endfor %}
+ {% endif %}
{% 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(