[ResourceTypes] Add ability to pick the latest available resource

In case of several regexp matches which not fail in most cases. Taking
the latest resource is more user-friednly and allows to remove hardcode
of the resource version.

Change-Id: Id82b26634d3e3712ad48eb1134a9464cde991c4b
This commit is contained in:
Andrey Kurilin 2018-05-10 18:24:51 +03:00
parent 272349796c
commit 930f5a46dd
2 changed files with 177 additions and 7 deletions

View File

@ -13,6 +13,8 @@
# under the License.
import copy
import operator
import re
import traceback
from rally.common import logging
@ -29,6 +31,9 @@ from rally_openstack.services.storage import block
LOG = logging.getLogger(__name__)
configure = plugin.configure
class OpenStackResourceType(types.ResourceType):
"""A base class for OpenStack ResourceTypes plugins with help-methods"""
@ -50,6 +55,81 @@ class OpenStackResourceType(types.ResourceType):
self._clients = osclients.Clients(
self._context["users"][0]["credential"])
def _find_resource(self, resource_spec, resources):
"""Return the resource whose name matches the pattern.
.. note:: This method is a modified version of
`rally.task.types.obj_from_name`. The difference is supporting the
case of returning the latest version of resource in case of
`accurate=False` option.
:param resource_spec: resource specification to find.
Expected keys:
* name - The exact name of resource to search. If no exact match
and value of *accurate* key is False (default behaviour), name
will be interpreted as a regexp
* regexp - a regexp of resource name to match. If several resources
match and value of *accurate* key is False (default behaviour),
the latest resource will be returned.
:param resources: iterable containing all resources
:raises InvalidScenarioArgument: if the pattern does
not match anything.
:returns: resource object mapped to `name` or `regex`
"""
if "name" in resource_spec:
# In a case of pattern string exactly matches resource name
matching_exact = [resource for resource in resources
if resource.name == resource_spec["name"]]
if len(matching_exact) == 1:
return matching_exact[0]
elif len(matching_exact) > 1:
raise exceptions.InvalidScenarioArgument(
"%(typename)s with name '%(pattern)s' "
"is ambiguous, possible matches "
"by id: %(ids)s" % {
"typename": self.get_name().title(),
"pattern": resource_spec["name"],
"ids": ", ".join(map(operator.attrgetter("id"),
matching_exact))})
if resource_spec.get("accurate", False):
raise exceptions.InvalidScenarioArgument(
"%(typename)s with name '%(name)s' not found" % {
"typename": self.get_name().title(),
"name": resource_spec["name"]})
# Else look up as regex
patternstr = resource_spec["name"]
elif "regex" in resource_spec:
patternstr = resource_spec["regex"]
else:
raise exceptions.InvalidScenarioArgument(
"%(typename)s 'id', 'name', or 'regex' not found "
"in '%(resource_spec)s' " % {
"typename": self.get_name().title(),
"resource_spec": resource_spec})
pattern = re.compile(patternstr)
matching = [resource for resource in resources
if re.search(pattern, resource.name or "")]
if not matching:
raise exceptions.InvalidScenarioArgument(
"%(typename)s with pattern '%(pattern)s' not found" % {
"typename": self.get_name().title(),
"pattern": pattern.pattern})
elif len(matching) > 1:
if not resource_spec.get("accurate", False):
return sorted(matching, key=lambda o: o.name or "")[-1]
raise exceptions.InvalidScenarioArgument(
"%(typename)s with name '%(pattern)s' is ambiguous, possible "
"matches by id: %(ids)s" % {
"typename": self.get_name().title(),
"pattern": pattern.pattern,
"ids": ", ".join(map(operator.attrgetter("id"),
matching))})
return matching[0]
if rally_openstack.__rally_version__ < (0, 12):
@classmethod
def _get_doc(cls):
@ -121,10 +201,8 @@ class GlanceImage(DeprecatedBehaviourMixin, OpenStackResourceType):
glance = image.Image(self._clients)
self._cache[cache_id] = glance.list_images(**list_kwargs)
images = self._cache[cache_id]
resource_id = types._id_from_name(
resource_config=resource_spec,
resources=images,
typename="image")
resource = self._find_resource(resource_spec, images)
return resource.id
return resource_id

View File

@ -21,6 +21,96 @@ from tests.unit import fakes
from tests.unit import test
class OpenStackResourceTypeTestCase(test.TestCase):
def test__find_resource(self):
@types.configure(name=self.id())
class FooType(types.OpenStackResourceType):
def pre_process(self, resource_spec, config):
pass
ftype = FooType({})
resources = dict(
(name, fakes.FakeResource(name=name))
for name in ["Fake1", "Fake2", "Fake3"])
# case #1: 100% name match
self.assertEqual(
resources["Fake2"],
ftype._find_resource({"name": "Fake2"}, resources.values()))
# case #2: pick the latest one
self.assertEqual(
resources["Fake3"],
ftype._find_resource({"name": "Fake"}, resources.values()))
# case #3: regex one match
self.assertEqual(
resources["Fake2"],
ftype._find_resource({"regex": ".ake2"}, resources.values()))
# case #4: regex, pick the latest one
self.assertEqual(
resources["Fake3"],
ftype._find_resource({"regex": "Fake"}, resources.values()))
def test__find_resource_negative(self):
@types.configure(name=self.id())
class FooType(types.OpenStackResourceType):
def pre_process(self, resource_spec, config):
pass
ftype = FooType({})
# case #1: the wrong resource spec
e = self.assertRaises(exceptions.InvalidScenarioArgument,
ftype._find_resource, {}, [])
self.assertIn("'id', 'name', or 'regex' not found",
e.format_message())
# case #2: two matches for one name
resources = [fakes.FakeResource(name="Fake1"),
fakes.FakeResource(name="Fake2"),
fakes.FakeResource(name="Fake1")]
e = self.assertRaises(
exceptions.InvalidScenarioArgument,
ftype._find_resource, {"name": "Fake1"}, resources)
self.assertIn("with name 'Fake1' is ambiguous, possible matches",
e.format_message())
# case #3: no matches at all
resources = [fakes.FakeResource(name="Fake1"),
fakes.FakeResource(name="Fake2"),
fakes.FakeResource(name="Fake3")]
e = self.assertRaises(
exceptions.InvalidScenarioArgument,
ftype._find_resource, {"name": "Foo"}, resources)
self.assertIn("with pattern 'Foo' not found",
e.format_message())
# case #4: two matches for one name, but 'accurate' is True
resources = [fakes.FakeResource(name="Fake1"),
fakes.FakeResource(name="Fake2"),
fakes.FakeResource(name="Fake3")]
e = self.assertRaises(
exceptions.InvalidScenarioArgument,
ftype._find_resource, {"name": "Fake", "accurate": True},
resources)
self.assertIn("with name 'Fake' not found",
e.format_message())
# case #5: two matches for one name, but 'accurate' is True
resources = [fakes.FakeResource(name="Fake1"),
fakes.FakeResource(name="Fake2"),
fakes.FakeResource(name="Fake3")]
e = self.assertRaises(
exceptions.InvalidScenarioArgument,
ftype._find_resource, {"regex": "Fake", "accurate": True},
resources)
self.assertIn("with name 'Fake' is ambiguous, possible matches",
e.format_message())
class FlavorTestCase(test.TestCase):
def setUp(self):
@ -168,9 +258,11 @@ class GlanceImageTestCase(test.TestCase):
def test_preprocess_by_regex_match_multiple(self):
resource_spec = {"regex": "^cirros"}
self.assertRaises(exceptions.InvalidScenarioArgument,
self.type_cls.pre_process,
resource_spec=resource_spec, config={})
image_id = self.type_cls.pre_process(resource_spec=resource_spec,
config={})
# matching resources are sorted by the names. It is impossible to
# predict which resource will be luckiest
self.assertIn(image_id, ["102", "103"])
def test_preprocess_by_regex_no_match(self):
resource_spec = {"regex": "-boot$"}