Add wrapper for Glance API versions

Adds a new Glance wrapper that abstracts creation and deletion of
images for the two versions of the Glance API. A functional test is
added as well.

Change-Id: I3a6857f07415da1c8fc8761b3dd012aab0c67460
This commit is contained in:
Chris St. Pierre 2016-01-07 15:00:03 -06:00 committed by Chris St. Pierre
parent dc92455119
commit f1d204ed0b
5 changed files with 420 additions and 114 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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)