Add tests for user-initiated migrations

these are not meant to test the full extent of migration
capabilities, but just to functionally test the correctness
of applied policy changes that must allow ordinary users
to request cold or live migration without specifying target host.

Closes-Issue: PRODX-42800
Change-Id: I92650067c7f5c6973c9ddf4e6109034213df4c5b
(cherry picked from commit d7d58d6c40629c3448950cea531e954dfa666988)
diff --git a/tempest/api/compute/servers/test_migration.py b/tempest/api/compute/servers/test_migration.py
new file mode 100644
index 0000000..37a16a0
--- /dev/null
+++ b/tempest/api/compute/servers/test_migration.py
@@ -0,0 +1,102 @@
+# Copyright 2024 Mirantis, Inc
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+from tempest.api.compute import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class UserMigrationsTestBase(base.BaseV2ComputeTest):
+
+    credentials = ['primary', 'admin']
+
+    @classmethod
+    def skip_checks(cls):
+        super().skip_checks()
+        if CONF.compute.min_compute_nodes < 2:
+            raise cls.skipException(
+                "Less than 2 compute nodes, skipping multinode tests."
+            )
+
+    @classmethod
+    def setup_clients(cls):
+        super().setup_clients()
+        cls.admin_servers_client = cls.os_admin.servers_client
+
+    @classmethod
+    def get_host_for_server(cls, server_id):
+        server_details = cls.admin_servers_client.show_server(server_id)
+        return server_details['server']['OS-EXT-SRV-ATTR:host']
+
+
+class UserColdMigrationTestCase(UserMigrationsTestBase):
+
+    @classmethod
+    def skip_checks(cls):
+        super().skip_checks()
+        if not CONF.compute_feature_enabled.cold_migration:
+            raise cls.skipException("Cold migration is not available")
+        if not CONF.compute_feature_enabled.cold_migration_nonadmins:
+            raise cls.skipException(
+                "Cold migration is not available for non-admin users"
+            )
+
+    @decorators.idempotent_id('f58acd67-2d01-4613-8baf-6e22a19b4e0b')
+    def test_user_cold_migrate_server(self):
+        server = self.create_test_server(wait_until="ACTIVE")
+        src_host = self.get_host_for_server(server['id'])
+
+        self.servers_client.migrate_server(server['id'])
+
+        waiters.wait_for_server_status(self.servers_client,
+                                       server['id'], 'VERIFY_RESIZE')
+        self.servers_client.confirm_resize_server(server['id'])
+        waiters.wait_for_server_status(self.servers_client,
+                                       server['id'], 'ACTIVE')
+        dst_host = self.get_host_for_server(server['id'])
+        self.assertNotEqual(src_host, dst_host)
+
+
+class UserLiveMigrationTestCase(UserMigrationsTestBase):
+    min_microversion = '2.25'
+    max_microversion = 'latest'
+    if CONF.compute_feature_enabled.volume_multiattach:
+        min_microversion = '2.60'
+
+    @classmethod
+    def skip_checks(cls):
+        super().skip_checks()
+        if not CONF.compute_feature_enabled.live_migration:
+            raise cls.skipException("Live migration is not available")
+        if not CONF.compute_feature_enabled.live_migration_nonadmins:
+            raise cls.skipException(
+                "Live migration is not available for non-admin users"
+            )
+
+    @decorators.idempotent_id('94100e69-2641-4152-9580-d16631bb228a')
+    def test_user_live_migrate_server(self):
+        server = self.create_test_server(wait_until="ACTIVE")
+        src_host = self.get_host_for_server(server['id'])
+
+        self.servers_client.live_migrate_server(
+            server['id'], host=None, block_migration="auto",
+        )
+
+        waiters.wait_for_server_status(self.servers_client,
+                                       server['id'], 'ACTIVE')
+        dst_host = self.get_host_for_server(server['id'])
+        self.assertNotEqual(src_host, dst_host)
diff --git a/tempest/config.py b/tempest/config.py
index 82a0a93..84fe453 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -486,9 +486,17 @@
     cfg.BoolOpt('cold_migration',
                 default=True,
                 help="Does the test environment support cold migration?"),
+    cfg.BoolOpt('cold_migration_nonadmins',
+                default=False,
+                help="Does the test environment support cold migration "
+                     "by non-admin users?"),
     cfg.BoolOpt('live_migration',
                 default=True,
                 help="Does the test environment support live migration?"),
+    cfg.BoolOpt('live_migration_nonadmins',
+                default=False,
+                help="Does the test environment support live migration "
+                     "by non-admin users?"),
     cfg.BoolOpt('live_migrate_back_and_forth',
                 default=False,
                 help="Does the test environment support live migrating "