diff --git a/README.md b/README.md index e7c62e22..8890a49b 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/computes/migrations/0008_migration_address_help.py b/computes/migrations/0008_migration_address_help.py new file mode 100644 index 00000000..1e0a7215 --- /dev/null +++ b/computes/migrations/0008_migration_address_help.py @@ -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'), + ), + ] diff --git a/computes/models.py b/computes/models.py index 91633534..7a078e42 100644 --- a/computes/models.py +++ b/computes/models.py @@ -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): diff --git a/computes/tests/test_migration_address.py b/computes/tests/test_migration_address.py index 3cb46450..0427abac 100644 --- a/computes/tests/test_migration_address.py +++ b/computes/tests/test_migration_address.py @@ -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")) diff --git a/instances/tests/test_migrate_job.py b/instances/tests/test_migrate_job.py index ed4ef8f1..f538c6f4 100644 --- a/instances/tests/test_migrate_job.py +++ b/instances/tests/test_migrate_job.py @@ -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: diff --git a/instances/utils.py b/instances/utils.py index 44371406..e27334c2 100644 --- a/instances/utils.py +++ b/instances/utils.py @@ -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 diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index fdab9388..afad99a4 100644 Binary files a/locale/tr/LC_MESSAGES/django.mo and b/locale/tr/LC_MESSAGES/django.mo differ diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po index 8102ee44..15c6af53 100644 --- a/locale/tr/LC_MESSAGES/django.po +++ b/locale/tr/LC_MESSAGES/django.po @@ -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 , 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