1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00

fix(instances): a recovered clone gets the size and start asked

When the worker stopped after it defined a clone and before it resized
and started it, recovery recorded the clone at the source's size and
shut off, and the job read as a plain success. Recovery now finishes a
defined clone that is shut off as the run does; one that runs went
through the last step, starting, and is left as it is.
This commit is contained in:
catborise 2026-11-05 08:47:51 +03:00
parent 02ab78fe94
commit 297f9a8a53
2 changed files with 43 additions and 4 deletions

View file

@ -13,7 +13,7 @@ from django.template.loader import render_to_string
from django.utils.translation import gettext_noop
from django.utils.translation import gettext_noop as _
from libvirt import (VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE, VIR_DOMAIN_JOB_NONE, VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL,
VIR_ERR_NO_DOMAIN, VIR_ERR_NO_STORAGE_VOL, libvirtError)
VIR_DOMAIN_SHUTOFF, VIR_ERR_NO_DOMAIN, VIR_ERR_NO_STORAGE_VOL, libvirtError)
from lxml import etree
from vrtManager import util
from vrtManager.connection import wvmConnect
@ -224,6 +224,14 @@ def _finish_clone(op, clone, warning):
return {"warning": notes} if notes else {}
def _recovered_clone(op):
"""A clone defined before the worker stopped: given the size asked and
started if asked, unless it runs, as starting is the last step."""
if _domain(op.intent["compute"], op.intent["uuid"]).state()[0] != VIR_DOMAIN_SHUTOFF:
return {}
return _finish_clone(op, op.intent["clone"], [])
def _clone_made(op, result):
"""Records the clone; the job's result, with the clone's page, which the
job panel opens: the page the job ran from is the VM cloned."""
@ -632,7 +640,7 @@ class CloneInstance(Kind):
disks = [v for k, v in op.intent["clone"].items() if k.startswith("disk-") and v]
outcome = vm_outcome(op, owner=True, volumes=disks)
if outcome.state == Operation.SUCCEEDED:
outcome.result = {**outcome.result, "open": _clone_page(op)}
outcome.result = {**outcome.result, **_recovered_clone(op), "open": _clone_page(op)}
return outcome
proxy = _recovery_proxy(op)
if proxy is None:
@ -654,7 +662,7 @@ class CloneInstance(Kind):
"""The spec's recovery table: a defined clone first, then a volume
still being created, since it comes before the copy."""
if _domain(op.intent["compute"], op.intent["uuid"]) is not None:
return Outcome(Operation.SUCCEEDED, result=_clone_made(op, {}))
return Outcome(Operation.SUCCEEDED, result=_clone_made(op, _recovered_clone(op)))
created = op.intent.get("created", [])
paths = [c["path"] for c in created]
ended = Operation.CANCELLED if op.cancel_requested else Operation.FAILED

View file

@ -166,6 +166,18 @@ class CloneJobTestCase(TransactionTestCase):
self.assertEqual(outcome.state, Operation.UNCERTAIN)
self.assertIn("copy.qcow2", words(outcome.error))
@patch("instances.jobs.wvmConnect")
def test_recovery_gives_a_defined_shut_off_clone_the_size_asked(self, conn):
conn.return_value.wvm.lookupByUUIDString.return_value.state.return_value = [libvirt.VIR_DOMAIN_SHUTOFF, 0]
op = Operation(user=self.admin, intent={"compute": self.compute.pk, "uuid": "abababab-0000-1111-2222-333333333333",
"name": "copy", "clone": {"name": "copy", "disk-vda": "copy.qcow2",
"vcpu": 4, "memory": None, "start": True}})
recovered = CloneInstance().reconcile(op)
self.assertEqual(recovered.state, Operation.SUCCEEDED)
self.proxy.resize_cpu.assert_called_once_with("4", "4")
self.proxy.start.assert_called_once_with()
self.assertIn("open", recovered.result)
@patch("instances.jobs.wvmConnect")
def test_a_clone_that_left_nothing_failed(self, conn):
op = Operation(user=self.admin, intent={"compute": self.compute.pk, "uuid": "abababab-0000-1111-2222-333333333333",
@ -513,7 +525,7 @@ class LiveCloneJobTestCase(TransactionTestCase):
# recovery
def recovered(self, cancel=False, defined=False, **intent):
def recovered(self, cancel=False, defined=False, state=libvirt.VIR_DOMAIN_RUNNING, **intent):
base = {"live": True, "targets": [target("vda")],
"planned": [{"dev": "vda", "path": "/pool/copy-vda.qcow2"}], "created": self.CREATED,
"copy_started": True}
@ -523,6 +535,7 @@ class LiveCloneJobTestCase(TransactionTestCase):
with patch("instances.jobs.wvmConnect") as conn:
if not defined:
conn.return_value.wvm.lookupByUUIDString.side_effect = libvirt_error(libvirt.VIR_ERR_NO_DOMAIN)
conn.return_value.wvm.lookupByUUIDString.return_value.state.return_value = [state, 0]
return CloneInstance().reconcile(op), op
def test_recovery_before_the_copy_deletes_what_was_created(self):
@ -587,6 +600,24 @@ class LiveCloneJobTestCase(TransactionTestCase):
clone = Instance.objects.get(uuid=op.intent["uuid"])
self.assertEqual(outcome.result["open"], reverse("instances:instance", args=[clone.pk]))
def test_recovery_of_a_defined_clone_gives_it_the_size_asked_unless_it_runs(self):
# the worker stopped after it defined the clone, before or while it resized and started it
self.proxy.backup_result.return_value = None
self.proxy.backup_targets.return_value = {"/other/x.qcow2"}
clone = {"name": "copy", "disk-vda": "copy-vda.qcow2", "vcpu": 2, "memory": 1024, "start": True}
outcome, op = self.recovered(defined=True, state=libvirt.VIR_DOMAIN_SHUTOFF, clone=clone)
self.assertEqual(outcome.state, Operation.SUCCEEDED)
self.proxy.resize_cpu.assert_called_once_with("2", "2")
self.proxy.resize_mem.assert_called_once_with(1024, 1024)
self.proxy.start.assert_called_once_with()
Instance.objects.filter(uuid=op.intent["uuid"]).delete()
op.delete()
self.proxy.reset_mock()
outcome, _op = self.recovered(defined=True, clone=clone) # it runs: the last step was done
self.assertEqual(outcome.state, Operation.SUCCEEDED)
self.proxy.resize_cpu.assert_not_called()
self.proxy.start.assert_not_called()
def test_recovery_of_a_failed_copy_deletes_what_was_created(self):
outcome, _op = self.recovered(copy_result=[False, "No space left on device"])
self.assertEqual(outcome.state, Operation.FAILED)