diff --git a/instances/jobs.py b/instances/jobs.py index de7be21b..69e4dc72 100644 --- a/instances/jobs.py +++ b/instances/jobs.py @@ -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 diff --git a/instances/tests/test_clone_job.py b/instances/tests/test_clone_job.py index b96aa225..9db8fd38 100644 --- a/instances/tests/test_clone_job.py +++ b/instances/tests/test_clone_job.py @@ -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)