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

feat(instances): migrate a VM with its disks copied into pools of the destination

A migration needed every disk on storage both hosts share: it was refused
when the destination lacked a disk file, and a file of the same name
there was taken for the VM's own disk.

The Migrate tab now lists the VM's disk files for the chosen destination.
For each disk it offers the destination's pools that keep disk files
(directory, filesystem and NFS pools). A disk on storage both hosts share
can also stay where it is.

A directory counts as shared when both hosts have it as a storage pool
and a marker file created on the source exists on the destination. A
directory the probe cannot tell is never taken as shared.

libvirt copies the chosen disks during the migration: migrate3 with
NON_SHARED_DISK for those targets, and destination and persistent
definitions naming the new paths, where libvirt creates the files.

- A shut-off VM is started paused for the copy and shut off on the
  destination; its CPUs never run. Its UEFI variables are copied first,
  as the destination would start from the firmware's template. If the
  migration fails, the VM is shut off on the source again.
- Refused before anything changes, each disk with its reason:
  - a disk neither shared nor given a pool, also one only the next
    start's definition has;
  - CD-ROM images, read-only or encrypted disks, formats other than raw
    and qcow2, and disks with a backing file;
  - a pool that is not of disk files, or that has a file of that name,
    which libvirt would reuse and overwrite;
  - two disks copied to one file.
- The job claims each copy's name in its pool, and the source volume.
- A failed or cancelled migration deletes the copies libvirt made, and
  the UEFI variables copy, unless a VM on the destination has them.
- The source files stay and are named in the log. With "Delete the
  source files of copied disks" they go, except one another VM or volume
  uses; a failed delete is logged and the migration stands.

The NVRAM sharing check uses the same probe: a shut-off move stops when
it cannot be told, and the source file stays.

The API's migrate call takes disk_pools ({target: pool}) and
delete_source.
This commit is contained in:
catborise 2026-10-29 18:20:50 +03:00
parent 10eb0d1c2e
commit 51d24fce30
21 changed files with 828 additions and 42 deletions

View file

@ -880,8 +880,11 @@ class wvmConnect(object):
if os.path.dirname(path) != NVRAM_DIR:
return False
with self._nvram_pool() as pool, destination._nvram_pool() as other:
if self._nvram_shared(pool, other):
return False
try:
if self.same_directory(pool, other):
return False
except libvirtError:
return False # not known to be the source's own file
vol = self._find_volume(pool, os.path.basename(path))
if vol is None:
return False
@ -889,11 +892,11 @@ class wvmConnect(object):
return True
@staticmethod
def _nvram_shared(pool, other):
"""Whether pools on NVRAM_DIR of two hosts are one directory, or other
is not known to be one of its own: creating the same file in both
fails then. The create is exclusive on the file server, unlike a
directory listing, which an NFS client may cache."""
def same_directory(pool, other):
"""Whether pools of two hosts are one directory: creating the same
file in both fails then, as it exists already. The create is
exclusive on the file server, unlike a directory listing, which an
NFS client may cache. Any other failure raises: it tells nothing."""
marker = (
f"<volume><name>webvirtcloud-check-{uuid.uuid4().hex}</name><capacity>0</capacity>"
"<target><format type='raw'/></target></volume>"
@ -901,22 +904,32 @@ class wvmConnect(object):
mine = pool.createXML(marker, 0)
try:
other.createXML(marker, 0).delete(0)
except libvirtError:
return True
except libvirtError as error:
# the file exists (OPERATION_INVALID), or the pool lists it already
if error.get_error_code() in (libvirt.VIR_ERR_OPERATION_INVALID, libvirt.VIR_ERR_STORAGE_VOL_EXIST):
return True
raise
finally:
mine.delete(0)
return False
def pool_at(self, directory):
"""The active pool whose directory is directory, or None. A pool
without a target path (rbd, gluster, ...) holds no files."""
for pool in self.wvm.listAllStoragePools(libvirt.VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE):
if (util.get_xml_path(pool.XMLDesc(0), "/pool/target/path") or "").rstrip("/") == directory:
return pool
return None
@contextlib.contextmanager
def _nvram_pool(self):
"""An active, refreshed pool on NVRAM_DIR: libvirt reads and deletes
files only as volumes of a pool. A transient one when none exists."""
for pool in self.wvm.listAllStoragePools(libvirt.VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE):
# a pool without a target path (rbd, gluster, ...) holds no files
if (util.get_xml_path(pool.XMLDesc(0), "/pool/target/path") or "").rstrip("/") == NVRAM_DIR:
pool.refresh(0)
yield pool
return
pool = self.pool_at(NVRAM_DIR)
if pool is not None:
pool.refresh(0)
yield pool
return
pool = self.wvm.storagePoolCreateXML(
f"<pool type='dir'><name>webvirtcloud-nvram-{uuid.uuid4().hex[:8]}</name>"
f"<target><path>{NVRAM_DIR}</path></target></pool>",