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:
Pavel Boldin 2014-12-12 20:27:43 +02:00
parent a818e330b4
commit 9f18969054
8 changed files with 424 additions and 8 deletions

View 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
}
]
}
}
]
}

View 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

View File

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

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

View File

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

View File

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

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

View File

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