diff --git a/oswin_tempest_plugin/exceptions.py b/oswin_tempest_plugin/exceptions.py new file mode 100644 index 0000000..5711beb --- /dev/null +++ b/oswin_tempest_plugin/exceptions.py @@ -0,0 +1,21 @@ +# Copyright 2017 Cloudbase Solutions SRL +# 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.lib import exceptions + + +class ResizeException(exceptions.TempestException): + message = ("Server %(server_id)s failed to resize to the given " + "flavor %(flavor)s") diff --git a/oswin_tempest_plugin/tests/_mixins/resize.py b/oswin_tempest_plugin/tests/_mixins/resize.py new file mode 100644 index 0000000..1c73d0f --- /dev/null +++ b/oswin_tempest_plugin/tests/_mixins/resize.py @@ -0,0 +1,108 @@ +# Copyright 2017 Cloudbase Solutions SRL +# 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 time + +from oslo_log import log as logging +import testtools + +from oswin_tempest_plugin import config +from oswin_tempest_plugin import exceptions + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class _ResizeUtils(object): + + def _get_server_migration(self, server_id): + final_states = ['error', 'confirmed'] + for i in range(10): + migrations = ( + self.admin_migrations_client.list_migrations()['migrations']) + server_migration = [m for m in migrations + if m['instance_uuid'] == server_id] + if server_migration: + migration_status = server_migration[0]['status'] + LOG.debug("Server's %s migration status: %s", + server_id, migration_status) + if migration_status in final_states: + return server_migration[0] + else: + # NOTE(claudiub): the migration might not appear *immediately* + # after the cold resize was requested. + LOG.info("Server's %s migration was not found.", server_id) + + time.sleep(1) + + return server_migration[0] if server_migration else None + + def _resize_server(self, server_tuple, new_flavor): + server = server_tuple.server + self.servers_client.resize_server(server['id'], + flavor_ref=new_flavor['id']) + + migration = self._get_server_migration(server['id']) + if migration and migration['status'] == 'error': + # the migration ended up in an error state. Raise an exception. + raise exceptions.ResizeException(server_id=server['id'], + flavor=new_flavor) + + self._wait_for_server_status(server, 'VERIFY_RESIZE') + self.servers_client.confirm_resize_server(server['id']) + + +class _ResizeMixin(_ResizeUtils): + """Cold resize mixin. + + This mixin will add cold resize tests. The tests will create a new + instance, resize it to a new flavor, and check its network connectivity. + + The new flavor is based on the configured compute.flavor_ref, with some + updates. For example, if the vNUMA configuration is to be tested, the new + flavor would contain the flavor extra_spec 'hw:numa_nodes=1'. + """ + + # NOTE(claudiub): These flavor dicts are updates to the base flavor + # tempest is configured with. For example, _BIGGER_FLAVOR can be: + # _BIGGER_FLAVOR = {'disk': 1} + # which means a flavor having +1 GB disk size will be created, and + # a created instance will be resized to it. + + _SMALLER_FLAVOR = {} + _BIGGER_FLAVOR = {} + _BAD_FLAVOR = {} + + @testtools.skipUnless(CONF.compute_feature_enabled.resize, + 'Resize is not available.') + def test_resize(self): + new_flavor = self._create_new_flavor(self._get_flavor_ref(), + self._BIGGER_FLAVOR) + server_tuple = self._create_server() + self._resize_server(server_tuple, new_flavor) + self._check_server_connectivity(server_tuple) + + @testtools.skipUnless(CONF.compute_feature_enabled.resize, + 'Resize is not available.') + def test_resize_negative(self): + new_flavor = self._create_new_flavor(self._get_flavor_ref(), + self._BAD_FLAVOR) + server_tuple = self._create_server() + + self.assertRaises(exceptions.ResizeException, self._resize_server, + server_tuple, new_flavor) + # assert that the server is still reachable, even if the resize + # failed. + self._check_server_connectivity(server_tuple) diff --git a/oswin_tempest_plugin/tests/scenario/test_disks.py b/oswin_tempest_plugin/tests/scenario/test_disks.py index 6eea107..9a06a3e 100644 --- a/oswin_tempest_plugin/tests/scenario/test_disks.py +++ b/oswin_tempest_plugin/tests/scenario/test_disks.py @@ -16,11 +16,13 @@ from oswin_tempest_plugin import config from oswin_tempest_plugin.tests import test_base from oswin_tempest_plugin.tests._mixins import migrate +from oswin_tempest_plugin.tests._mixins import resize CONF = config.CONF -class _BaseDiskTestMixin(migrate._MigrateMixin): +class _BaseDiskTestMixin(migrate._MigrateMixin, + resize._ResizeMixin): """Image types / formats test suite. This test suite will spawn instances with a configured image and will @@ -30,6 +32,9 @@ class _BaseDiskTestMixin(migrate._MigrateMixin): _CONF_OPTION_NAME = '' + _BIGGER_FLAVOR = {'disk': 1} + _BAD_FLAVOR = {'disk': -1} + @classmethod def skip_checks(cls): super(_BaseDiskTestMixin, cls).skip_checks() @@ -48,6 +53,7 @@ class VhdDiskTest(test_base.TestBase, _BaseDiskTestMixin): _IMAGE_REF = CONF.hyperv.vhd_image_ref _CONF_OPTION_NAME = 'hyperv.vhd_image_ref' + _FLAVOR_SUFFIX = 'vhd' # TODO(claudiub): validate that the images really are VHD / VHDX. @@ -56,6 +62,7 @@ class VhdxDiskTest(test_base.TestBase, _BaseDiskTestMixin): _IMAGE_REF = CONF.hyperv.vhdx_image_ref _CONF_OPTION_NAME = 'hyperv.vhdx_image_ref' + _FLAVOR_SUFFIX = 'vhdx' class Generation2DiskTest(test_base.TestBase, _BaseDiskTestMixin): @@ -65,6 +72,7 @@ class Generation2DiskTest(test_base.TestBase, _BaseDiskTestMixin): _IMAGE_REF = CONF.hyperv.gen2_image_ref _CONF_OPTION_NAME = 'hyperv.gen2_image_ref' + _FLAVOR_SUFFIX = 'gen2' # TODO(claudiub): Add validation that the given gen2_image_ref really has # the 'hw_machine_type=hyperv-gen2' property. diff --git a/oswin_tempest_plugin/tests/test_base.py b/oswin_tempest_plugin/tests/test_base.py index ee031fa..0160c30 100644 --- a/oswin_tempest_plugin/tests/test_base.py +++ b/oswin_tempest_plugin/tests/test_base.py @@ -45,6 +45,9 @@ class TestBase(tempest.test.BaseTestCase): # Inheriting TestCases should change this image ref if needed. _IMAGE_REF = CONF.compute.image_ref + # suffix to use for the newly created flavors. + _FLAVOR_SUFFIX = '' + @classmethod def skip_checks(cls): super(TestBase, cls).skip_checks() @@ -71,6 +74,8 @@ class TestBase(tempest.test.BaseTestCase): cls.keypairs_client = cls.os_primary.keypairs_client cls.servers_client = cls.os_primary.servers_client cls.admin_servers_client = cls.os_admin.servers_client + cls.admin_flavors_client = cls.os_admin.flavors_client + cls.admin_migrations_client = cls.os_admin.migrations_client # Neutron network client cls.security_groups_client = ( @@ -101,6 +106,34 @@ class TestBase(tempest.test.BaseTestCase): def _get_image_ref(self): return self._IMAGE_REF + def _flavor_cleanup(self, flavor_id): + try: + self.admin_flavors_client.delete_flavor(flavor_id) + self.admin_flavors_client.wait_for_resource_deletion(flavor_id) + except exceptions.NotFound: + pass + + def _create_new_flavor(self, flavor_ref, flavor_updates): + """Creates a new flavor based on the given flavor and flavor updates. + + :returns: the newly created flavor's ID. + """ + flavor = self.admin_flavors_client.show_flavor(flavor_ref)['flavor'] + + flavor_name = 'test_resize' + if self._FLAVOR_SUFFIX: + flavor_name += '_%s' % self._FLAVOR_SUFFIX + + new_flavor = self.admin_flavors_client.create_flavor( + name=data_utils.rand_name(flavor_name), + ram=flavor['ram'] + flavor_updates.get('ram', 0), + disk=flavor['disk'] + flavor_updates.get('disk', 0), + vcpus=flavor['vcpus'] + flavor_updates.get('vcpus', 0), + )['flavor'] + + self.addCleanup(self._flavor_cleanup, new_flavor['id']) + return new_flavor + def _get_flavor_ref(self): return CONF.compute.flavor_ref