diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample index 830d8d71bf..2987c56184 100644 --- a/etc/tempest.conf.sample +++ b/etc/tempest.conf.sample @@ -127,6 +127,13 @@ path_to_private_key = /home/user/.ssh/id_rsa # 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 1525da860a..5e2ee7feae 100644 --- a/etc/tempest.conf.tpl +++ b/etc/tempest.conf.tpl @@ -106,6 +106,13 @@ path_to_private_key = %COMPUTE_PATH_TO_PRIVATE_KEY% # 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 ab8aca4c20..c46a0070f1 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -190,6 +190,17 @@ class ComputeConfig(BaseConfig): """Does the test environment support resizing?""" 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?""" diff --git a/tempest/services/nova/json/hosts_client.py b/tempest/services/nova/json/hosts_client.py new file mode 100644 index 0000000000..a53d00d772 --- /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 df239dd306..a96dacbb41 100644 --- a/tempest/services/nova/json/servers_client.py +++ b/tempest/services/nova/json/servers_client.py @@ -372,3 +372,18 @@ class ServersClientJSON(RestClient): 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 d9c1592f25..ebf3b541d9 100644 --- a/tempest/tests/compute/base.py +++ b/tempest/tests/compute/base.py @@ -82,6 +82,16 @@ class BaseCompTest(unittest.TestCase): admin_client = os.admin_client 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): """ 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 0000000000..fb175f3be7 --- /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()