1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(instances): a clone stays on the host of the VM it was made from

With "VM Clone Auto Migrate" on, a new clone was migrated to a random
compute, whatever storage, CPU or network that host had, and a failed
migration showed as a failed clone although the clone existed.

The setting and its code are removed; migration appsettings.0015
deletes the setting. A clone can be migrated afterwards like any VM.
The README's update notes list the migration changes: the migration
address, storage pools for the disk check, and this setting.
This commit is contained in:
catborise 2026-10-06 15:58:35 +03:00
parent 5c22fc8436
commit 1016f745c2
5 changed files with 51 additions and 7 deletions

View file

@ -415,6 +415,8 @@ The template is not copied over an existing settings file during an upgrade.
Failed logins are limited with `django-axes` (installed from `conf/requirements.txt`). Copy the `axes` entries of `webvirtcloud/settings.py.template` into your `webvirtcloud/settings.py`: `"axes"` in `INSTALLED_APPS`, `"axes.middleware.AxesMiddleware"` at the end of `MIDDLEWARE`, `"axes.backends.AxesStandaloneBackend"` first in `AUTHENTICATION_BACKENDS`, and the `AXES_*` settings; then run `migrate`. The number of failed logins and the lock time are on the Settings page; unlock a user with `python3 manage.py axes_reset_username <username>`. Behind a proxy other than the bundled nginx, the proxy must set `X-Real-IP` to the client address and its address must be listed in `LOGIN_TRUSTED_PROXIES`.
Migration: where the hosts cannot resolve each other's host names, set each compute's **Migration address** (the compute's edit page) to the IP the other hosts reach it on for migrations. 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 (network disks are not checked). The "VM Clone Auto Migrate" setting is removed (migration `appsettings.0015`); a clone stays on the host of the VM it was made from.
Migration `accounts.0007` transfers the first-login requirement from any existing
`data/admin_password` file to the user record. Run migrations as a user who can read
that file. Thereafter, deleting or losing access to the file does not bypass the

View file

@ -0,0 +1,31 @@
from django.db import migrations
def remove_auto_migrate(apps, schema_editor):
"""A clone stays on the host of the VM it was made from: the setting sent
it to a random host, whatever storage, CPU or network that host had."""
AppSettings = apps.get_model("appsettings", "AppSettings")
AppSettings.objects.using(schema_editor.connection.alias).filter(key="CLONE_INSTANCE_AUTO_MIGRATE").delete()
def restore_auto_migrate(apps, schema_editor):
AppSettings = apps.get_model("appsettings", "AppSettings")
AppSettings.objects.using(schema_editor.connection.alias).get_or_create(
key="CLONE_INSTANCE_AUTO_MIGRATE",
defaults={
"name": "VM Clone Auto Migrate",
"value": "False",
"choices": "True,False",
"description": "Auto migrate instance after clone",
},
)
class Migration(migrations.Migration):
dependencies = [
("appsettings", "0014_login_lockout_settings"),
]
operations = [
migrations.RunPython(remove_auto_migrate, restore_auto_migrate),
]

View file

@ -43,3 +43,8 @@ class AppSettingsTestCase(TestCase):
self.client.force_login(regular_user)
response = self.client.get(reverse("appsettings"))
self.assertEqual(response.status_code, 403)
def test_clones_are_not_migrated_automatically(self):
# a clone stays on the host of the VM it was made from; the setting
# that sent it to a random host is removed by migration 0015
self.assertFalse(AppSettings.objects.filter(key="CLONE_INSTANCE_AUTO_MIGRATE").exists())

View file

@ -107,6 +107,19 @@ class ClonePermissionTestCase(TestCase):
self.assertEqual(res.status_code, 403)
proxy.clone_instance.assert_not_called()
def test_a_clone_stays_on_the_host_of_its_vm(self):
# the removed "VM Clone Auto Migrate" setting sent it to a random host
other = Compute.objects.create(name="other-host", hostname="198.51.100.20", login="", password="", type=4)
with self.mocked_libvirt() as proxy, \
patch("instances.views.utils.migrate_instance") as migrate, \
patch.object(app_settings, "CLONE_INSTANCE_AUTO_MIGRATE", "True", create=True), \
patch("instances.views.Compute.objects.order_by") as random_host:
random_host.return_value.first.return_value = other
self._clone(self.owner_change, self._valid_post())
proxy.clone_instance.assert_called_once()
migrate.assert_not_called()
self.assertEqual(Instance.objects.get(name="clone-vm").compute, self.instance.compute)
def test_viewer_can_clone_a_template(self):
self.instance.is_template = True
self.instance.save(update_fields=["is_template"])

View file

@ -1854,13 +1854,6 @@ def clone(request, pk):
request.user.username, instance.compute.name, new_instance.name, msg
)
if app_settings.CLONE_INSTANCE_AUTO_MIGRATE == "True":
new_compute = Compute.objects.order_by("?").first()
if new_compute != instance.compute:
utils.migrate_instance(
new_compute, new_instance, request.user, offline=True
)
return redirect(reverse("instances:instance", args=[new_instance.id]))
except (libvirtError, ValueError, OSError) as e: # OSError includes a lock timeout
messages.error(request, error_text(request, e))