mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
feat(instances): a running migration can be cancelled; its time limit goes
This commit is contained in:
parent
31cbe60aa4
commit
c2d3285e0f
7 changed files with 69 additions and 127 deletions
|
|
@ -5,7 +5,6 @@ import logging
|
|||
import os.path
|
||||
import re
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -77,7 +76,7 @@ def _rate(before, after, seconds, scale=1):
|
|||
|
||||
|
||||
class wvmInstances(wvmConnect):
|
||||
def moveto(self, conn, name, live, unsafe, offline, autoconverge=False, compress=False, uri=None, timeout=None):
|
||||
def moveto(self, conn, name, live, unsafe, offline, autoconverge=False, compress=False, uri=None):
|
||||
"""Migrates the VM of conn (a wvmInstance on the source) here.
|
||||
|
||||
The mode follows the VM's state, read once: a running VM migrates
|
||||
|
|
@ -86,9 +85,8 @@ class wvmInstances(wvmConnect):
|
|||
fit the state is refused. unsafe, autoconverge (live only) and
|
||||
compress apply to a running VM. uri: the native migration URI
|
||||
(tcp://address) on the destination; None lets libvirt use the
|
||||
destination's own hostname. timeout: seconds after which a migration
|
||||
that has not finished is cancelled (None: no limit). Returns the mode
|
||||
and options used, for the log."""
|
||||
destination's own hostname. Returns the mode and options used, for
|
||||
the log."""
|
||||
state = conn.get_status()
|
||||
if state == 5:
|
||||
if live:
|
||||
|
|
@ -150,7 +148,7 @@ class wvmInstances(wvmConnect):
|
|||
# dom.migrate.
|
||||
self.move_definition(dom)
|
||||
else:
|
||||
self.migrate_within(dom, flags, uri, timeout)
|
||||
dom.migrate(self.wvm, flags, None, uri, 0)
|
||||
return ", ".join(mode)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -172,40 +170,6 @@ class wvmInstances(wvmConnect):
|
|||
"host. Start the VM and migrate it live."
|
||||
)
|
||||
|
||||
def migrate_within(self, dom, flags, uri, timeout):
|
||||
"""dom.migrate, cancelled when it has not finished after timeout
|
||||
seconds: a VM that changes its memory faster than the network carries
|
||||
it never finishes, and the request would be ended by the web server
|
||||
with no word. Cancelled, the VM stays on the source."""
|
||||
cancelled = threading.Event()
|
||||
|
||||
def cancel():
|
||||
with contextlib.suppress(libvirtError): # finished meanwhile
|
||||
dom.abortJob()
|
||||
cancelled.set()
|
||||
|
||||
timer = threading.Timer(timeout, cancel) if timeout else None
|
||||
if timer:
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
try:
|
||||
dom.migrate(self.wvm, flags, None, uri, 0)
|
||||
except libvirtError as error:
|
||||
if timer:
|
||||
timer.cancel()
|
||||
timer.join() # a cancel under way ends before the locks go
|
||||
if cancelled.is_set():
|
||||
raise util.OperationError(
|
||||
f"The migration did not finish within {timeout} s and was cancelled ({error}); the VM stays on "
|
||||
"the source host. A VM that changes its memory faster than the network carries it needs auto "
|
||||
"converge."
|
||||
) from error
|
||||
raise
|
||||
finally:
|
||||
if timer:
|
||||
timer.cancel()
|
||||
timer.join()
|
||||
|
||||
def missing_disks(self, definitions):
|
||||
"""(target, source) of the disks in the VM definitions (XML) that are
|
||||
not volumes of this host's active storage pools: the VM's own files,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
"""Migration: the source definition never stays behind; a shut-off VM's
|
||||
definition is moved by the app, not by libvirt's offline migration."""
|
||||
|
||||
import threading
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import libvirt
|
||||
from django.conf import settings
|
||||
|
|
@ -427,64 +425,3 @@ class DestinationDisksTestCase(unittest.TestCase):
|
|||
xml = with_disks("<disk type='volume' device='disk'><source pool='p' volume='v'/><target dev='vdc'/></disk>")
|
||||
with self.assertRaisesRegex(util.OperationError, "vdc \\(p/v\\)"):
|
||||
self.migrate(host, xml)
|
||||
|
||||
|
||||
class SlowDomain(FakeDomain):
|
||||
"""A migration that does not finish until its job is aborted."""
|
||||
|
||||
def __init__(self, **kw):
|
||||
super().__init__(**kw)
|
||||
self.aborted = threading.Event()
|
||||
|
||||
def migrate(self, dconn, flags, dname, uri, bandwidth):
|
||||
if not self.aborted.wait(5):
|
||||
raise AssertionError("the migration was not aborted")
|
||||
raise libvirt.libvirtError("operation aborted: job 'migration out' canceled by client")
|
||||
|
||||
def abortJob(self):
|
||||
self.aborted.set()
|
||||
|
||||
|
||||
class MigrationTimeLimitTestCase(unittest.TestCase):
|
||||
"""A migration that does not converge is cancelled after the time limit,
|
||||
before the web server ends the request; the VM stays on the source."""
|
||||
|
||||
def test_a_migration_over_the_limit_is_cancelled(self):
|
||||
source = FakeSource(1)
|
||||
source.instance = SlowDomain()
|
||||
with self.assertRaisesRegex(util.OperationError, "did not finish within 0.05 s and was cancelled.*auto converge"):
|
||||
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=0.05)
|
||||
self.assertTrue(source.instance.aborted.is_set())
|
||||
|
||||
def test_a_migration_within_the_limit_stops_its_timer(self):
|
||||
timers = []
|
||||
|
||||
class Timer(threading.Timer):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.joined = False
|
||||
timers.append(self)
|
||||
|
||||
def join(self, timeout=None):
|
||||
self.joined = True
|
||||
super().join(timeout)
|
||||
|
||||
source = FakeSource(1)
|
||||
with patch("vrtManager.instance.threading.Timer", Timer):
|
||||
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=5)
|
||||
self.assertEqual(len(source.instance.calls), 1)
|
||||
self.assertTrue(timers[0].finished.is_set() and timers[0].joined)
|
||||
self.assertFalse(timers[0].is_alive())
|
||||
|
||||
def test_a_failure_the_cancel_did_not_cause_keeps_its_error(self):
|
||||
source = FakeSource(1)
|
||||
source.instance = SlowDomain()
|
||||
|
||||
def refused():
|
||||
source.instance.aborted.set() # migrate() gives up, but the abort itself failed
|
||||
raise libvirt.libvirtError("cannot abort migration in confirm phase")
|
||||
|
||||
source.instance.abortJob = refused
|
||||
with self.assertRaisesRegex(libvirt.libvirtError, "operation aborted") as caught:
|
||||
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, timeout=0.05)
|
||||
self.assertNotIsInstance(caught.exception, util.OperationError)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue