From bea6b1454beee24705bd6467f52a285ff37221ba Mon Sep 17 00:00:00 2001
From: catborise <catborise@gmail.com>
Date: Mon, 13 Mar 2023 11:22:40 +0300
Subject: [PATCH 1/8] fix grouped non grouped instance view

---
 instances/templates/allinstances.html | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

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 %}
     <div class="col-lg-12">
-        {% 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 %}
     </div>
 {% endblock content %}

From ad9f1db643c26b9b72a047f11aad222228450303 Mon Sep 17 00:00:00 2001
From: cserma <emre.serdengecti@hotmail.com>
Date: Wed, 1 Mar 2023 14:11:30 +0300
Subject: [PATCH 2/8] added filter for logs

---
 admin/templates/admin/logs.html | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/admin/templates/admin/logs.html b/admin/templates/admin/logs.html
index 3305641..a6c8f0d 100644
--- a/admin/templates/admin/logs.html
+++ b/admin/templates/admin/logs.html
@@ -6,6 +6,12 @@
 
 {% block page_heading %}{% trans "Logs" %}{% endblock page_heading %}
 
+{% block page_heading_extra %}
+    {% include 'search_block.html' %}
+{% endblock page_heading_extra %}
+
+
+
 {% block content %}
 <div class="row">
     <div class="col-lg-12">
@@ -28,7 +34,7 @@
                             <th scope="col">{% trans "Message" %}</th>
                             </tr>
                     </thead>
-                    <tbody>
+                    <tbody class="searchable">
                         {% for log in logs %}
                             <tr>
                                 <td>{{ log.id }}</td>
@@ -47,3 +53,6 @@
     </div>
 </div>
 {% endblock %}
+{% block script %}
+    <script src="/static/js/filter-table.js"></script>
+{% endblock script %}

From fd6b2ec4bfc7855ba2ab8f45f6cb218eb39819a2 Mon Sep 17 00:00:00 2001
From: cserma <emre.serdengecti@hotmail.com>
Date: Tue, 4 Apr 2023 09:51:59 +0300
Subject: [PATCH 3/8] external snapshot implementation

---
 .../templates/instances/snapshots_tab.html    | 101 +++++++++++-
 instances/urls.py                             |   3 +
 instances/views.py                            |  65 +++++++-
 vrtManager/instance.py                        | 153 +++++++++++++++++-
 4 files changed, 315 insertions(+), 7 deletions(-)
 mode change 100644 => 100755 instances/templates/instances/snapshots_tab.html
 mode change 100644 => 100755 instances/urls.py
 mode change 100644 => 100755 instances/views.py

diff --git a/instances/templates/instances/snapshots_tab.html b/instances/templates/instances/snapshots_tab.html
old mode 100644
new mode 100755
index d6eb88e..986bf90
--- a/instances/templates/instances/snapshots_tab.html
+++ b/instances/templates/instances/snapshots_tab.html
@@ -13,6 +13,16 @@
                 {% trans "Manage Snapshots" %}
             </button>
         </li>
+        <li class="nav-item" role="presentation">
+            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#externalSnapshot" type="button" role="tab" aria-controls="externalSnapshot" aria-selected="false">
+                External Snapshot
+            </button>
+        </li>
+        <li class="nav-item" role="presentation">
+            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#manageExternalSnapshots" type="button" role="tab" aria-controls="manageExternalSnapshots" aria-selected="false">
+                Manage External Snapshots
+            </button>
+        </li>
     </ul>
     <!-- Tab panes -->
     <div class="tab-content">
@@ -29,7 +39,11 @@
                         <input type="text" class="form-control form-control-lg" name="name" placeholder="{% trans "Snapshot Name" %}" maxlength="14">
                         <span class="input-group-text">|</span>
                         <input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
-                        <input type="submit" class="btn btn-lg btn-success float-end" name="snapshot" value="{% trans "Take Snapshot" %}" onclick="showPleaseWaitDialog();">
+                        {% if external_snapshots|length > 0 %}
+                            <input type="submit" class="btn btn-lg btn-success disabled float-end" name="snapshot" value="{% trans "Take Snapshot" %}">
+                        {% else %}
+                            <input type="submit" class="btn btn-lg btn-success float-end" name="snapshot" value="{% trans "Take Snapshot" %}" onclick="showPleaseWaitDialog();">
+                        {% endif %}
                     </div>
             </form>
             <div class="clearfix"></div>
@@ -56,11 +70,11 @@
                                         {% csrf_token %}
                                         <input type="hidden" name="name" value="{{ snap.name }}">
                                         {% if instance.status == 5 %}
-                                            <button type="submit" class="btn btn-sm btn-secondary" name="revert_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('Are you sure?')">
+                                            <button type="submit" class="btn btn-sm btn-primary" name="revert_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('Are you sure?')">
                                                     <span class="fa fa-download"></span>
                                             </button>
                                         {% else %}
-                                            <button type="button" class="btn btn-sm btn-secondary disabled"
+                                            <button type="button" class="btn btn-sm btn-primary disabled"
                                                     title="{% trans "To restore snapshots you need Power Off the instance." %}">
                                                     <span class="fa fa-download"></span>
                                             </button>
@@ -84,5 +98,86 @@
                 <p>{% trans "You do not have any snapshots" %}</p>
             {% endif %}
         </div>
+        <div role="tabpanel" class="tab-pane tab-pane-bordered" id="externalSnapshot">
+            {% if instance.status != 5 %}
+            <p>You can get external snapshots within this tab.</p>
+            <p class="text-primary">External snapshots are experimental in this stage, use it if you know what you are doing.</p>
+            {% else %}
+                <p>Create an external snapshot</p>
+            {% endif %}
+            <p class="text-danger">Give your External Snapshot a <b>distinctive description</b> so it wouldn't get mixed with other snapshots.</p>
+            <form action="{% url 'instances:create_external_snapshot' instance.id %}" method="post" role="form" aria-label="Create snapshot form">
+                {% csrf_token %}
+                <div class="mb-3" style="white-space:pre-line">
+                    <input type="text" class="form-control form-control-lg" name="name" placeholder="{% trans "Snapshot Name" %}" maxlength="14">
+                    <input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
+                    {% if external_snapshots|length > 0 or instance.snapshots|length > 0 %}
+                        <p class="text-danger">WebVirtCloud supports only one External Snapshot at the moment.</p>
+                        <input type="submit" class="btn btn-lg btn-success disabled float-end" name="snapshot" value="{% trans "Take Snapshot" %}">
+                    {% else %}
+                        <input type="submit" class="btn btn-lg btn-success float-end" name="snapshot" value="{% trans "Take Snapshot" %}" onclick="showPleaseWaitDialog();">
+                    {% endif %}
+                    
+                </div>
+            </form>
+            <div class="clearfix"></div>
+        </div>
+        <div role="tabpanel" class="tab-pane tab-pane-bordered" id="manageExternalSnapshots">
+            {% if external_snapshots %}
+            <div class="table-responsive">
+                <table class="table">
+                    <thead>
+                        <th scope="col">Name</th>
+                        <th scope="col">Date</th>
+                        <th scope="col">Description</th>
+                        <th scope="colgroup" colspan="2">{% trans "Action" %}</th>
+                    </thead>
+                    <tbody>
+                        {% for external_snapshot in external_snapshots %}
+                            <tr>
+                            {% for snapshot_cols in external_snapshot %}
+                                <td>{{snapshot_cols}}</td>
+                            {% endfor %}
+                            <td style="width:30px;">
+                                <form action="{% url 'instances:revert_external_snapshot' instance.id %}" method="post" role="form" aria-label="Restore external snapshot form">
+                                    {% csrf_token %}
+                                    <input type="hidden" name="name" value="{{ external_snapshot.0 }}">
+                                    <input type="hidden" name="date" value="{{ external_snapshot.1 }}">
+                                    <input type="hidden" name="desc" value="{{ external_snapshot.2 }}">
+                            {% if instance.status == 5 %}
+                                    <button type="submit" class="btn btn-sm btn-primary" name="revert_external_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('You are going to lose your unsaved work by reverting to this snapshot state. Are you sure?')">
+                                            <span class="fa fa-download"></span>
+                                    </button>
+                            {% else %}
+                                    <button type="button" class="btn btn-sm btn-primary disabled"
+                                            title="{% trans "To restore snapshots you need Power Off the instance." %}">
+                                            <span class="fa fa-download"></span>
+                                    </button>
+                            {% endif %}
+                                </form>
+                            </td>
+                            <td style="width:30px;">
+                                <form action="{% url 'instances:delete_external_snapshot' instance.id %}" method="post" role="form" aria-label="Delete external snapshot form">{% csrf_token %}
+                                    <input type="hidden" name="name" value="{{ external_snapshot.0 }}">
+                                    {% if instance.status != 5 %}
+                                    <button type="submit" class="btn btn-sm btn-danger" name="delete_external_snapshot" title="{% trans 'Delete Snapshot' %}" onclick="return confirm('You are about to delete this snapshot and merge it with base image. Are you sure?')">
+                                        {% icon 'trash' %}
+                                    </button>
+                                    {% else %}
+                                    <button type="submit" class="btn btn-sm btn-danger disabled" title="{% trans 'Delete Snapshot' %}">
+                                        {% icon 'trash' %}
+                                    </button>
+                                    {% endif %}
+                                </form>
+                            </td>
+                            </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+            {% else%}
+                    <p>{% trans "You do not have any snapshots" %}</p>
+            {% endif %}
+        </div>
     </div>
 </div>
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("<int:pk>/snapshot/", views.snapshot, name="snapshot"),
     path("<int:pk>/delete_snapshot/", views.delete_snapshot, name="delete_snapshot"),
     path("<int:pk>/revert_snapshot/", views.revert_snapshot, name="revert_snapshot"),
+    path("<int:pk>/create_external_snapshot/", views.create_external_snapshot, name="create_external_snapshot"),
+    path("<int:pk>/revert_external_snapshot/", views.revert_external_snapshot, name="revert_external_snapshot"),
+    path("<int:pk>/delete_external_snapshot/", views.delete_external_snapshot, name="delete_external_snapshot"),
     path("<int:pk>/set_vcpu/", views.set_vcpu, name="set_vcpu"),
     path("<int:pk>/set_vcpu_hotplug/", views.set_vcpu_hotplug, name="set_vcpu_hotplug"),
     path("<int:pk>/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..f603c51
--- a/instances/views.py
+++ b/instances/views.py
@@ -146,7 +146,9 @@ def instance(request, pk):
         instance.drbd = drbd_status(request, pk)
         instance.save()
 
-    return render(request, "instance.html", locals())
+    external_snapshots = get_external_snapshots(request,pk)
+
+    return render(request, "instance.html", locals(),)
 
 
 def status(request, pk):
@@ -1014,6 +1016,67 @@ def revert_snapshot(request, pk):
         addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
     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(name, instance, desc=desc)
+        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
+        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+    return redirect(request.META.get("HTTP_REFERER") + "#manageExternalSnapshots")
+
+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()
+        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
+        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+    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"
+    ):
+        name = request.POST.get("name", "")
+        date = request.POST.get("date", "")
+        desc = request.POST.get("desc", "")
+        instance.proxy.revert_external_snapshot(name, instance, date, desc)
+        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
+        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+    return redirect(request.META.get("HTTP_REFERER") + "#manageExternalSnapshots")
+
+def delete_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", "")
+        instance.proxy.delete_external_snapshot(name, instance)
+        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
+        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+    return redirect(request.META.get("HTTP_REFERER") + "#manageExternalSnapshots")
 
 @superuser_only
 def set_vcpu(request, pk):
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index e51f8bc..890d270 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,10 @@ 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,
         libvirtError,
     )
     from libvirt_qemu import VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT, qemuAgentCommand
@@ -34,7 +39,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)
@@ -1283,9 +1287,152 @@ class wvmInstance(wvmConnect):
         )
         self._defineXML(xml_temp)
 
-    def get_snapshot(self):
+    def create_external_snapshot(self, name, instance, date=None, desc=None):
+        if self.instance.isActive() == False:
+            result = self.instance.create()
+            if result < 0:
+                return 0
+        
+        creation_time = time.time()
+        state = "shutoff" if self.get_status() == 5 else "running"
+        xml = """<domainsnapshot>
+                     <name>%s</name>
+                     <description>%s</description>
+                     <state>%s</state>
+                     <creationTime>%d</creationTime>""" % (
+            name,
+            desc,
+            state,
+            creation_time,
+        )
+        self.change_snapshot_xml()
+        xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE)
+        xml += """<active>0</active>
+                  </domainsnapshot>"""
+        # 
+        # flag number for libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
+        # is 16 (0x10; 1 << 4)
+        #
+        self._snapshotCreateXML(xml, VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY)
+
+        tree = ElementTree.fromstring(self._XMLDesc(0))
+        for disks in tree.findall("devices/disk"):
+            if disks.get('device') == "disk":
+                backingStore = disks.find("backingStore")
+                if backingStore is not None:
+                    temp_backing_file = backingStore.find('source').get('file')
+                    vol_base = self.get_volume_by_path(temp_backing_file)
+                    pool = vol_base.storagePoolLookupByVolume()
+                    pool.refresh(0)
+    
+    def get_external_snapshots(self):
+        external_snapshots = []
+        temp_snapshots = self.get_snapshot(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL)
+        for temp_snapshot in temp_snapshots:
+            external_snapshot = []
+            external_snapshot.append(temp_snapshot['name'])
+            external_snapshot.append(temp_snapshot['date'])
+            external_snapshot.append(temp_snapshot['description'])
+            external_snapshots.append(external_snapshot)
+        return external_snapshots
+    
+    def delete_external_snapshot(self, name, instance):
+
+        base_xml = ElementTree.fromstring(self._XMLDesc(0))
+        for disk in base_xml.findall('devices/disk'):
+            if disk.get('device') == 'disk':
+                backingStore = disk.find('backingStore')
+                if backingStore is not None:
+                    if backingStore.find('source') is not None:
+                        target_dev = disk.find('target').get('dev')
+                        backing_file = backingStore.find('source').get('file')
+                        source_file = disk.find('source').get('file')
+                        self.instance.blockCommit(target_dev, backing_file, source_file, flags=4|2)
+                        while True:
+                            info = self.instance.blockJobInfo(target_dev, 0)
+                            if info.get('cur') == info.get('end'):
+                                self.instance.blockJobAbort(target_dev,flags=2)
+                                break
+                        
+        snap = self.instance.snapshotLookupByName(name, 0)
+        snapXML = ElementTree.fromstring(snap.getXMLDesc(0))
+        disks = []
+        for disk_backup in snapXML.findall('inactiveDomain/devices/disk'):
+            if disk_backup.get('device') == 'disk':
+                disk_dict = {}
+                if disk_backup.find('source') is not None:
+                    disk_dict['backing_file'] = disk_backup.find('source').get('file')
+                if disk_backup.find('driver') is not None:
+                    disk_dict['driver_name'] = disk_backup.find('driver').get('name')
+                    disk_dict['driver_type'] = disk_backup.find('driver').get('type')
+                if disk_backup.find('target') is not None:
+                    disk_dict['target_dev'] = disk_backup.find('target').get('dev')
+                    disk_dict['target_bus'] = disk_backup.find('target').get('bus')
+                if disk_backup.find('boot') is not None:
+                    disk_dict['boot_order'] = disk_backup.find('boot').get('order')     
+                disks.append(disk_dict)
+
+        for disk in disks:
+            self.instance.updateDeviceFlags("""<disk type='file' device='disk'>
+    <driver name='{}' type='{}'/>
+    <source file='{}'/>
+    <target dev='{}' bus='{}'/>
+    <boot order='{}'/>
+</disk>""".format(disk["driver_name"],disk["driver_type"],disk["backing_file"],disk["target_dev"],disk["target_bus"],disk["boot_order"]))
+
+        snap = self.instance.snapshotLookupByName(name, 0)
+        # flag number for delete snapshot metadata only
+        # is 2 (0x2; 1 << 1)
+        snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY)
+    
+    def revert_external_snapshot(self, name, instance, date, desc):
+        snap = self.instance.snapshotLookupByName(name, 0)
+        snapXML = ElementTree.fromstring(snap.getXMLDesc(0))
+        disks = []
+        for disk_backup in snapXML.findall('inactiveDomain/devices/disk'):
+            if disk_backup.get('device') == 'disk':
+                disk_dict = {}
+                if disk_backup.find('source') is not None:
+                    disk_dict['backing_file'] = disk_backup.find('source').get('file')
+                if disk_backup.find('driver') is not None:
+                    disk_dict['driver_name'] = disk_backup.find('driver').get('name')
+                    disk_dict['driver_type'] = disk_backup.find('driver').get('type')
+                if disk_backup.find('target') is not None:
+                    disk_dict['target_dev'] = disk_backup.find('target').get('dev')
+                    disk_dict['target_bus'] = disk_backup.find('target').get('bus')
+                if disk_backup.find('boot') is not None:
+                    disk_dict['boot_order'] = disk_backup.find('boot').get('order')     
+                disks.append(disk_dict)
+        
+        # flag number for delete snapshot metadata only
+        # is 2 (0x2; 1 << 1)
+        snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY)
+        base_xml = ElementTree.fromstring(self._XMLDesc(0))
+        for disk in base_xml.findall('devices/disk'):
+            if disk.get('device') == 'disk':
+                backingStore = disk.find('backingStore')
+                if backingStore is not None:
+                    if backingStore.find('source') is not None:
+                        vol_base = self.get_volume_by_path(backingStore.find('source').get('file'))
+                        pool = vol_base.storagePoolLookupByVolume()
+                        pool.refresh(0)
+                        vol_snap = self.get_volume_by_path(disk.find('source').get('file'))
+                        vol_snap.wipe(0)
+                        vol_snap.delete(0)
+
+        for disk in disks:
+            self.instance.updateDeviceFlags("""<disk type='file' device='disk'>
+    <driver name='{}' type='{}'/>
+    <source file='{}'/>
+    <target dev='{}' bus='{}'/>
+    <boot order='{}'/>
+</disk>""".format(disk["driver_name"],disk["driver_type"],disk["backing_file"],disk["target_dev"],disk["target_bus"],disk["boot_order"]))
+
+        self.create_external_snapshot(name, instance, date, desc)
+
+    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(

From 68b04943507e570a2747616ab26ae213bd137aff Mon Sep 17 00:00:00 2001
From: cserma <emre.serdengecti@hotmail.com>
Date: Tue, 4 Apr 2023 16:38:12 +0300
Subject: [PATCH 4/8] external snapshot logs and bug fixes

---
 instances/views.py     |  14 ++---
 vrtManager/instance.py | 126 ++++++++++++++++-------------------------
 2 files changed, 55 insertions(+), 85 deletions(-)

diff --git a/instances/views.py b/instances/views.py
index f603c51..7bfd8ce 100755
--- a/instances/views.py
+++ b/instances/views.py
@@ -1028,8 +1028,8 @@ def create_external_snapshot(request, pk):
         name = request.POST.get("name", "")
         desc = request.POST.get("description", "")
         instance.proxy.create_external_snapshot(name, instance, desc=desc)
-        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
-        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+        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") + "#manageExternalSnapshots")
 
 def get_external_snapshots(request, pk):
@@ -1042,8 +1042,6 @@ def get_external_snapshots(request, pk):
         "instances.snapshot_instances"
     ):
         external_snapshots = instance.proxy.get_external_snapshots()
-        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
-        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
     return external_snapshots
 
 def revert_external_snapshot(request, pk):
@@ -1059,8 +1057,8 @@ def revert_external_snapshot(request, pk):
         date = request.POST.get("date", "")
         desc = request.POST.get("desc", "")
         instance.proxy.revert_external_snapshot(name, instance, date, desc)
-        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
-        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+        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") + "#manageExternalSnapshots")
 
 def delete_external_snapshot(request, pk):
@@ -1074,8 +1072,8 @@ def delete_external_snapshot(request, pk):
     ):
         name = request.POST.get("name", "")
         instance.proxy.delete_external_snapshot(name, instance)
-        #msg = _("Create snapshot: %(snap)s") % {"snap": name}
-        #addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
+        msg = _("Delete external snapshot: %(snap)s") % {"snap": name}
+        addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
     return redirect(request.META.get("HTTP_REFERER") + "#manageExternalSnapshots")
 
 @superuser_only
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index 890d270..f8658f6 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -23,6 +23,9 @@ try:
         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,
         libvirtError,
     )
     from libvirt_qemu import VIR_DOMAIN_QEMU_AGENT_COMMAND_DEFAULT, qemuAgentCommand
@@ -453,6 +456,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":
@@ -484,6 +488,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 = (
@@ -513,6 +520,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,
@@ -1309,21 +1317,15 @@ class wvmInstance(wvmConnect):
         xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE)
         xml += """<active>0</active>
                   </domainsnapshot>"""
-        # 
-        # flag number for libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
-        # is 16 (0x10; 1 << 4)
-        #
+
         self._snapshotCreateXML(xml, VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY)
 
-        tree = ElementTree.fromstring(self._XMLDesc(0))
-        for disks in tree.findall("devices/disk"):
-            if disks.get('device') == "disk":
-                backingStore = disks.find("backingStore")
-                if backingStore is not None:
-                    temp_backing_file = backingStore.find('source').get('file')
-                    vol_base = self.get_volume_by_path(temp_backing_file)
-                    pool = vol_base.storagePoolLookupByVolume()
-                    pool.refresh(0)
+        disk_info = self.get_disk_devices()
+        for disk in disk_info:
+            backing_file = disk["backing_file"]
+            vol_base = self.get_volume_by_path(backing_file)
+            pool = vol_base.storagePoolLookupByVolume()
+            pool.refresh(0)
     
     def get_external_snapshots(self):
         external_snapshots = []
@@ -1337,52 +1339,20 @@ class wvmInstance(wvmConnect):
         return external_snapshots
     
     def delete_external_snapshot(self, name, instance):
-
-        base_xml = ElementTree.fromstring(self._XMLDesc(0))
-        for disk in base_xml.findall('devices/disk'):
-            if disk.get('device') == 'disk':
-                backingStore = disk.find('backingStore')
-                if backingStore is not None:
-                    if backingStore.find('source') is not None:
-                        target_dev = disk.find('target').get('dev')
-                        backing_file = backingStore.find('source').get('file')
-                        source_file = disk.find('source').get('file')
-                        self.instance.blockCommit(target_dev, backing_file, source_file, flags=4|2)
-                        while True:
-                            info = self.instance.blockJobInfo(target_dev, 0)
-                            if info.get('cur') == info.get('end'):
-                                self.instance.blockJobAbort(target_dev,flags=2)
-                                break
-                        
-        snap = self.instance.snapshotLookupByName(name, 0)
-        snapXML = ElementTree.fromstring(snap.getXMLDesc(0))
-        disks = []
-        for disk_backup in snapXML.findall('inactiveDomain/devices/disk'):
-            if disk_backup.get('device') == 'disk':
-                disk_dict = {}
-                if disk_backup.find('source') is not None:
-                    disk_dict['backing_file'] = disk_backup.find('source').get('file')
-                if disk_backup.find('driver') is not None:
-                    disk_dict['driver_name'] = disk_backup.find('driver').get('name')
-                    disk_dict['driver_type'] = disk_backup.find('driver').get('type')
-                if disk_backup.find('target') is not None:
-                    disk_dict['target_dev'] = disk_backup.find('target').get('dev')
-                    disk_dict['target_bus'] = disk_backup.find('target').get('bus')
-                if disk_backup.find('boot') is not None:
-                    disk_dict['boot_order'] = disk_backup.find('boot').get('order')     
-                disks.append(disk_dict)
-
-        for disk in disks:
-            self.instance.updateDeviceFlags("""<disk type='file' device='disk'>
-    <driver name='{}' type='{}'/>
-    <source file='{}'/>
-    <target dev='{}' bus='{}'/>
-    <boot order='{}'/>
-</disk>""".format(disk["driver_name"],disk["driver_type"],disk["backing_file"],disk["target_dev"],disk["target_bus"],disk["boot_order"]))
+        disk_info = self.get_disk_devices()
+        for disk in disk_info:
+            target_dev = disk["dev"]
+            backing_file = disk["backing_file"]
+            source_file = disk["path"]
+            self.instance.blockCommit(target_dev, backing_file, 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)
+                    break
 
         snap = self.instance.snapshotLookupByName(name, 0)
-        # flag number for delete snapshot metadata only
-        # is 2 (0x2; 1 << 1)
         snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY)
     
     def revert_external_snapshot(self, name, instance, date, desc):
@@ -1404,29 +1374,31 @@ class wvmInstance(wvmConnect):
                     disk_dict['boot_order'] = disk_backup.find('boot').get('order')     
                 disks.append(disk_dict)
         
-        # flag number for delete snapshot metadata only
-        # is 2 (0x2; 1 << 1)
         snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY)
-        base_xml = ElementTree.fromstring(self._XMLDesc(0))
-        for disk in base_xml.findall('devices/disk'):
-            if disk.get('device') == 'disk':
-                backingStore = disk.find('backingStore')
-                if backingStore is not None:
-                    if backingStore.find('source') is not None:
-                        vol_base = self.get_volume_by_path(backingStore.find('source').get('file'))
-                        pool = vol_base.storagePoolLookupByVolume()
-                        pool.refresh(0)
-                        vol_snap = self.get_volume_by_path(disk.find('source').get('file'))
-                        vol_snap.wipe(0)
-                        vol_snap.delete(0)
+        disk_info = self.get_disk_devices()
+        for disk in disk_info:
+            source_file = disk["path"]
+            backing_file = disk["backing_file"]
+            vol_base = self.get_volume_by_path(backing_file)
+            pool = vol_base.storagePoolLookupByVolume()
+            pool.refresh(0)
+            vol_snap = self.get_volume_by_path(source_file)
+            vol_snap.wipe(0)
+            vol_snap.delete(0)
 
         for disk in disks:
-            self.instance.updateDeviceFlags("""<disk type='file' device='disk'>
-    <driver name='{}' type='{}'/>
-    <source file='{}'/>
-    <target dev='{}' bus='{}'/>
-    <boot order='{}'/>
-</disk>""".format(disk["driver_name"],disk["driver_type"],disk["backing_file"],disk["target_dev"],disk["target_bus"],disk["boot_order"]))
+            self.instance.updateDeviceFlags(
+                """<disk type='file' device='disk'>
+                    <driver name='{}' type='{}'/>
+                    <source file='{}'/>
+                    <target dev='{}' bus='{}'/>
+                    <boot order='{}'/>
+                    </disk>""".format(disk["driver_name"],
+                               disk["driver_type"],
+                               disk["backing_file"],
+                               disk["target_dev"],
+                               disk["target_bus"],
+                               disk["boot_order"]))
 
         self.create_external_snapshot(name, instance, date, desc)
 

From 33d49a68a7fa30bc693752ddbde39b15427825ef Mon Sep 17 00:00:00 2001
From: catborise <catborise@gmail.com>
Date: Thu, 13 Apr 2023 12:24:13 +0300
Subject: [PATCH 5/8] experimental: external snapshot fixes

---
 instances/models.py                           |   4 +
 instances/templates/instance.html             |   4 +-
 .../templates/instances/snapshots_tab.html    | 227 ++++++++----------
 instances/views.py                            |  33 ++-
 vrtManager/instance.py                        |  95 +++-----
 5 files changed, 156 insertions(+), 207 deletions(-)

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/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/instances/snapshots_tab.html b/instances/templates/instances/snapshots_tab.html
index 986bf90..5048e7e 100755
--- a/instances/templates/instances/snapshots_tab.html
+++ b/instances/templates/instances/snapshots_tab.html
@@ -5,7 +5,12 @@
     <ul class="nav nav-tabs" role="tablist" aria-label="Instance snapshot menu">
         <li class="nav-item" role="presentation">
             <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#takesnapshot" type="button" role="tab" aria-controls="takesnapshot" aria-selected="true">
-                {% trans "Take Snapshot" %}
+                {% trans "Take Snapshot - Internal" %}
+            </button>
+        </li>
+        <li class="nav-item" role="presentation">
+            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#takeextsnapshot" type="button" role="tab" aria-controls="externalSnapshot" aria-selected="false">
+                {% trans "Take Snapshot - External" %}
             </button>
         </li>
         <li class="nav-item" role="presentation">
@@ -13,16 +18,6 @@
                 {% trans "Manage Snapshots" %}
             </button>
         </li>
-        <li class="nav-item" role="presentation">
-            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#externalSnapshot" type="button" role="tab" aria-controls="externalSnapshot" aria-selected="false">
-                External Snapshot
-            </button>
-        </li>
-        <li class="nav-item" role="presentation">
-            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#manageExternalSnapshots" type="button" role="tab" aria-controls="manageExternalSnapshots" aria-selected="false">
-                Manage External Snapshots
-            </button>
-        </li>
     </ul>
     <!-- Tab panes -->
     <div class="tab-content">
@@ -39,7 +34,7 @@
                         <input type="text" class="form-control form-control-lg" name="name" placeholder="{% trans "Snapshot Name" %}" maxlength="14">
                         <span class="input-group-text">|</span>
                         <input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
-                        {% if external_snapshots|length > 0 %}
+                        {% if instance.external_snapshots|length > 0 %}
                             <input type="submit" class="btn btn-lg btn-success disabled float-end" name="snapshot" value="{% trans "Take Snapshot" %}">
                         {% else %}
                             <input type="submit" class="btn btn-lg btn-success float-end" name="snapshot" value="{% trans "Take Snapshot" %}" onclick="showPleaseWaitDialog();">
@@ -47,137 +42,109 @@
                     </div>
             </form>
             <div class="clearfix"></div>
-        </div>
+        </div> <!-- Tab pane - takesnapshot -->
+        <div role="tabpanel" class="tab-pane tab-pane-bordered" id="takeextsnapshot">
+            {% if instance.status != 5 %}
+            <p>{% trans "You can get external snapshots within this tab." %}</p>
+            <p class="text-primary">{% trans "External snapshots are experimental in this stage, use it if you know what you are doing. It may require manual intervention." %}</p>
+            {% else %}
+                <p>{% trans "Create an external snapshot" %}</p>
+            {% endif %}
+            <p class="text-danger">{% trans "Give your External Snapshot a <b>distinctive description</b> so it wouldn't get mixed with other snapshots." %}</p>
+            <form action="{% url 'instances:create_external_snapshot' instance.id %}" method="post" role="form" aria-label="Create snapshot form">
+                {% csrf_token %}
+                <div class="input-group mb-3">
+                    <input type="text" class="form-control form-control-lg" name="name" placeholder="{% trans "Snapshot Name" %}" maxlength="14">
+                    <span class="input-group-text">|</span>
+                    <input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
+                    {% if instance.external_snapshots|length > 0 or instance.snapshots|length > 0 %}
+                        <input type="submit" class="btn btn-lg btn-success disabled float-end" name="snapshot" value="{% trans "Take Snapshot" %}">
+                    {% else %}
+                        <input type="submit" class="btn btn-lg btn-success float-end" name="snapshot" value="{% trans "Take Snapshot" %}" onclick="showPleaseWaitDialog();">
+                    {% endif %}
+                </div>
+                <p class="text-danger">{% trans "WebVirtCloud supports only one external snapshot at the moment." %}</p>
+            </form>
+            <div class="clearfix"></div>
+        </div> <!--tab pane takeextsnapshot-->
         <div role="tabpanel" class="tab-pane tab-pane-bordered" id="managesnapshot">
-            {% if instance.snapshots %}
+            {% if instance.snapshots or instance.external_snapshots %}
                 <p>{% trans "Choose a snapshot for restore/delete" %}</p>
                 <div class="table-responsive">
                     <table class="table">
                         <thead>
                             <th scope="col">{% trans "Date" %}</th>
                             <th scope="col">{% trans "Name" %}</th>
-                            <th scope="col">{% trans "Description" %}</th>
+                            <th scope="col">{% trans "Type - Description" %}</th>
                             <th scope="colgroup" colspan="2">{% trans "Action" %}</th>
                         </thead>
                         <tbody>
-                        {% for snap in instance.snapshots %}
-                            <tr>
-                                <td>{{ snap.date|date:"M d H:i:s" }}</td>
-                                <td><strong>{{ snap.name }}</strong></td>
-                                <td>{{ snap.description }}</td>
-                                <td style="width:30px;">
-                                    <form action="{% url 'instances:revert_snapshot' instance.id %}" method="post" role="form" aria-label="Restore snapshot form">
-                                        {% csrf_token %}
-                                        <input type="hidden" name="name" value="{{ snap.name }}">
-                                        {% if instance.status == 5 %}
-                                            <button type="submit" class="btn btn-sm btn-primary" name="revert_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('Are you sure?')">
-                                                    <span class="fa fa-download"></span>
-                                            </button>
-                                        {% else %}
-                                            <button type="button" class="btn btn-sm btn-primary disabled"
-                                                    title="{% trans "To restore snapshots you need Power Off the instance." %}">
-                                                    <span class="fa fa-download"></span>
-                                            </button>
-                                        {% endif %}
-                                    </form>
-                                </td>
-                                <td style="width:30px;">
-                                    <form action="{% url 'instances:delete_snapshot' instance.id %}" method="post" role="form" aria-label="Delete snapshot form">{% csrf_token %}
-                                        <input type="hidden" name="name" value="{{ snap.name }}">
-                                        <button type="submit" class="btn btn-sm btn-danger" title="{% trans 'Delete Snapshot' %}" onclick="return confirm('{% trans "Are you sure?" %}')">
-                                            {% icon 'trash' %}
-                                        </button>
-                                    </form>
-                                </td>
-                            </tr>
-                        {% endfor %}
+                            {% if instance.snapshots %}
+                                {% for snap in instance.snapshots %}
+                                    <tr>
+                                        <td>{{ snap.date|date:"M d H:i:s" }}</td>
+                                        <td><strong>{{ snap.name }}</strong></td>
+                                        <td>({% trans "Internal" %}) - {{ snap.description }}</td>
+                                        <td style="width:30px;">
+                                            <form action="{% url 'instances:revert_snapshot' instance.id %}" method="post" role="form" aria-label="Restore snapshot form">
+                                                {% csrf_token %}
+                                                <input type="hidden" name="name" value="{{ snap.name }}">
+                                                {% if instance.status == 5 %}
+                                                    <button type="submit" class="btn btn-sm btn-primary" name="revert_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('Are you sure?')">
+                                                            <span class="fa fa-download"></span>
+                                                    </button>
+                                                {% else %}
+                                                    <button type="button" class="btn btn-sm btn-primary disabled"
+                                                            title="{% trans "To restore snapshots you need Power Off the instance." %}">
+                                                            <span class="fa fa-download"></span>
+                                                    </button>
+                                                {% endif %}
+                                            </form>
+                                        </td>
+                                        <td style="width:30px;">
+                                            <form action="{% url 'instances:delete_snapshot' instance.id %}" method="post" role="form" aria-label="Delete snapshot form">{% csrf_token %}
+                                                <input type="hidden" name="name" value="{{ snap.name }}">
+                                                <button type="submit" class="btn btn-sm btn-danger" title="{% trans 'Delete Snapshot' %}" onclick="return confirm('{% trans "Are you sure?" %}')">
+                                                    {% icon 'trash' %}
+                                                </button>
+                                            </form>
+                                        </td>
+                                    </tr>
+                                {% endfor %}
+                            {% elif instance.external_snapshots %}
+                                {% for ext_snap in instance.external_snapshots %}
+                                    <tr>
+                                        <td>{{ ext_snap.date|date:"M d H:i:s" }}</td>
+                                        <td><strong>{{ ext_snap.name }}</strong></td>
+                                        <td>({% trans "External" %}) - {{ ext_snap.description }}</td>
+                                        <td style="width:30px;">
+                                            <form action="{% url 'instances:revert_external_snapshot' instance.id %}" method="post" role="form" aria-label="Restore external snapshot form">
+                                                {% csrf_token %}
+                                                <input type="hidden" name="name" value="{{ ext_snap.name }}">
+                                                <input type="hidden" name="date" value="{{ ext_snap.date }}">
+                                                <input type="hidden" name="desc" value="{{ ext_snap.description }}">
+                                                <button type="submit" class="btn btn-sm btn-primary" name="revert_external_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('You are going to lose your unsaved work by reverting to this snapshot state. Are you sure?')">
+                                                        <span class="fa fa-download"></span>
+                                                </button>
+                                            </form>
+                                        </td>
+                                        <td style="width:30px;">
+                                            <form action="{% url 'instances:delete_external_snapshot' instance.id %}" method="post" role="form" aria-label="Delete external snapshot form">{% csrf_token %}
+                                                <input type="hidden" name="name" value="{{ ext_snap.name }}">
+                                                <button type="submit" class="btn btn-sm btn-danger" name="delete_external_snapshot" title="{% trans 'Delete Snapshot' %}" onclick="return confirm('You are about to delete this snapshot and merge it with base image. Are you sure?')">
+                                                    {% icon 'trash' %}
+                                                </button>
+                                            </form>
+                                        </td>
+                                    </tr>
+                                {% endfor %}
+                            {% endif %}
                         </tbody>
                     </table>
                 </div>
             {% else %}
                 <p>{% trans "You do not have any snapshots" %}</p>
             {% endif %}
-        </div>
-        <div role="tabpanel" class="tab-pane tab-pane-bordered" id="externalSnapshot">
-            {% if instance.status != 5 %}
-            <p>You can get external snapshots within this tab.</p>
-            <p class="text-primary">External snapshots are experimental in this stage, use it if you know what you are doing.</p>
-            {% else %}
-                <p>Create an external snapshot</p>
-            {% endif %}
-            <p class="text-danger">Give your External Snapshot a <b>distinctive description</b> so it wouldn't get mixed with other snapshots.</p>
-            <form action="{% url 'instances:create_external_snapshot' instance.id %}" method="post" role="form" aria-label="Create snapshot form">
-                {% csrf_token %}
-                <div class="mb-3" style="white-space:pre-line">
-                    <input type="text" class="form-control form-control-lg" name="name" placeholder="{% trans "Snapshot Name" %}" maxlength="14">
-                    <input type="text" class="form-control form-control-lg" name="description" placeholder="{% trans "Snapshot Description" %}" maxlength="45">
-                    {% if external_snapshots|length > 0 or instance.snapshots|length > 0 %}
-                        <p class="text-danger">WebVirtCloud supports only one External Snapshot at the moment.</p>
-                        <input type="submit" class="btn btn-lg btn-success disabled float-end" name="snapshot" value="{% trans "Take Snapshot" %}">
-                    {% else %}
-                        <input type="submit" class="btn btn-lg btn-success float-end" name="snapshot" value="{% trans "Take Snapshot" %}" onclick="showPleaseWaitDialog();">
-                    {% endif %}
-                    
-                </div>
-            </form>
-            <div class="clearfix"></div>
-        </div>
-        <div role="tabpanel" class="tab-pane tab-pane-bordered" id="manageExternalSnapshots">
-            {% if external_snapshots %}
-            <div class="table-responsive">
-                <table class="table">
-                    <thead>
-                        <th scope="col">Name</th>
-                        <th scope="col">Date</th>
-                        <th scope="col">Description</th>
-                        <th scope="colgroup" colspan="2">{% trans "Action" %}</th>
-                    </thead>
-                    <tbody>
-                        {% for external_snapshot in external_snapshots %}
-                            <tr>
-                            {% for snapshot_cols in external_snapshot %}
-                                <td>{{snapshot_cols}}</td>
-                            {% endfor %}
-                            <td style="width:30px;">
-                                <form action="{% url 'instances:revert_external_snapshot' instance.id %}" method="post" role="form" aria-label="Restore external snapshot form">
-                                    {% csrf_token %}
-                                    <input type="hidden" name="name" value="{{ external_snapshot.0 }}">
-                                    <input type="hidden" name="date" value="{{ external_snapshot.1 }}">
-                                    <input type="hidden" name="desc" value="{{ external_snapshot.2 }}">
-                            {% if instance.status == 5 %}
-                                    <button type="submit" class="btn btn-sm btn-primary" name="revert_external_snapshot" title="{% trans 'Revert to this Snapshot' %}" onclick="return confirm('You are going to lose your unsaved work by reverting to this snapshot state. Are you sure?')">
-                                            <span class="fa fa-download"></span>
-                                    </button>
-                            {% else %}
-                                    <button type="button" class="btn btn-sm btn-primary disabled"
-                                            title="{% trans "To restore snapshots you need Power Off the instance." %}">
-                                            <span class="fa fa-download"></span>
-                                    </button>
-                            {% endif %}
-                                </form>
-                            </td>
-                            <td style="width:30px;">
-                                <form action="{% url 'instances:delete_external_snapshot' instance.id %}" method="post" role="form" aria-label="Delete external snapshot form">{% csrf_token %}
-                                    <input type="hidden" name="name" value="{{ external_snapshot.0 }}">
-                                    {% if instance.status != 5 %}
-                                    <button type="submit" class="btn btn-sm btn-danger" name="delete_external_snapshot" title="{% trans 'Delete Snapshot' %}" onclick="return confirm('You are about to delete this snapshot and merge it with base image. Are you sure?')">
-                                        {% icon 'trash' %}
-                                    </button>
-                                    {% else %}
-                                    <button type="submit" class="btn btn-sm btn-danger disabled" title="{% trans 'Delete Snapshot' %}">
-                                        {% icon 'trash' %}
-                                    </button>
-                                    {% endif %}
-                                </form>
-                            </td>
-                            </tr>
-                        {% endfor %}
-                    </tbody>
-                </table>
-            </div>
-            {% else%}
-                    <p>{% trans "You do not have any snapshots" %}</p>
-            {% endif %}
-        </div>
-    </div>
-</div>
+        </div> <!--tab pane managesnapshot-->
+    </div> <!-- Tab content-->
+</div> <!-- Tab panel-->
diff --git a/instances/views.py b/instances/views.py
index 7bfd8ce..eb6d0bb 100755
--- 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,8 +148,6 @@ def instance(request, pk):
         instance.drbd = drbd_status(request, pk)
         instance.save()
 
-    external_snapshots = get_external_snapshots(request,pk)
-
     return render(request, "instance.html", locals(),)
 
 
@@ -1016,6 +1016,7 @@ def revert_snapshot(request, pk):
         addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
     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 = (
@@ -1027,10 +1028,11 @@ def create_external_snapshot(request, pk):
     ):
         name = request.POST.get("name", "")
         desc = request.POST.get("description", "")
-        instance.proxy.create_external_snapshot(name, instance, desc=desc)
+        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") + "#manageExternalSnapshots")
+    return redirect(request.META.get("HTTP_REFERER") + "#managesnapshot")
+
 
 def get_external_snapshots(request, pk):
     instance = get_instance(request.user, pk)
@@ -1044,6 +1046,7 @@ def get_external_snapshots(request, pk):
         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 = (
@@ -1059,10 +1062,12 @@ def revert_external_snapshot(request, pk):
         instance.proxy.revert_external_snapshot(name, instance, date, desc)
         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") + "#manageExternalSnapshots")
+    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
     )
@@ -1071,10 +1076,18 @@ def delete_external_snapshot(request, pk):
         "instances.snapshot_instances"
     ):
         name = request.POST.get("name", "")
-        instance.proxy.delete_external_snapshot(name, instance)
-        msg = _("Delete external snapshot: %(snap)s") % {"snap": name}
-        addlogmsg(request.user.username, instance.compute.name, instance.name, msg)
-    return redirect(request.META.get("HTTP_REFERER") + "#manageExternalSnapshots")
+
+        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):
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index f8658f6..680dd09 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -209,8 +209,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()
@@ -1295,24 +1295,22 @@ class wvmInstance(wvmConnect):
         )
         self._defineXML(xml_temp)
 
-    def create_external_snapshot(self, name, instance, date=None, desc=None):
-        if self.instance.isActive() == False:
-            result = self.instance.create()
-            if result < 0:
-                return 0
-        
+    def create_external_snapshot(self, name, date=None, desc=None):
         creation_time = time.time()
         state = "shutoff" if self.get_status() == 5 else "running"
         xml = """<domainsnapshot>
                      <name>%s</name>
                      <description>%s</description>
                      <state>%s</state>
-                     <creationTime>%d</creationTime>""" % (
+                     <creationTime>%d</creationTime>
+                     <seclabel type='none' model='dac' relabel='no'/>
+                     """ % (
             name,
             desc,
             state,
             creation_time,
         )
+
         self.change_snapshot_xml()
         xml += self._XMLDesc(VIR_DOMAIN_XML_SECURE)
         xml += """<active>0</active>
@@ -1320,87 +1318,54 @@ class wvmInstance(wvmConnect):
 
         self._snapshotCreateXML(xml, VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY)
 
-        disk_info = self.get_disk_devices()
-        for disk in disk_info:
-            backing_file = disk["backing_file"]
-            vol_base = self.get_volume_by_path(backing_file)
-            pool = vol_base.storagePoolLookupByVolume()
-            pool.refresh(0)
     
     def get_external_snapshots(self):
-        external_snapshots = []
-        temp_snapshots = self.get_snapshot(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL)
-        for temp_snapshot in temp_snapshots:
-            external_snapshot = []
-            external_snapshot.append(temp_snapshot['name'])
-            external_snapshot.append(temp_snapshot['date'])
-            external_snapshot.append(temp_snapshot['description'])
-            external_snapshots.append(external_snapshot)
-        return external_snapshots
+        return self.get_snapshot(VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL)
     
-    def delete_external_snapshot(self, name, instance):
+    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"]
-            source_file = disk["path"]
-            self.instance.blockCommit(target_dev, backing_file, source_file, 
-                                                  flags=VIR_DOMAIN_BLOCK_COMMIT_DELETE|VIR_DOMAIN_BLOCK_COMMIT_ACTIVE)
+            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)
                     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, instance, date, desc):
+    def revert_external_snapshot(self, name, date, desc):
         snap = self.instance.snapshotLookupByName(name, 0)
-        snapXML = ElementTree.fromstring(snap.getXMLDesc(0))
-        disks = []
-        for disk_backup in snapXML.findall('inactiveDomain/devices/disk'):
-            if disk_backup.get('device') == 'disk':
-                disk_dict = {}
-                if disk_backup.find('source') is not None:
-                    disk_dict['backing_file'] = disk_backup.find('source').get('file')
-                if disk_backup.find('driver') is not None:
-                    disk_dict['driver_name'] = disk_backup.find('driver').get('name')
-                    disk_dict['driver_type'] = disk_backup.find('driver').get('type')
-                if disk_backup.find('target') is not None:
-                    disk_dict['target_dev'] = disk_backup.find('target').get('dev')
-                    disk_dict['target_bus'] = disk_backup.find('target').get('bus')
-                if disk_backup.find('boot') is not None:
-                    disk_dict['boot_order'] = disk_backup.find('boot').get('order')     
-                disks.append(disk_dict)
+        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')
         
-        snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY)
         disk_info = self.get_disk_devices()
         for disk in disk_info:
-            source_file = disk["path"]
-            backing_file = disk["backing_file"]
-            vol_base = self.get_volume_by_path(backing_file)
-            pool = vol_base.storagePoolLookupByVolume()
+            vol_snap = self.get_volume_by_path(disk["path"])
+            pool = vol_snap.storagePoolLookupByVolume()
             pool.refresh(0)
-            vol_snap = self.get_volume_by_path(source_file)
-            vol_snap.wipe(0)
             vol_snap.delete(0)
 
+        snap.delete(VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY)
         for disk in disks:
-            self.instance.updateDeviceFlags(
-                """<disk type='file' device='disk'>
-                    <driver name='{}' type='{}'/>
-                    <source file='{}'/>
-                    <target dev='{}' bus='{}'/>
-                    <boot order='{}'/>
-                    </disk>""".format(disk["driver_name"],
-                               disk["driver_type"],
-                               disk["backing_file"],
-                               disk["target_dev"],
-                               disk["target_bus"],
-                               disk["boot_order"]))
+            self.instance.updateDeviceFlags(ElementTree.tostring(disk).decode("UTF-8"))
 
-        self.create_external_snapshot(name, instance, date, desc)
+        self.create_external_snapshot(name, date, desc)
 
     def get_snapshot(self, flag=VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL):
         snapshots = []

From f4aa925e2a9264ee62da364479680466effd2400 Mon Sep 17 00:00:00 2001
From: catborise <catborise@gmail.com>
Date: Fri, 14 Apr 2023 16:11:13 +0300
Subject: [PATCH 6/8] experimental: external snapshot fixes 2

---
 instances/views.py     | 5 ++++-
 vrtManager/instance.py | 1 +
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/instances/views.py b/instances/views.py
index eb6d0bb..5ba91e9 100755
--- a/instances/views.py
+++ b/instances/views.py
@@ -1056,10 +1056,13 @@ def revert_external_snapshot(request, pk):
     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.revert_external_snapshot(name, instance, date, 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")
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index 680dd09..b434751 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -1335,6 +1335,7 @@ class wvmInstance(wvmConnect):
                 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):

From 71f05089f62723d10bd38c107aaa7fd615bf3864 Mon Sep 17 00:00:00 2001
From: catborise <catborise@gmail.com>
Date: Mon, 24 Apr 2023 14:38:51 +0300
Subject: [PATCH 7/8] experimental: external snapshot fixes 3

---
 vrtManager/instance.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index b434751..b85f0a0 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -26,6 +26,7 @@ try:
         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
@@ -1298,12 +1299,12 @@ class wvmInstance(wvmConnect):
     def create_external_snapshot(self, name, date=None, desc=None):
         creation_time = time.time()
         state = "shutoff" if self.get_status() == 5 else "running"
+        #<seclabel type='none' model='dac' relabel='no'/>
         xml = """<domainsnapshot>
                      <name>%s</name>
                      <description>%s</description>
                      <state>%s</state>
                      <creationTime>%d</creationTime>
-                     <seclabel type='none' model='dac' relabel='no'/>
                      """ % (
             name,
             desc,
@@ -1349,24 +1350,29 @@ class wvmInstance(wvmConnect):
 
     
     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 = []

From 012bc97a59b4c7f4901e453ae665951603501e92 Mon Sep 17 00:00:00 2001
From: catborise <catborise@gmail.com>
Date: Mon, 24 Apr 2023 14:39:22 +0300
Subject: [PATCH 8/8] add showPleasewait dialog while starting instance.

---
 instances/templates/instance_actions.html    | 4 ++--
 instances/templates/instances/power_tab.html | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

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' %}
             </a>
         {% else %}
-            <a class="btn btn-sm btn-secondary" href="{% url 'instances:poweron' instance.id %}" title="{% trans "Power On" %}">
+            <a class="btn btn-sm btn-secondary" href="{% url 'instances:poweron' instance.id %}" onclick="showPleaseWaitDialog()" title="{% trans "Power On" %}">
                 {% icon 'play' %}
             </a>
         {% endif %}
@@ -32,7 +32,7 @@
         </button>
     {% endif %}
     {% if instance.proxy.instance.info.0 == 1 %}
-        <a class="btn btn-sm btn-secondary disabled" title="{% trans "Power On" %}">{% icon 'play' %}</a>
+        <a class="btn btn-sm btn-secondary disabled" onclick="showPleaseWaitDialog()" title="{% trans "Power On" %}">{% icon 'play' %}</a>
         <a class="btn btn-sm btn-secondary" href="{% url 'instances:suspend' instance.id %}"
             title="{% trans "Suspend" %}">{% icon 'pause' %}</a>
         <a class="btn btn-sm btn-secondary" href="{% url 'instances:poweroff' instance.id %}">{% icon 'power-off' %}</a>
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 @@
             </div>
             <div class="tab-pane tab-pane-bordered" id="powerforce" role="tabpanel" aria-labelledby="powerforce-tab">
                 <p>{% trans "This action forcibly powers off the instance and may cause data corruption." %}</p>
-                <form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shotdown instance form">
+                <form action="{% url 'instances:force_off' instance.id %}" method="post" role="form" aria-label="Force to shutdown instance form">
                     {% csrf_token %}
                     <input type="submit" name="powerforce" class="btn btn-lg btn-success float-end" value="{% trans "Force Off" %}">
                     <div class="clearfix"></div>
@@ -119,7 +119,7 @@
                         <p>{% trans "Template instance cannot be started." %}</p>
                         <input type="submit" name="poweron" class="btn btn-lg btn-success float-end disabled" value="{% trans "Power On" %}">
                     {% else %}
-                        <input type="submit" name="poweron" class="btn btn-lg btn-success float-end" value="{% trans "Power On" %}">
+                        <input type="submit" name="poweron" class="btn btn-lg btn-success float-end" onclick="showPleaseWaitDialog()" value="{% trans "Power On" %}">
                     {% endif %}
                     <div class="clearfix"></div>
                 </form>