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()