diff --git a/rally/benchmark/scenarios/nova/__init__.py b/rally/benchmark/scenarios/nova/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rally/benchmark/scenarios/nova/utils.py b/rally/benchmark/scenarios/nova/utils.py new file mode 100644 index 0000000000..cfe912b8b3 --- /dev/null +++ b/rally/benchmark/scenarios/nova/utils.py @@ -0,0 +1,170 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013: 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. + +import random +import string +import time + +from novaclient import exceptions + +from rally.benchmark import base +from rally import exceptions as rally_exceptions +from rally import utils + + +def _resource_is(status): + return lambda resource: resource.status == status + + +def _get_from_manager(resource): + resource = resource.manager.get(resource) + if resource.status == "FAILED": + raise rally_exceptions.GetResourceFailure() + return resource + + +def _false(resource): + return False + + +class NovaScenario(base.Scenario): + + @classmethod + def _boot_server(cls, server_name, image_id, flavor_id, **kwargs): + """Boots one server. + + Returns when the server is actually booted and is in the "Active" + state. + + :param server_name: String used to name the server + :param image_id: ID of the image to be used for server creation + :param flavor_id: ID of the flavor to be used for server creation + :param **kwargs: Other optional parameters to initialize the server + + :returns: Created server object + """ + server = cls.nova.servers.create(server_name, image_id, flavor_id, + **kwargs) + # NOTE(msdubov): It is reasonable to wait 5 secs before starting to + # check whether the server is ready => less API calls. + time.sleep(5) + server = utils.wait_for(server, is_ready=_resource_is("ACTIVE"), + update_resource=_get_from_manager, + timeout=600, check_interval=3) + return server + + @classmethod + def _suspend_server(cls, server): + """Suspends the given server. + + Returns when the server is actually suspended and is in the "Suspended" + state. + + :param server: Server object + """ + server.suspend() + time.sleep(2) + utils.wait_for(server, is_ready=_resource_is("SUSPENDED"), + update_resource=_get_from_manager, + timeout=600, check_interval=3) + + @classmethod + def _delete_server(cls, server): + """Deletes the given server. + + Returns when the server is actually deleted. + + :param server: Server object + """ + server.delete() + # NOTE(msdubov): When the server gets deleted, the nova.servers.get() + # method raises a NotFound exception. + try: + utils.wait_for(server, is_ready=_false, + update_resource=_get_from_manager, + timeout=600, check_interval=3) + except exceptions.NotFound: + pass + + @classmethod + def _delete_image(cls, image): + """Deletes the given image. + + Returns when the image is actually deleted. + + :param image: Image object + """ + image.delete() + utils.wait_for(image, is_ready=_resource_is("DELETED"), + update_resource=_get_from_manager, + timeout=600, check_interval=3) + + @classmethod + def _create_image(cls, server): + """Creates an image of the given server + + Uses the server name to name the created image. Returns when the image + is actually created and is in the "Active" state. + + :param server: Server object for which the image will be created + + :returns: Created image object + """ + image_uuid = cls.nova.servers.create_image(server, server.name) + image = cls.nova.images.get(image_uuid) + image = utils.wait_for(image, is_ready=_resource_is("ACTIVE"), + update_resource=_get_from_manager, + timeout=600, check_interval=3) + return image + + @classmethod + def _boot_servers(cls, name_prefix, image_id, flavor_id, + requests, instances_per_request=1, **kwargs): + """Boots multiple servers. + + Returns when all the servers are actually booted and are in the + "Active" state. + + :param name_prefix: The prefix to use while naming the created servers. + The rest of the server names will be '_No.' + :param image_id: ID of the image to be used for server creation + :param flavor_id: ID of the flavor to be used for server creation + :param requests: Number of booting requests to perform + :param instances_per_request: Number of instances to boot + per each request + + :returns: List of created server objects + """ + for i in range(requests): + cls.nova.servers.create('%s_%d' % (name_prefix, i), image_id, + flavor_id, min_count=instances_per_request, + max_count=instances_per_request, **kwargs) + # NOTE(msdubov): Nova python client returns only one server even when + # min_count > 1, so we have to rediscover all the + # created servers manyally. + servers = filter(lambda server: server.name.startswith(name_prefix), + cls.nova.servers.list()) + time.sleep(5) + servers = [utils.wait_for(server, is_ready=_resource_is("ACTIVE"), + update_resource=_get_from_manager, + timeout=600, check_interval=3) + for server in servers] + return servers + + @classmethod + def _generate_random_name(cls, length): + return ''.join(random.choice(string.lowercase) for i in range(length)) diff --git a/rally/exceptions.py b/rally/exceptions.py index e128c4bb2b..4edb6cf190 100644 --- a/rally/exceptions.py +++ b/rally/exceptions.py @@ -122,3 +122,7 @@ class TaskNotFound(NotFoundException): class TimeoutException(RallyException): msg_fmt = _("Timeout exceeded.") + + +class GetResourceFailure(RallyException): + msg_fmt = _("Failed to get the resource.") diff --git a/tests/benchmark/scenarios/__init__.py b/tests/benchmark/scenarios/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/benchmark/scenarios/nova/__init__.py b/tests/benchmark/scenarios/nova/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/benchmark/scenarios/nova/test_utils.py b/tests/benchmark/scenarios/nova/test_utils.py new file mode 100644 index 0000000000..55e011da50 --- /dev/null +++ b/tests/benchmark/scenarios/nova/test_utils.py @@ -0,0 +1,165 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013: 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. + +import mock + +from rally.benchmark.scenarios.nova import utils +from rally import test + + +# NOTE(msdubov): A set of 'Fake' classes below is of great use in the test case +# for utils and also in the test cases for bechmark scenarios. + + +class FakeResource(object): + + def __init__(self, manager=None): + self.name = "resource" + self.status = "ACTIVE" + self.manager = manager + + def __getattr__(self, name): + # NOTE(msdubov): e.g. server.delete() -> manager.delete(server) + def manager_func(*args, **kwargs): + getattr(self.manager, name)(self, *args, **kwargs) + return manager_func + + +class FakeServer(FakeResource): + + def suspend(self): + self.status = "SUSPENDED" + + +class FakeImage(FakeResource): + pass + + +class FakeFloatingIP(FakeResource): + pass + + +class FakeManager(object): + + def get(self, resource): + return resource + + def delete(self, resource): + pass + + +class FakeServerManager(FakeManager): + + def create(self, name, image_id, flavor_id): + return FakeServer(self) + + def create_image(self, server, name): + return "img_uuid" + + def add_floating_ip(self, server, fip): + pass + + def remove_floating_ip(self, server, fip): + pass + + +class FakeImageManager(FakeManager): + + def create(self): + return FakeImage(self) + + +class FakeFloatingIPsManager(FakeManager): + + def create(self): + return FakeFloatingIP(self) + + +class FakeNovaClient(object): + + def __init__(self): + self.servers = FakeServerManager() + self.images = FakeImageManager() + self.floating_ips = FakeFloatingIPsManager() + + +class FakeClients(object): + + def get_keystone_client(self): + return "keystone" + + def get_nova_client(self): + return FakeNovaClient() + + def get_glance_client(self): + return "glance" + + def get_cinder_client(self): + return "cinder" + + +class NovaScenarioTestCase(test.NoDBTestCase): + + def test_generate_random_name(self): + for length in [8, 16, 32, 64]: + name = utils.NovaScenario._generate_random_name(length) + self.assertEqual(len(name), length) + self.assertTrue(name.isalpha()) + + def test_server_helper_methods(self): + + rally_utils = "rally.benchmark.scenarios.nova.utils.utils" + utils_resource_is = "rally.benchmark.scenarios.nova.utils._resource_is" + osclients = "rally.benchmark.base.osclients" + servers_create = ("rally.benchmark.scenarios.nova.utils.NovaScenario." + "nova.servers.create") + sleep = "rally.benchmark.scenarios.nova.utils.time.sleep" + + with mock.patch(rally_utils) as mock_rally_utils: + with mock.patch(utils_resource_is) as mock_resource_is: + mock_resource_is.return_value = {} + with mock.patch(osclients) as mock_osclients: + mock_osclients.Clients.return_value = FakeClients() + keys = ["admin_username", "admin_password", + "admin_tenant_name", "uri"] + kw = dict(zip(keys, keys)) + utils.NovaScenario.class_init(kw) + with mock.patch(servers_create) as mock_create: + fake_server = FakeServerManager().create("s1", "i1", 1) + mock_create.return_value = fake_server + with mock.patch(sleep): + utils.NovaScenario._boot_server("s1", "i1", 1) + utils.NovaScenario._create_image(fake_server) + utils.NovaScenario._suspend_server(fake_server) + utils.NovaScenario._delete_server(fake_server) + + expected = [ + mock.call.wait_for(fake_server, is_ready={}, + update_resource=utils._get_from_manager, + timeout=600, check_interval=3), + mock.call.wait_for("img_uuid", is_ready={}, + update_resource=utils._get_from_manager, + timeout=600, check_interval=3), + mock.call.wait_for(fake_server, is_ready={}, + update_resource=utils._get_from_manager, + timeout=600, check_interval=3), + mock.call.wait_for(fake_server, is_ready=utils._false, + update_resource=utils._get_from_manager, + timeout=600, check_interval=3) + ] + + self.assertEqual(mock_rally_utils.mock_calls, expected)