diff --git a/README.md b/README.md index 93fe54f7..10bc23d4 100644 --- a/README.md +++ b/README.md @@ -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 `. 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 diff --git a/appsettings/migrations/0015_remove_clone_instance_auto_migrate.py b/appsettings/migrations/0015_remove_clone_instance_auto_migrate.py new file mode 100644 index 00000000..8442288a --- /dev/null +++ b/appsettings/migrations/0015_remove_clone_instance_auto_migrate.py @@ -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), + ] diff --git a/appsettings/test_appsettings.py b/appsettings/test_appsettings.py index 19b6e6d9..e8cac7aa 100644 --- a/appsettings/test_appsettings.py +++ b/appsettings/test_appsettings.py @@ -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()) diff --git a/instances/test_clone_permission.py b/instances/test_clone_permission.py index bbba2119..5007f422 100644 --- a/instances/test_clone_permission.py +++ b/instances/test_clone_permission.py @@ -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"]) diff --git a/instances/views.py b/instances/views.py index 8debc2eb..d2ae2040 100755 --- a/instances/views.py +++ b/instances/views.py @@ -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))