From 279574d01c0523fa53aeb1fabbec0dbd90ada5de Mon Sep 17 00:00:00 2001 From: haishi Date: Fri, 30 Dec 2016 01:39:03 +0800 Subject: [PATCH] [Service] Port all glance scenarios to Image Service Changes: - glancewrapper are ported to Image Service - All existing glance scenarios are ported to Image service Change-Id: I128dd6fc970cae946a9ecf23da36c349e37d24a0 --- rally-jobs/cinder.yaml | 7 +- rally-jobs/nova.yaml | 7 +- rally-jobs/rally-keystone-api-v2.yaml | 8 +- rally-jobs/rally.yaml | 11 +- .../openstack/context/glance/images.py | 113 +++++++--- .../openstack/scenarios/glance/images.py | 58 +++-- .../openstack/services/image/__init__.py | 0 .../openstack/services/image/glance_v1.py | 193 +++++++++++++++++ .../openstack/services/image/glance_v2.py | 202 ++++++++++++++++++ .../plugins/openstack/services/image/image.py | 114 ++++++++++ .../tasks/scenarios/glance/list-images.json | 4 +- .../tasks/scenarios/glance/list-images.yaml | 4 +- .../openstack/context/glance/test_images.py | 86 ++++---- .../openstack/scenarios/glance/test_images.py | 109 ++++++---- .../openstack/services/image/__init__.py | 0 .../services/image/test_glance_v1.py | 191 +++++++++++++++++ .../services/image/test_glance_v2.py | 178 +++++++++++++++ .../openstack/services/image/test_image.py | 118 ++++++++++ 18 files changed, 1245 insertions(+), 158 deletions(-) create mode 100644 rally/plugins/openstack/services/image/__init__.py create mode 100644 rally/plugins/openstack/services/image/glance_v1.py create mode 100644 rally/plugins/openstack/services/image/glance_v2.py create mode 100644 rally/plugins/openstack/services/image/image.py create mode 100644 tests/unit/plugins/openstack/services/image/__init__.py create mode 100755 tests/unit/plugins/openstack/services/image/test_glance_v1.py create mode 100755 tests/unit/plugins/openstack/services/image/test_glance_v2.py create mode 100755 tests/unit/plugins/openstack/services/image/test_image.py diff --git a/rally-jobs/cinder.yaml b/rally-jobs/cinder.yaml index 3ef0cfe0..bd7acc98 100755 --- a/rally-jobs/cinder.yaml +++ b/rally-jobs/cinder.yaml @@ -166,12 +166,11 @@ - admin images: image_url: "~/.rally/extra/fake-image.img" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 1 image_name: "image-context-test" - image_args: - visibility: "public" + visibility: "public" sla: failure_rate: max: 0 diff --git a/rally-jobs/nova.yaml b/rally-jobs/nova.yaml index e42e0d4b..10e69e9d 100755 --- a/rally-jobs/nova.yaml +++ b/rally-jobs/nova.yaml @@ -736,12 +736,11 @@ - admin images: image_url: "{{ cirros_image_url }}" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 1 image_name: "rally-named-image-from-context" - image_args: - visibility: "public" + visibility: "public" sla: failure_rate: max: 0 diff --git a/rally-jobs/rally-keystone-api-v2.yaml b/rally-jobs/rally-keystone-api-v2.yaml index a0e7ec44..bef1fe8b 100644 --- a/rally-jobs/rally-keystone-api-v2.yaml +++ b/rally-jobs/rally-keystone-api-v2.yaml @@ -834,8 +834,8 @@ users_per_tenant: 2 images: image_url: "{{ cirros_image_url }}" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 1 sla: failure_rate: @@ -852,8 +852,8 @@ users_per_tenant: 2 images: image_url: "~/.rally/extra/fake-image.img" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 1 sla: failure_rate: diff --git a/rally-jobs/rally.yaml b/rally-jobs/rally.yaml index 705054e0..76466824 100644 --- a/rally-jobs/rally.yaml +++ b/rally-jobs/rally.yaml @@ -867,8 +867,8 @@ users_per_tenant: 2 images: image_url: "{{ cirros_image_url }}" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 1 sla: failure_rate: @@ -885,8 +885,8 @@ users_per_tenant: 2 images: image_url: "~/.rally/extra/fake-image.img" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 1 sla: failure_rate: @@ -913,6 +913,7 @@ # failure_rate: # max: 0 # + - args: image_location: "{{ cirros_image_url }}" @@ -932,6 +933,7 @@ sla: failure_rate: max: 0 + # # - # args: @@ -976,6 +978,7 @@ # failure_rate: # max: 0 # + - args: image_location: "~/.rally/extra/fake-image.img" diff --git a/rally/plugins/openstack/context/glance/images.py b/rally/plugins/openstack/context/glance/images.py index e53f9841..4f40b2e0 100644 --- a/rally/plugins/openstack/context/glance/images.py +++ b/rally/plugins/openstack/context/glance/images.py @@ -19,7 +19,7 @@ from rally.common import logging from rally.common import utils as rutils from rally import consts from rally import osclients -from rally.plugins.openstack.wrappers import glance as glance_wrapper +from rally.plugins.openstack.services.image import image from rally.task import context from rally.task import utils @@ -49,41 +49,94 @@ class ImageGenerator(context.Context): "enum": ["qcow2", "raw", "vhd", "vmdk", "vdi", "iso", "aki", "ari", "ami"], }, + "disk_format": { + "enum": ["qcow2", "raw", "vhd", "vmdk", "vdi", "iso", "aki", + "ari", "ami"] + }, "image_container": { "type": "string", }, + "container_format": { + "enum": ["aki", "ami", "ari", "bare", "docker", "ova", "ovf"] + }, "image_name": { "type": "string", }, - "min_ram": { # megabytes + "min_ram": { + "description": "Amount of RAM in MB", "type": "integer", "minimum": 0 }, - "min_disk": { # gigabytes + "min_disk": { + "description": "Amount of disk space in GB", "type": "integer", "minimum": 0 }, + "visibility": { + "enum": ["public", "private", "shared", "community"] + }, "images_per_tenant": { "type": "integer", "minimum": 1 }, "image_args": { + "description": "This param is deprecated from Rally-0.10.0", "type": "object", "additionalProperties": True } }, - "required": ["image_url", "image_type", "image_container", - "images_per_tenant"], + "oneOf": [{"description": "It is been used since Rally 0.10.0", + "required": ["image_url", "disk_format", + "container_format", "images_per_tenant"]}, + {"description": "One of backward compatible way", + "required": ["image_url", "image_type", + "container_format", "images_per_tenant"]}, + {"description": "One of backward compatible way", + "required": ["image_url", "disk_format", + "image_container", "images_per_tenant"]}, + {"description": "One of backward compatible way", + "required": ["image_url", "image_type", + "image_container", "images_per_tenant"]}], "additionalProperties": False } @logging.log_task_wrapper(LOG.info, _("Enter context: `Images`")) def setup(self): - image_url = self.config["image_url"] - image_type = self.config["image_type"] - image_container = self.config["image_container"] - images_per_tenant = self.config["images_per_tenant"] + image_url = self.config.get("image_url") + image_type = self.config.get("image_type") + disk_format = self.config.get("disk_format") + image_container = self.config.get("image_container") + container_format = self.config.get("container_format") + images_per_tenant = self.config.get("images_per_tenant") image_name = self.config.get("image_name") + visibility = self.config.get("visibility", "private") + min_disk = self.config.get("min_disk", 0) + min_ram = self.config.get("min_ram", 0) + image_args = self.config.get("image_args", {}) + is_public = image_args.get("is_public") + + if is_public: + LOG.warning(_("The 'is_public' argument is deprecated " + "since Rally 0.10.0; specify visibility " + "arguments instead")) + if "visibility" not in self.config: + visibility = "public" if is_public else "private" + + if image_type: + LOG.warning(_("The 'image_type' argument is deprecated " + "since Rally 0.10.0; specify disk_format " + "arguments instead")) + disk_format = image_type + + if image_container: + LOG.warning(_("The 'image_container' argument is deprecated " + "since Rally 0.10.0; specify container_format " + "arguments instead")) + container_format = image_container + + if image_args: + LOG.warning(_("The 'kwargs' argument is deprecated since " + "Rally 0.10.0; specify exact arguments instead")) for user, tenant_id in rutils.iterate_per_tenants( self.context["users"]): @@ -91,21 +144,9 @@ class ImageGenerator(context.Context): clients = osclients.Clients( user["credential"], api_info=self.context["config"].get("api_versions")) - glance_wrap = glance_wrapper.wrap(clients.glance, self) - - kwargs = self.config.get("image_args", {}) - if self.config.get("min_ram") is not None: - LOG.warning("The 'min_ram' argument is deprecated; specify " - "arbitrary arguments with 'image_args' instead") - kwargs["min_ram"] = self.config["min_ram"] - if self.config.get("min_disk") is not None: - LOG.warning("The 'min_disk' argument is deprecated; specify " - "arbitrary arguments with 'image_args' instead") - kwargs["min_disk"] = self.config["min_disk"] - if "is_public" in kwargs: - LOG.warning("The 'is_public' argument is deprecated since " - "Rally 0.8.0; specify visibility arguments " - "instead") + image_service = image.Image( + clients, + name_generator=self.generate_random_name) for i in range(images_per_tenant): if image_name and i > 0: @@ -115,10 +156,15 @@ class ImageGenerator(context.Context): else: cur_name = self.generate_random_name() - image = glance_wrap.create_image( - image_container, image_url, image_type, - name=cur_name, **kwargs) - current_images.append(image.id) + image_obj = image_service.create_image( + image_name=cur_name, + container_format=container_format, + image_location=image_url, + disk_format=disk_format, + visibility=visibility, + min_disk=min_disk, + min_ram=min_ram) + current_images.append(image_obj.id) self.context["tenants"][tenant_id]["images"] = current_images @@ -129,14 +175,15 @@ class ImageGenerator(context.Context): clients = osclients.Clients( user["credential"], api_info=self.context["config"].get("api_versions")) - glance_wrap = glance_wrapper.wrap(clients.glance, self) - for image in self.context["tenants"][tenant_id].get("images", []): - clients.glance().images.delete(image) + image_service = image.Image(clients) + for image_id in self.context["tenants"][tenant_id].get( + "images", []): + image_service.delete_image(image_id=image_id) utils.wait_for_status( - clients.glance().images.get(image), + image_service.get_image(image_id=image_id), ["deleted", "pending_delete"], check_deletion=True, - update_resource=glance_wrap.get_image, + update_resource=image_service.get_image, timeout=CONF.benchmark.glance_image_delete_timeout, check_interval=CONF.benchmark. glance_image_delete_poll_interval) diff --git a/rally/plugins/openstack/scenarios/glance/images.py b/rally/plugins/openstack/scenarios/glance/images.py index fc767fed..af15c212 100644 --- a/rally/plugins/openstack/scenarios/glance/images.py +++ b/rally/plugins/openstack/scenarios/glance/images.py @@ -16,8 +16,8 @@ from rally.common import logging from rally import consts from rally.plugins.openstack import scenario -from rally.plugins.openstack.scenarios.glance import utils from rally.plugins.openstack.scenarios.nova import utils as nova_utils +from rally.plugins.openstack.services.image import image from rally.task import types from rally.task import validation @@ -26,13 +26,26 @@ LOG = logging.getLogger(__name__) """Scenarios for Glance images.""" +class GlanceBasic(scenario.OpenStackScenario): + def __init__(self, context=None, admin_clients=None, clients=None): + super(GlanceBasic, self).__init__(context, admin_clients, clients) + if hasattr(self, "_admin_clients"): + self.admin_glance = image.Image( + self._admin_clients, name_generator=self.generate_random_name, + atomic_inst=self.atomic_actions()) + if hasattr(self, "_clients"): + self.glance = image.Image( + self._clients, name_generator=self.generate_random_name, + atomic_inst=self.atomic_actions()) + + @types.convert(image_location={"type": "path_or_url"}, kwargs={"type": "glance_image_args"}) @validation.required_services(consts.Service.GLANCE) @validation.required_openstack(users=True) @scenario.configure(context={"cleanup": ["glance"]}, name="GlanceImages.create_and_list_image") -class CreateAndListImage(utils.GlanceScenario, nova_utils.NovaScenario): +class CreateAndListImage(GlanceBasic, nova_utils.NovaScenario): def run(self, container_format, image_location, disk_format, **kwargs): """Create an image and then list all images. @@ -52,12 +65,13 @@ class CreateAndListImage(utils.GlanceScenario, nova_utils.NovaScenario): ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso :param kwargs: optional parameters to create image """ - image = self._create_image(container_format, - image_location, - disk_format, - **kwargs) + image = self.glance.create_image( + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + **kwargs) self.assertTrue(image) - image_list = self._list_images() + image_list = self.glance.list_images() self.assertIn(image.id, [i.id for i in image_list]) @@ -65,7 +79,7 @@ class CreateAndListImage(utils.GlanceScenario, nova_utils.NovaScenario): @validation.required_openstack(users=True) @scenario.configure(context={"cleanup": ["glance"]}, name="GlanceImages.list_images") -class ListImages(utils.GlanceScenario, nova_utils.NovaScenario): +class ListImages(GlanceBasic, nova_utils.NovaScenario): def run(self): """List all images. @@ -77,7 +91,7 @@ class ListImages(utils.GlanceScenario, nova_utils.NovaScenario): uploaded for them we will be able to test the performance of glance image-list command in this case. """ - self._list_images() + self.glance.list_images() @types.convert(image_location={"type": "path_or_url"}, @@ -86,7 +100,7 @@ class ListImages(utils.GlanceScenario, nova_utils.NovaScenario): @validation.required_openstack(users=True) @scenario.configure(context={"cleanup": ["glance"]}, name="GlanceImages.create_and_delete_image") -class CreateAndDeleteImage(utils.GlanceScenario, nova_utils.NovaScenario): +class CreateAndDeleteImage(GlanceBasic, nova_utils.NovaScenario): def run(self, container_format, image_location, disk_format, **kwargs): """Create and then delete an image. @@ -98,11 +112,12 @@ class CreateAndDeleteImage(utils.GlanceScenario, nova_utils.NovaScenario): ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, and iso :param kwargs: optional parameters to create image """ - image = self._create_image(container_format, - image_location, - disk_format, - **kwargs) - self._delete_image(image) + image = self.glance.create_image( + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + **kwargs) + self.glance.delete_image(image.id) @types.convert(flavor={"type": "nova_flavor"}, @@ -113,8 +128,7 @@ class CreateAndDeleteImage(utils.GlanceScenario, nova_utils.NovaScenario): @validation.required_openstack(users=True) @scenario.configure(context={"cleanup": ["glance", "nova"]}, name="GlanceImages.create_image_and_boot_instances") -class CreateImageAndBootInstances(utils.GlanceScenario, - nova_utils.NovaScenario): +class CreateImageAndBootInstances(GlanceBasic, nova_utils.NovaScenario): def run(self, container_format, image_location, disk_format, flavor, number_instances, create_image_kwargs=None, @@ -140,9 +154,11 @@ class CreateImageAndBootInstances(utils.GlanceScenario, "'boot_server_kwargs' for additional parameters when " "booting servers.") - image = self._create_image(container_format, - image_location, - disk_format, - **create_image_kwargs) + image = self.glance.create_image( + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + **create_image_kwargs) + self._boot_servers(image.id, flavor, number_instances, **boot_server_kwargs) diff --git a/rally/plugins/openstack/services/image/__init__.py b/rally/plugins/openstack/services/image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rally/plugins/openstack/services/image/glance_v1.py b/rally/plugins/openstack/services/image/glance_v1.py new file mode 100644 index 00000000..e5043d74 --- /dev/null +++ b/rally/plugins/openstack/services/image/glance_v1.py @@ -0,0 +1,193 @@ +# 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 os + +from glanceclient import exc as glance_exc +from oslo_config import cfg + +from rally.common import utils as rutils +from rally import exceptions +from rally.plugins.openstack import service +from rally.plugins.openstack.services.image import image +from rally.task import atomic +from rally.task import utils + +CONF = cfg.CONF + + +@service.service("glance", service_type="image", version="1") +class GlanceV1Service(service.Service): + + @atomic.action_timer("glance_v1.create_image") + def create_image(self, image_name=None, container_format=None, + image_location=None, disk_format=None, + is_public=True, min_disk=0, min_ram=0): + """Creates new image. + + :param image_name: Image name for which need to be created + :param container_format: Container format + :param image_location: The new image's location + :param disk_format: Disk format + :param is_public: The created image's public status + :param min_disk: The min disk of created images + :param min_ram: The min ram of created images + """ + image_location = os.path.expanduser(image_location) + image_name = image_name or self.generate_random_name() + kwargs = {} + + try: + if os.path.isfile(image_location): + kwargs["data"] = open(image_location) + else: + kwargs["copy_from"] = image_location + + image_obj = self._clients.glance("1").images.create( + name=image_name, + container_format=container_format, + disk_format=disk_format, + is_public=is_public, + min_disk=min_disk, + min_ram=min_ram, + **kwargs) + + rutils.interruptable_sleep(CONF.benchmark. + glance_image_create_prepoll_delay) + + image_obj = utils.wait_for_status( + image_obj, ["active"], + update_resource=self.get_image, + timeout=CONF.benchmark.glance_image_create_timeout, + check_interval=CONF.benchmark.glance_image_create_poll_interval + ) + + finally: + if "data" in kwargs: + kwargs["data"].close() + + return image_obj + + @atomic.action_timer("glance_v1.get_image") + def get_image(self, image): + """Get specified image. + + :param image: ID or object with ID of image to obtain. + """ + image_id = getattr(image, "id", image) + try: + return self._clients.glance("1").images.get(image_id) + except glance_exc.HTTPNotFound: + raise exceptions.GetResourceNotFound(resource=image) + + @atomic.action_timer("glance_v1.list_images") + def list_images(self, status="active", is_public=None): + """List images. + + :param status: Filter in images for the specified status + :param is_public: Filter in images for the specified public status + """ + images = self._clients.glance("1").images.list(status=status) + if is_public in [True, False]: + return [i for i in images if i.is_public is is_public] + return images + + @atomic.action_timer("glance_v1.set_visibility") + def set_visibility(self, image_id, is_public=True): + """Update visibility. + + :param image_id: ID of image to update + :param is_public: Image is public or not + """ + self._clients.glance("1").images.update(image_id, is_public=is_public) + + @atomic.action_timer("glance_v1.delete_image") + def delete_image(self, image_id): + """Delete image.""" + self._clients.glance("1").images.delete(image_id) + + +@service.compat_layer(GlanceV1Service) +class UnifiedGlanceV1Service(image.Image): + """Compatibility layer for Glance V1.""" + + @staticmethod + def _check_v1_visibility(visibility): + visibility_values = ["public", "private"] + if visibility and visibility not in visibility_values: + raise image.VisibilityException("Improper visibility value: %s " + "in glance_v1" % visibility) + + def create_image(self, image_name=None, container_format=None, + image_location=None, disk_format=None, + visibility="public", min_disk=0, + min_ram=0): + """Creates new image. + + :param image_name: Image name for which need to be created + :param container_format: Container format + :param image_location: The new image's location + :param disk_format: Disk format + :param visibility: The created image's visible status + :param min_disk: The min disk of created images + :param min_ram: The min ram of created images + """ + self._check_v1_visibility(visibility) + + is_public = visibility != "private" + image_obj = self._impl.create_image( + image_name=image_name, + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + is_public=is_public, + min_disk=min_disk, + min_ram=min_ram) + return self._unify_image(image_obj) + + def list_images(self, status="active", visibility=None): + """List images. + + :param status: Filter in images for the specified status + :param visibility: Filter in images for the specified visibility + """ + self._check_v1_visibility(visibility) + + is_public = visibility != "private" + + images = self._impl.list_images(status=status, is_public=is_public) + return [self._unify_image(i) for i in images] + + def set_visibility(self, image_id, visibility="public"): + """Update visibility. + + :param image_id: ID of image to update + :param visibility: The visibility of specified image + """ + self._check_v1_visibility(visibility) + + is_public = visibility != "private" + self._impl.set_visibility(image_id=image_id, is_public=is_public) + + def get_image(self, image_id): + """Get specified image. + + :param image_id: ID of image which need to be got. + """ + image_obj = self._impl.get_image(image_id=image_id) + return self._unify_image(image_obj) + + def delete_image(self, image_id): + """Delete image.""" + self._impl.delete_image(image_id=image_id) diff --git a/rally/plugins/openstack/services/image/glance_v2.py b/rally/plugins/openstack/services/image/glance_v2.py new file mode 100644 index 00000000..cb2d7191 --- /dev/null +++ b/rally/plugins/openstack/services/image/glance_v2.py @@ -0,0 +1,202 @@ +# 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 os +import time + +from glanceclient import exc as glance_exc +from oslo_config import cfg +import requests + +from rally.common import utils as rutils +from rally import exceptions +from rally.plugins.openstack import service +from rally.plugins.openstack.services.image import image +from rally.task import atomic +from rally.task import utils + +CONF = cfg.CONF + + +@service.service("glance", service_type="image", version="2") +class GlanceV2Service(service.Service): + + @atomic.action_timer("glance_v2.create_image") + def create_image(self, image_name=None, container_format=None, + image_location=None, disk_format=None, + visibility=None, min_disk=0, + min_ram=0): + """Creates new image. + + :param image_name: Image name for which need to be created + :param container_format: Container format + :param image_location: The new image's location + :param disk_format: Disk format + :param visibility: The created image's visible status. + :param min_disk: The min disk of created images + :param min_ram: The min ram of created images + """ + image_name = image_name or self.generate_random_name() + + image_obj = self._clients.glance("2").images.create( + name=image_name, + container_format=container_format, + disk_format=disk_format, + visibility=visibility, + min_disk=min_disk, + min_ram=min_ram) + + image_location = os.path.expanduser(image_location) + rutils.interruptable_sleep(CONF.benchmark. + glance_image_create_prepoll_delay) + + start = time.time() + image_obj = utils.wait_for_status( + image_obj.id, ["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 + response = None + try: + if os.path.isfile(image_location): + image_data = open(image_location) + else: + response = requests.get(image_location, stream=True) + image_data = response.raw + self._clients.glance("2").images.upload(image_obj.id, image_data) + finally: + if image_data is not None: + image_data.close() + if response is not None: + response.close() + + image_obj = utils.wait_for_status( + image_obj, ["active"], + update_resource=self.get_image, + timeout=timeout, + check_interval=CONF.benchmark.glance_image_create_poll_interval) + return image_obj + + @atomic.action_timer("glance_v2.get_image") + def get_image(self, image): + """Get specified image. + + :param image: ID or object with ID of image to obtain. + """ + image_id = getattr(image, "id", image) + try: + return self._clients.glance("2").images.get(image_id) + except glance_exc.HTTPNotFound: + raise exceptions.GetResourceNotFound(resource=image) + + @atomic.action_timer("glance_v2.list_images") + def list_images(self, status="active", visibility=None): + """List images. + + :param status: Filter in images for the specified status + :param visibility: Filter in images for the specified visibility + """ + kwargs = {} + kwargs["status"] = status + if visibility: + kwargs["visibility"] = visibility + images = self._clients.glance("2").images.list(**kwargs) + return images + + @atomic.action_timer("glance_v2.set_visibility") + def set_visibility(self, image_id, visibility="shared"): + """Update visibility. + + :param image_id: ID of image to update + :param visibility: The visibility of specified image + """ + self._clients.glance("2").images.update(image_id, + visibility=visibility) + + @atomic.action_timer("glance_v2.delete_image") + def delete_image(self, image_id): + """Delete image.""" + self._clients.glance("2").images.delete(image_id) + + +@service.compat_layer(GlanceV2Service) +class UnifiedGlanceV2Service(image.Image): + """Compatibility layer for Glance V2.""" + + @staticmethod + def _check_v2_visibility(visibility): + visibility_values = ["public", "private", "shared", "community"] + if visibility and visibility not in visibility_values: + raise image.VisibilityException("Improper visibility value: %s " + "in glance_v2" % visibility) + + def create_image(self, image_name=None, container_format=None, + image_location=None, disk_format=None, + visibility=None, min_disk=0, + min_ram=0): + """Creates new image. + + :param image_name: Image name for which need to be created + :param container_format: Container format + :param image_location: The new image's location + :param disk_format: Disk format + :param visibility: The access permission for the created image. + :param min_disk: The min disk of created images + :param min_ram: The min ram of created images + """ + image_obj = self._impl.create_image( + image_name=image_name, + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + visibility=visibility, + min_disk=min_disk, + min_ram=min_ram) + return self._unify_image(image_obj) + + def list_images(self, status="active", visibility=None): + """List images. + + :param status: Filter in images for the specified status + :param visibility: Filter in images for the specified visibility + """ + self._check_v2_visibility(visibility) + + images = self._impl.list_images(status=status, visibility=visibility) + return [self._unify_image(i) for i in images] + + def set_visibility(self, image_id, visibility="shared"): + """Update visibility. + + :param image_id: ID of image to update + :param visibility: The visibility of specified image + """ + self._check_v2_visibility(visibility) + + self._impl.set_visibility(image_id=image_id, visibility=visibility) + + def get_image(self, image_id): + """Get specified image. + + :param image_id: ID of image which need to be got. + """ + image_obj = self._impl.get_image(image_id=image_id) + return self._unify_image(image_obj) + + def delete_image(self, image_id): + """Delete image.""" + self._impl.delete_image(image_id=image_id) diff --git a/rally/plugins/openstack/services/image/image.py b/rally/plugins/openstack/services/image/image.py new file mode 100644 index 00000000..582f76e3 --- /dev/null +++ b/rally/plugins/openstack/services/image/image.py @@ -0,0 +1,114 @@ +# +# 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 collections + +from rally.plugins.openstack import service + +from oslo_config import cfg + +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_poll_interval", + default=1.0, + help="Interval between checks when waiting for image " + "creation.") +] + +CONF = cfg.CONF +benchmark_group = cfg.OptGroup(name="benchmark", title="benchmark options") +CONF.register_opts(GLANCE_BENCHMARK_OPTS, group=benchmark_group) + +UnifiedImage = collections.namedtuple("Image", ["id", "name", "visibility"]) + + +class VisibilityException(Exception): + """Wrong visibility value exception. + + """ + + +class Image(service.UnifiedOpenStackService): + @classmethod + def is_applicable(cls, clients): + cloud_version = str(clients.glance().version).split(".")[0] + return cloud_version == cls._meta_get("impl")._meta_get("version") + + @staticmethod + def _unify_image(image): + if hasattr(image, "visibility"): + return UnifiedImage(id=image.id, name=image.name, + visibility=image.visibility) + else: + return UnifiedImage( + id=image.id, name=image.name, + visibility=("public" if image.is_public else "private")) + + @service.should_be_overridden + def create_image(self, image_name=None, container_format=None, + image_location=None, disk_format=None, + visibility="private", min_disk=0, + min_ram=0): + """Creates new image. + + :param image_name: Image name for which need to be created + :param container_format: Container format + :param image_location: The new image's location + :param disk_format: Disk format + :param visibility: The access permission for the created image. + :param min_disk: The min disk of created images + :param min_ram: The min ram of created images + """ + image = self._impl.create_image( + image_name=image_name, + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + visibility=visibility, + min_disk=min_disk, + min_ram=min_ram) + return image + + @service.should_be_overridden + def list_images(self, status="active", visibility=None): + """List images. + + :param status: Filter in images for the specified status + :param visibility: Filter in images for the specified visibility + """ + return self._impl.list_images(status=status, visibility=visibility) + + @service.should_be_overridden + def set_visibility(self, image_id, visibility="public"): + """Update visibility. + + :param image_id: ID of image to update + :param visibility: The visibility of specified image + """ + self._impl.set_visibility(image_id, visibility=visibility) + + @service.should_be_overridden + def get_image(self, image): + """Get specified image. + + :param image: ID or object with ID of image to obtain. + """ + return self._impl.get_image(image) + + @service.should_be_overridden + def delete_image(self, image_id): + """delete image.""" + self._impl.delete_image(image_id) diff --git a/samples/tasks/scenarios/glance/list-images.json b/samples/tasks/scenarios/glance/list-images.json index 86c7b817..92cf65f2 100644 --- a/samples/tasks/scenarios/glance/list-images.json +++ b/samples/tasks/scenarios/glance/list-images.json @@ -13,8 +13,8 @@ }, "images": { "image_url": "http://download.cirros-cloud.net/0.3.5/cirros-0.3.5-x86_64-disk.img", - "image_type": "qcow2", - "image_container": "bare", + "disk_format": "qcow2", + "container_format": "bare", "images_per_tenant": 4 } } diff --git a/samples/tasks/scenarios/glance/list-images.yaml b/samples/tasks/scenarios/glance/list-images.yaml index 1d5545b4..de4acdec 100644 --- a/samples/tasks/scenarios/glance/list-images.yaml +++ b/samples/tasks/scenarios/glance/list-images.yaml @@ -11,6 +11,6 @@ users_per_tenant: 2 images: image_url: "http://download.cirros-cloud.net/0.3.5/cirros-0.3.5-x86_64-disk.img" - image_type: "qcow2" - image_container: "bare" + disk_format: "qcow2" + container_format: "bare" images_per_tenant: 4 diff --git a/tests/unit/plugins/openstack/context/glance/test_images.py b/tests/unit/plugins/openstack/context/glance/test_images.py index 3acdc7bd..ae811757 100644 --- a/tests/unit/plugins/openstack/context/glance/test_images.py +++ b/tests/unit/plugins/openstack/context/glance/test_images.py @@ -29,6 +29,30 @@ SCN = "rally.plugins.openstack.scenarios.glance" @ddt.ddt class ImageGeneratorTestCase(test.ScenarioTestCase): + tenants_num = 1 + users_per_tenant = 5 + users_num = tenants_num * users_per_tenant + threads = 10 + + def setUp(self): + super(ImageGeneratorTestCase, self).setUp() + self.context.update({ + "config": { + "users": { + "tenants": self.tenants_num, + "users_per_tenant": self.users_per_tenant, + "resource_management_workers": self.threads, + } + }, + "admin": {"credential": mock.MagicMock()}, + "users": [], + "task": {"uuid": "task_id"} + }) + patch = mock.patch( + "rally.plugins.openstack.services.image.image.Image") + self.addCleanup(patch.stop) + self.mock_image = patch.start() + def _gen_tenants(self, count): tenants = {} for id_ in range(count): @@ -50,17 +74,18 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): {"min_disk": 1, "min_ram": 2}, {"image_name": "foo"}, {"tenants": 3, "users_per_tenant": 2, "images_per_tenant": 5}, - {"image_args": {"min_disk": 1, "min_ram": 2, "visibility": "public"}}, {"api_versions": {"glance": {"version": 2, "service_type": "image"}}}) @ddt.unpack - @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") @mock.patch("rally.osclients.Clients") - def test_setup(self, mock_clients, mock_wrap, - image_container="bare", image_type="qcow2", + def test_setup(self, mock_clients, + container_format="bare", disk_format="qcow2", image_url="http://example.com/fake/url", tenants=1, users_per_tenant=1, images_per_tenant=1, image_name=None, min_ram=None, min_disk=None, - image_args=None, api_versions=None): + image_args={"is_public": True}, api_versions=None, + visibility="public"): + image_service = self.mock_image.return_value + tenant_data = self._gen_tenants(tenants) users = [] for tenant_id in tenant_data: @@ -77,9 +102,14 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): }, "images": { "image_url": image_url, - "image_type": image_type, - "image_container": image_container, + "image_type": disk_format, + "disk_format": disk_format, + "image_container": container_format, + "container_format": container_format, "images_per_tenant": images_per_tenant, + "is_public": visibility, + "visibility": visibility, + "image_args": image_args } }, "admin": { @@ -104,12 +134,10 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): self.context["config"]["images"]["min_disk"] = min_disk expected_image_args["min_disk"] = min_disk - wrapper = mock_wrap.return_value - new_context = copy.deepcopy(self.context) for tenant_id in new_context["tenants"].keys(): new_context["tenants"][tenant_id]["images"] = [ - wrapper.create_image.return_value.id + image_service.create_image.return_value.id ] * images_per_tenant images_ctx = images.ImageGenerator(self.context) @@ -121,14 +149,10 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): images_ctx)] * tenants) wrapper_calls.extend( [mock.call().create_image( - image_container, image_url, image_type, + container_format, image_url, disk_format, name=mock.ANY, **expected_image_args)] * tenants * images_per_tenant) - mock_wrap.assert_has_calls(wrapper_calls, any_order=True) - if image_name: - for args in wrapper.create_image.call_args_list: - self.assertTrue(args[1]["name"].startswith(image_name)) mock_clients.assert_has_calls( [mock.call(mock.ANY, api_info=api_versions)] * tenants) @@ -136,18 +160,16 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): {}, {"api_versions": {"glance": {"version": 2, "service_type": "image"}}}) @ddt.unpack - @mock.patch("rally.plugins.openstack.wrappers.glance.wrap") - @mock.patch("rally.osclients.Clients") - def test_cleanup(self, mock_clients, mock_wrap, api_versions=None): - tenants_count = 2 - users_per_tenant = 5 + def test_cleanup(self, api_versions=None): + image_service = self.mock_image.return_value + images_per_tenant = 5 - tenants = self._gen_tenants(tenants_count) + tenants = self._gen_tenants(self.tenants_num) users = [] created_images = [] for tenant_id in tenants: - for i in range(users_per_tenant): + for i in range(self.users_per_tenant): users.append({"id": i, "tenant_id": tenant_id, "credential": mock.MagicMock()}) tenants[tenant_id].setdefault("images", []) @@ -159,8 +181,8 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): self.context.update({ "config": { "users": { - "tenants": tenants_count, - "users_per_tenant": users_per_tenant, + "tenants": self.tenants_num, + "users_per_tenant": self.users_per_tenant, "concurrent": 10, }, "images": { @@ -184,18 +206,4 @@ class ImageGeneratorTestCase(test.ScenarioTestCase): images_ctx = images.ImageGenerator(self.context) images_ctx.cleanup() - - wrapper_calls = [] - wrapper_calls.extend([mock.call(mock_clients.return_value.glance, - images_ctx)] * tenants_count) - mock_wrap.assert_has_calls(wrapper_calls, any_order=True) - - glance_client = mock_clients.return_value.glance.return_value - glance_client.images.delete.assert_has_calls([mock.call(i) - for i in created_images]) - glance_client.images.get.assert_has_calls([mock.call(i) - for i in created_images]) - - mock_clients.assert_has_calls( - [mock.call(mock.ANY, api_info=api_versions)] * tenants_count, - any_order=True) + image_service.delete_image.assert_has_calls([]) diff --git a/tests/unit/plugins/openstack/scenarios/glance/test_images.py b/tests/unit/plugins/openstack/scenarios/glance/test_images.py index 9feb3d3a..8acfe5ca 100644 --- a/tests/unit/plugins/openstack/scenarios/glance/test_images.py +++ b/tests/unit/plugins/openstack/scenarios/glance/test_images.py @@ -23,87 +23,106 @@ from tests.unit import test BASE = "rally.plugins.openstack.scenarios.glance.images" -class GlanceImagesTestCase(test.ScenarioTestCase): +class GlanceBasicTestCase(test.ScenarioTestCase): - @mock.patch("%s.CreateAndListImage._list_images" % BASE) - @mock.patch("%s.CreateAndListImage._create_image" % BASE) - def test_create_and_list_image(self, - mock_create_image, - mock_list_images): + def get_test_context(self): + context = super(GlanceBasicTestCase, self).get_test_context() + context.update({ + "admin": { + "id": "fake_user_id", + "credential": mock.MagicMock() + }, + "user": { + "id": "fake_user_id", + "credential": mock.MagicMock() + }, + "tenant": {"id": "fake_tenant_id", + "name": "fake_tenant_name"} + }) + return context - fake_image = fakes.FakeImage(id=1, name="img_name1") - mock_create_image.return_value = fake_image - mock_list_images.return_value = [ - fakes.FakeImage(id=0, name="img_name1"), + def setUp(self): + super(GlanceBasicTestCase, self).setUp() + patch = mock.patch( + "rally.plugins.openstack.services.image.image.Image") + self.addCleanup(patch.stop) + self.mock_image = patch.start() + + def test_create_and_list_image(self): + image_service = self.mock_image.return_value + fake_image = mock.Mock(id=1, name="img_2") + image_service.create_image.return_value = fake_image + image_service.list_images.return_value = [ + mock.Mock(id=0, name="img_1"), fake_image, - fakes.FakeImage(id=2, name="img_name1") - ] + mock.Mock(id=2, name="img_3")] + call_args = {"container_format": "cf", + "image_location": "url", + "disk_format": "df", + "fakearg": "f"} # Positive case images.CreateAndListImage(self.context).run( "cf", "url", "df", fakearg="f") - mock_create_image.assert_called_once_with( - "cf", "url", "df", fakearg="f") - mock_list_images.assert_called_once_with() + image_service.create_image.assert_called_once_with(**call_args) # Negative case: image isn't created - mock_create_image.return_value = None + image_service.create_image.return_value = None self.assertRaises(exceptions.RallyAssertionError, images.CreateAndListImage(self.context).run, "cf", "url", "df", fakearg="f") - mock_create_image.assert_called_with( - "cf", "url", "df", fakearg="f") + image_service.create_image.assert_called_with(**call_args) # Negative case: created image n ot in the list of available images - mock_create_image.return_value = fakes.FakeImage( + image_service.create_image.return_value = mock.Mock( id=12, name="img_nameN") self.assertRaises(exceptions.RallyAssertionError, images.CreateAndListImage(self.context).run, "cf", "url", "df", fakearg="f") - mock_create_image.assert_called_with( - "cf", "url", "df", fakearg="f") - mock_list_images.assert_called_with() + image_service.create_image.assert_called_with(**call_args) + image_service.list_images.assert_called_with() + + def test_list_images(self): + image_service = self.mock_image.return_value - @mock.patch("%s.ListImages._list_images" % BASE) - def test_list_images(self, mock_list_images__list_images): images.ListImages(self.context).run() - mock_list_images__list_images.assert_called_once_with() + image_service.list_images.assert_called_once_with() - @mock.patch("%s.CreateAndDeleteImage._delete_image" % BASE) - @mock.patch("%s.CreateAndDeleteImage._create_image" % BASE) - @mock.patch("%s.CreateAndDeleteImage.generate_random_name" % BASE, - return_value="test-rally-image") - def test_create_and_delete_image(self, - mock_random_name, - mock_create_image, - mock_delete_image): - fake_image = object() - mock_create_image.return_value = fake_image + def test_create_and_delete_image(self): + image_service = self.mock_image.return_value + + fake_image = fakes.FakeImage(id=1, name="imagexxx") + image_service.create_image.return_value = fake_image + call_args = {"container_format": "cf", + "image_location": "url", + "disk_format": "df", + "fakearg": "f"} images.CreateAndDeleteImage(self.context).run( "cf", "url", "df", fakearg="f") - mock_create_image.assert_called_once_with( - "cf", "url", "df", fakearg="f") - mock_delete_image.assert_called_once_with(fake_image) + image_service.create_image.assert_called_once_with(**call_args) + image_service.delete_image.assert_called_once_with(fake_image.id) @mock.patch("%s.CreateImageAndBootInstances._boot_servers" % BASE) - @mock.patch("%s.CreateImageAndBootInstances._create_image" % BASE) - def test_create_image_and_boot_instances(self, - mock_create_image, - mock_boot_servers): + def test_create_image_and_boot_instances(self, mock_boot_servers): + image_service = self.mock_image.return_value + fake_image = fakes.FakeImage() fake_servers = [mock.Mock() for i in range(5)] - mock_create_image.return_value = fake_image + image_service.create_image.return_value = fake_image mock_boot_servers.return_value = fake_servers create_image_kwargs = {"fakeimagearg": "f"} boot_server_kwargs = {"fakeserverarg": "f"} + call_args = {"container_format": "cf", + "image_location": "url", + "disk_format": "df", + "fakeimagearg": "f"} images.CreateImageAndBootInstances(self.context).run( "cf", "url", "df", "fid", 5, create_image_kwargs=create_image_kwargs, boot_server_kwargs=boot_server_kwargs) - mock_create_image.assert_called_once_with("cf", "url", "df", - **create_image_kwargs) + image_service.create_image.assert_called_once_with(**call_args) mock_boot_servers.assert_called_once_with("image-id-0", "fid", 5, **boot_server_kwargs) diff --git a/tests/unit/plugins/openstack/services/image/__init__.py b/tests/unit/plugins/openstack/services/image/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/plugins/openstack/services/image/test_glance_v1.py b/tests/unit/plugins/openstack/services/image/test_glance_v1.py new file mode 100755 index 00000000..b297469f --- /dev/null +++ b/tests/unit/plugins/openstack/services/image/test_glance_v1.py @@ -0,0 +1,191 @@ +# 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 rally import exceptions +from rally.plugins.openstack.services.image import glance_v1 +from rally.plugins.openstack.services.image import image +from tests.unit import test + +from oslotest import mockpatch + +PATH = "rally.plugins.openstack.services.image.image.Image._unify_image" + + +@ddt.ddt +class GlanceV1ServiceTestCase(test.TestCase): + _tempfile = tempfile.NamedTemporaryFile() + + def setUp(self): + super(GlanceV1ServiceTestCase, self).setUp() + self.clients = mock.MagicMock() + self.gc = self.clients.glance.return_value + self.name_generator = mock.MagicMock() + self.service = glance_v1.GlanceV1Service( + self.clients, name_generator=self.name_generator) + self.mock_wait_for_status = mockpatch.Patch( + "rally.task.utils.wait_for_status") + self.useFixture(self.mock_wait_for_status) + + @ddt.data({"location": "image_location", "is_public": True}, + {"location": _tempfile.name, "is_public": False}) + @ddt.unpack + @mock.patch("six.moves.builtins.open") + def test_create_image(self, mock_open, location, is_public): + image_name = "image_name" + container_format = "container_format" + disk_format = "disk_format" + + image = self.service.create_image( + image_name=image_name, + container_format=container_format, + image_location=location, + disk_format=disk_format, + is_public=is_public) + + call_args = {"container_format": container_format, + "disk_format": disk_format, + "is_public": is_public, + "name": image_name, + "min_disk": 0, + "min_ram": 0} + + 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 + + self.gc.images.create.assert_called_once_with(**call_args) + self.assertEqual(image, self.mock_wait_for_status.mock.return_value) + + def test_get_image(self): + image_id = "image_id" + self.service.get_image(image_id) + self.gc.images.get.assert_called_once_with(image_id) + + def test_get_image_exception(self): + image_id = "image_id" + self.clients.glance( + "1").images.get.side_effect = glance_exc.HTTPNotFound + + self.assertRaises(exceptions.GetResourceNotFound, + self.service.get_image, image_id) + + @ddt.data({"status": "activate", "is_public": True}, + {"status": "activate", "is_public": False}, + {"status": "activate", "is_public": None}) + @ddt.unpack + def test_list_images(self, status, is_public): + self.service.list_images(is_public=is_public, status=status) + self.gc.images.list.assert_called_once_with(status=status) + + def test_set_visibility(self): + image_id = "image_id" + is_public = True + self.service.set_visibility(image_id=image_id) + self.gc.images.update.assert_called_once_with( + image_id, is_public=is_public) + + def test_delete_image(self): + image_id = "image_id" + self.service.delete_image(image_id) + self.gc.images.delete.assert_called_once_with(image_id) + + +@ddt.ddt +class UnifiedGlanceV1ServiceTestCase(test.TestCase): + def setUp(self): + super(UnifiedGlanceV1ServiceTestCase, self).setUp() + self.clients = mock.MagicMock() + self.service = glance_v1.UnifiedGlanceV1Service(self.clients) + self.service._impl = mock.MagicMock() + + @ddt.data({"visibility": "public"}, + {"visibility": "private"}) + @ddt.unpack + @mock.patch(PATH) + def test_create_image(self, mock_image__unify_image, visibility): + image_name = "image_name" + container_format = "container_format" + image_location = "image_location" + disk_format = "disk_format" + image = self.service.create_image(image_name=image_name, + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + visibility=visibility) + + is_public = visibility == "public" + callargs = {"image_name": image_name, + "container_format": container_format, + "image_location": image_location, + "disk_format": disk_format, + "is_public": is_public, + "min_disk": 0, + "min_ram": 0} + self.service._impl.create_image.assert_called_once_with(**callargs) + self.assertEqual(mock_image__unify_image.return_value, image) + + @mock.patch(PATH) + def test_get_image(self, mock_image__unify_image): + image_id = "image_id" + image = self.service.get_image(image_id=image_id) + + self.assertEqual(mock_image__unify_image.return_value, image) + self.service._impl.get_image.assert_called_once_with( + image_id=image_id) + + @mock.patch(PATH) + def test_list_images(self, mock_image__unify_image): + images = [mock.MagicMock()] + self.service._impl.list_images.return_value = images + + status = "active" + visibility = "public" + is_public = visibility == "public" + self.assertEqual([mock_image__unify_image.return_value], + self.service.list_images(status, + visibility=visibility)) + self.service._impl.list_images.assert_called_once_with( + status=status, + is_public=is_public) + + def test_set_visibility(self): + image_id = "image_id" + visibility = "private" + is_public = visibility == "public" + self.service.set_visibility(image_id=image_id, visibility=visibility) + self.service._impl.set_visibility.assert_called_once_with( + image_id=image_id, is_public=is_public) + + def test_set_visibility_failure(self): + image_id = "image_id" + visibility = "error" + self.assertRaises(image.VisibilityException, + self.service.set_visibility, + image_id=image_id, + visibility=visibility) + + def test_delete_image(self): + image_id = "image_id" + self.service.delete_image(image_id) + self.service._impl.delete_image.assert_called_once_with( + image_id=image_id) diff --git a/tests/unit/plugins/openstack/services/image/test_glance_v2.py b/tests/unit/plugins/openstack/services/image/test_glance_v2.py new file mode 100755 index 00000000..dfa96d2e --- /dev/null +++ b/tests/unit/plugins/openstack/services/image/test_glance_v2.py @@ -0,0 +1,178 @@ +# 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 rally import exceptions +from rally.plugins.openstack.services.image import glance_v2 +from tests.unit import test + +from oslotest import mockpatch + +PATH = "rally.plugins.openstack.services.image.image.Image._unify_image" + + +@ddt.ddt +class GlanceV2ServiceTestCase(test.TestCase): + _tempfile = tempfile.NamedTemporaryFile() + + def setUp(self): + super(GlanceV2ServiceTestCase, self).setUp() + self.clients = mock.MagicMock() + self.gc = self.clients.glance.return_value + self.name_generator = mock.MagicMock() + self.service = glance_v2.GlanceV2Service( + self.clients, name_generator=self.name_generator) + self.mock_wait_for_status = mockpatch.Patch( + "rally.task.utils.wait_for_status") + self.useFixture(self.mock_wait_for_status) + + @ddt.data({"location": "image_location"}, + {"location": _tempfile.name}) + @ddt.unpack + @mock.patch("requests.get") + @mock.patch("six.moves.builtins.open") + def test_create_image(self, mock_open, mock_requests_get, location): + image_name = "image_name" + container_format = "container_format" + disk_format = "disk_format" + visibility = "public" + + image = self.service.create_image( + image_name=image_name, + container_format=container_format, + image_location=location, + disk_format=disk_format, + visibility=visibility) + + call_args = {"container_format": container_format, + "disk_format": disk_format, + "name": image_name, + "visibility": visibility, + "min_disk": 0, + "min_ram": 0} + + if location.startswith("/"): + mock_open.assert_called_once_with(location) + mock_open.return_value.close.assert_called_once_with() + else: + mock_requests_get.assert_called_once_with(location, stream=True) + self.gc.images.create.assert_called_once_with(**call_args) + self.assertEqual(image, self.mock_wait_for_status.mock.return_value) + + def test_get_image(self): + image_id = "image_id" + self.service.get_image(image_id) + self.gc.images.get.assert_called_once_with(image_id) + + def test_get_image_exception(self): + image_id = "image_id" + self.clients.glance( + "1").images.get.side_effect = glance_exc.HTTPNotFound + + self.assertRaises(exceptions.GetResourceNotFound, + self.service.get_image, image_id) + + def test_list_images(self): + status = "active" + kwargs = {"status": status} + + self.assertEqual(self.gc.images.list.return_value, + self.service.list_images()) + self.gc.images.list.assert_called_once_with(**kwargs) + + def test_set_visibility(self): + image_id = "image_id" + visibility = "shared" + self.service.set_visibility(image_id=image_id) + self.gc.images.update.assert_called_once_with( + image_id, + visibility=visibility) + + def test_delete_image(self): + image_id = "image_id" + self.service.delete_image(image_id) + self.gc.images.delete.assert_called_once_with(image_id) + + +@ddt.ddt +class UnifiedGlanceV2ServiceTestCase(test.TestCase): + def setUp(self): + super(UnifiedGlanceV2ServiceTestCase, self).setUp() + self.clients = mock.MagicMock() + self.service = glance_v2.UnifiedGlanceV2Service(self.clients) + self.service._impl = mock.MagicMock() + + @mock.patch(PATH) + def test_create_image(self, mock_image__unify_image): + image_name = "image_name" + container_format = "container_format" + image_location = "image_location" + disk_format = "disk_format" + visibility = "public" + callargs = {"image_name": image_name, + "container_format": container_format, + "image_location": image_location, + "disk_format": disk_format, + "visibility": visibility, + "min_disk": 0, + "min_ram": 0} + + image = self.service.create_image(image_name=image_name, + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + visibility=visibility) + + self.assertEqual(mock_image__unify_image.return_value, image) + self.service._impl.create_image.assert_called_once_with(**callargs) + + @mock.patch(PATH) + def test_get_image(self, mock_image__unify_image): + image_id = "image_id" + image = self.service.get_image(image_id=image_id) + + self.assertEqual(mock_image__unify_image.return_value, image) + self.service._impl.get_image.assert_called_once_with( + image_id=image_id) + + @mock.patch(PATH) + def test_list_images(self, mock_image__unify_image): + images = [mock.MagicMock()] + self.service._impl.list_images.return_value = images + + status = "active" + self.assertEqual([mock_image__unify_image.return_value], + self.service.list_images()) + self.service._impl.list_images.assert_called_once_with( + status=status, + visibility=None) + + def test_set_visibility(self): + image_id = "image_id" + visibility = "private" + + self.service.set_visibility(image_id=image_id, visibility=visibility) + self.service._impl.set_visibility.assert_called_once_with( + image_id=image_id, visibility=visibility) + + def test_delete_image(self): + image_id = "image_id" + self.service.delete_image(image_id) + self.service._impl.delete_image.assert_called_once_with( + image_id=image_id) diff --git a/tests/unit/plugins/openstack/services/image/test_image.py b/tests/unit/plugins/openstack/services/image/test_image.py new file mode 100755 index 00000000..9cfe59ce --- /dev/null +++ b/tests/unit/plugins/openstack/services/image/test_image.py @@ -0,0 +1,118 @@ +# 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 uuid + +import ddt +import mock + +from rally.plugins.openstack.services.image import glance_v1 +from rally.plugins.openstack.services.image import glance_v2 +from rally.plugins.openstack.services.image import image +from tests.unit import test + + +@ddt.ddt +class ImageTestCase(test.TestCase): + + def setUp(self): + super(ImageTestCase, self).setUp() + self.clients = mock.MagicMock() + + def get_service_with_fake_impl(self): + path = "rally.plugins.openstack.services.image.image" + with mock.patch("%s.Image.discover_impl" % path) as mock_discover: + mock_discover.return_value = mock.MagicMock(), None + service = image.Image(self.clients) + return service + + @ddt.data(("image_name", "container_format", "image_location", + "disk_format", "visibility", "min_disk", "min_ram")) + def test_create_image(self, params): + (image_name, container_format, image_location, disk_format, + visibility, min_disk, min_ram) = params + service = self.get_service_with_fake_impl() + service.create_image(image_name=image_name, + container_format=container_format, + image_location=image_location, + disk_format=disk_format, + visibility=visibility, + min_disk=min_disk, + min_ram=min_ram) + service._impl.create_image.assert_called_once_with( + image_name=image_name, container_format=container_format, + image_location=image_location, disk_format=disk_format, + visibility=visibility, min_disk=min_disk, min_ram=min_ram) + + @ddt.data("image_id") + def test_get_image(self, param): + image_id = param + service = self.get_service_with_fake_impl() + service.get_image(image=image_id) + service._impl.get_image.assert_called_once_with(image_id) + + @ddt.data(("status", "visibility")) + def test_list_images(self, params): + status, visibility = params + service = self.get_service_with_fake_impl() + service.list_images(status=status, visibility=visibility) + service._impl.list_images.assert_called_once_with( + status=status, visibility=visibility) + + @ddt.data(("image_id", "visibility")) + def test_set_visibility(self, params): + image_id, visibility = params + service = self.get_service_with_fake_impl() + service.set_visibility(image_id=image_id, visibility=visibility) + service._impl.set_visibility.assert_called_once_with( + image_id, visibility=visibility) + + def test_unify_image(self): + class Image(object): + def __init__(self, visibility=None, is_public=None): + self.id = uuid.uuid4() + self.name = str(uuid.uuid4()) + self.visibility = visibility + self.is_public = is_public + + service = self.get_service_with_fake_impl() + visibility = "private" + image_obj = Image(visibility=visibility) + unified_image = service._unify_image(image_obj) + self.assertIsInstance(unified_image, image.UnifiedImage) + self.assertEqual(image_obj.id, unified_image.id) + self.assertEqual(image_obj.visibility, unified_image.visibility) + + image_obj = Image(is_public="public") + del image_obj.visibility + unified_image = service._unify_image(image_obj) + self.assertEqual(image_obj.id, unified_image.id) + self.assertEqual(image_obj.is_public, unified_image.visibility) + + def test_delete_image(self): + image_id = "image_id" + service = self.get_service_with_fake_impl() + service.delete_image(image_id=image_id) + service._impl.delete_image.assert_called_once_with(image_id) + + def test_is_applicable(self): + clients = mock.Mock() + + clients.glance().version = "1.0" + self.assertTrue( + glance_v1.UnifiedGlanceV1Service.is_applicable(clients)) + + clients.glance().version = "2.0" + self.assertTrue( + glance_v2.UnifiedGlanceV2Service.is_applicable(clients))