mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +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
|
|
@ -556,7 +556,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat
|
|||
- gstfsd finds the guest's operating system the way libguestfs inspects it, so a root on LVM works, and it always answers (an error when it finds no or several operating systems). Update `/usr/local/bin/gstfsd` on every compute (see [Adding a Compute Node](#adding-a-compute-node), step 3).
|
||||
- A theme picked on the Settings page rewrites two tracked files; the update and rollback steps put back the shipped theme. Pick yours again afterwards, also after Docker recreates the container.
|
||||
- With the bundled nginx config, browsers revalidate static files on each load (304 when unchanged), so an update or a theme change shows without a hard reload. Files cached before this update can stay until a hard reload (Ctrl+F5) of each affected page, or until the site's browser cache is cleared once; an external caching proxy may need a purge.
|
||||
- External snapshots use libvirt's snapshot API and need libvirt 9.9 or newer on the compute hosts. They work on UEFI VMs and running, paused or shut-off VMs, can save memory too, and a revert keeps disks added later (it detaches them). A VM's snapshots form one chain: after a revert, delete the newer snapshots before taking another. Snapshot names no longer get an `s1.` prefix; existing ones keep theirs and can still be reverted and deleted, except that a snapshot an older version took of a UEFI VM can only be deleted. On NFS, libvirt removes merged snapshot files as the qemu user, who needs write access to the image directory; files it could not remove are named in a warning after the delete or revert. Deleting a VM with its data removes its snapshot files too, except a template it grew from and files another VM uses.
|
||||
- External snapshots use libvirt's snapshot API and need libvirt 9.9 or newer on the compute hosts. They work on UEFI VMs and running, paused or shut-off VMs, can save memory too or, on a running VM with a connected QEMU guest agent, freeze the guest's file systems (the default there), and a revert keeps disks added later (it detaches them). A VM's snapshots form one chain: after a revert, delete the newer snapshots before taking another. Snapshot names no longer get an `s1.` prefix; existing ones keep theirs and can still be reverted and deleted, except that a snapshot an older version took of a UEFI VM can only be deleted. On NFS, libvirt removes merged snapshot files as the qemu user, who needs write access to the image directory; files it could not remove are named in a warning after the delete or revert. Deleting a VM with its data removes its snapshot files too, except a template it grew from and files another VM uses.
|
||||
- A storage volume a VM still needs (a disk, a backing file or a snapshot's file) is no longer deleted from the storage page or the API; the delete is refused with the reason. Only VMs on the same compute are checked.
|
||||
- Docker no longer publishes port 6080; the console goes through nginx on the panel's port (`WS_PUBLIC_PORT`, default 80).
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ try:
|
|||
VIR_MIGRATE_UNSAFE,
|
||||
VIR_DOMAIN_SNAPSHOT_CREATE_ATOMIC,
|
||||
VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY,
|
||||
VIR_DOMAIN_SNAPSHOT_CREATE_QUIESCE,
|
||||
VIR_DOMAIN_SNAPSHOT_REVERT_PAUSED,
|
||||
VIR_DOMAIN_SNAPSHOT_REVERT_RUNNING,
|
||||
VIR_ERR_CONFIG_UNSUPPORTED,
|
||||
|
|
@ -1575,14 +1576,23 @@ class wvmInstance(wvmConnect):
|
|||
</domainsnapshot>"""
|
||||
self._snapshotCreateXML(xml, 0)
|
||||
|
||||
def create_external_snapshot(self, name, desc="", memory=False):
|
||||
def create_external_snapshot(self, name, desc="", memory=False, quiesce=False):
|
||||
"""A disk-only snapshot, or with memory too (running VMs only). Every
|
||||
disk gets a libvirt-named overlay next to it; the memory file goes
|
||||
next to the first disk."""
|
||||
next to the first disk. quiesce: the guest agent freezes the guest's
|
||||
file systems for a disk-only snapshot of a running VM (libvirt
|
||||
accepts it only without memory, which keeps the whole state anyway)."""
|
||||
if not SNAPSHOT_NAME.fullmatch(name):
|
||||
raise util.OperationError(
|
||||
"Snapshot names are 1-64 letters, digits, '.', '_' or '-' and do not start with '.'"
|
||||
)
|
||||
if quiesce:
|
||||
if memory:
|
||||
raise util.OperationError("A snapshot with memory is not quiesced; it keeps the whole guest state")
|
||||
if self.get_status() != 1:
|
||||
raise util.OperationError("Only a running VM's file systems can be frozen")
|
||||
if not self.is_agent_ready():
|
||||
raise util.OperationError("The QEMU guest agent is not connected; it freezes the file systems")
|
||||
self._check_snapshot_state()
|
||||
if self.instance.snapshotListNames(VIR_DOMAIN_SNAPSHOT_LIST_INTERNAL):
|
||||
raise util.OperationError("The VM has internal snapshots; external snapshots cannot be mixed with them")
|
||||
|
|
@ -1610,6 +1620,8 @@ class wvmInstance(wvmConnect):
|
|||
flags = VIR_DOMAIN_SNAPSHOT_CREATE_ATOMIC
|
||||
if not memory:
|
||||
flags |= VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY
|
||||
if quiesce:
|
||||
flags |= VIR_DOMAIN_SNAPSHOT_CREATE_QUIESCE
|
||||
self._snapshotCreateXML(xml, flags)
|
||||
self._refresh_pools_after_snapshot()
|
||||
|
||||
|
|
|
|||
|
|
@ -212,6 +212,24 @@ class CreateTestCase(unittest.TestCase):
|
|||
"<memory snapshot='external' file='/pool/11111111-2222-3333-4444-555555555555.m.mem'/>", xml
|
||||
)
|
||||
|
||||
def test_a_quiesced_snapshot_freezes_the_guest_file_systems(self):
|
||||
p = proxy()
|
||||
p.is_agent_ready = lambda: True
|
||||
p.create_external_snapshot("q", quiesce=True)
|
||||
_, flags = created(p)
|
||||
self.assertEqual(flags, DISK_ONLY_ATOMIC | libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_QUIESCE)
|
||||
|
||||
def test_quiesce_needs_a_running_vm_with_its_agent_and_no_memory(self):
|
||||
# libvirt accepts quiesce only for disk-only snapshots, through the agent
|
||||
for status, ready, memory, pattern in (
|
||||
(5, True, False, "running VM"), (3, True, False, "running VM"),
|
||||
(1, False, False, "guest agent is not connected"), (1, True, True, "memory"),
|
||||
):
|
||||
with self.subTest(status=status, ready=ready, memory=memory):
|
||||
p = proxy(status=status)
|
||||
p.is_agent_ready = lambda ready=ready: ready
|
||||
self.refused(p, pattern, quiesce=True, memory=memory)
|
||||
|
||||
def refused(self, p, pattern, **kwargs):
|
||||
with self.assertRaisesRegex(util.OperationError, pattern):
|
||||
p.create_external_snapshot(kwargs.pop("name", "s"), **kwargs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue