diff --git a/rally/plugins/openstack/scenarios/glance/images.py b/rally/plugins/openstack/scenarios/glance/images.py index d4865486..a05b679b 100644 --- a/rally/plugins/openstack/scenarios/glance/images.py +++ b/rally/plugins/openstack/scenarios/glance/images.py @@ -200,7 +200,7 @@ class CreateAndDeleteImage(GlanceBasic): values=["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso"]) @validation.add("restricted_parameters", param_names=["image_name", "name"]) -@validation.flavor_exists("flavor") +@validation.add("flavor_exists", param_name="flavor") @validation.add("required_services", services=[consts.Service.GLANCE, consts.Service.NOVA]) @validation.add("required_platform", platform="openstack", users=True) diff --git a/rally/plugins/openstack/scenarios/manila/shares.py b/rally/plugins/openstack/scenarios/manila/shares.py index af8f1b46..c06a1b8e 100644 --- a/rally/plugins/openstack/scenarios/manila/shares.py +++ b/rally/plugins/openstack/scenarios/manila/shares.py @@ -24,7 +24,9 @@ from rally.task import validation """Scenarios for Manila shares.""" -@validation.validate_share_proto() +@validation.add("enum", param_name="share_proto", + values=["NFS", "CIFS", "GLUSTERFS", "HDFS", "CEPHFS"], + case_insensitive=True, missed=False) @validation.add("required_services", services=[consts.Service.MANILA]) @validation.add("required_platform", platform="openstack", users=True) @scenario.configure(context={"cleanup": ["manila"]}, @@ -70,8 +72,8 @@ class ListShares(utils.ManilaScenario): self._list_shares(detailed=detailed, search_opts=search_opts) -@validation.add("enum", param_name="share_proto", values=["nfs", "cephfs", - "cifs", "glusterfs", "hdfs"], missed=False, +@validation.add("enum", param_name="share_proto", + values=["NFS", "CIFS", "GLUSTERFS", "HDFS", "CEPHFS"], case_insensitive=True) @validation.add("required_services", services=[consts.Service.MANILA]) @validation.add("required_platform", platform="openstack", users=True) @@ -114,8 +116,8 @@ class CreateAndExtendShare(utils.ManilaScenario): self._extend_share(share, new_size) -@validation.add("enum", param_name="share_proto", values=["nfs", "cephfs", - "cifs", "glusterfs", "hdfs"], missed=False, +@validation.add("enum", param_name="share_proto", + values=["NFS", "CIFS", "GLUSTERFS", "HDFS", "CEPHFS"], case_insensitive=True) @validation.add("required_services", services=[consts.Service.MANILA]) @validation.add("required_platform", platform="openstack", users=True) @@ -296,7 +298,9 @@ class AttachSecurityServiceToShareNetwork(utils.ManilaScenario): self._add_security_service_to_share_network(sn, ss) -@validation.validate_share_proto() +@validation.add("enum", param_name="share_proto", + values=["NFS", "CIFS", "GLUSTERFS", "HDFS", "CEPHFS"], + case_insensitive=True) @validation.add("required_services", services=[consts.Service.MANILA]) @validation.add("required_platform", platform="openstack", users=True) @scenario.configure(context={"cleanup": ["manila"]}, diff --git a/rally/plugins/openstack/scenarios/mistral/executions.py b/rally/plugins/openstack/scenarios/mistral/executions.py index 7d90c540..49f8f566 100644 --- a/rally/plugins/openstack/scenarios/mistral/executions.py +++ b/rally/plugins/openstack/scenarios/mistral/executions.py @@ -51,14 +51,15 @@ class ListExecutions(utils.MistralScenario): sort_keys=sort_keys, sort_dirs=sort_dirs) -@validation.add("file_exists", param_name="definition") @types.convert(definition={"type": "file"}) @types.convert(params={"type": "file"}) @types.convert(wf_input={"type": "file"}) +@validation.add("file_exists", param_name="definition") @validation.add("required_platform", platform="openstack", users=True) @validation.add("required_services", services=[consts.Service.MISTRAL]) -@validation.workbook_contains_workflow("definition", "workflow_name") +@validation.add("workbook_contains_workflow", param_name="definition", + workflow_name="workflow_name") @scenario.configure(name="MistralExecutions.create_execution_from_workbook", context={"cleanup": ["mistral"]}, platform="openstack") diff --git a/rally/plugins/openstack/scenarios/mistral/workbooks.py b/rally/plugins/openstack/scenarios/mistral/workbooks.py index 71478c81..86d738a1 100644 --- a/rally/plugins/openstack/scenarios/mistral/workbooks.py +++ b/rally/plugins/openstack/scenarios/mistral/workbooks.py @@ -39,8 +39,8 @@ class ListWorkbooks(utils.MistralScenario): self._list_workbooks() -@validation.add("file_exists", param_name="definition") @types.convert(definition={"type": "file"}) +@validation.add("file_exists", param_name="definition") @validation.add("required_platform", platform="openstack", users=True) @validation.add("required_services", services=[consts.Service.MISTRAL]) diff --git a/rally/plugins/openstack/scenarios/sahara/clusters.py b/rally/plugins/openstack/scenarios/sahara/clusters.py index 6902d18e..d603871c 100644 --- a/rally/plugins/openstack/scenarios/sahara/clusters.py +++ b/rally/plugins/openstack/scenarios/sahara/clusters.py @@ -30,8 +30,8 @@ LOG = logging.getLogger(__name__) worker_flavor={"type": "nova_flavor"}, neutron_net={"type": "neutron_network"}, floating_ip_pool={"type": "neutron_network"}) -@validation.flavor_exists("master_flavor") -@validation.flavor_exists("worker_flavor") +@validation.add("flavor_exists", param_name="master_flavor") +@validation.add("flavor_exists", param_name="worker_flavor") @validation.add("required_contexts", contexts=["users", "sahara_image"]) @validation.add("number", param_name="workers_count", minval=1, integer_only=True) @@ -122,8 +122,8 @@ class CreateAndDeleteCluster(utils.SaharaScenario): @types.convert(flavor={"type": "nova_flavor"}, master_flavor={"type": "nova_flavor"}, worker_flavor={"type": "nova_flavor"}) -@validation.flavor_exists("master_flavor") -@validation.flavor_exists("worker_flavor") +@validation.add("flavor_exists", param_name="master_flavor") +@validation.add("flavor_exists", param_name="worker_flavor") @validation.add("required_services", services=[consts.Service.SAHARA]) @validation.add("required_contexts", contexts=["users", "sahara_image"]) @validation.add("number", param_name="workers_count", minval=1, diff --git a/rally/plugins/openstack/scenarios/sahara/node_group_templates.py b/rally/plugins/openstack/scenarios/sahara/node_group_templates.py index 5357b1a7..5cc5a665 100644 --- a/rally/plugins/openstack/scenarios/sahara/node_group_templates.py +++ b/rally/plugins/openstack/scenarios/sahara/node_group_templates.py @@ -23,7 +23,7 @@ from rally.task import validation @types.convert(flavor={"type": "nova_flavor"}) -@validation.flavor_exists("flavor") +@validation.add("flavor_exists", param_name="flavor") @validation.add("required_services", services=[consts.Service.SAHARA]) @validation.add("required_platform", platform="openstack", users=True) @scenario.configure( @@ -69,7 +69,7 @@ class CreateAndListNodeGroupTemplates(utils.SaharaScenario): @types.convert(flavor={"type": "nova_flavor"}) -@validation.flavor_exists("flavor") +@validation.add("flavor_exists", param_name="flavor") @validation.add("required_services", services=[consts.Service.SAHARA]) @validation.add("required_platform", platform="openstack", users=True) @scenario.configure( diff --git a/rally/plugins/openstack/scenarios/vm/vmtasks.py b/rally/plugins/openstack/scenarios/vm/vmtasks.py index 9c66bd09..6d589a97 100644 --- a/rally/plugins/openstack/scenarios/vm/vmtasks.py +++ b/rally/plugins/openstack/scenarios/vm/vmtasks.py @@ -14,19 +14,21 @@ # under the License. import json +import os import pkgutil from rally.common import logging from rally.common import sshutils +from rally.common import validation from rally import consts from rally import exceptions +from rally.plugins.common import validators from rally.plugins.openstack import scenario from rally.plugins.openstack.scenarios.cinder import utils as cinder_utils from rally.plugins.openstack.scenarios.vm import utils as vm_utils from rally.plugins.openstack.services import heat from rally.task import atomic from rally.task import types -from rally.task import validation """Scenarios that are to be run inside VM instances.""" @@ -35,11 +37,91 @@ from rally.task import validation LOG = logging.getLogger(__name__) +# TODO(andreykurilin): replace by advanced jsonschema(lollipop?!) someday +@validation.configure(name="valid_command", platform="openstack") +class ValidCommandValidator(validation.Validator): + + def __init__(self, param_name, required=True): + """Checks that parameter is a proper command-specifying dictionary. + + Ensure that the command dictionary is a proper command-specifying + dictionary described in `vmtasks.VMTasks.boot_runcommand_delete' + docstring. + + :param param_name: Name of parameter to validate + :param required: Boolean indicating that the command dictionary is + required + """ + super(ValidCommandValidator, self).__init__() + + self.param_name = param_name + self.required = required + + def check_command_dict(self, command): + """Check command-specifying dict `command' + + :raises ValueError: on error + """ + + if not isinstance(command, dict): + raise ValueError("Command must be a dictionary") + + # NOTE(pboldin): Here we check for the values not for presence of the + # keys due to template-driven configuration generation that can leave + # keys defined but values empty. + if command.get("interpreter"): + script_file = command.get("script_file") + if script_file: + if "script_inline" in command: + raise ValueError( + "Exactly one of script_inline or script_file with " + "interpreter is expected: %r" % command) + # User tries to upload a shell? Make sure it is same as interpreter + interpreter = command.get("interpreter") + interpreter = (interpreter[-1] + if isinstance(interpreter, (tuple, list)) + else interpreter) + if (command.get("local_path") and + command.get("remote_path") != interpreter): + raise ValueError( + "When uploading an interpreter its path should be as well" + " specified as the `remote_path' string: %r" % command) + elif not command.get("remote_path"): + # No interpreter and no remote command to execute is given + raise ValueError( + "Supplied dict specifies no command to execute, either " + "interpreter or remote_path is required: %r" % command) + + unexpected_keys = set(command) - {"script_file", "script_inline", + "interpreter", "remote_path", + "local_path", "command_args"} + if unexpected_keys: + raise ValueError( + "Unexpected command parameters: %s" % ", ".join( + unexpected_keys)) + + def validate(self, config, credentials, plugin_cls, plugin_cfg): + command = config.get("args", {}).get(self.param_name) + if command is None and not self.required: + return + + try: + self.check_command_dict(command) + except ValueError as e: + return self.fail(str(e)) + + for key in "script_file", "local_path": + if command.get(key): + return validators.ValidatorUtils._file_access_ok( + filename=command[key], mode=os.R_OK, + param_name=self.param_name, required=self.required) + + @types.convert(image={"type": "glance_image"}, flavor={"type": "nova_flavor"}) @validation.add("image_valid_on_flavor", flavor_param="flavor", image_param="image", fail_on_404_image=False) -@validation.valid_command("command") +@validation.add("valid_command", param_name="command") @validation.add("number", param_name="port", minval=1, maxval=65535, nullable=True, integer_only=True) @validation.add("external_network_exists", param_name="floating_network") @@ -398,7 +480,7 @@ EOF flavor={"type": "nova_flavor"}) @validation.add("image_valid_on_flavor", flavor_param="flavor", image_param="image") -@validation.valid_command("command") +@validation.add("valid_command", param_name="command") @validation.add("number", param_name="port", minval=1, maxval=65535, nullable=True, integer_only=True) @validation.add("external_network_exists", param_name="floating_network") diff --git a/rally/plugins/openstack/validators.py b/rally/plugins/openstack/validators.py index 94852e30..4a3d2f74 100644 --- a/rally/plugins/openstack/validators.py +++ b/rally/plugins/openstack/validators.py @@ -23,8 +23,10 @@ from rally.task import types from rally.common import logging from rally.common import validation +from rally.common import yamlutils as yaml from rally import consts from rally import exceptions +from rally.plugins.common import validators from rally.plugins.openstack.context.nova import flavors as flavors_ctx from rally.plugins.openstack import types as openstack_types @@ -151,9 +153,62 @@ class RequiredNeutronExtensionsValidator(validation.Validator): return self.fail(msg) +@validation.add("required_platform", platform="openstack", users=True) +@validation.configure(name="flavor_exists", platform="openstack") +class FlavorExistsValidator(validation.Validator): + + def __init__(self, param_name): + """Returns validator for flavor + + :param param_name: defines which variable should be used + to get flavor id value. + """ + super(FlavorExistsValidator, self).__init__() + + self.param_name = param_name + + def _get_flavor_from_context(self, 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 = "" % flavor.name + return flavor + + def _get_validated_flavor(self, config, clients, param_name): + flavor_value = config.get("args", {}).get(param_name) + if not flavor_value: + msg = "Parameter %s is not specified." % param_name + return ValidationResult(False, msg), None + try: + flavor_id = openstack_types.Flavor.transform( + clients=clients, resource_config=flavor_value) + flavor = clients.nova().flavors.get(flavor=flavor_id) + return ValidationResult(True), flavor + except (nova_exc.NotFound, exceptions.InvalidScenarioArgument): + try: + return ValidationResult(True), self._get_flavor_from_context( + config, flavor_value) + except exceptions.InvalidScenarioArgument: + pass + message = "Flavor '%s' not found" % flavor_value + return ValidationResult(False, message), None + + def validate(self, config, credentials, plugin_cls, plugin_cfg): + # flavors do not depend on user or tenant, so checking for one user + # should be enough + user = credentials["openstack"]["users"][0] + clients = user["credential"].clients() + return self._get_validated_flavor(config, clients, self.param_name)[0] + + @validation.add("required_platform", platform="openstack", users=True) @validation.configure(name="image_valid_on_flavor", platform="openstack") -class ImageValidOnFlavorValidator(validation.Validator): +class ImageValidOnFlavorValidator(FlavorExistsValidator): def __init__(self, flavor_param, image_param, fail_on_404_image=True, validate_disk=True): @@ -170,8 +225,7 @@ class ImageValidOnFlavorValidator(validation.Validator): :param fail_on_404_image: flag what indicate whether to validate image or not. """ - super(ImageValidOnFlavorValidator, self).__init__() - self.flavor_name = flavor_param + super(ImageValidOnFlavorValidator, self).__init__(flavor_param) self.image_name = image_param self.fail_on_404_image = fail_on_404_image self.validate_disk = validate_disk @@ -219,36 +273,6 @@ class ImageValidOnFlavorValidator(validation.Validator): message = ("Image '%s' not found") % image_args return (ValidationResult(False, message), None) - def _get_flavor_from_context(self, 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 = "" % flavor.name - return (ValidationResult(True), flavor) - - def _get_validated_flavor(self, config, clients, param_name): - flavor_value = config.get("args", {}).get(param_name) - if not flavor_value: - msg = "Parameter %s is not specified." % param_name - return (ValidationResult(False, msg), None) - try: - flavor_id = openstack_types.Flavor.transform( - clients=clients, resource_config=flavor_value) - flavor = clients.nova().flavors.get(flavor=flavor_id) - return (ValidationResult(True), flavor) - except (nova_exc.NotFound, exceptions.InvalidScenarioArgument): - try: - return self._get_flavor_from_context(config, flavor_value) - except exceptions.InvalidScenarioArgument: - pass - message = ("Flavor '%s' not found") % flavor_value - return (ValidationResult(False, message), None) - def validate(self, config, credentials, plugin_cls, plugin_cfg): flavor = None @@ -257,7 +281,7 @@ class ImageValidOnFlavorValidator(validation.Validator): if not flavor: valid_result, flavor = self._get_validated_flavor( - config, clients, self.flavor_name) + config, clients, self.param_name) if not valid_result.is_valid: return valid_result @@ -555,3 +579,35 @@ class VolumeTypeExistsValidator(validation.Validator): else: msg = ("The parameter '{}' is required and should not be empty.") return self.fail(msg.format(self.param)) + + +@validation.configure(name="workbook_contains_workflow", platform="openstack") +class WorkbookContainsWorkflowValidator(validation.Validator): + + def __init__(self, param_name, workflow_name): + """Validate that workflow exist in workbook when workflow is passed + + :param param_name: parameter containing the workbook definition + :param workflow_name: parameter containing the workflow name + """ + super(WorkbookContainsWorkflowValidator, self).__init__() + self.param_name = param_name + self.workflow_name = workflow_name + + def validate(self, config, credentials, plugin_cls, plugin_cfg): + wf_name = config.get("args", {}).get(self.param_name) + if wf_name: + wb_path = config.get("args", {}).get(self.param_name) + wb_path = os.path.expanduser(wb_path) + file_result = validators.ValidatorUtils._file_access_ok( + config.get("args", {}).get(self.param_name), + os.R_OK, self.param_name) + if not file_result.is_valid: + return file_result + + with open(wb_path, "r") as wb_def: + wb_def = yaml.safe_load(wb_def) + if wf_name not in wb_def["workflows"]: + self.fail("workflow '{}' not found " + "in the definition '{}'".format(wf_name, + wb_def)) diff --git a/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py b/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py index 269eade1..2df9657e 100644 --- a/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py +++ b/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py @@ -13,9 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. +import os + import ddt import mock +from rally.common import validation from rally import exceptions from rally.plugins.openstack.scenarios.vm import vmtasks from tests.unit import test @@ -284,3 +287,123 @@ class VMTasksTestCase(test.ScenarioTestCase): "description": "Data generated by workload", "title": "Workload summary"} scenario.add_output.assert_called_once_with(complete=expected) + + +@ddt.ddt +class ValidCommandValidatorTestCase(test.TestCase): + + def setUp(self): + super(ValidCommandValidatorTestCase, self).setUp() + self.credentials = dict(openstack={"admin": mock.MagicMock(), + "users": [mock.MagicMock()], }) + + @ddt.data({"raises_message": "Command must be a dictionary"}, + {"command": "foo", + "raises_message": "Command must be a dictionary"}, + {"command": {"interpreter": "foobar", "script_file": "foo", + "script_inline": "bar"}, + "raises_message": "Exactly one of "}, + {"command": {"script_file": "foobar"}, + "raises_message": "Supplied dict specifies no"}, + {"command": {"script_inline": "foobar", + "interpreter": "foo", + "local_path": "bar"}, + "raises_message": "When uploading an interpreter its path"}, + {"command": {"interpreter": "/bin/bash", + "script_path": "foo"}, + "raises_message": ("Unexpected command parameters: " + "script_path")}, + {"command": {"script_inline": "foobar", + "interpreter": ["ENV=bar", "/bin/foo"], + "local_path": "bar", + "remote_path": "/bin/foo"}}, + {"command": {"script_inline": "foobar", "interpreter": "foo"}}) + @ddt.unpack + def test_check_command_dict(self, command=None, raises_message=None): + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + if raises_message: + self.assertRaises( + ValueError, validator.check_command_dict, command) + else: + self.assertIsNone(validator.check_command_dict(command)) + + @mock.patch("rally.plugins.common.validators.ValidatorUtils" + "._file_access_ok") + def test_validate(self, mock__file_access_ok): + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + mock__file_access_ok.return_value = None + command = {"script_file": "foobar", "interpreter": "foo"} + result = validator.validate({"args": {"p": command}}, + self.credentials, None, None) + self.assertIsNone(result) + mock__file_access_ok.assert_called_once_with( + filename="foobar", mode=os.R_OK, param_name="p", + required=True) + + def test_valid_command_not_required(self): + validator = vmtasks.ValidCommandValidator(param_name="p", + required=False) + result = validator.validate({"args": {"p": None}}, + self.credentials, None, None) + self.assertIsNone(result) + + def test_valid_command_required(self): + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + + result = validator.validate({"args": {"p": None}}, + self.credentials, None, None) + self.assertEqual("Command must be a dictionary", result.msg) + + @mock.patch("rally.plugins.common.validators.ValidatorUtils" + "._file_access_ok") + def test_valid_command_unreadable_script_file(self, mock__file_access_ok): + mock__file_access_ok.return_value = validation.ValidationResult(False) + + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + + command = {"script_file": "foobar", "interpreter": "foo"} + result = validator.validate({"args": {"p": command}}, + self.credentials, None, None) + self.assertFalse(result.is_valid, result.msg) + + @mock.patch("%s.ValidCommandValidator.check_command_dict" % BASE) + def test_valid_command_fail_check_command_dict(self, + mock_check_command_dict): + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + + mock_check_command_dict.side_effect = ValueError("foobar") + command = {"foo": "bar"} + result = validator.validate({"args": {"p": command}}, + self.credentials, None, None) + self.assertFalse(result.is_valid, result.msg) + self.assertEqual("foobar", result.msg) + + def test_valid_command_script_inline(self): + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + + command = {"script_inline": "bar", "interpreter": "/bin/sh"} + result = validator.validate({"args": {"p": command}}, self.credentials, + None, None) + self.assertIsNone(result) + + @mock.patch("rally.plugins.common.validators.ValidatorUtils" + "._file_access_ok") + def test_valid_command_local_path(self, mock__file_access_ok): + mock__file_access_ok.return_value = validation.ValidationResult(False) + + validator = vmtasks.ValidCommandValidator(param_name="p", + required=True) + + command = {"remote_path": "bar", "local_path": "foobar"} + result = validator.validate({"args": {"p": command}}, self.credentials, + None, None) + self.assertFalse(result.is_valid, result.msg) + mock__file_access_ok.assert_called_once_with( + filename="foobar", mode=os.R_OK, param_name="p", + required=True) diff --git a/tests/unit/plugins/openstack/test_validators.py b/tests/unit/plugins/openstack/test_validators.py index 5b9f5b65..975b1f14 100644 --- a/tests/unit/plugins/openstack/test_validators.py +++ b/tests/unit/plugins/openstack/test_validators.py @@ -197,6 +197,89 @@ class RequiredNeutronExtensionsValidatorTestCase(test.TestCase): self.assertIsNone(result) +class FlavorExistsValidatorTestCase(test.TestCase): + + def setUp(self): + super(FlavorExistsValidatorTestCase, self).setUp() + self.validator = validators.FlavorExistsValidator( + param_name="foo_flavor") + self.config = copy.deepcopy(config) + self.credentials = copy.deepcopy(credentials) + + def test__get_validated_flavor_wrong_value_in_config(self): + + result = self.validator._get_validated_flavor(self.config, + self.credentials, + "foo_flavor") + self.assertEqual("Parameter foo_flavor is not specified.", + result[0].msg) + + @mock.patch("rally.plugins.openstack.validators" + ".openstack_types.Flavor.transform", + return_value="flavor_id") + def test__get_validated_flavor(self, mock_flavor_transform): + + clients = mock.Mock() + clients.nova().flavors.get.return_value = "flavor" + + result = self.validator._get_validated_flavor(self.config, + clients, + "flavor") + self.assertTrue(result[0].is_valid, result[0].msg) + self.assertEqual("flavor", result[1]) + + mock_flavor_transform.assert_called_once_with( + clients=clients, resource_config=self.config["args"]["flavor"]) + clients.nova().flavors.get.assert_called_once_with(flavor="flavor_id") + + clients.side_effect = exceptions.InvalidScenarioArgument("") + result = self.validator._get_validated_flavor(self.config, + clients, + "flavor") + self.assertTrue(result[0].is_valid, result[0].msg) + self.assertEqual("flavor", result[1]) + mock_flavor_transform.assert_called_with( + clients=clients, resource_config=self.config["args"]["flavor"]) + clients.nova().flavors.get.assert_called_with(flavor="flavor_id") + + @mock.patch("rally.plugins.openstack.validators" + ".openstack_types.Flavor.transform") + def test__get_validated_flavor_not_found(self, mock_flavor_transform): + + clients = mock.MagicMock() + clients.nova().flavors.get.side_effect = nova_exc.NotFound("") + + result = self.validator._get_validated_flavor(self.config, + clients, + "flavor") + self.assertFalse(result[0].is_valid, result[0].msg) + self.assertEqual("Flavor '%s' not found" % + self.config["args"]["flavor"], + result[0].msg) + mock_flavor_transform.assert_called_once_with( + clients=clients, resource_config=self.config["args"]["flavor"]) + + @mock.patch("rally.plugins.openstack.validators" + ".types.obj_from_name") + @mock.patch("rally.plugins.openstack.validators" + ".flavors_ctx.FlavorConfig") + def test__get_flavor_from_context(self, mock_flavor_config, + mock_obj_from_name): + config = {"context": {"images": {"fake_parameter_name": "foo_image"}, + } + } + + self.assertRaises(exceptions.InvalidScenarioArgument, + self.validator._get_flavor_from_context, + config, "foo_flavor") + + config = {"context": {"images": {"fake_parameter_name": "foo_image"}, + "flavors": [{"flavor1": "fake_flavor1"}]} + } + result = self.validator._get_flavor_from_context(config, "foo_flavor") + self.assertEqual("" % result.name, result.id) + + @ddt.ddt class ImageValidOnFlavorValidatorTestCase(test.TestCase): @@ -295,59 +378,6 @@ class ImageValidOnFlavorValidatorTestCase(test.TestCase): result = validator.validate(self.config, self.credentials, None, None) self.assertIsNone(result) - def test__get_validated_flavor_wrong_value_in_config(self): - - result = self.validator._get_validated_flavor(self.config, - self.credentials, - "foo_flavor") - self.assertEqual("Parameter foo_flavor is not specified.", - result[0].msg) - - @mock.patch("rally.plugins.openstack.validators" - ".openstack_types.Flavor.transform", - return_value="flavor_id") - def test__get_validated_flavor(self, mock_flavor_transform): - - clients = mock.Mock() - clients.nova().flavors.get.return_value = "flavor" - - result = self.validator._get_validated_flavor(self.config, - clients, - "flavor") - self.assertTrue(result[0].is_valid, result[0].msg) - self.assertEqual("flavor", result[1]) - - mock_flavor_transform.assert_called_once_with( - clients=clients, resource_config=self.config["args"]["flavor"]) - clients.nova().flavors.get.assert_called_once_with(flavor="flavor_id") - - clients.side_effect = exceptions.InvalidScenarioArgument("") - result = self.validator._get_validated_flavor(self.config, - clients, - "flavor") - self.assertTrue(result[0].is_valid, result[0].msg) - self.assertEqual("flavor", result[1]) - mock_flavor_transform.assert_called_with( - clients=clients, resource_config=self.config["args"]["flavor"]) - clients.nova().flavors.get.assert_called_with(flavor="flavor_id") - - @mock.patch("rally.plugins.openstack.validators" - ".openstack_types.Flavor.transform") - def test__get_validated_flavor_not_found(self, mock_flavor_transform): - - clients = mock.MagicMock() - clients.nova().flavors.get.side_effect = nova_exc.NotFound("") - - result = self.validator._get_validated_flavor(self.config, - clients, - "flavor") - self.assertFalse(result[0].is_valid, result[0].msg) - self.assertEqual("Flavor '%s' not found" % - self.config["args"]["flavor"], - result[0].msg) - mock_flavor_transform.assert_called_once_with( - clients=clients, resource_config=self.config["args"]["flavor"]) - @mock.patch("rally.plugins.openstack.validators" ".openstack_types.GlanceImage.transform", return_value="image_id") @@ -452,29 +482,6 @@ class ImageValidOnFlavorValidatorTestCase(test.TestCase): clients=clients, resource_config=config["args"]["image"]) clients.glance().images.get.assert_called_with("image_id") - @mock.patch("rally.plugins.openstack.validators" - ".types.obj_from_name") - @mock.patch("rally.plugins.openstack.validators" - ".flavors_ctx.FlavorConfig") - def test__get_flavor_from_context(self, mock_flavor_config, - mock_obj_from_name): - config = {"context": {"images": {"fake_parameter_name": "foo_image"}, - } - } - - self.assertRaises(exceptions.InvalidScenarioArgument, - self.validator._get_flavor_from_context, - config, "foo_flavor") - - config = {"context": {"images": {"fake_parameter_name": "foo_image"}, - "flavors": [{"flavor1": "fake_flavor1"}]} - } - result = self.validator._get_flavor_from_context(config, "foo_flavor") - - self.assertIsInstance(result[0], validators.ValidationResult) - self.assertTrue(result[0].is_valid) - self.assertEqual("" % result[1].name, result[1].id) - class RequiredClientsValidatorTestCase(test.TestCase): @@ -837,3 +844,47 @@ class VolumeTypeExistsValidatorTestCase(test.TestCase): self.assertEqual(err_msg.format(fake_user), result.msg) else: self.assertIsNone(result) + + +@ddt.ddt +class WorkbookContainsWorkflowValidatorTestCase(test.TestCase): + + def setUp(self): + super(WorkbookContainsWorkflowValidatorTestCase, self).setUp() + self.config = copy.deepcopy(config) + self.credentials = copy.deepcopy(credentials) + + @mock.patch("rally.common.yamlutils.safe_load") + @mock.patch("rally.plugins.openstack.validators.os.access") + @mock.patch("rally.plugins.openstack.validators.open") + def test_validator(self, mock_open, mock_access, mock_safe_load): + mock_safe_load.return_value = { + "version": "2.0", + "name": "wb", + "workflows": { + "wf1": { + "type": "direct", + "tasks": { + "t1": { + "action": "std.noop" + } + } + } + } + } + validator = validators.WorkbookContainsWorkflowValidator( + param_name="definition", workflow_name="workflow_name") + + context = { + "args": { + "definition": "fake_path1", + "workflow_name": "wf1" + } + } + + result = validator.validate(context, None, None, None) + self.assertIsNone(result) + + self.assertEqual(1, mock_open.called) + self.assertEqual(1, mock_access.called) + self.assertEqual(1, mock_safe_load.called)