Add the flavor context
Add the flavor context (moved from samples) that is creating and destroying a context specified by the configuration. Change-Id: I8072867c1af49faeaf299d4da07930e42b3060bf
This commit is contained in:
parent
a818e330b4
commit
9f18969054
31
doc/samples/tasks/contexts/flavors/boot.json
Normal file
31
doc/samples/tasks/contexts/flavors/boot.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"NovaServers.boot_server": [
|
||||
{
|
||||
"args": {
|
||||
"flavor": {
|
||||
"name": "^ram64$"
|
||||
},
|
||||
"image": {
|
||||
"name": "^cirros.*uec$"
|
||||
}
|
||||
},
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
"times": 10,
|
||||
"concurrency": 2
|
||||
},
|
||||
"context": {
|
||||
"users": {
|
||||
"tenants": 3,
|
||||
"users_per_tenant": 2
|
||||
},
|
||||
"flavors": [
|
||||
{
|
||||
"name": "ram64",
|
||||
"ram": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
20
doc/samples/tasks/contexts/flavors/boot.yaml
Normal file
20
doc/samples/tasks/contexts/flavors/boot.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
NovaServers.boot_server:
|
||||
-
|
||||
args:
|
||||
flavor:
|
||||
name: "^ram64$"
|
||||
image:
|
||||
name: "^cirros.*uec$"
|
||||
runner:
|
||||
type: "constant"
|
||||
times: 10
|
||||
concurrency: 2
|
||||
context:
|
||||
users:
|
||||
tenants: 3
|
||||
users_per_tenant: 2
|
||||
flavors:
|
||||
-
|
||||
name: "ram64"
|
||||
ram: 64
|
@ -952,7 +952,7 @@
|
||||
-
|
||||
args:
|
||||
flavor:
|
||||
name: "m1.tiny"
|
||||
name: "^ram64$"
|
||||
image:
|
||||
name: "^cirros.*uec$"
|
||||
auto_assign_nics: false
|
||||
@ -964,6 +964,10 @@
|
||||
users:
|
||||
tenants: 3
|
||||
users_per_tenant: 2
|
||||
flavors:
|
||||
-
|
||||
name: "ram64"
|
||||
ram: 64
|
||||
sla:
|
||||
failure_rate:
|
||||
max: 0
|
||||
|
131
rally/benchmark/context/flavors.py
Executable file
131
rally/benchmark/context/flavors.py
Executable file
@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
|
||||
from rally.benchmark.context import base
|
||||
from rally.common import utils as rutils
|
||||
from rally import log as logging
|
||||
from rally import osclients
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@base.context(name="flavors", order=340)
|
||||
class FlavorsGenerator(base.Context):
|
||||
"""Context creates a list of flavors."""
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
"type": "array",
|
||||
"$schema": rutils.JSON_SCHEMA,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
"ram": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"vcpus": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"disk": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"swap": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"ephemeral": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"extra_specs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name", "ram"]
|
||||
}
|
||||
}
|
||||
|
||||
@rutils.log_task_wrapper(LOG.info, _("Enter context: `flavors`"))
|
||||
def setup(self):
|
||||
"""Create list of flavors."""
|
||||
self.context["flavors"] = {}
|
||||
|
||||
clients = osclients.Clients(self.context["admin"]["endpoint"])
|
||||
for flavor_config in self.config:
|
||||
|
||||
extra_specs = flavor_config.get("extra_specs")
|
||||
|
||||
flavor_config = FlavorConfig(**flavor_config)
|
||||
try:
|
||||
flavor = clients.nova().flavors.create(**flavor_config)
|
||||
except osclients.nova.exceptions.Conflict as e:
|
||||
LOG.warning("Using already existing flavor %s" %
|
||||
flavor_config["name"])
|
||||
if logging.is_debug():
|
||||
LOG.exception(e)
|
||||
continue
|
||||
|
||||
if extra_specs:
|
||||
flavor.set_keys(extra_specs)
|
||||
|
||||
self.context["flavors"][flavor_config["name"]] = flavor.to_dict()
|
||||
LOG.debug("Created flavor with id '%s'" % flavor.id)
|
||||
|
||||
@rutils.log_task_wrapper(LOG.info, _("Exit context: `flavors`"))
|
||||
def cleanup(self):
|
||||
"""Delete created flavors."""
|
||||
clients = osclients.Clients(self.context["admin"]["endpoint"])
|
||||
for flavor in self.context["flavors"].values():
|
||||
try:
|
||||
rutils.retry(3, clients.nova().flavors.delete, flavor["id"])
|
||||
LOG.debug("Flavor is deleted %s" % flavor["id"])
|
||||
except Exception as e:
|
||||
LOG.error(
|
||||
"Can't delete flavor %s: %s" % (flavor["id"], e.message))
|
||||
if logging.is_debug():
|
||||
LOG.exception(e)
|
||||
|
||||
|
||||
class FlavorConfig(dict):
|
||||
def __init__(self, name, ram, vcpus=1, disk=0, swap=0, ephemeral=0,
|
||||
extra_specs=None):
|
||||
"""Flavor configuration for context and flavor & image validation code.
|
||||
|
||||
Context code uses this code to provide default values for flavor
|
||||
creation. Validation code uses this class as a Flavor instance to
|
||||
check image validity against a flavor that is to be created by
|
||||
the context.
|
||||
|
||||
:param name: name of the newly created flavor
|
||||
:param ram: RAM amount for the flavor (MBs)
|
||||
:param vcpus: VCPUs amount for the flavor
|
||||
:param disk: disk amount for the flavor (GBs)
|
||||
:param swap: swap amount for the flavor (MBs)
|
||||
:param ephemeral: ephemeral disk amount for the flavor (GBs)
|
||||
:param extra_specs: is ignored
|
||||
"""
|
||||
super(FlavorConfig, self).__init__(
|
||||
name=name, ram=ram, vcpus=vcpus, disk=disk,
|
||||
swap=swap, ephemeral=ephemeral)
|
||||
self.__dict__.update(self)
|
@ -76,10 +76,10 @@ class ResourceType(object):
|
||||
"""
|
||||
|
||||
|
||||
def _id_from_name(resource_config, resources, typename):
|
||||
"""Return the id of the resource whose name matches the pattern.
|
||||
def obj_from_name(resource_config, resources, typename):
|
||||
"""Return the resource whose name matches the pattern.
|
||||
|
||||
resource_config has to contain `name`, as it is used to lookup an id.
|
||||
resource_config has to contain `name`, as it is used to lookup a resource.
|
||||
Value of the name will be treated as regexp.
|
||||
|
||||
An `InvalidScenarioArgument` is thrown if the pattern does
|
||||
@ -89,14 +89,14 @@ def _id_from_name(resource_config, resources, typename):
|
||||
:param resources: iterable containing all resources
|
||||
:param typename: name which describes the type of resource
|
||||
|
||||
:returns: resource id uniquely mapped to `name` or `regex`
|
||||
:returns: resource object uniquely mapped to `name` or `regex`
|
||||
"""
|
||||
if "name" in resource_config:
|
||||
# In a case of pattern string exactly maches resource name
|
||||
matching_exact = filter(lambda r: r.name == resource_config["name"],
|
||||
resources)
|
||||
if len(matching_exact) == 1:
|
||||
return matching_exact[0].id
|
||||
return matching_exact[0]
|
||||
elif len(matching_exact) > 1:
|
||||
raise exceptions.InvalidScenarioArgument(
|
||||
"{typename} with name '{pattern}' "
|
||||
@ -130,7 +130,25 @@ def _id_from_name(resource_config, resources, typename):
|
||||
pattern=pattern.pattern,
|
||||
ids=", ".join(map(operator.attrgetter("id"),
|
||||
matching))))
|
||||
return matching[0].id
|
||||
return matching[0]
|
||||
|
||||
|
||||
def _id_from_name(resource_config, resources, typename):
|
||||
"""Return the id of the resource whose name matches the pattern.
|
||||
|
||||
resource_config has to contain `name`, as it is used to lookup an id.
|
||||
Value of the name will be treated as regexp.
|
||||
|
||||
An `InvalidScenarioArgument` is thrown if the pattern does
|
||||
not match unambiguously.
|
||||
|
||||
:param resource_config: resource to be transformed
|
||||
:param resources: iterable containing all resources
|
||||
:param typename: name which describes the type of resource
|
||||
|
||||
:returns: resource id uniquely mapped to `name` or `regex`
|
||||
"""
|
||||
return obj_from_name(resource_config, resources, typename).id
|
||||
|
||||
|
||||
class FlavorResourceType(ResourceType):
|
||||
|
@ -20,6 +20,7 @@ import os
|
||||
from glanceclient import exc as glance_exc
|
||||
from novaclient import exceptions as nova_exc
|
||||
|
||||
from rally.benchmark.context import flavors as flavors_ctx
|
||||
from rally.benchmark import types as types
|
||||
from rally.common.i18n import _
|
||||
from rally import consts
|
||||
@ -166,6 +167,19 @@ def _get_validated_image(config, clients, param_name):
|
||||
return (ValidationResult(False, message), None)
|
||||
|
||||
|
||||
def _get_flavor_from_context(config, flavor_value):
|
||||
if "flavors" not in config.get("context", {}):
|
||||
raise exceptions.InvalidScenarioArgument("No flavors context")
|
||||
|
||||
flavors = [flavors_ctx.FlavorConfig(**f)
|
||||
for f in config["context"]["flavors"]]
|
||||
resource = types.obj_from_name(resource_config=flavor_value,
|
||||
resources=flavors, typename="flavor")
|
||||
flavor = flavors_ctx.FlavorConfig(**resource)
|
||||
flavor.id = "<context flavor: %s>" % flavor.name
|
||||
return (ValidationResult(), flavor)
|
||||
|
||||
|
||||
def _get_validated_flavor(config, clients, param_name):
|
||||
flavor_value = config.get("args", {}).get(param_name)
|
||||
if not flavor_value:
|
||||
@ -177,6 +191,10 @@ def _get_validated_flavor(config, clients, param_name):
|
||||
flavor = clients.nova().flavors.get(flavor=flavor_id)
|
||||
return (ValidationResult(), flavor)
|
||||
except (nova_exc.NotFound, exceptions.InvalidScenarioArgument):
|
||||
try:
|
||||
return _get_flavor_from_context(config, flavor_value)
|
||||
except exceptions.InvalidScenarioArgument:
|
||||
pass
|
||||
message = _("Flavor '%s' not found") % flavor_value
|
||||
return (ValidationResult(False, message), None)
|
||||
|
||||
|
121
tests/unit/benchmark/context/test_flavors.py
Normal file
121
tests/unit/benchmark/context/test_flavors.py
Normal file
@ -0,0 +1,121 @@
|
||||
# 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 copy
|
||||
|
||||
import mock
|
||||
|
||||
from rally.benchmark.context import flavors
|
||||
from rally import osclients
|
||||
from tests.unit import test
|
||||
|
||||
CTX = "rally.benchmark.context"
|
||||
|
||||
|
||||
class FlavorsGeneratorTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(FlavorsGeneratorTestCase, self).setUp()
|
||||
self.context = {
|
||||
"config": {
|
||||
"flavors": [{
|
||||
"name": "flavor_name",
|
||||
"ram": 2048,
|
||||
"disk": 10,
|
||||
"vcpus": 3,
|
||||
"ephemeral": 3,
|
||||
"swap": 5,
|
||||
"extra_specs": {
|
||||
"key": "value"
|
||||
}
|
||||
}]
|
||||
},
|
||||
"admin": {
|
||||
"endpoint": mock.MagicMock()
|
||||
},
|
||||
"task": mock.MagicMock(),
|
||||
}
|
||||
|
||||
@mock.patch("%s.flavors.osclients.Clients" % CTX)
|
||||
def test_setup(self, mock_osclients):
|
||||
# Setup and mock
|
||||
mock_create = mock_osclients().nova().flavors.create
|
||||
mock_create().to_dict.return_value = {"flavor_key": "flavor_value"}
|
||||
|
||||
# Run
|
||||
flavors_ctx = flavors.FlavorsGenerator(self.context)
|
||||
flavors_ctx.setup()
|
||||
|
||||
# Assertions
|
||||
self.assertEqual(flavors_ctx.context["flavors"],
|
||||
{"flavor_name": {"flavor_key": "flavor_value"}})
|
||||
|
||||
mock_osclients.assert_called_with(self.context["admin"]["endpoint"])
|
||||
|
||||
mock_create.assert_called_with(
|
||||
name="flavor_name", ram=2048, vcpus=3,
|
||||
disk=10, ephemeral=3, swap=5)
|
||||
mock_create().set_keys.assert_called_with({"key": "value"})
|
||||
mock_create().to_dict.assert_called_with()
|
||||
|
||||
@mock.patch("%s.flavors.osclients.Clients" % CTX)
|
||||
def test_setup_failexists(self, mock_osclients):
|
||||
# Setup and mock
|
||||
new_context = copy.deepcopy(self.context)
|
||||
new_context["flavors"] = {}
|
||||
|
||||
mock_flavor_create = mock_osclients().nova().flavors.create
|
||||
|
||||
exception = osclients.nova.exceptions.Conflict("conflict")
|
||||
mock_flavor_create.side_effect = exception
|
||||
|
||||
# Run
|
||||
flavors_ctx = flavors.FlavorsGenerator(self.context)
|
||||
flavors_ctx.setup()
|
||||
|
||||
# Assertions
|
||||
self.assertEqual(new_context, flavors_ctx.context)
|
||||
|
||||
mock_osclients.assert_called_with(self.context["admin"]["endpoint"])
|
||||
|
||||
mock_flavor_create.assert_called_once_with(
|
||||
name="flavor_name", ram=2048, vcpus=3,
|
||||
disk=10, ephemeral=3, swap=5)
|
||||
|
||||
@mock.patch("%s.flavors.osclients.Clients" % CTX)
|
||||
def test_cleanup(self, mock_osclients):
|
||||
# Setup and mock
|
||||
real_context = {
|
||||
"flavors": {
|
||||
"flavor_name": {
|
||||
"flavor_name": "flavor_name",
|
||||
"id": "flavor_name"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"endpoint": mock.MagicMock()
|
||||
},
|
||||
"task": mock.MagicMock(),
|
||||
}
|
||||
|
||||
# Run
|
||||
flavors_ctx = flavors.FlavorsGenerator(real_context)
|
||||
flavors_ctx.cleanup()
|
||||
|
||||
# Assertions
|
||||
mock_osclients.assert_called_with(real_context["admin"]["endpoint"])
|
||||
|
||||
mock_flavors_delete = mock_osclients().nova().flavors.delete
|
||||
mock_flavors_delete.assert_called_with("flavor_name")
|
@ -185,6 +185,46 @@ class ValidatorsTestCase(test.TestCase):
|
||||
clients, "a")
|
||||
self.assertFalse(result[0].is_valid, result[0].msg)
|
||||
|
||||
@mock.patch("rally.benchmark.validation.types.FlavorResourceType."
|
||||
"transform")
|
||||
def test__get_validated_flavor_from_context(self, mock_transform):
|
||||
clients = mock.MagicMock()
|
||||
clients.nova().flavors.get.side_effect = nova_exc.NotFound("")
|
||||
config = {
|
||||
"args": {"flavor": {"name": "test"}},
|
||||
"context": {
|
||||
"flavors": [{
|
||||
"name": "test",
|
||||
"ram": 32,
|
||||
}]
|
||||
}
|
||||
}
|
||||
result = validation._get_validated_flavor(config, clients, "flavor")
|
||||
self.assertTrue(result[0].is_valid, result[0].msg)
|
||||
|
||||
@mock.patch("rally.benchmark.validation.types.FlavorResourceType."
|
||||
"transform")
|
||||
def test__get_validated_flavor_from_context_failed(self, mock_transform):
|
||||
clients = mock.MagicMock()
|
||||
clients.nova().flavors.get.side_effect = nova_exc.NotFound("")
|
||||
config = {
|
||||
"args": {"flavor": {"name": "test"}},
|
||||
"context": {
|
||||
"flavors": [{
|
||||
"name": "othername",
|
||||
"ram": 32,
|
||||
}]
|
||||
}
|
||||
}
|
||||
result = validation._get_validated_flavor(config, clients, "flavor")
|
||||
self.assertFalse(result[0].is_valid, result[0].msg)
|
||||
|
||||
config = {
|
||||
"args": {"flavor": {"name": "test"}},
|
||||
}
|
||||
result = validation._get_validated_flavor(config, clients, "flavor")
|
||||
self.assertFalse(result[0].is_valid, result[0].msg)
|
||||
|
||||
def test_image_exists(self):
|
||||
validator = self._unwrap_validator(validation.image_exists, "param")
|
||||
result = validator({}, "clients", "deployment")
|
||||
@ -243,6 +283,39 @@ class ValidatorsTestCase(test.TestCase):
|
||||
result = validator(None, None, None)
|
||||
self.assertFalse(result.is_valid, result.msg)
|
||||
|
||||
@mock.patch("rally.benchmark.validation.types.FlavorResourceType."
|
||||
"transform")
|
||||
@mock.patch("rally.benchmark.validation._get_validated_image")
|
||||
def test_image_valid_on_flavor_context(self, mock_get_image,
|
||||
mock_transform):
|
||||
clients = mock.MagicMock()
|
||||
clients.nova().flavors.get.side_effect = nova_exc.NotFound("")
|
||||
|
||||
image = mock.MagicMock()
|
||||
success = validation.ValidationResult()
|
||||
mock_get_image.return_value = (success, image)
|
||||
|
||||
validator = self._unwrap_validator(validation.image_valid_on_flavor,
|
||||
"flavor", "image")
|
||||
config = {
|
||||
"args": {"flavor": {"name": "test"}},
|
||||
"context": {
|
||||
"flavors": [{
|
||||
"name": "test",
|
||||
"ram": 32,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
# test ram
|
||||
image.min_ram = None
|
||||
result = validator(config, clients, None)
|
||||
self.assertTrue(result.is_valid, result.msg)
|
||||
|
||||
image.min_ram = 64
|
||||
result = validator(config, clients, None)
|
||||
self.assertFalse(result.is_valid, result.msg)
|
||||
|
||||
def test_network_exists(self):
|
||||
validator = self._unwrap_validator(validation.network_exists, "net")
|
||||
|
||||
@ -484,4 +557,4 @@ class ValidatorsTestCase(test.TestCase):
|
||||
context = {"args": {"volume_type": True}}
|
||||
|
||||
result = validator(context, clients, mock.MagicMock())
|
||||
self.assertFalse(result.is_valid, result.msg)
|
||||
self.assertFalse(result.is_valid, result.msg)
|
||||
|
Loading…
x
Reference in New Issue
Block a user