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 d324f6b76b
commit 067af09a41
7 changed files with 451 additions and 126 deletions

View File

@ -825,6 +825,26 @@
failure_rate: failure_rate:
max: 0 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: GlanceImages.create_and_list_image:
- -
args: args:

View File

@ -13,41 +13,9 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import time
from oslo_config import cfg
from rally.plugins.openstack import scenario 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 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): class GlanceScenario(scenario.OpenStackScenario):
@ -72,38 +40,9 @@ class GlanceScenario(scenario.OpenStackScenario):
:returns: image object :returns: image object
""" """
kw = { client = glance_wrapper.wrap(self._clients.glance, self)
"name": self.generate_random_name(), return client.create_image(container_format, image_location,
"container_format": container_format, disk_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
@atomic.action_timer("glance.delete_image") @atomic.action_timer("glance.delete_image")
def _delete_image(self, image): def _delete_image(self, image):
@ -113,11 +52,5 @@ class GlanceScenario(scenario.OpenStackScenario):
:param image: Image object :param image: Image object
""" """
image.delete() client = glance_wrapper.wrap(self._clients.glance, self)
utils.wait_for_status( client.delete_image(image)
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)

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

@ -30,6 +30,7 @@ from rally.common import objects
from rally.common import utils from rally.common import utils
from rally import exceptions from rally import exceptions
from rally import osclients from rally import osclients
from rally.plugins.openstack.wrappers import glance as glance_wrapper
from rally.plugins.openstack.wrappers import network from rally.plugins.openstack.wrappers import network
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -406,13 +407,13 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin):
.format(opt=option, opt_val=option_value)) .format(opt=option, opt_val=option_value))
def _discover_or_create_image(self): def _discover_or_create_image(self):
glanceclient = self.clients.glance() glance_wrap = glance_wrapper.wrap(self.clients.glance, self)
if CONF.image.name_regex: if CONF.image.name_regex:
LOG.debug("Trying to discover an image with name matching " LOG.debug("Trying to discover an image with name matching "
"regular expression '%s'. Note that case insensitive " "regular expression '%s'. Note that case insensitive "
"matching is performed" % CONF.image.name_regex) "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] if img.status.lower() == "active" and img.name]
for img in img_list: for img in img_list:
if re.match(CONF.image.name_regex, img.name, re.IGNORECASE): if re.match(CONF.image.name_regex, img.name, re.IGNORECASE):
@ -427,13 +428,13 @@ class TempestResourcesContext(utils.RandomNameGeneratorMixin):
"name": self.generate_random_name(), "name": self.generate_random_name(),
"disk_format": CONF.image.disk_format, "disk_format": CONF.image.disk_format,
"container_format": CONF.image.container_format, "container_format": CONF.image.container_format,
"image_location": os.path.join(_create_or_get_data_dir(),
self.image_name),
"is_public": True "is_public": True
} }
LOG.debug("Creating image '%s'" % params["name"]) LOG.debug("Creating image '%s'" % params["name"])
image = glanceclient.images.create(**params) image = glance_wrap.create_image(**params)
self._created_images.append(image) self._created_images.append(image)
image.update(data=open(
os.path.join(_create_or_get_data_dir(), self.image_name), "rb"))
return image return image

View File

@ -15,13 +15,11 @@
import tempfile import tempfile
import mock import mock
from oslo_config import cfg
from rally.plugins.openstack.scenarios.glance import utils from rally.plugins.openstack.scenarios.glance import utils
from tests.unit import test from tests.unit import test
GLANCE_UTILS = "rally.plugins.openstack.scenarios.glance.utils" GLANCE_UTILS = "rally.plugins.openstack.scenarios.glance.utils"
CONF = cfg.CONF
class GlanceScenarioTestCase(test.ScenarioTestCase): class GlanceScenarioTestCase(test.ScenarioTestCase):
@ -30,6 +28,8 @@ class GlanceScenarioTestCase(test.ScenarioTestCase):
super(GlanceScenarioTestCase, self).setUp() super(GlanceScenarioTestCase, self).setUp()
self.image = mock.Mock() self.image = mock.Mock()
self.image1 = 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): def test_list_images(self):
scenario = utils.GlanceScenario(context=self.context) scenario = utils.GlanceScenario(context=self.context)
@ -40,52 +40,28 @@ class GlanceScenarioTestCase(test.ScenarioTestCase):
self._test_atomic_action_timer(scenario.atomic_actions(), self._test_atomic_action_timer(scenario.atomic_actions(),
"glance.list_images") "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() image_location = tempfile.NamedTemporaryFile()
self.clients("glance").images.create.return_value = self.image mock_wrap.return_value.create_image.return_value = self.image
scenario = utils.GlanceScenario(context=self.context) scenario = utils.GlanceScenario(context=self.context,
clients=self.scenario_clients)
return_image = scenario._create_image("container_format", return_image = scenario._create_image("container_format",
image_location.name, image_location.name,
"disk_format") "disk_format")
self.mock_wait_for.mock.assert_called_once_with( self.assertEqual(self.image, return_image)
self.image, mock_wrap.assert_called_once_with(scenario._clients.glance, scenario)
update_resource=self.mock_get_from_manager.mock.return_value, mock_wrap.return_value.create_image.assert_called_once_with(
ready_statuses=["active"], "container_format", image_location.name, "disk_format")
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(), self._test_atomic_action_timer(scenario.atomic_actions(),
"glance.create_image") "glance.create_image")
def test_create_image_with_location(self): @mock.patch("rally.plugins.openstack.wrappers.glance.wrap")
self.clients("glance").images.create.return_value = self.image def test_delete_image(self, mock_wrap):
scenario = utils.GlanceScenario(context=self.context) scenario = utils.GlanceScenario(context=self.context,
return_image = scenario._create_image("container_format", clients=self.scenario_clients)
"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)
scenario._delete_image(self.image) scenario._delete_image(self.image)
self.image.delete.assert_called_once_with() mock_wrap.assert_called_once_with(scenario._clients.glance, scenario)
self.mock_wait_for_status.mock.assert_called_once_with( mock_wrap.return_value.delete_image.assert_called_once_with(self.image)
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()
self._test_atomic_action_timer(scenario.atomic_actions(), self._test_atomic_action_timer(scenario.atomic_actions(),
"glance.delete_image") "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)

View File

@ -396,7 +396,8 @@ class TempestResourcesContextTestCase(test.TestCase):
result = self.context.conf.get("compute", "flavor_ref") result = self.context.conf.get("compute", "flavor_ref")
self.assertEqual("id1", result) 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 = self.context.clients.glance()
client.images.list.return_value = [fakes.FakeResource(name="CirrOS", client.images.list.return_value = [fakes.FakeResource(name="CirrOS",
status="active")] status="active")]
@ -404,14 +405,31 @@ class TempestResourcesContextTestCase(test.TestCase):
self.assertEqual("CirrOS", image.name) self.assertEqual("CirrOS", image.name)
self.assertEqual(0, len(self.context._created_images)) self.assertEqual(0, len(self.context._created_images))
@mock.patch("six.moves.builtins.open") # @mock.patch("six.moves.builtins.open")
def test__discover_or_create_image(self, mock_open): # def test__discover_or_create_image(self, mock_wrap, mock_open):
client = self.context.clients.glance() # client = self.context.clients.glance()
client.images.create.side_effect = [fakes.FakeImage(id="id1")] # 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() image = self.context._discover_or_create_image()
self.assertEqual("id1", image.id) self.assertEqual(image, client.create_image.return_value)
self.assertEqual("id1", self.context._created_images[0].id) 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): def test__create_flavor(self):
client = self.context.clients.nova() client = self.context.clients.nova()