mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
feat(instances): external snapshots can freeze the guest file systems
A disk-only snapshot of a running VM is crash-consistent: data the guest has not written yet is missing from it. With a connected QEMU guest agent, the snapshot form now offers to freeze the guest's file systems while the disks are snapshotted (libvirt's QUIESCE flag); the box is checked by default and can be cleared. It is offered only for a running VM whose agent is connected, and not together with memory: libvirt accepts quiesce only for disk-only snapshots, and a memory snapshot keeps the whole guest state anyway. A request that does not fit is refused before libvirt is called.
This commit is contained in:
parent
64ea12b0ce
commit
f766890825
7 changed files with 68 additions and 10 deletions
|
|
@ -66,9 +66,18 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="memory" id="snapshot-memory" {% if instance.status != 1 %}disabled{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" name="memory" id="snapshot-memory" {% if instance.status != 1 %}disabled{% endif %}
|
||||
onchange="var q = document.getElementById('snapshot-quiesce'); if (q.dataset.offered) { q.disabled = this.checked; q.checked = !this.checked; }">
|
||||
<label class="form-check-label" for="snapshot-memory">{% trans "Save memory too (running VMs only); the VM resumes as it was when reverted" %}</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
{% if instance.status == 1 and instance.guest_agent_ready %}
|
||||
<input class="form-check-input" type="checkbox" name="quiesce" id="snapshot-quiesce" data-offered="1" checked>
|
||||
{% else %}
|
||||
<input class="form-check-input" type="checkbox" name="quiesce" id="snapshot-quiesce" checked disabled>
|
||||
{% endif %}
|
||||
<label class="form-check-label" for="snapshot-quiesce">{% trans "Freeze the guest's file systems while the disks are snapshotted (running VMs with a connected QEMU guest agent; not with memory)" %}</label>
|
||||
</div>
|
||||
<p class="small">{% trans "A VM with snapshots cannot be migrated." %}</p>
|
||||
</form>
|
||||
<div class="clearfix"></div>
|
||||
|
|
|
|||
|
|
@ -166,3 +166,13 @@ class LiveExternalSnapshotTestCase(SimpleTestCase):
|
|||
dom.snapshotCreateXML("<domainsnapshot><name>i</name></domainsnapshot>", 0)
|
||||
with self.assertRaisesRegex(util.OperationError, "internal snapshots"):
|
||||
vm.create_external_snapshot("x")
|
||||
|
||||
def test_quiesce_without_a_connected_agent_is_refused(self):
|
||||
# no guest OS runs an agent here: the channel exists but is not connected
|
||||
channel = "<channel type='unix'><target type='virtio' name='org.qemu.guest_agent.0'/></channel>"
|
||||
for name, extra in (("snap-noagent", ""), ("snap-agentchan", channel)):
|
||||
with self.subTest(name):
|
||||
dom, vm = self.vm(name, "running", extra_devices=extra)
|
||||
with self.assertRaisesRegex(util.OperationError, "guest agent is not connected"):
|
||||
vm.create_external_snapshot("q", quiesce=True)
|
||||
self.assertEqual(dom.snapshotNum(0), 0)
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ class SnapshotViewsTestCase(TestCase):
|
|||
self.assertEqual(response.status_code, 302)
|
||||
return proxy, method, [str(m) for m in get_messages(response.wsgi_request)]
|
||||
|
||||
def test_create_passes_name_description_and_memory(self):
|
||||
def test_create_passes_name_description_memory_and_quiesce(self):
|
||||
_, method, _ = self.post("create_external_snapshot", {"name": "a", "description": "d", "memory": "on"})
|
||||
method.assert_called_once_with("a", "d", True)
|
||||
_, method, _ = self.post("create_external_snapshot", {"name": "a", "description": ""})
|
||||
method.assert_called_once_with("a", "", False)
|
||||
method.assert_called_once_with("a", "d", True, False)
|
||||
_, method, _ = self.post("create_external_snapshot", {"name": "a", "description": "", "quiesce": "on"})
|
||||
method.assert_called_once_with("a", "", False, True)
|
||||
|
||||
def test_revert_and_delete_call_the_backend_once_and_leave_power_alone(self):
|
||||
for view in ("revert_external_snapshot", "delete_external_snapshot"):
|
||||
|
|
@ -83,7 +83,7 @@ class SnapshotViewsTestCase(TestCase):
|
|||
class SnapshotTabTestCase(TestCase):
|
||||
setUp = SnapshotViewsTestCase.setUp # same VM and superuser, without rerunning the view tests
|
||||
|
||||
def page(self, external=(), internal=(), jobs=(), status=1, nvram=None):
|
||||
def page(self, external=(), internal=(), jobs=(), status=1, nvram=None, agent=False):
|
||||
with patch("instances.models.wvmInstance") as mock_wvm, patch.object(
|
||||
Compute, "status", new_callable=PropertyMock, return_value=True
|
||||
):
|
||||
|
|
@ -101,6 +101,7 @@ class SnapshotTabTestCase(TestCase):
|
|||
proxy.get_snapshot_jobs.return_value = list(jobs)
|
||||
proxy.get_status.return_value = status
|
||||
proxy.get_nvram.return_value = nvram
|
||||
proxy.is_agent_ready.return_value = agent
|
||||
proxy.get_memory.return_value = proxy.get_cur_memory.return_value = 1024
|
||||
proxy.get_max_memory.return_value = 4096
|
||||
proxy.get_vcpu.return_value = proxy.get_cur_vcpu.return_value = 1
|
||||
|
|
@ -144,6 +145,13 @@ class SnapshotTabTestCase(TestCase):
|
|||
[button] = doc.xpath("//button[@name='revert_external_snapshot']")
|
||||
self.assertEqual("powered off" in button.get("data-confirm"), restarts)
|
||||
|
||||
def test_quiesce_is_checked_and_offered_with_a_running_vm_and_its_agent(self):
|
||||
for status, agent, offered in ((1, True, True), (1, False, False), (3, True, False), (5, True, False)):
|
||||
with self.subTest(status=status, agent=agent):
|
||||
[box] = self.page(status=status, agent=agent).xpath("//input[@name='quiesce']")
|
||||
self.assertIsNotNone(box.get("checked"))
|
||||
self.assertEqual(box.get("disabled") is None, offered)
|
||||
|
||||
def test_memory_checkbox_only_for_a_running_vm(self):
|
||||
self.assertFalse(self.page(status=1).xpath("//input[@name='memory']")[0].get("disabled"))
|
||||
self.assertIsNotNone(self.page(status=5).xpath("//input[@name='memory']")[0].get("disabled"))
|
||||
|
|
|
|||
|
|
@ -1400,8 +1400,9 @@ def create_external_snapshot(request, pk):
|
|||
name = request.POST.get("name", "")
|
||||
desc = request.POST.get("description", "")
|
||||
memory = request.POST.get("memory") == "on"
|
||||
quiesce = request.POST.get("quiesce") == "on"
|
||||
try:
|
||||
instance.proxy.create_external_snapshot(name, desc, memory)
|
||||
instance.proxy.create_external_snapshot(name, desc, memory, quiesce)
|
||||
except libvirtError as err:
|
||||
messages.error(request, _("Snapshot was not created: %(err)s") % {"err": error_text(request, err)})
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue