diff --git a/rally-jobs/rally.yaml b/rally-jobs/rally.yaml index 3619e132ed..ff62c53e69 100644 --- a/rally-jobs/rally.yaml +++ b/rally-jobs/rally.yaml @@ -825,6 +825,26 @@ failure_rate: max: 0 + - + args: + image_location: "{{ cirros_image_url }}" + container_format: "bare" + disk_format: "qcow2" + runner: + type: "constant" + times: 1 + concurrency: 1 + context: + users: + tenants: 2 + users_per_tenant: 3 + api_versions: + glance: + version: 2 + sla: + failure_rate: + max: 0 + GlanceImages.create_and_list_image: - args: diff --git a/rally/plugins/openstack/scenarios/glance/utils.py b/rally/plugins/openstack/scenarios/glance/utils.py index e5886ec7b5..eb5e101e6f 100644 --- a/rally/plugins/openstack/scenarios/glance/utils.py +++ b/rally/plugins/openstack/scenarios/glance/utils.py @@ -13,41 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -import os -import time - -from oslo_config import cfg - from rally.plugins.openstack import scenario +from rally.plugins.openstack.wrappers import glance as glance_wrapper from rally.task import atomic -from rally.task import utils - - -GLANCE_BENCHMARK_OPTS = [ - cfg.FloatOpt("glance_image_create_prepoll_delay", - default=2.0, - help="Time to sleep after creating a resource before " - "polling for it status"), - cfg.FloatOpt("glance_image_create_timeout", - default=120.0, - help="Time to wait for glance image to be created."), - cfg.FloatOpt("glance_image_create_poll_interval", - default=1.0, - help="Interval between checks when waiting for image " - "creation."), - cfg.FloatOpt("glance_image_delete_timeout", - default=120.0, - help="Time to wait for glance image to be deleted."), - cfg.FloatOpt("glance_image_delete_poll_interval", - default=1.0, - help="Interval between checks when waiting for image " - "deletion.") -] - - -CONF = cfg.CONF -benchmark_group = cfg.OptGroup(name="benchmark", title="benchmark options") -CONF.register_opts(GLANCE_BENCHMARK_OPTS, group=benchmark_group) class GlanceScenario(scenario.OpenStackScenario): @@ -72,38 +40,9 @@ class GlanceScenario(scenario.OpenStackScenario): :returns: image object """ - kw = { - "name": self.generate_random_name(), - "container_format": container_format, - "disk_format": disk_format, - } - - kw.update(kwargs) - image_location = os.path.expanduser(image_location) - - try: - if os.path.isfile(image_location): - kw["data"] = open(image_location) - else: - kw["copy_from"] = image_location - - image = self.clients("glance").images.create(**kw) - - time.sleep(CONF.benchmark.glance_image_create_prepoll_delay) - - image = utils.wait_for( - image, - ready_statuses=["active"], - update_resource=utils.get_from_manager(), - timeout=CONF.benchmark.glance_image_create_timeout, - check_interval=CONF.benchmark. - glance_image_create_poll_interval) - - finally: - if "data" in kw: - kw["data"].close() - - return image + client = glance_wrapper.wrap(self._clients.glance, self) + return client.create_image(container_format, image_location, + disk_format) @atomic.action_timer("glance.delete_image") def _delete_image(self, image): @@ -113,11 +52,5 @@ class GlanceScenario(scenario.OpenStackScenario): :param image: Image object """ - image.delete() - utils.wait_for_status( - image, - ready_statuses=["deleted"], - check_deletion=True, - update_resource=utils.get_from_manager(), - timeout=CONF.benchmark.glance_image_delete_timeout, - check_interval=CONF.benchmark.glance_image_delete_poll_interval) + client = glance_wrapper.wrap(self._clients.glance, self) + client.delete_image(image) diff --git a/rally/plugins/openstack/wrappers/glance.py b/rally/plugins/openstack/wrappers/glance.py new file mode 100644 index 0000000000..d2f4cd1e6c --- /dev/null +++ b/rally/plugins/openstack/wrappers/glance.py @@ -0,0 +1,187 @@ +# Copyright 2016: 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 abc +import os +import time + +from rally.common import logging +from rally import exceptions +from rally.task import utils + +from glanceclient import exc as glance_exc +from oslo_config import cfg +import requests +import six + +LOG = logging.getLogger(__name__) + +GLANCE_BENCHMARK_OPTS = [ + cfg.FloatOpt("glance_image_create_prepoll_delay", + default=2.0, + help="Time to sleep after creating a resource before " + "polling for it status"), + cfg.FloatOpt("glance_image_create_timeout", + default=120.0, + help="Time to wait for glance image to be created."), + cfg.FloatOpt("glance_image_create_poll_interval", + default=1.0, + help="Interval between checks when waiting for image " + "creation."), + cfg.FloatOpt("glance_image_delete_timeout", + default=120.0, + help="Time to wait for glance image to be deleted."), + cfg.FloatOpt("glance_image_delete_poll_interval", + default=1.0, + help="Interval between checks when waiting for image " + "deletion.") +] + +CONF = cfg.CONF +benchmark_group = cfg.OptGroup(name="benchmark", title="benchmark options") +CONF.register_opts(GLANCE_BENCHMARK_OPTS, group=benchmark_group) + + +@six.add_metaclass(abc.ABCMeta) +class GlanceWrapper(object): + def __init__(self, client, owner): + self.owner = owner + self.client = client + + @abc.abstractmethod + def create_image(self, container_format, image_location, disk_format): + """Creates new image.""" + + @abc.abstractmethod + def delete_image(self, image): + """Deletes image.""" + + +class GlanceV1Wrapper(GlanceWrapper): + def create_image(self, container_format, image_location, + disk_format, **kwargs): + kw = { + "container_format": container_format, + "disk_format": disk_format, + } + kw.update(kwargs) + if "name" not in kw: + kw["name"] = self.owner.generate_random_name() + image_location = os.path.expanduser(image_location) + + try: + if os.path.isfile(image_location): + kw["data"] = open(image_location) + else: + kw["copy_from"] = image_location + + image = self.client.images.create(**kw) + + time.sleep(CONF.benchmark.glance_image_create_prepoll_delay) + + image = utils.wait_for_status( + image, ["active"], + update_resource=utils.get_from_manager(), + timeout=CONF.benchmark.glance_image_create_timeout, + check_interval=CONF.benchmark. + glance_image_create_poll_interval) + finally: + if "data" in kw: + kw["data"].close() + + return image + + def delete_image(self, image): + image.delete() + utils.wait_for_status( + image, ["deleted"], + check_deletion=True, + update_resource=utils.get_from_manager(), + timeout=CONF.benchmark.glance_image_delete_timeout, + check_interval=CONF.benchmark.glance_image_delete_poll_interval) + + +class GlanceV2Wrapper(GlanceWrapper): + def _get_image(self, image): + try: + return self.client.images.get(image.id) + except glance_exc.HTTPNotFound: + raise exceptions.GetResourceNotFound(resource=image) + + def create_image(self, container_format, image_location, + disk_format, **kwargs): + kw = { + "container_format": container_format, + "disk_format": disk_format, + } + kw.update(kwargs) + if "name" not in kw: + kw["name"] = self.owner.generate_random_name() + + image_location = os.path.expanduser(image_location) + + image = self.client.images.create(**kw) + + time.sleep(CONF.benchmark.glance_image_create_prepoll_delay) + + start = time.time() + image = utils.wait_for_status( + image, ["queued"], + update_resource=self._get_image, + timeout=CONF.benchmark.glance_image_create_timeout, + check_interval=CONF.benchmark. + glance_image_create_poll_interval) + timeout = time.time() - start + + image_data = None + try: + if os.path.isfile(image_location): + image_data = open(image_location) + else: + response = requests.get(image_location) + image_data = response.raw + self.client.images.upload(image.id, image_data) + finally: + if image_data is not None: + image_data.close() + + return utils.wait_for_status( + image, ["active"], + update_resource=self._get_image, + timeout=timeout, + check_interval=CONF.benchmark. + glance_image_create_poll_interval) + + def delete_image(self, image): + self.client.images.delete(image.id) + utils.wait_for_status( + image, ["deleted"], + check_deletion=True, + update_resource=self._get_image, + timeout=CONF.benchmark.glance_image_delete_timeout, + check_interval=CONF.benchmark.glance_image_delete_poll_interval) + + +def wrap(client, owner): + """Returns glanceclient wrapper based on glance client version.""" + version = client.choose_version() + if version == "1": + return GlanceV1Wrapper(client(), owner) + elif version == "2": + return GlanceV2Wrapper(client(), owner) + else: + msg = "Version %s of the glance API could not be identified." % version + LOG.warning(msg) + raise exceptions.InvalidArgumentsException(msg) diff --git a/rally/verification/tempest/config.py b/rally/verification/tempest/config.py index 1243cee5ce..28314bbc82 100644 --- a/rally/verification/tempest/config.py +++ b/rally/verification/tempest/config.py @@ -30,6 +30,7 @@ from rally.common import objects from rally.common import utils from rally import exceptions from rally import osclients +from rally.plugins.openstack.wrappers import glance as glance_wrapper from rally.plugins.openstack.wrappers import network LOG = logging.getLogger(__name__) @@ -406,13 +407,13 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): .format(opt=option, opt_val=option_value)) def _discover_or_create_image(self): - glanceclient = self.clients.glance() + glance_wrap = glance_wrapper.wrap(self.clients.glance, self) if CONF.image.name_regex: LOG.debug("Trying to discover an image with name matching " "regular expression '%s'. Note that case insensitive " "matching is performed" % CONF.image.name_regex) - img_list = [img for img in glanceclient.images.list() + img_list = [img for img in self.clients.glance().images.list() if img.status.lower() == "active" and img.name] for img in img_list: if re.match(CONF.image.name_regex, img.name, re.IGNORECASE): @@ -427,13 +428,13 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin): "name": self.generate_random_name(), "disk_format": CONF.image.disk_format, "container_format": CONF.image.container_format, + "image_location": os.path.join(_create_or_get_data_dir(), + self.image_name), "is_public": True } LOG.debug("Creating image '%s'" % params["name"]) - image = glanceclient.images.create(**params) + image = glance_wrap.create_image(**params) self._created_images.append(image) - image.update(data=open( - os.path.join(_create_or_get_data_dir(), self.image_name), "rb")) return image diff --git a/tests/unit/plugins/openstack/scenarios/glance/test_utils.py b/tests/unit/plugins/openstack/scenarios/glance/test_utils.py index 98148c2ed3..a2020ac590 100644 --- a/tests/unit/plugins/openstack/scenarios/glance/test_utils.py +++ b/tests/unit/plugins/openstack/scenarios/glance/test_utils.py @@ -15,13 +15,11 @@ import tempfile import mock -from oslo_config import cfg from rally.plugins.openstack.scenarios.glance import utils from tests.unit import test GLANCE_UTILS = "rally.plugins.openstack.scenarios.glance.utils" -CONF = cfg.CONF class GlanceScenarioTestCase(test.ScenarioTestCase): @@ -30,6 +28,8 @@ class GlanceScenarioTestCase(test.ScenarioTestCase): super(GlanceScenarioTestCase, self).setUp() self.image = mock.Mock() self.image1 = mock.Mock() + self.scenario_clients = mock.Mock() + self.scenario_clients.glance.choose_version.return_value = 1 def test_list_images(self): scenario = utils.GlanceScenario(context=self.context) @@ -40,52 +40,28 @@ class GlanceScenarioTestCase(test.ScenarioTestCase): self._test_atomic_action_timer(scenario.atomic_actions(), "glance.list_images") - def test_create_image(self): + @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") + def test_create_image(self, mock_wrap): image_location = tempfile.NamedTemporaryFile() - self.clients("glance").images.create.return_value = self.image - scenario = utils.GlanceScenario(context=self.context) + mock_wrap.return_value.create_image.return_value = self.image + scenario = utils.GlanceScenario(context=self.context, + clients=self.scenario_clients) return_image = scenario._create_image("container_format", image_location.name, "disk_format") - self.mock_wait_for.mock.assert_called_once_with( - self.image, - update_resource=self.mock_get_from_manager.mock.return_value, - ready_statuses=["active"], - check_interval=CONF.benchmark.glance_image_create_poll_interval, - timeout=CONF.benchmark.glance_image_create_timeout) - self.mock_get_from_manager.mock.assert_called_once_with() - self.assertEqual(self.mock_wait_for.mock.return_value, return_image) + self.assertEqual(self.image, return_image) + mock_wrap.assert_called_once_with(scenario._clients.glance, scenario) + mock_wrap.return_value.create_image.assert_called_once_with( + "container_format", image_location.name, "disk_format") self._test_atomic_action_timer(scenario.atomic_actions(), "glance.create_image") - def test_create_image_with_location(self): - self.clients("glance").images.create.return_value = self.image - scenario = utils.GlanceScenario(context=self.context) - return_image = scenario._create_image("container_format", - "image_location", - "disk_format") - self.mock_wait_for.mock.assert_called_once_with( - self.image, - update_resource=self.mock_get_from_manager.mock.return_value, - ready_statuses=["active"], - check_interval=CONF.benchmark.glance_image_create_poll_interval, - timeout=CONF.benchmark.glance_image_create_timeout) - self.mock_get_from_manager.mock.assert_called_once_with() - self.assertEqual(self.mock_wait_for.mock.return_value, return_image) - self._test_atomic_action_timer(scenario.atomic_actions(), - "glance.create_image") - - def test_delete_image(self): - scenario = utils.GlanceScenario(context=self.context) + @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") + def test_delete_image(self, mock_wrap): + scenario = utils.GlanceScenario(context=self.context, + clients=self.scenario_clients) scenario._delete_image(self.image) - self.image.delete.assert_called_once_with() - self.mock_wait_for_status.mock.assert_called_once_with( - self.image, - ready_statuses=["deleted"], - check_deletion=True, - update_resource=self.mock_get_from_manager.mock.return_value, - check_interval=CONF.benchmark.glance_image_delete_poll_interval, - timeout=CONF.benchmark.glance_image_delete_timeout) - self.mock_get_from_manager.mock.assert_called_once_with() + mock_wrap.assert_called_once_with(scenario._clients.glance, scenario) + mock_wrap.return_value.delete_image.assert_called_once_with(self.image) self._test_atomic_action_timer(scenario.atomic_actions(), "glance.delete_image") diff --git a/tests/unit/plugins/openstack/wrappers/test_glance.py b/tests/unit/plugins/openstack/wrappers/test_glance.py new file mode 100644 index 0000000000..68cb9c411a --- /dev/null +++ b/tests/unit/plugins/openstack/wrappers/test_glance.py @@ -0,0 +1,190 @@ +# Copyright 2014: 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 tempfile + +import ddt +from glanceclient import exc as glance_exc +import mock +from oslo_config import cfg + +from rally import exceptions +from rally.plugins.openstack.wrappers import glance as glance_wrapper +from tests.unit import test + +CONF = cfg.CONF + + +class GlanceWrapperTestBase(object): + def test_wrap(self): + client = mock.MagicMock() + owner = mock.Mock() + client.version = "dummy" + self.assertRaises(exceptions.InvalidArgumentsException, + glance_wrapper.wrap, client, owner) + + +@ddt.ddt +class GlanceV1WrapperTestCase(test.ScenarioTestCase, GlanceWrapperTestBase): + _tempfile = tempfile.NamedTemporaryFile() + + def setUp(self): + super(GlanceV1WrapperTestCase, self).setUp() + self.client = mock.MagicMock() + self.client.choose_version.return_value = "1" + self.owner = mock.Mock() + self.wrapped_client = glance_wrapper.wrap(self.client, self.owner) + + @ddt.data( + {"location": "image_location"}, + {"location": "image_location", "fakearg": "fake"}, + {"location": "image_location", "name": "image_name"}, + {"location": _tempfile.name}) + @ddt.unpack + @mock.patch("six.moves.builtins.open") + def test_create_image(self, mock_open, location, **kwargs): + return_image = self.wrapped_client.create_image("container_format", + location, + "disk_format", + **kwargs) + call_args = dict(kwargs) + call_args["container_format"] = "container_format" + call_args["disk_format"] = "disk_format" + if location.startswith("/"): + call_args["data"] = mock_open.return_value + mock_open.assert_called_once_with(location) + mock_open.return_value.close.assert_called_once_with() + else: + call_args["copy_from"] = location + if "name" not in kwargs: + call_args["name"] = self.owner.generate_random_name.return_value + + self.client().images.create.assert_called_once_with(**call_args) + + self.mock_wait_for_status.mock.assert_called_once_with( + self.client().images.create.return_value, ["active"], + update_resource=self.mock_get_from_manager.mock.return_value, + check_interval=CONF.benchmark.glance_image_create_poll_interval, + timeout=CONF.benchmark.glance_image_create_timeout) + self.mock_get_from_manager.mock.assert_called_once_with() + self.assertEqual(self.mock_wait_for_status.mock.return_value, + return_image) + + def test_delete_image(self): + image = mock.Mock() + self.wrapped_client.delete_image(image) + image.delete.assert_called_once_with() + self.mock_wait_for_status.mock.assert_called_once_with( + image, ["deleted"], + check_deletion=True, + update_resource=self.mock_get_from_manager.mock.return_value, + check_interval=CONF.benchmark.glance_image_delete_poll_interval, + timeout=CONF.benchmark.glance_image_delete_timeout) + self.mock_get_from_manager.mock.assert_called_once_with() + + +@ddt.ddt +class GlanceV2WrapperTestCase(test.ScenarioTestCase, GlanceWrapperTestBase): + _tempfile = tempfile.NamedTemporaryFile() + + def setUp(self): + super(GlanceV2WrapperTestCase, self).setUp() + self.client = mock.MagicMock() + self.client.choose_version.return_value = "2" + self.owner = mock.Mock() + self.wrapped_client = glance_wrapper.wrap(self.client, self.owner) + + def test__get_image(self): + image = mock.Mock() + + return_image = self.wrapped_client._get_image(image) + + self.client.return_value.images.get.assert_called_once_with(image.id) + self.assertEqual(return_image, + self.client.return_value.images.get.return_value) + + def test__get_image_not_found(self): + image = mock.Mock() + self.client.return_value.images.get.side_effect = ( + glance_exc.HTTPNotFound) + + self.assertRaises(exceptions.GetResourceNotFound, + self.wrapped_client._get_image, image) + self.client.return_value.images.get.assert_called_once_with(image.id) + + @ddt.data( + {"location": "image_location"}, + {"location": "image_location", "fakearg": "fake"}, + {"location": "image_location", "name": "image_name"}, + {"location": _tempfile.name}) + @ddt.unpack + @mock.patch("six.moves.builtins.open") + @mock.patch("requests.get") + def test_create_image(self, mock_requests_get, mock_open, location, + **kwargs): + self.wrapped_client._get_image = mock.Mock() + created_image = mock.Mock() + uploaded_image = mock.Mock() + self.mock_wait_for_status.mock.side_effect = [created_image, + uploaded_image] + + return_image = self.wrapped_client.create_image("container_format", + location, + "disk_format", + **kwargs) + create_args = dict(kwargs) + create_args["container_format"] = "container_format" + create_args["disk_format"] = "disk_format" + if "name" not in kwargs: + create_args["name"] = self.owner.generate_random_name.return_value + + self.client().images.create.assert_called_once_with(**create_args) + + if location.startswith("/"): + data = mock_open.return_value + mock_open.assert_called_once_with(location) + else: + data = mock_requests_get.return_value.raw + mock_requests_get.assert_called_once_with(location) + data.close.assert_called_once_with() + self.client().images.upload.assert_called_once_with(created_image.id, + data) + + self.mock_wait_for_status.mock.assert_has_calls([ + mock.call( + self.client().images.create.return_value, ["queued"], + update_resource=self.wrapped_client._get_image, + check_interval=CONF.benchmark. + glance_image_create_poll_interval, + timeout=CONF.benchmark.glance_image_create_timeout), + mock.call( + created_image, ["active"], + update_resource=self.wrapped_client._get_image, + check_interval=CONF.benchmark. + glance_image_create_poll_interval, + timeout=mock.ANY)]) + self.assertEqual(uploaded_image, return_image) + + def test_delete_image(self): + image = mock.Mock() + self.wrapped_client.delete_image(image) + self.client.return_value.images.delete.assert_called_once_with( + image.id) + self.mock_wait_for_status.mock.assert_called_once_with( + image, ["deleted"], + check_deletion=True, + update_resource=self.wrapped_client._get_image, + check_interval=CONF.benchmark.glance_image_delete_poll_interval, + timeout=CONF.benchmark.glance_image_delete_timeout) diff --git a/tests/unit/verification/test_config.py b/tests/unit/verification/test_config.py index 73b3eae1dd..c97b206221 100644 --- a/tests/unit/verification/test_config.py +++ b/tests/unit/verification/test_config.py @@ -396,7 +396,8 @@ class TempestResourcesContextTestCase(test.TestCase): result = self.context.conf.get("compute", "flavor_ref") self.assertEqual("id1", result) - def test__discover_or_create_image_when_image_exists(self): + @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") + def test__discover_or_create_image_when_image_exists(self, mock_wrap): client = self.context.clients.glance() client.images.list.return_value = [fakes.FakeResource(name="CirrOS", status="active")] @@ -404,14 +405,31 @@ class TempestResourcesContextTestCase(test.TestCase): self.assertEqual("CirrOS", image.name) self.assertEqual(0, len(self.context._created_images)) - @mock.patch("six.moves.builtins.open") - def test__discover_or_create_image(self, mock_open): - client = self.context.clients.glance() - client.images.create.side_effect = [fakes.FakeImage(id="id1")] + # @mock.patch("six.moves.builtins.open") + # def test__discover_or_create_image(self, mock_wrap, mock_open): + # client = self.context.clients.glance() + # client.images.create.side_effect = [fakes.FakeImage(id="id1")] + + # image = self.context._discover_or_create_image() + # self.assertEqual("id1", image.id) + # self.assertEqual("id1", self.context._created_images[0].id) + + @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") + def test__discover_or_create_image(self, mock_wrap): + client = mock_wrap.return_value image = self.context._discover_or_create_image() - self.assertEqual("id1", image.id) - self.assertEqual("id1", self.context._created_images[0].id) + self.assertEqual(image, client.create_image.return_value) + self.assertEqual(self.context._created_images[0], + client.create_image.return_value) + mock_wrap.assert_called_once_with(self.context.clients.glance, + self.context) + client.create_image.assert_called_once_with( + container_format=CONF.image.container_format, + image_location=mock.ANY, + disk_format=CONF.image.disk_format, + name=mock.ANY, + is_public=True) def test__create_flavor(self): client = self.context.clients.nova()