Introduce command-dict and validator

Refactor `vm.utils._run_command_over_ssh' making it accept a dict to
specify command as either local script file, inline script or remote
script path. Both local and inline scripts require `interpreter' being
specified. Any of the above can be a list specifying e.g. environment
variables (for `interpreter') or command args.

Introduce `valid_command' validator that checks whether the specified
command dictionary is a valid one.

Change-Id: I3c495297251c87529b9adf1e769cfe277338f5fc
Implements: blueprint vm-workloads-framework
This commit is contained in:
Pavel Boldin 2015-04-23 22:01:44 +03:00
parent c6ac4ef45e
commit 0c4de04da0
6 changed files with 240 additions and 72 deletions

View File

@ -31,6 +31,9 @@ from rally import osclients
from rally.plugins.openstack.context import flavors as flavors_ctx
from rally.verification.tempest import tempest
# TODO(boris-42): make the validators usable as a functions as well.
# At the moment validators can only be used as decorators.
class ValidationResult(object):
@ -165,6 +168,87 @@ def file_exists(config, clients, deployment, param_name, mode=os.R_OK,
param_name, required)
def check_command_dict(command):
"""Check command-specifying dict `command', raise ValueError on error."""
# 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 len([1 for k in ("remote_path", "script_file", "script_inline")
if command.get(k)]) != 1:
raise ValueError(
"Exactly one of script_inline, script_file or remote_path"
" is expected: %r" % command)
if ((command.get("script_file") or command.get("script_inline")) and
"interpreter" not in command):
raise ValueError(
"An `interpreter' is required for both script_file and"
" script_inline: %r" % command)
@validator
def valid_command(config, clients, deployment, param_name, required=True):
"""Checks that parameter is a proper command-specifying dictionary.
Ensure that the command dictionary either specifies remote command path
via `remote_path' (optionally copied from a local file specified by
`local_path`), an inline script via `script_inline' or a local script
file path using `script_file'. `script_file' and `local_path' are checked
to be accessible like in `file_exists' validator.
The `script_inline' and `script_file' both require an `interpreter' value
to specify the interpreter script should be run with.
Note that `interpreter' and `remote_path' can be an array specifying
environment variables and args.
Examples::
# Run a `local_script.pl' file sending it to a remote Perl interpreter
command = {
"script_file": "local_script.pl",
"interpreter": "/usr/bin/perl"
}
# Run an inline script sending it to a remote interpreter
command = {
"script_inline": "echo 'Hello, World!'",
"interpreter": "/bin/sh"
}
# Run a remote command
command = {
"remote_path": "/bin/false"
}
# Run an inline script sending it to a remote interpreter
command = {
"script_inline": "echo \"Hello, ${NAME:-World}\"",
"interpreter": ["NAME=Earth", "/bin/sh"]
}
:param param_name: Name of parameter to validate
:param required: Boolean indicating that the command dictionary is required
"""
# TODO(pboldin): Make that a `jsonschema' check once generic validator
# is available.
command = config.get("args", {}).get(param_name)
if command is None:
return ValidationResult(not required,
"Command dicitionary is required")
try:
check_command_dict(command)
except ValueError as e:
return ValidationResult(False, str(e))
if command.get("script_file"):
return _file_access_ok(command["script_file"], os.R_OK,
param_name + ".script_file", True)
return ValidationResult(True)
def _get_validated_image(config, clients, param_name):
image_context = config.get("context", {}).get("images", {})
image_args = config.get("args", {}).get(param_name)

View File

@ -21,10 +21,10 @@ import six
from rally.benchmark.scenarios import base
from rally.benchmark import utils as bench_utils
from rally.benchmark import validation
from rally.common.i18n import _
from rally.common import log as logging
from rally.common import sshutils
from rally import exceptions
from rally.plugins.openstack.wrappers import network as network_wrapper
LOG = logging.getLogger(__name__)
@ -40,32 +40,33 @@ class VMScenario(base.Scenario):
"""
@base.atomic_action_timer("vm.run_command_over_ssh")
def _run_command_over_ssh(self, ssh, interpreter, script,
is_file=True):
def _run_command_over_ssh(self, ssh, command):
"""Run command inside an instance.
This is a separate function so that only script execution is timed.
:param ssh: A SSHClient instance.
:param interpreter: The interpreter that will be used to execute
the script.
:param script: Path to the script file or its content in a StringIO.
:param is_file: if True, script represent a path,
else, script contains an inline script.
:param command: Dictionary specifying command to execute.
See `validation.valid_command' docstring for details.
:returns: tuple (exit_status, stdout, stderr)
"""
if not is_file:
stdin = script
elif isinstance(script, six.string_types):
stdin = open(script, "rb")
elif isinstance(script, six.moves.StringIO):
stdin = script
else:
raise exceptions.ScriptError(
"Either file path or StringIO expected, given %s" %
type(script).__name__)
validation.check_command_dict(command)
return ssh.execute(interpreter, stdin=stdin)
# NOTE(pboldin): Here we `get' the values and not check for the keys
# due to template-driven configuration generation that can leave keys
# defined but values empty.
if command.get("script_file") or command.get("script_inline"):
cmd = command["interpreter"]
if command.get("script_file"):
stdin = open(command["script_file"], "rb")
elif command.get("script_inline"):
stdin = six.moves.StringIO(command["script_inline"])
elif command.get("remote_path"):
cmd = command["remote_path"]
stdin = None
return ssh.execute(cmd, stdin=stdin)
def _boot_server_with_fip(self, image, flavor,
use_floating_ip=True, floating_network=None,
@ -135,30 +136,30 @@ class VMScenario(base.Scenario):
timeout=120
)
def _run_command(self, server_ip, port, username, password, interpreter,
script, pkey=None, is_file=True):
def _run_command(self, server_ip, port, username, password, command,
pkey=None):
"""Run command via SSH on server.
Create SSH connection for server, wait for server to become
available (there is a delay between server being set to ACTIVE
and sshd being available). Then call run_command_over_ssh to actually
execute the command.
Create SSH connection for server, wait for server to become available
(there is a delay between server being set to ACTIVE and sshd being
available). Then call run_command_over_ssh to actually execute the
command.
:param server_ip: server ip address
:param port: ssh port for SSH connection
:param username: str. ssh username for server
:param password: Password for SSH authentication
:param interpreter: server's interpreter to execute the script
:param script: script to run on server
:param command: Dictionary specifying command to execute.
See `valiation.valid_command' docstring for explanation.
:param pkey: key for SSH authentication
:param is_file: if True, script represent a path,
else, script contains an inline script.
:returns: tuple (exit_status, stdout, stderr)
"""
pkey = pkey if pkey else self.context["user"]["keypair"]["private"]
ssh = sshutils.SSH(username, server_ip, port=port,
pkey=pkey, password=password)
self._wait_for_ssh(ssh)
return self._run_command_over_ssh(ssh, interpreter,
script, is_file)
return self._run_command_over_ssh(ssh, command)
@staticmethod
def _ping_ip_address(host):

View File

@ -84,8 +84,9 @@ class VMTasks(nova_utils.NovaScenario, vm_utils.VMScenario,
key_name=self.context["user"]["keypair"]["name"],
**kwargs)
try:
code, out, err = self._run_command(fip["ip"], port, username,
password, interpreter, script)
code, out, err = self._run_command(
fip["ip"], port, username, password,
command={"script_file": script, "interpreter": interpreter})
if code:
raise exceptions.ScriptError(
"Error running script %(script)s. "

View File

@ -132,6 +132,77 @@ class ValidatorsTestCase(test.TestCase):
mock__file_access_ok.assert_called_once_with(
"test_file", os.R_OK, "p", False)
def test_check_command_valid(self):
e = self.assertRaises(
ValueError, validation.check_command_dict,
{"script_file": "foo", "remote_path": "bar"})
self.assertIn("Exactly one of ", str(e))
e = self.assertRaises(
ValueError, validation.check_command_dict,
{"script_file": "foobar"})
self.assertIn("An `interpreter' is required for", str(e))
e = self.assertRaises(
ValueError, validation.check_command_dict,
{"script_inline": "foobar"})
self.assertIn("An `interpreter' is required for", str(e))
command = {"script_inline": "foobar", "interpreter": "foo"}
result = validation.check_command_dict(command)
self.assertIsNone(result)
@mock.patch("rally.benchmark.validation._file_access_ok")
def test_valid_command(self, mock__file_access_ok):
validator = self._unwrap_validator(validation.valid_command,
param_name="p")
mock__file_access_ok.return_value = validation.ValidationResult(True)
command = {"script_file": "foobar", "interpreter": "foo"}
result = validator({"args": {"p": command}}, None, None)
self.assertTrue(result.is_valid, result.msg)
mock__file_access_ok.assert_called_once_with(
"foobar", os.R_OK, "p.script_file", True)
def test_valid_command_required(self):
validator = self._unwrap_validator(validation.valid_command,
param_name="p")
result = validator({"args": {"p": None}}, None, None)
self.assertFalse(result.is_valid, result.msg)
@mock.patch("rally.benchmark.validation._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 = self._unwrap_validator(validation.valid_command,
param_name="p")
command = {"script_file": "foobar", "interpreter": "foo"}
result = validator({"args": {"p": command}}, None, None)
self.assertFalse(result.is_valid, result.msg)
@mock.patch("rally.benchmark.validation.check_command_dict")
def test_valid_command_fail_check_command_dict(self,
mock_check_command_dict):
validator = self._unwrap_validator(validation.valid_command,
param_name="p")
mock_check_command_dict.side_effect = ValueError("foobar")
command = {"foo": "bar"}
result = validator({"args": {"p": command}}, None, None)
self.assertFalse(result.is_valid, result.msg)
self.assertEqual("foobar", result.msg)
def test_valid_command_script_inline(self):
validator = self._unwrap_validator(validation.valid_command,
param_name="p")
command = {"script_inline": "bar", "interpreter": "/bin/sh"}
result = validator({"args": {"p": command}}, None, None)
self.assertTrue(result.is_valid, result.msg)
def test__get_validated_image_no_value_in_config(self):
result = validation._get_validated_image({}, None, "non_existing")
self.assertFalse(result[0].is_valid, result[0].msg)

View File

@ -19,9 +19,7 @@ import subprocess
import mock
import netaddr
from oslotest import mockpatch
import six
from rally import exceptions
from rally.plugins.openstack.scenarios.vm import utils
from tests.unit import test
@ -38,26 +36,55 @@ class VMScenarioTestCase(test.TestCase):
@mock.patch("%s.open" % VMTASKS_UTILS,
side_effect=mock.mock_open(), create=True)
def test__run_command_over_ssh(self, mock_open):
def test__run_command_over_ssh_script_file(self, mock_open):
mock_ssh = mock.MagicMock()
vm_scenario = utils.VMScenario()
vm_scenario._run_command_over_ssh(mock_ssh, "interpreter", "script")
mock_ssh.execute.assert_called_once_with("interpreter",
stdin=mock_open.side_effect())
vm_scenario._run_command_over_ssh(
mock_ssh,
{
"script_file": "foobar",
"interpreter": ["interpreter", "interpreter_arg"],
}
)
mock_ssh.execute.assert_called_once_with(
["interpreter", "interpreter_arg"],
stdin=mock_open.side_effect())
mock_open.assert_called_once_with("foobar", "rb")
def test__run_command_over_ssh_stringio(self):
@mock.patch("%s.six.moves.StringIO" % VMTASKS_UTILS)
def test__run_command_over_ssh_script_inline(self, mock_stringio):
mock_ssh = mock.MagicMock()
vm_scenario = utils.VMScenario()
script = six.moves.StringIO("script")
vm_scenario._run_command_over_ssh(mock_ssh, "interpreter", script)
mock_ssh.execute.assert_called_once_with("interpreter",
stdin=script)
vm_scenario._run_command_over_ssh(
mock_ssh,
{
"script_inline": "foobar",
"interpreter": ["interpreter", "interpreter_arg"],
}
)
mock_ssh.execute.assert_called_once_with(
["interpreter", "interpreter_arg"],
stdin=mock_stringio.return_value)
mock_stringio.assert_called_once_with("foobar")
def test__run_command_over_ssh_remote_path(self):
mock_ssh = mock.MagicMock()
vm_scenario = utils.VMScenario()
vm_scenario._run_command_over_ssh(
mock_ssh,
{
"remote_path": ["foo", "bar"],
}
)
mock_ssh.execute.assert_called_once_with(
["foo", "bar"],
stdin=None)
def test__run_command_over_ssh_fails(self):
vm_scenario = utils.VMScenario()
self.assertRaises(exceptions.ScriptError,
self.assertRaises(ValueError,
vm_scenario._run_command_over_ssh,
None, "interpreter", 10)
None, command={})
def test__wait_for_ssh(self):
ssh = mock.MagicMock()
@ -85,35 +112,17 @@ class VMScenarioTestCase(test.TestCase):
vm_scenario = utils.VMScenario()
vm_scenario.context = {"user": {"keypair": {"private": "ssh"}}}
vm_scenario._run_command("1.2.3.4", 22, "username",
"password", "int", "/path/to/foo/script.sh",
is_file=True)
vm_scenario._run_command("1.2.3.4", 22, "username", "password",
command={"script_file": "foo",
"interpreter": "bar"})
mock_ssh_class.assert_called_once_with("username", "1.2.3.4", port=22,
pkey="ssh",
password="password")
mock_ssh_instance.wait.assert_called_once_with()
mock_run_command_over_ssh.assert_called_once_with(
mock_ssh_instance, "int", "/path/to/foo/script.sh", True)
@mock.patch(VMTASKS_UTILS + ".sshutils.SSH")
def test__run_command_inline_script(self, mock_ssh):
mock_ssh_instance = mock.MagicMock()
mock_ssh.return_value = mock_ssh_instance
mock_ssh_instance.execute.return_value = "foobar"
vm_scenario = utils.VMScenario()
vm_scenario._wait_for_ssh = mock.Mock()
vm_scenario.context = {"user": {"keypair": {"private": "foo_pkey"}}}
result = vm_scenario._run_command("foo_ip", "foo_port", "foo_username",
"foo_password", "foo_interpreter",
"foo_script", is_file=False)
mock_ssh.assert_called_once_with("foo_username", "foo_ip",
port="foo_port", pkey="foo_pkey",
password="foo_password")
vm_scenario._wait_for_ssh.assert_called_once_with(mock_ssh_instance)
mock_ssh_instance.execute.assert_called_once_with("foo_interpreter",
stdin="foo_script")
self.assertEqual(result, "foobar")
mock_ssh_instance,
{"script_file": "foo", "interpreter": "bar"})
@mock.patch(VMTASKS_UTILS + ".sys")
@mock.patch("subprocess.Popen")

View File

@ -37,8 +37,9 @@ class VMTasksTestCase(test.TestCase):
def test_boot_runcommand_delete(self):
self.scenario.boot_runcommand_delete(
"foo_image", "foo_flavor", "foo_script",
"foo_interpreter", "foo_username",
"foo_image", "foo_flavor",
script="foo_script", interpreter="foo_interpreter",
username="foo_username",
password="foo_password",
use_floating_ip="use_fip",
floating_network="ext_network",
@ -56,7 +57,8 @@ class VMTasksTestCase(test.TestCase):
self.scenario._run_command.assert_called_once_with(
"foo_ip", 22, "foo_username", "foo_password",
"foo_interpreter", "foo_script")
command={"script_file": "foo_script",
"interpreter": "foo_interpreter"})
self.scenario._delete_server_with_fip.assert_called_once_with(
"foo_server", self.ip, force_delete="foo_force")