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(