pass id or name of resources in scenario config
* added types.set(...) decorator to apply transform to scenario args * abstract ResourceType implements generic transform logic * support use of flavor name through FlavorResourceType * support use of image name through ImageResourceType * transform is applied during validation and during preprocessing which follows validation * modified all sample scenarios to use flavor-by-name and image-by-name * passing resource as simple key-value pair will no longer work, and instead resource must be object containig id, name, or regex * unit tests for image and flavor transformations Change-Id: If35959ece8f247b5376f5ff91ae3d3e008a84447
This commit is contained in:
parent
de36c14184
commit
4fe39730dc
@ -5,7 +5,9 @@
|
||||
"image_location": "http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-disk.img",
|
||||
"container_format": "bare",
|
||||
"disk_format": "qcow2",
|
||||
"flavor_id": 42,
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"number_instances": 2
|
||||
},
|
||||
"runner": {
|
||||
|
@ -5,7 +5,8 @@
|
||||
image_location: "http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-disk.img"
|
||||
container_format: "bare"
|
||||
disk_format: "qcow2"
|
||||
flavor_id: 42
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
number_instances: 2
|
||||
runner:
|
||||
type: "constant"
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.boot_and_delete_server": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
}
|
||||
},
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.boot_and_delete_server:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
runner:
|
||||
type: "constant"
|
||||
times: 10
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.boot_and_list_server": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "ccc8fe37-9289-437b-a88c-fc4678656c75",
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
},
|
||||
"detailed" : True
|
||||
},
|
||||
"runner": {
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.boot_and_list_server:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "ccc8fe37-9289-437b-a88c-fc4678656c75"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
detailed: True
|
||||
runner:
|
||||
type: "constant"
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.boot_and_bounce_server": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "3fa4482f-677a-4488-adaf-c48befac5e5a",
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
},
|
||||
"actions": [
|
||||
{"hard_reboot": 1},
|
||||
{"soft_reboot": 1},
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.boot_and_bounce_server:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "3fa4482f-677a-4488-adaf-c48befac5e5a"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
actions:
|
||||
-
|
||||
hard_reboot: 1
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.boot_server_from_volume_and_delete": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "73257560-c59b-4275-a1ec-ab140e5b9979",
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
},
|
||||
"volume_size": 10
|
||||
},
|
||||
"runner": {
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.boot_server_from_volume_and_delete:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
volume_size: 10
|
||||
runner:
|
||||
type: "constant"
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.boot_server_from_volume": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "73257560-c59b-4275-a1ec-ab140e5b9979",
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
},
|
||||
"volume_size": 10
|
||||
},
|
||||
"runner": {
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.boot_server_from_volume:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
volume_size: 10
|
||||
runner:
|
||||
type: "constant"
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.snapshot_server": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
}
|
||||
},
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.snapshot_server:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
runner:
|
||||
type: "constant"
|
||||
times: 10
|
||||
|
@ -2,8 +2,12 @@
|
||||
"NovaServers.boot_server": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": 1,
|
||||
"image_id": "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
}
|
||||
},
|
||||
"runner": {
|
||||
"type": "constant",
|
||||
|
@ -2,8 +2,10 @@
|
||||
NovaServers.boot_server:
|
||||
-
|
||||
args:
|
||||
flavor_id: 1
|
||||
image_id: "73257560-c59b-4275-a1ec-ab140e5b9979"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
runner:
|
||||
type: "constant"
|
||||
times: 10
|
||||
|
@ -2,8 +2,12 @@
|
||||
"VMTasks.boot_runcommand_delete": [
|
||||
{
|
||||
"args": {
|
||||
"flavor_id": "100",
|
||||
"image_id": "8ec0b1bc-bb9c-4176-9d06-e23016091a51",
|
||||
"flavor": {
|
||||
"name": "m1.nano"
|
||||
},
|
||||
"image": {
|
||||
"name": "cirros-0.3.1-x86_64-uec"
|
||||
},
|
||||
"script": "doc/samples/support/instance_dd_test.sh",
|
||||
"interpreter": "/bin/sh",
|
||||
"username": "cirros"
|
||||
|
@ -2,8 +2,10 @@
|
||||
VMTasks.boot_runcommand_delete:
|
||||
-
|
||||
args:
|
||||
flavor_id: "1"
|
||||
image_id: "231bdb41-f959-4496-a272-3ef9f6d9f790"
|
||||
flavor:
|
||||
name: "m1.nano"
|
||||
image:
|
||||
name: "cirros-0.3.1-x86_64-uec"
|
||||
script: "doc/samples/support/instance_dd_test.sh"
|
||||
interpreter: "/bin/sh"
|
||||
username: "ubuntu"
|
||||
|
@ -113,8 +113,8 @@ class BenchmarkEngine(object):
|
||||
reason=six.text_type(e)
|
||||
)
|
||||
|
||||
def _validate_config_sematic_helper(self, admin, user, name, pos,
|
||||
task, kwargs):
|
||||
def _validate_config_semantic_helper(self, admin, user, name, pos,
|
||||
task, kwargs):
|
||||
args = {} if not kwargs else kwargs.get("args", {})
|
||||
try:
|
||||
base_scenario.Scenario.validate(name, args, admin=admin,
|
||||
@ -139,9 +139,9 @@ class BenchmarkEngine(object):
|
||||
|
||||
for name, values in config.iteritems():
|
||||
for pos, kwargs in enumerate(values):
|
||||
self._validate_config_sematic_helper(admin, user, name,
|
||||
pos, self.task,
|
||||
kwargs)
|
||||
self._validate_config_semantic_helper(admin, user, name,
|
||||
pos, self.task,
|
||||
kwargs)
|
||||
|
||||
@rutils.log_task_wrapper(LOG.info, _("Task validation."))
|
||||
def validate(self):
|
||||
|
@ -207,6 +207,7 @@ class ScenarioRunner(object):
|
||||
"config": scenario_context
|
||||
}
|
||||
|
||||
args = cls.preprocess(method_name, context_obj, args)
|
||||
results = base_ctx.ContextManager.run(context_obj, self._run_scenario,
|
||||
cls, method_name, context_obj,
|
||||
args)
|
||||
|
@ -20,6 +20,7 @@ import time
|
||||
|
||||
from rally import consts
|
||||
from rally import exceptions
|
||||
from rally import osclients
|
||||
from rally import utils
|
||||
|
||||
|
||||
@ -138,6 +139,22 @@ class Scenario(object):
|
||||
method = getattr(cls, method_name)
|
||||
return getattr(method, attr_name, default)
|
||||
|
||||
@classmethod
|
||||
def preprocess(cls, method_name, context, args):
|
||||
"""Run preprocessor on scenario arguments."""
|
||||
preprocessors = Scenario.meta(cls, method_name=method_name,
|
||||
attr_name="preprocessors", default={})
|
||||
clients = osclients.Clients(context["admin"]["endpoint"])
|
||||
|
||||
for src, preprocessor in preprocessors.items():
|
||||
resource_config = args.get(src)
|
||||
if resource_config:
|
||||
args[src] = preprocessor.transform(
|
||||
clients=clients,
|
||||
resource_config=resource_config)
|
||||
|
||||
return args
|
||||
|
||||
def context(self):
|
||||
"""Returns the context of the current benchmark scenario."""
|
||||
return self._context
|
||||
|
@ -16,6 +16,7 @@
|
||||
from rally.benchmark.scenarios import base
|
||||
from rally.benchmark.scenarios.glance import utils
|
||||
from rally.benchmark.scenarios.nova import utils as nova_utils
|
||||
from rally.benchmark import types as types
|
||||
from rally.benchmark import validation
|
||||
|
||||
|
||||
@ -57,11 +58,12 @@ class GlanceImages(utils.GlanceScenario, nova_utils.NovaScenario):
|
||||
**kwargs)
|
||||
self._delete_image(image)
|
||||
|
||||
@validation.add_validator(validation.flavor_exists("flavor_id"))
|
||||
@types.set(flavor=types.FlavorResourceType)
|
||||
@validation.add_validator(validation.flavor_exists("flavor"))
|
||||
@base.scenario(context={"cleanup": ["glance", "nova"]})
|
||||
def create_image_and_boot_instances(self, container_format,
|
||||
image_location, disk_format,
|
||||
flavor_id, number_instances,
|
||||
flavor, number_instances,
|
||||
**kwargs):
|
||||
"""Test adds image, boots instance from it and then deletes them."""
|
||||
image_name = self._generate_random_name()
|
||||
@ -73,4 +75,4 @@ class GlanceImages(utils.GlanceScenario, nova_utils.NovaScenario):
|
||||
image_id = image.id
|
||||
server_name = self._generate_random_name(prefix="rally_novaserver_")
|
||||
self._boot_servers(server_name, image_id,
|
||||
flavor_id, number_instances, **kwargs)
|
||||
flavor, number_instances, **kwargs)
|
||||
|
@ -20,6 +20,7 @@ from rally.benchmark.scenarios import base
|
||||
from rally.benchmark.scenarios.cinder import utils as cinder_utils
|
||||
from rally.benchmark.scenarios.nova import utils
|
||||
from rally.benchmark.scenarios import utils as scenario_utils
|
||||
from rally.benchmark import types as types
|
||||
from rally.benchmark import validation as valid
|
||||
from rally import exceptions as rally_exceptions
|
||||
from rally.openstack.common.gettextutils import _ # noqa
|
||||
@ -38,9 +39,11 @@ class NovaServers(utils.NovaScenario,
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NovaServers, self).__init__(*args, **kwargs)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova"]})
|
||||
def boot_and_list_server(self, image_id, flavor_id,
|
||||
def boot_and_list_server(self, image, flavor,
|
||||
detailed=True, **kwargs):
|
||||
"""Tests booting an image and then listing servers.
|
||||
|
||||
@ -54,37 +57,43 @@ class NovaServers(utils.NovaScenario,
|
||||
the number of servers owned by users.
|
||||
"""
|
||||
self._boot_server(
|
||||
self._generate_random_name(), image_id, flavor_id, **kwargs)
|
||||
self._generate_random_name(), image, flavor, **kwargs)
|
||||
self._list_servers(detailed)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova"]})
|
||||
def boot_and_delete_server(self, image_id, flavor_id,
|
||||
def boot_and_delete_server(self, image, flavor,
|
||||
min_sleep=0, max_sleep=0, **kwargs):
|
||||
"""Tests booting and then deleting an image."""
|
||||
server = self._boot_server(
|
||||
self._generate_random_name(), image_id, flavor_id, **kwargs)
|
||||
self._generate_random_name(), image, flavor, **kwargs)
|
||||
self.sleep_between(min_sleep, max_sleep)
|
||||
self._delete_server(server)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova", "cinder"]})
|
||||
def boot_server_from_volume_and_delete(self, image_id, flavor_id,
|
||||
def boot_server_from_volume_and_delete(self, image, flavor,
|
||||
volume_size,
|
||||
min_sleep=0, max_sleep=0, **kwargs):
|
||||
"""Tests booting from volume and then deleting an image and volume."""
|
||||
volume = self._create_volume(volume_size, imageRef=image_id)
|
||||
volume = self._create_volume(volume_size, imageRef=image)
|
||||
block_device_mapping = {'vda': '%s:::1' % volume.id}
|
||||
server = self._boot_server(self._generate_random_name(),
|
||||
image_id, flavor_id,
|
||||
image, flavor,
|
||||
block_device_mapping=block_device_mapping,
|
||||
**kwargs)
|
||||
self.sleep_between(min_sleep, max_sleep)
|
||||
self._delete_server(server)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova"]})
|
||||
def boot_and_bounce_server(self, image_id, flavor_id, **kwargs):
|
||||
def boot_and_bounce_server(self, image, flavor, **kwargs):
|
||||
"""Tests booting a server then performing stop/start or hard/soft
|
||||
reboot a number of times.
|
||||
"""
|
||||
@ -97,28 +106,32 @@ class NovaServers(utils.NovaScenario,
|
||||
"Invalid server actions configuration \'%(actions)s\' due to: "
|
||||
"%(error)s" % {'actions': str(actions), 'error': str(error)})
|
||||
server = self._boot_server(self._generate_random_name(),
|
||||
image_id, flavor_id, **kwargs)
|
||||
image, flavor, **kwargs)
|
||||
for action in action_builder.build_actions(actions, server):
|
||||
action()
|
||||
self._delete_server(server)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova", "glance"]})
|
||||
def snapshot_server(self, image_id, flavor_id, **kwargs):
|
||||
def snapshot_server(self, image, flavor, **kwargs):
|
||||
"""Tests Nova instance snapshotting."""
|
||||
server_name = self._generate_random_name()
|
||||
|
||||
server = self._boot_server(server_name, image_id, flavor_id, **kwargs)
|
||||
server = self._boot_server(server_name, image, flavor, **kwargs)
|
||||
image = self._create_image(server)
|
||||
self._delete_server(server)
|
||||
|
||||
server = self._boot_server(server_name, image.id, flavor_id, **kwargs)
|
||||
server = self._boot_server(server_name, image.id, flavor, **kwargs)
|
||||
self._delete_server(server)
|
||||
self._delete_image(image)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova"]})
|
||||
def boot_server(self, image_id, flavor_id, **kwargs):
|
||||
def boot_server(self, image, flavor, **kwargs):
|
||||
"""Test VM boot - assumed clean-up is done elsewhere."""
|
||||
if 'nics' not in kwargs:
|
||||
nets = self.clients("nova").networks.list()
|
||||
@ -126,11 +139,13 @@ class NovaServers(utils.NovaScenario,
|
||||
random_nic = random.choice(nets)
|
||||
kwargs['nics'] = [{'net-id': random_nic.id}]
|
||||
self._boot_server(
|
||||
self._generate_random_name(), image_id, flavor_id, **kwargs)
|
||||
self._generate_random_name(), image, flavor, **kwargs)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@base.scenario(context={"cleanup": ["nova", "cinder"]})
|
||||
def boot_server_from_volume(self, image_id, flavor_id,
|
||||
def boot_server_from_volume(self, image, flavor,
|
||||
volume_size, **kwargs):
|
||||
"""Test VM boot from volume - assumed clean-up is done elsewhere."""
|
||||
if 'nics' not in kwargs:
|
||||
@ -138,10 +153,10 @@ class NovaServers(utils.NovaScenario,
|
||||
if nets:
|
||||
random_nic = random.choice(nets)
|
||||
kwargs['nics'] = [{'net-id': random_nic.id}]
|
||||
volume = self._create_volume(volume_size, imageRef=image_id)
|
||||
volume = self._create_volume(volume_size, imageRef=image)
|
||||
block_device_mapping = {'vda': '%s:::1' % volume.id}
|
||||
self._boot_server(self._generate_random_name(),
|
||||
image_id, flavor_id,
|
||||
image, flavor,
|
||||
block_device_mapping=block_device_mapping,
|
||||
**kwargs)
|
||||
|
||||
|
@ -18,6 +18,7 @@ import json
|
||||
from rally.benchmark.scenarios import base
|
||||
from rally.benchmark.scenarios.nova import utils as nova_utils
|
||||
from rally.benchmark.scenarios.vm import utils as vm_utils
|
||||
from rally.benchmark import types as types
|
||||
from rally.benchmark import validation as valid
|
||||
from rally.openstack.common.gettextutils import _ # noqa
|
||||
from rally.openstack.common import log as logging
|
||||
@ -31,13 +32,15 @@ class VMTasks(nova_utils.NovaScenario, vm_utils.VMScenario):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VMTasks, self).__init__(*args, **kwargs)
|
||||
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor_id", "image_id"))
|
||||
@types.set(image=types.ImageResourceType,
|
||||
flavor=types.FlavorResourceType)
|
||||
@valid.add_validator(valid.image_valid_on_flavor("flavor", "image"))
|
||||
@valid.add_validator(valid.file_exists("script"))
|
||||
@valid.add_validator(valid.number("port", minval=1, maxval=65535,
|
||||
nullable=True, integer_only=True))
|
||||
@base.scenario(context={"cleanup": ["nova"],
|
||||
"keypair": {}, "allow_ssh": {}})
|
||||
def boot_runcommand_delete(self, image_id, flavor_id,
|
||||
def boot_runcommand_delete(self, image, flavor,
|
||||
script, interpreter, network='private',
|
||||
username='ubuntu', ip_version=4,
|
||||
port=22, **kwargs):
|
||||
@ -59,7 +62,7 @@ class VMTasks(nova_utils.NovaScenario, vm_utils.VMScenario):
|
||||
"""
|
||||
server = self._boot_server(
|
||||
self._generate_random_name("rally_novaserver_"),
|
||||
image_id, flavor_id, key_name='rally_ssh_key', **kwargs)
|
||||
image, flavor, key_name='rally_ssh_key', **kwargs)
|
||||
|
||||
code, out, err = self.run_command(server, username, network, port,
|
||||
ip_version, interpreter, script)
|
||||
|
130
rally/benchmark/types.py
Normal file
130
rally/benchmark/types.py
Normal file
@ -0,0 +1,130 @@
|
||||
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
|
||||
# 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 operator
|
||||
import re
|
||||
|
||||
from rally import exceptions
|
||||
|
||||
|
||||
def set(**kwargs):
|
||||
"""Decorator to define resource transformation(s) on scenario parameters.
|
||||
|
||||
The `kwargs` passed as arguments to the decorator are used to
|
||||
map a key in the scenario config to the subclass of ResourceType
|
||||
used to perform a transformation on the value of that key.
|
||||
"""
|
||||
def wrapper(func):
|
||||
func.preprocessors = getattr(func, 'preprocessors', {})
|
||||
func.preprocessors.update(kwargs)
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
|
||||
class ResourceType(object):
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def transform(cls, clients, resource_config):
|
||||
"""Transform the resource.
|
||||
|
||||
:param clients: openstack admin client handles
|
||||
:param resource_config: scenario config of resource
|
||||
|
||||
:returns: transformed value of resource
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _id_from_name(resource_config, resources, typename):
|
||||
"""Return the id of the resource whose name matches the pattern.
|
||||
|
||||
When resource_config contains `name`, an exact match is used.
|
||||
When resource_config contains `regex`, a pattern match is used.
|
||||
|
||||
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`
|
||||
"""
|
||||
if resource_config.get('name'):
|
||||
patternstr = "^{0}$".format(resource_config.get('name'))
|
||||
elif resource_config.get('regex'):
|
||||
patternstr = resource_config.get('regex')
|
||||
else:
|
||||
raise exceptions.InvalidScenarioArgument(
|
||||
"{typename} 'id', 'name', or 'regex' not found "
|
||||
"in '{resource_config}' ".format(typename=typename.title(),
|
||||
resource_config=resource_config))
|
||||
|
||||
pattern = re.compile(patternstr)
|
||||
matching = filter(lambda resource: re.search(pattern, resource.name),
|
||||
resources)
|
||||
if not matching:
|
||||
raise exceptions.InvalidScenarioArgument(
|
||||
"{typename} with pattern '{pattern}' not found".format(
|
||||
typename=typename.title(), pattern=pattern.pattern))
|
||||
elif len(matching) > 1:
|
||||
raise exceptions.InvalidScenarioArgument(
|
||||
"{typename} with name '{pattern}' is ambiguous, "
|
||||
"possible matches by id: {ids}".format(
|
||||
typename=typename.title(), pattern=pattern.pattern,
|
||||
ids=", ".join(map(operator.attrgetter("id"), matching))))
|
||||
return matching[0].id
|
||||
|
||||
|
||||
class FlavorResourceType(ResourceType):
|
||||
|
||||
@classmethod
|
||||
def transform(cls, clients, resource_config):
|
||||
"""Transform the resource config to id.
|
||||
|
||||
:param clients: openstack admin client handles
|
||||
:param resource_config: scenario config with `id`, `name` or `regex`
|
||||
|
||||
:returns: id matching resource
|
||||
"""
|
||||
resource_id = resource_config.get('id')
|
||||
if not resource_id:
|
||||
novaclient = clients.nova()
|
||||
resource_id = _id_from_name(resource_config=resource_config,
|
||||
resources=novaclient.
|
||||
flavors.list(), typename='flavor')
|
||||
return resource_id
|
||||
|
||||
|
||||
class ImageResourceType(ResourceType):
|
||||
|
||||
@classmethod
|
||||
def transform(cls, clients, resource_config):
|
||||
"""Transform the resource config to id.
|
||||
|
||||
:param clients: openstack admin client handles
|
||||
:param resource_config: scenario config with `id`, `name` or `regex`
|
||||
|
||||
:returns: id matching resource
|
||||
"""
|
||||
resource_id = resource_config.get('id')
|
||||
if not resource_id:
|
||||
glanceclient = clients.glance()
|
||||
resource_id = _id_from_name(resource_config=resource_config,
|
||||
resources=glanceclient.
|
||||
images.list(), typename='image')
|
||||
return resource_id
|
@ -19,6 +19,7 @@ import os
|
||||
from glanceclient import exc as glance_exc
|
||||
from novaclient import exceptions as nova_exc
|
||||
|
||||
from rally.benchmark import types as types
|
||||
from rally import consts
|
||||
from rally.openstack.common.gettextutils import _
|
||||
from rally.verification.verifiers.tempest import tempest
|
||||
@ -144,10 +145,12 @@ def image_exists(param_name):
|
||||
to get image id value.
|
||||
"""
|
||||
def image_exists_validator(**kwargs):
|
||||
image_id = kwargs.get(param_name)
|
||||
glanceclient = kwargs["clients"].glance()
|
||||
clients = kwargs.get('clients')
|
||||
image_id = types.ImageResourceType.transform(clients=clients,
|
||||
resource_config=
|
||||
kwargs.get(param_name))
|
||||
try:
|
||||
glanceclient.images.get(image=image_id)
|
||||
clients.glance().images.get(image=image_id)
|
||||
return ValidationResult()
|
||||
except glance_exc.HTTPNotFound:
|
||||
message = _("Image with id '%s' not found") % image_id
|
||||
@ -162,10 +165,12 @@ def flavor_exists(param_name):
|
||||
to get flavor id value.
|
||||
"""
|
||||
def flavor_exists_validator(**kwargs):
|
||||
flavor_id = kwargs.get(param_name)
|
||||
novaclient = kwargs["clients"].nova()
|
||||
clients = kwargs.get('clients')
|
||||
flavor_id = types.FlavorResourceType.transform(clients=clients,
|
||||
resource_config=
|
||||
kwargs.get(param_name))
|
||||
try:
|
||||
novaclient.flavors.get(flavor=flavor_id)
|
||||
clients.nova().flavors.get(flavor=flavor_id)
|
||||
return ValidationResult()
|
||||
except nova_exc.NotFound:
|
||||
message = _("Flavor with id '%s' not found") % flavor_id
|
||||
@ -183,20 +188,22 @@ def image_valid_on_flavor(flavor_name, image_name):
|
||||
|
||||
"""
|
||||
def image_valid_on_flavor_validator(**kwargs):
|
||||
flavor_id = kwargs.get(flavor_name)
|
||||
novaclient = kwargs["clients"].nova()
|
||||
clients = kwargs.get('clients')
|
||||
|
||||
flavor_id = types.FlavorResourceType.transform(clients=clients,
|
||||
resource_config=
|
||||
kwargs.get(flavor_name))
|
||||
try:
|
||||
flavor = novaclient.flavors.get(flavor=flavor_id)
|
||||
flavor = clients.nova().flavors.get(flavor=flavor_id)
|
||||
except nova_exc.NotFound:
|
||||
message = _("Flavor with id '%s' not found") % flavor_id
|
||||
return ValidationResult(False, message)
|
||||
|
||||
image_id = kwargs.get(image_name)
|
||||
glanceclient = kwargs["clients"].glance()
|
||||
|
||||
image_id = types.ImageResourceType.transform(clients=clients,
|
||||
resource_config=
|
||||
kwargs.get(image_name))
|
||||
try:
|
||||
image = glanceclient.images.get(image=image_id)
|
||||
image = clients.glance().images.get(image=image_id)
|
||||
except glance_exc.HTTPNotFound:
|
||||
message = _("Image with id '%s' not found") % image_id
|
||||
return ValidationResult(False, message)
|
||||
@ -216,7 +223,6 @@ def image_valid_on_flavor(flavor_name, image_name):
|
||||
message = _("The disk size for flavor '%s' is too small "
|
||||
"for requested image '%s'") % (flavor_id, image_id)
|
||||
return ValidationResult(False, message)
|
||||
|
||||
return ValidationResult()
|
||||
return image_valid_on_flavor_validator
|
||||
|
||||
|
@ -204,14 +204,16 @@ class ScenarioRunnerTestCase(test.TestCase):
|
||||
config,
|
||||
serial.SerialScenarioRunner.CONFIG_SCHEMA)
|
||||
|
||||
@mock.patch("rally.benchmark.runners.base.osclients")
|
||||
@mock.patch("rally.benchmark.runners.base.base_ctx.ContextManager")
|
||||
def test_run(self, mock_ctx_manager):
|
||||
def test_run(self, mock_ctx_manager, mock_osclients):
|
||||
runner = constant.ConstantScenarioRunner(mock.MagicMock(),
|
||||
self.fake_endpoints,
|
||||
mock.MagicMock())
|
||||
mock_ctx_manager.run.return_value = base.ScenarioRunnerResult([])
|
||||
scenario_name = "NovaServers.boot_server_from_volume_and_delete"
|
||||
result = runner.run(scenario_name, {"some_ctx": 2}, [1, 2, 3])
|
||||
config_kwargs = {"image": {"id": 1}, "flavor": {"id": 1}}
|
||||
result = runner.run(scenario_name, {"some_ctx": 2}, config_kwargs)
|
||||
|
||||
self.assertEqual(result, mock_ctx_manager.run.return_value)
|
||||
|
||||
@ -228,7 +230,7 @@ class ScenarioRunnerTestCase(test.TestCase):
|
||||
}
|
||||
|
||||
expected = [context_obj, runner._run_scenario, cls, method_name,
|
||||
context_obj, [1, 2, 3]]
|
||||
context_obj, config_kwargs]
|
||||
mock_ctx_manager.run.assert_called_once_with(*expected)
|
||||
|
||||
@mock.patch("rally.benchmark.runners.base.base_ctx.ContextManager")
|
||||
|
@ -158,8 +158,8 @@ class BenchmarkEngineTestCase(test.TestCase):
|
||||
def test__validate_config_semantic_helper(self, mock_validate):
|
||||
task = mock.MagicMock()
|
||||
eng = engine.BenchmarkEngine(mock.MagicMock(), mock.MagicMock())
|
||||
eng._validate_config_sematic_helper("admin", "user", "name", "pos",
|
||||
task, {"args": "args"})
|
||||
eng._validate_config_semantic_helper("admin", "user", "name", "pos",
|
||||
task, {"args": "args"})
|
||||
mock_validate.assert_called_once_with(
|
||||
"name", "args", admin="admin", users=["user"],
|
||||
task=task)
|
||||
@ -170,15 +170,15 @@ class BenchmarkEngineTestCase(test.TestCase):
|
||||
eng = engine.BenchmarkEngine(mock.MagicMock(), mock.MagicMock())
|
||||
|
||||
self.assertRaises(exceptions.InvalidBenchmarkConfig,
|
||||
eng._validate_config_sematic_helper, "a", "u", "n",
|
||||
eng._validate_config_semantic_helper, "a", "u", "n",
|
||||
"p", mock.MagicMock(), {})
|
||||
|
||||
@mock.patch("rally.benchmark.engine.osclients.Clients")
|
||||
@mock.patch("rally.benchmark.engine.users_ctx")
|
||||
@mock.patch("rally.benchmark.engine.BenchmarkEngine"
|
||||
"._validate_config_sematic_helper")
|
||||
def test__validate_config_sematic(self, mock_helper, mock_userctx,
|
||||
mock_osclients):
|
||||
"._validate_config_semantic_helper")
|
||||
def test__validate_config_semantic(self, mock_helper, mock_userctx,
|
||||
mock_osclients):
|
||||
mock_userctx.UserGenerator = fakes.FakeUserContext
|
||||
mock_osclients.return_value = mock.MagicMock()
|
||||
config = {
|
||||
|
122
tests/benchmark/test_types.py
Normal file
122
tests/benchmark/test_types.py
Normal file
@ -0,0 +1,122 @@
|
||||
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
|
||||
# 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 import types
|
||||
from rally import exceptions
|
||||
|
||||
from tests import fakes
|
||||
from tests import test
|
||||
|
||||
|
||||
class FlavorResourceTypeTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(FlavorResourceTypeTestCase, self).setUp()
|
||||
self.clients = fakes.FakeClients()
|
||||
self.clients.nova().flavors._cache(fakes.FakeResource(name='m1.tiny',
|
||||
id=1))
|
||||
self.clients.nova().flavors._cache(fakes.FakeResource(name='m1.nano',
|
||||
id=42))
|
||||
|
||||
def test_transform_by_id(self):
|
||||
resource_config = {"id": 42}
|
||||
flavor_id = types.FlavorResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(flavor_id, 42)
|
||||
|
||||
def test_transform_by_name(self):
|
||||
resource_config = {"name": "m1.nano"}
|
||||
flavor_id = types.FlavorResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(flavor_id, 42)
|
||||
|
||||
def test_transform_by_name_to_dest(self):
|
||||
resource_config = {"name": "m1.nano"}
|
||||
flavor_id = types.FlavorResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(flavor_id, 42)
|
||||
|
||||
def test_transform_by_name_no_match(self):
|
||||
resource_config = {"name": "m1.medium"}
|
||||
self.assertRaises(exceptions.InvalidScenarioArgument,
|
||||
types.FlavorResourceType.transform, self.clients,
|
||||
resource_config)
|
||||
|
||||
def test_transform_by_regex(self):
|
||||
resource_config = {"regex": "m(1|2)\.nano"}
|
||||
flavor_id = types.FlavorResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(flavor_id, 42)
|
||||
|
||||
def test_transform_by_regex_no_match(self):
|
||||
resource_config = {"regex": "m(2|3)\.nano"}
|
||||
self.assertRaises(exceptions.InvalidScenarioArgument,
|
||||
types.FlavorResourceType.transform, self.clients,
|
||||
resource_config)
|
||||
|
||||
|
||||
class ImageResourceTypeTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ImageResourceTypeTestCase, self).setUp()
|
||||
self.clients = fakes.FakeClients()
|
||||
image1 = fakes.FakeResource(name='cirros-0.3.1-uec', id=100)
|
||||
self.clients.glance().images._cache(image1)
|
||||
image2 = fakes.FakeResource(name='cirros-0.3.1-uec-ramdisk', id=101)
|
||||
self.clients.glance().images._cache(image2)
|
||||
|
||||
def test_transform_by_id(self):
|
||||
resource_config = {"id": 100}
|
||||
image_id = types.ImageResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(image_id, 100)
|
||||
|
||||
def test_transform_by_name(self):
|
||||
resource_config = {"name": "cirros-0.3.1-uec"}
|
||||
image_id = types.ImageResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(image_id, 100)
|
||||
|
||||
def test_transform_by_name_to_dest(self):
|
||||
resource_config = {"name": "cirros-0.3.1-uec"}
|
||||
image_id = types.ImageResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(image_id, 100)
|
||||
|
||||
def test_transform_by_name_no_match(self):
|
||||
resource_config = {"name": "cirros-0.3.1-uec-boot"}
|
||||
self.assertRaises(exceptions.InvalidScenarioArgument,
|
||||
types.ImageResourceType.transform, self.clients,
|
||||
resource_config)
|
||||
|
||||
def test_transform_by_regex(self):
|
||||
resource_config = {"regex": "-uec$"}
|
||||
image_id = types.ImageResourceType.transform(clients=self.clients,
|
||||
resource_config=
|
||||
resource_config)
|
||||
self.assertEqual(image_id, 100)
|
||||
|
||||
def test_transform_by_regex_no_match(self):
|
||||
resource_config = {"regex": "-boot$"}
|
||||
self.assertRaises(exceptions.InvalidScenarioArgument,
|
||||
types.ImageResourceType.transform, self.clients,
|
||||
resource_config)
|
@ -124,10 +124,11 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakegclient = fakes.FakeGlanceClient()
|
||||
fakegclient.images.get = mock.MagicMock()
|
||||
mock_osclients.glance.return_value = fakegclient
|
||||
validator = validation.image_exists("image_id")
|
||||
validator = validation.image_exists("image")
|
||||
test_img_id = "test_image_id"
|
||||
resource = {"id": test_img_id}
|
||||
result = validator(clients=mock_osclients,
|
||||
image_id=test_img_id)
|
||||
image=resource)
|
||||
fakegclient.images.get.assert_called_once_with(image=test_img_id)
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertIsNone(result.msg)
|
||||
@ -138,10 +139,11 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakegclient.images.get = mock.MagicMock()
|
||||
fakegclient.images.get.side_effect = glance_exc.HTTPNotFound
|
||||
mock_osclients.glance.return_value = fakegclient
|
||||
validator = validation.image_exists("image_id")
|
||||
validator = validation.image_exists("image")
|
||||
test_img_id = "test_image_id"
|
||||
resource = {"id": test_img_id}
|
||||
result = validator(clients=mock_osclients,
|
||||
image_id=test_img_id)
|
||||
image=resource)
|
||||
fakegclient.images.get.assert_called_once_with(image=test_img_id)
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertIsNotNone(result.msg)
|
||||
@ -151,10 +153,11 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakenclient = fakes.FakeNovaClient()
|
||||
fakenclient.flavors = mock.MagicMock()
|
||||
mock_osclients.nova.return_value = fakenclient
|
||||
validator = validation.flavor_exists("flavor_id")
|
||||
validator = validation.flavor_exists("flavor")
|
||||
test_flavor_id = 1
|
||||
resource = {"id": test_flavor_id}
|
||||
result = validator(clients=mock_osclients,
|
||||
flavor_id=test_flavor_id)
|
||||
flavor=resource)
|
||||
fakenclient.flavors.get.assert_called_once_with(flavor=test_flavor_id)
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertIsNone(result.msg)
|
||||
@ -165,10 +168,11 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakenclient.flavors = mock.MagicMock()
|
||||
fakenclient.flavors.get.side_effect = nova_exc.NotFound(code=404)
|
||||
mock_osclients.nova.return_value = fakenclient
|
||||
validator = validation.flavor_exists("flavor_id")
|
||||
validator = validation.flavor_exists("flavor")
|
||||
test_flavor_id = 101
|
||||
resource = {"id": test_flavor_id}
|
||||
result = validator(clients=mock_osclients,
|
||||
flavor_id=test_flavor_id)
|
||||
flavor=resource)
|
||||
fakenclient.flavors.get.assert_called_once_with(flavor=test_flavor_id)
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertIsNotNone(result.msg)
|
||||
@ -190,11 +194,11 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakenclient.flavors.get = mock.MagicMock(return_value=flavor)
|
||||
mock_osclients.nova.return_value = fakenclient
|
||||
|
||||
validator = validation.image_valid_on_flavor("flavor_id", "image_id")
|
||||
validator = validation.image_valid_on_flavor("flavor", "image")
|
||||
|
||||
result = validator(clients=mock_osclients,
|
||||
flavor_id=flavor.id,
|
||||
image_id=image.id)
|
||||
flavor={"id": flavor.id},
|
||||
image={"id": image.id})
|
||||
|
||||
fakenclient.flavors.get.assert_called_once_with(flavor=flavor.id)
|
||||
fakegclient.images.get.assert_called_once_with(image=image.id)
|
||||
@ -219,11 +223,11 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakenclient.flavors.get = mock.MagicMock(return_value=flavor)
|
||||
mock_osclients.nova.return_value = fakenclient
|
||||
|
||||
validator = validation.image_valid_on_flavor("flavor_id", "image_id")
|
||||
validator = validation.image_valid_on_flavor("flavor", "image")
|
||||
|
||||
result = validator(clients=mock_osclients,
|
||||
flavor_id=flavor.id,
|
||||
image_id=image.id)
|
||||
flavor={"id": flavor.id},
|
||||
image={"id": image.id})
|
||||
|
||||
fakenclient.flavors.get.assert_called_once_with(flavor=flavor.id)
|
||||
fakegclient.images.get.assert_called_once_with(image=image.id)
|
||||
@ -243,13 +247,13 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakenclient.flavors.get = mock.MagicMock(return_value=flavor)
|
||||
mock_osclients.nova.return_value = fakenclient
|
||||
|
||||
validator = validation.image_valid_on_flavor("flavor_id", "image_id")
|
||||
validator = validation.image_valid_on_flavor("flavor", "image")
|
||||
|
||||
test_img_id = "test_image_id"
|
||||
|
||||
result = validator(clients=mock_osclients,
|
||||
flavor_id=flavor.id,
|
||||
image_id=test_img_id)
|
||||
flavor={"id": flavor.id},
|
||||
image={"id": test_img_id})
|
||||
|
||||
fakenclient.flavors.get.assert_called_once_with(flavor=flavor.id)
|
||||
fakegclient.images.get.assert_called_once_with(image=test_img_id)
|
||||
@ -266,14 +270,14 @@ class ValidationUtilsTestCase(test.TestCase):
|
||||
fakenclient.flavors.get.side_effect = nova_exc.NotFound(code=404)
|
||||
mock_osclients.nova.return_value = fakenclient
|
||||
|
||||
validator = validation.image_valid_on_flavor("flavor_id", "image_id")
|
||||
validator = validation.image_valid_on_flavor("flavor", "image")
|
||||
|
||||
test_img_id = "test_image_id"
|
||||
test_flavor_id = 101
|
||||
|
||||
result = validator(clients=mock_osclients,
|
||||
flavor_id=test_flavor_id,
|
||||
image_id=test_img_id)
|
||||
flavor={"id": test_flavor_id},
|
||||
image={"id": test_img_id})
|
||||
|
||||
fakenclient.flavors.get.assert_called_once_with(flavor=test_flavor_id)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user