Tempest tests to cover live-block-migration

Related to bug 1051881.

Two test cases: one to cover the live block migration on XenServer, and
the other to indicate that the machine status is left in an inconsistent
state in case a non-existing host is specified as target.
To run the live-migration tests with block migration, include:

    [compute]
    live_migration_available = true
    use_block_migration_for_live_migration = true

in your tempest.conf

To run these tests, the hypervisors must support the Storage XenMotion
feature.

Change-Id: I1e6cc903fa573ecd219a08bf4dddd9cc1f4f60df
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 8d3f6c9..9c7868c 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -127,6 +127,13 @@
 # Connection string to the database of Compute service
 db_uri = mysql://user:pass@localhost/nova
 
+# Run live migration tests (requires 2 hosts)
+live_migration_available = false
+
+# Use block live migration (Otherwise, non-block migration will be
+# performed, which requires XenServer pools in case of using XS)
+use_block_migration_for_live_migration = false
+
 [image]
 # This section contains configuration options used when executing tests
 # against the OpenStack Images API
diff --git a/etc/tempest.conf.tpl b/etc/tempest.conf.tpl
index ecb020a..67fc025 100644
--- a/etc/tempest.conf.tpl
+++ b/etc/tempest.conf.tpl
@@ -106,6 +106,13 @@
 # Connection string to the database of Compute service
 db_uri = %COMPUTE_DB_URI%
 
+# Run live migration tests (requires 2 hosts)
+live_migration_available = %LIVE_MIGRATION_AVAILABLE%
+
+# Use block live migration (Otherwise, non-block migration will be
+# performed, which requires XenServer pools in case of using XS)
+use_block_migration_for_live_migration = %USE_BLOCK_MIGRATION_FOR_LIVE_MIGRATION%
+
 [image]
 # This section contains configuration options used when executing tests
 # against the OpenStack Images API
diff --git a/tempest/config.py b/tempest/config.py
index 52a4aad..95dffa8 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -191,6 +191,17 @@
         return self.get("resize_available", 'false').lower() != 'false'
 
     @property
+    def live_migration_available(self):
+        return self.get(
+            "live_migration_available", 'false').lower() == 'true'
+
+    @property
+    def use_block_migration_for_live_migration(self):
+        return self.get(
+            "use_block_migration_for_live_migration", 'false'
+        ).lower() == 'true'
+
+    @property
     def change_password_available(self):
         """Does the test environment support changing the admin password?"""
         return self.get("change_password_available", 'false').lower() != \
diff --git a/tempest/services/nova/json/hosts_client.py b/tempest/services/nova/json/hosts_client.py
new file mode 100644
index 0000000..a53d00d
--- /dev/null
+++ b/tempest/services/nova/json/hosts_client.py
@@ -0,0 +1,18 @@
+from tempest.common.rest_client import RestClient
+import json
+
+
+class HostsClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(HostsClientJSON, self).__init__(config, username, password,
+                                              auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def list_hosts(self):
+        """Lists all hosts"""
+
+        url = 'os-hosts'
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['hosts']
diff --git a/tempest/services/nova/json/servers_client.py b/tempest/services/nova/json/servers_client.py
index 32ec740..a5e06c9 100644
--- a/tempest/services/nova/json/servers_client.py
+++ b/tempest/services/nova/json/servers_client.py
@@ -372,3 +372,18 @@
         post_body = json.dumps(post_body)
         return self.post('servers/%s/action' % server_id,
                          post_body, self.headers)
+
+    def live_migrate_server(self, server_id, dest_host, use_block_migration):
+        """ This should be called with administrator privileges """
+
+        migrate_params = {
+            "disk_over_commit": False,
+            "block_migration": use_block_migration,
+            "host": dest_host
+        }
+
+        req_body = json.dumps({'os-migrateLive': migrate_params})
+
+        resp, body = self.post("servers/%s/action" % str(server_id),
+                               req_body, self.headers)
+        return resp, body
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index f36c8f2..ac5f524 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -83,6 +83,16 @@
         return admin_client
 
     @classmethod
+    def _get_client_args(cls):
+
+        return (
+            cls.config,
+            cls.config.identity_admin.username,
+            cls.config.identity_admin.password,
+            cls.config.identity.auth_url
+        )
+
+    @classmethod
     def _get_isolated_creds(cls):
         """
         Creates a new set of user/tenant/password credentials for a
diff --git a/tempest/tests/compute/test_live_block_migration.py b/tempest/tests/compute/test_live_block_migration.py
new file mode 100644
index 0000000..fb175f3
--- /dev/null
+++ b/tempest/tests/compute/test_live_block_migration.py
@@ -0,0 +1,146 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# 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.
+
+import nose
+import unittest2 as unittest
+from nose.plugins.attrib import attr
+import random
+import string
+
+from tempest.tests.compute import base
+from tempest.common.utils.linux.remote_client import RemoteClient
+from tempest import config
+from tempest import exceptions
+
+from tempest.services.nova.json.hosts_client import HostsClientJSON
+from tempest.services.nova.json.servers_client import ServersClientJSON
+
+
+@attr(category='live-migration')
+class LiveBlockMigrationTest(base.BaseComputeTest):
+
+    live_migration_available = (
+        config.TempestConfig().compute.live_migration_available)
+    use_block_migration_for_live_migration = (
+        config.TempestConfig().compute.use_block_migration_for_live_migration)
+    run_ssh = config.TempestConfig().compute.run_ssh
+
+    @classmethod
+    def setUpClass(cls):
+        super(LiveBlockMigrationTest, cls).setUpClass()
+
+        tenant_name = cls.config.identity_admin.tenant_name
+        cls.admin_hosts_client = HostsClientJSON(
+            *cls._get_client_args(), tenant_name=tenant_name)
+
+        cls.admin_servers_client = ServersClientJSON(
+            *cls._get_client_args(), tenant_name=tenant_name)
+
+        cls.created_server_ids = []
+
+    def _get_compute_hostnames(self):
+        _resp, body = self.admin_hosts_client.list_hosts()
+        return [
+            host_record['host_name']
+            for host_record in body
+            if host_record['service'] == 'compute'
+        ]
+
+    def _get_server_details(self, server_id):
+        _resp, body = self.admin_servers_client.get_server(server_id)
+        return body
+
+    def _get_host_for_server(self, server_id):
+        return self._get_server_details(server_id)['OS-EXT-SRV-ATTR:host']
+
+    def _migrate_server_to(self, server_id, dest_host):
+        _resp, body = self.admin_servers_client.live_migrate_server(
+            server_id, dest_host, self.use_block_migration_for_live_migration)
+        return body
+
+    def _get_host_other_than(self, host):
+        for target_host in self._get_compute_hostnames():
+            if host != target_host:
+                return target_host
+
+    def _get_non_existing_host_name(self):
+        random_name = ''.join(
+            random.choice(string.ascii_uppercase) for x in range(20))
+
+        self.assertFalse(random_name in self._get_compute_hostnames())
+
+        return random_name
+
+    def _get_server_status(self, server_id):
+        return self._get_server_details(server_id)['status']
+
+    def _get_an_active_server(self):
+        for server_id in self.created_server_ids:
+            if 'ACTIVE' == self._get_server_status(server_id):
+                return server_id
+        else:
+            server = self.create_server()
+            server_id = server['id']
+            self.password = server['adminPass']
+            self.password = 'password'
+            self.created_server_ids.append(server_id)
+            return server_id
+
+    @attr(type='positive')
+    @unittest.skipIf(not live_migration_available,
+                     'Block Live migration not available')
+    def test_001_live_block_migration(self):
+        """Live block migrate an instance to another host"""
+
+        if len(self._get_compute_hostnames()) < 2:
+            raise nose.SkipTest(
+                "Less than 2 compute nodes, skipping migration test.")
+
+        server_id = self._get_an_active_server()
+
+        actual_host = self._get_host_for_server(server_id)
+
+        target_host = self._get_host_other_than(actual_host)
+
+        self._migrate_server_to(server_id, target_host)
+
+        self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+
+        self.assertTrue(target_host == self._get_host_for_server(server_id))
+
+    @attr(type='positive', bug='lp1051881')
+    @unittest.skip('Until bug 1051881 is dealt with.')
+    @unittest.skipIf(not live_migration_available,
+                     'Block Live migration not available')
+    def test_002_invalid_host_for_migration(self):
+        """Migrating to an invalid host should not change the status"""
+
+        server_id = self._get_an_active_server()
+
+        target_host = self._get_non_existing_host_name()
+
+        with self.assertRaises(exceptions.BadRequest) as cm:
+            self._migrate_server_to(server_id, target_host)
+
+        self.assertEquals('ACTIVE', self._get_server_status(server_id))
+
+    @classmethod
+    def tearDownClass(cls):
+        for server_id in cls.created_server_ids:
+            cls.servers_client.delete_server(server_id)
+
+        super(LiveBlockMigrationTest, cls).tearDownClass()