mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
fix(computes): a VM migrates to its host's panel address when it has no migration address
Without a migration address libvirt sends a migrating VM to the
destination's own host name, which the source host often cannot resolve:
a live migration to a host added without one failed ("Unable to resolve
address ..."), also when the other host had one. The destination's
address on the Computes page, which the panel reaches it at, is now the
default; the migration address stays for another network. A host the
panel reaches on its loopback keeps libvirt's default.
This commit is contained in:
parent
940e639149
commit
cf991adad5
8 changed files with 74 additions and 23 deletions
|
|
@ -296,7 +296,7 @@ A compute node is a KVM host the panel manages over libvirt.
|
|||
|
||||
Firewall: the compute's VNC ports (`5900`–`65535`) must accept connections **only** from the panel's IP, never from public networks.
|
||||
|
||||
For VM migration between computes, define shared storage as a storage pool on every host (a VM migrates only when its disk, backing and ISO files and block devices are volumes of the destination's pools), and where the hosts cannot resolve each other's names, set each compute's **Migration address** on its edit page. A VM's disks can instead be copied into storage pools of the destination during the migration: after choosing the destination, the **Disks** list shows each disk file and the destination pools that can take it (directory, filesystem and NFS pools; one with a file of the same name refuses the copy), and a disk on storage both hosts share can also stay there. Plain raw or qcow2 disks without a backing file are copied; a shut-off VM is started paused for the copy, its CPUs never run, and is shut off on the destination. A shut-off VM with a TPM (Windows 11) migrates the same way, also without copies: only a running VM's migration carries its TPM state. With **Delete the source files of copied disks** the source files go after the migration, unless another VM or volume uses them. The copy goes through the destination's migration port range (49152-49215 by default), like the memory.
|
||||
For VM migration between computes, define shared storage as a storage pool on every host (a VM migrates only when its disk, backing and ISO files and block devices are volumes of the destination's pools), and the hosts send a migrating VM to the destination's address on the panel's Computes page; for another network (a dedicated migration network), set the compute's **Migration address** on its edit page. A VM's disks can instead be copied into storage pools of the destination during the migration: after choosing the destination, the **Disks** list shows each disk file and the destination pools that can take it (directory, filesystem and NFS pools; one with a file of the same name refuses the copy), and a disk on storage both hosts share can also stay there. Plain raw or qcow2 disks without a backing file are copied; a shut-off VM is started paused for the copy, its CPUs never run, and is shut off on the destination. A shut-off VM with a TPM (Windows 11) migrates the same way, also without copies: only a running VM's migration carries its TPM state. With **Delete the source files of copied disks** the source files go after the migration, unless another VM or volume uses them. The copy goes through the destination's migration port range (49152-49215 by default), like the memory.
|
||||
|
||||
If libvirt warns `Host SMBIOS information is not available`, install `dmidecode` (`apt-get`, `dnf` or `zypper install -y dmidecode`) and restart `libvirtd`.
|
||||
|
||||
|
|
|
|||
19
computes/migrations/0008_migration_address_help.py
Normal file
19
computes/migrations/0008_migration_address_help.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.2.18 on 2026-11-01 14:52
|
||||
|
||||
import computes.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('computes', '0007_gstfsd_key'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='compute',
|
||||
name='migration_address',
|
||||
field=models.CharField(blank=True, default='', help_text='IP address or host name the other hosts send migrating VMs to; empty: the address the panel reaches the host at', max_length=64, validators=[computes.validators.validate_migration_address], verbose_name='migration address'),
|
||||
),
|
||||
]
|
||||
|
|
@ -26,7 +26,7 @@ class Compute(Model):
|
|||
validators=[validate_migration_address],
|
||||
help_text=_(
|
||||
"IP address or host name the other hosts send migrating VMs to; "
|
||||
"empty: the host's own host name, which they must be able to resolve"
|
||||
"empty: the address the panel reaches the host at"
|
||||
),
|
||||
)
|
||||
gstfsd_key = CharField(
|
||||
|
|
@ -43,16 +43,18 @@ class Compute(Model):
|
|||
|
||||
@property
|
||||
def migration_uri(self):
|
||||
"""The native migration URI for VMs migrated to this host, or None for
|
||||
libvirt's default."""
|
||||
if not self.migration_address:
|
||||
return None
|
||||
"""The native migration URI for VMs migrated to this host: its
|
||||
migration address, or else the address the panel reaches it at, so
|
||||
that the other hosts need not resolve its own name. None, libvirt's
|
||||
default (that name), for a host the panel reaches on its loopback."""
|
||||
address = self.migration_address or self.hostname
|
||||
try:
|
||||
if ip_address(self.migration_address).version == 6:
|
||||
return f"tcp://[{self.migration_address}]"
|
||||
ip = ip_address(address)
|
||||
except ValueError:
|
||||
pass
|
||||
return f"tcp://{self.migration_address}"
|
||||
ip = None
|
||||
if not self.migration_address and (address.lower().rstrip(".") == "localhost" or ip and ip.is_loopback):
|
||||
return None
|
||||
return f"tcp://[{address}]" if ip and ip.version == 6 else f"tcp://{address}"
|
||||
|
||||
@cached_property
|
||||
def status(self):
|
||||
|
|
|
|||
|
|
@ -26,11 +26,19 @@ class MigrationAddressTests(TestCase):
|
|||
validate_migration_address(value)
|
||||
|
||||
def test_migration_uri(self):
|
||||
cases = {"": None, "192.0.2.12": "tcp://192.0.2.12", "kvm102.example.org": "tcp://kvm102.example.org",
|
||||
cases = {"192.0.2.12": "tcp://192.0.2.12", "kvm102.example.org": "tcp://kvm102.example.org",
|
||||
"2001:db8::12": "tcp://[2001:db8::12]"}
|
||||
for address, uri in cases.items():
|
||||
with self.subTest(address=address):
|
||||
self.assertEqual(Compute(migration_address=address).migration_uri, uri)
|
||||
self.assertEqual(Compute(hostname="192.0.2.40", migration_address=address).migration_uri, uri)
|
||||
|
||||
def test_without_a_migration_address_the_panel_s_address_of_the_host_is_used(self):
|
||||
# no DNS name of the host's own; a host the panel reaches on its loopback: libvirt's default
|
||||
cases = {"192.0.2.40": "tcp://192.0.2.40", "kvm103.example.org": "tcp://kvm103.example.org",
|
||||
"localhost": None, "LOCALHOST": None, "localhost.": None, "127.0.0.1": None}
|
||||
for hostname, uri in cases.items():
|
||||
with self.subTest(hostname=hostname):
|
||||
self.assertEqual(Compute(hostname=hostname, migration_address="").migration_uri, uri)
|
||||
|
||||
def test_the_compute_form_saves_it(self):
|
||||
self.client.force_login(get_user_model().objects.create_superuser("ma-admin", "ma@example.com", "pw"))
|
||||
|
|
|
|||
|
|
@ -169,6 +169,26 @@ class MigrateJobTestCase(TransactionTestCase):
|
|||
get_kind(op.kind).run(op, ctx)
|
||||
self.assertIn("Unable to resolve address 'kvm2.example.com'", public_error_text(raised.exception))
|
||||
|
||||
def test_each_way_goes_to_the_destination_s_migration_address_or_its_panel_address(self):
|
||||
# one host with a migration address, the other without: no name resolution either way
|
||||
self.dest.migration_address = ""
|
||||
self.dest.save()
|
||||
self.source.migration_address = "198.51.100.50"
|
||||
self.source.save()
|
||||
for target, uri, key in ((self.dest, "tcp://192.0.2.51", "key-mig00011"), (self.source, "tcp://198.51.100.50", "key-mig00012")):
|
||||
with self.subTest(target=target.name):
|
||||
self.vm.compute = self.source if target == self.dest else self.dest
|
||||
self.vm.save()
|
||||
self.client.post(reverse("instances:migrate", args=[self.vm.pk]),
|
||||
{"compute_id": target.pk, "submission_key": key, "live_migrate": "true"})
|
||||
op = Operation.objects.latest("created_at")
|
||||
ctx = self.start(op)
|
||||
get_kind(op.kind).precheck(op, ctx)
|
||||
get_kind(op.kind).run(op, ctx)
|
||||
self.assertEqual(self.dest_conn.moveto.call_args.kwargs["uri"], uri)
|
||||
Operation.objects.all().delete()
|
||||
Claim.objects.all().delete()
|
||||
|
||||
def test_cancel_and_progress_ask_the_source_vm(self):
|
||||
op = self.queue(live_migrate="true")
|
||||
with patch("instances.jobs.wvmInstance") as proxy:
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ def record_migration(instance, new_compute):
|
|||
|
||||
def migration_target(compute):
|
||||
"""The destination for the log: its host name, and the migration address
|
||||
when one is set and differs."""
|
||||
when one is set and differs (else the migration goes to the host name)."""
|
||||
if compute.migration_address not in ("", compute.hostname):
|
||||
return _("%(hostname)s via %(address)s") % {"hostname": compute.hostname, "address": compute.migration_address}
|
||||
return compute.hostname
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -11,7 +11,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-11-01 14:22+0000\n"
|
||||
"POT-Creation-Date: 2026-11-01 15:30+0000\n"
|
||||
"PO-Revision-Date: 2020-11-04 12:00+0000\n"
|
||||
"Last-Translator: catborise <muhammetalisag@gmail.com>, 2020\n"
|
||||
"Language-Team: Turkish (https://www.transifex.com/catborise/teams/110663/"
|
||||
|
|
@ -1797,10 +1797,8 @@ msgstr "taşıma adresi"
|
|||
#: computes/models.py:28
|
||||
msgid ""
|
||||
"IP address or host name the other hosts send migrating VMs to; empty: the "
|
||||
"host's own host name, which they must be able to resolve"
|
||||
msgstr ""
|
||||
"Diğer sunucuların taşınan sanal makineleri gönderdiği IP adresi veya sunucu "
|
||||
"adı; boş: sunucunun kendi adı, diğer sunucular bu adı çözümleyebilmelidir"
|
||||
"address the panel reaches the host at"
|
||||
msgstr "Diğer sunucuların taşınan sanal makineleri gönderdiği IP adresi veya ana bilgisayar adı; boş: panelin bu sunucuya ulaştığı adres"
|
||||
|
||||
#: computes/models.py:33 computes/templates/computes/form.html:34
|
||||
msgid "gstfsd key"
|
||||
|
|
@ -2816,15 +2814,15 @@ msgstr "Sanal makineyi taşı"
|
|||
msgid "Migrating the VM"
|
||||
msgstr "Sanal makine taşınıyor"
|
||||
|
||||
#: instances/jobs.py:927
|
||||
#: instances/jobs.py:929
|
||||
msgid "libvirt is still migrating the VM"
|
||||
msgstr "libvirt sanal makineyi hâlâ taşıyor"
|
||||
|
||||
#: instances/jobs.py:968
|
||||
#: instances/jobs.py:970
|
||||
msgid "Move disk"
|
||||
msgstr "Diski taşı"
|
||||
|
||||
#: instances/jobs.py:999
|
||||
#: instances/jobs.py:1001
|
||||
msgid "Copying the disk"
|
||||
msgstr "Disk kopyalanıyor"
|
||||
|
||||
|
|
@ -3909,7 +3907,9 @@ msgstr ""
|
|||
#: instances/templates/instances/destroy_instance_form.html:47
|
||||
msgid ""
|
||||
"Its TPM state is deleted with it, and the keys in it, such as BitLocker's."
|
||||
msgstr "TPM durumu da onunla birlikte silinir, içindeki anahtarlar da, örneğin BitLocker'ınkiler."
|
||||
msgstr ""
|
||||
"TPM durumu da onunla birlikte silinir, içindeki anahtarlar da, örneğin "
|
||||
"BitLocker'ınkiler."
|
||||
|
||||
#: instances/templates/instances/destroy_instance_form.html:50
|
||||
msgid "This cannot be undone."
|
||||
|
|
@ -5109,7 +5109,9 @@ msgstr "Kaynak sanal makine: %(name)s"
|
|||
msgid ""
|
||||
"The clone gets a new, empty TPM: keys in this VM's TPM, such as BitLocker's, "
|
||||
"do not come along."
|
||||
msgstr "Klon yeni ve boş bir TPM alır: bu sanal makinenin TPM'indeki anahtarlar, örneğin BitLocker'ınkiler, klona geçmez."
|
||||
msgstr ""
|
||||
"Klon yeni ve boş bir TPM alır: bu sanal makinenin TPM'indeki anahtarlar, "
|
||||
"örneğin BitLocker'ınkiler, klona geçmez."
|
||||
|
||||
#: instances/templates/instances/settings_tab.html:765
|
||||
#: instances/templates/instances/settings_tab.html:776
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue