mirror of
https://github.com/retspen/webvirtcloud
synced 2024-12-24 15:15:22 +00:00
external snapshot implementation
This commit is contained in:
parent
ad9f1db643
commit
fd6b2ec4bf
4 changed files with 315 additions and 7 deletions
101
instances/templates/instances/snapshots_tab.html
Normal file → Executable file
101
instances/templates/instances/snapshots_tab.html
Normal file → Executable file
|
@ -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>
|
||||
|
|
3
instances/urls.py
Normal file → Executable file
3
instances/urls.py
Normal file → Executable file
|
@ -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"),
|
||||
|
|
65
instances/views.py
Normal file → Executable file
65
instances/views.py
Normal file → Executable file
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue