424 lines
19 KiB
Python
424 lines
19 KiB
Python
# Copyright 2013: Rackspace UK
|
|
# 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 os
|
|
from unittest import mock
|
|
|
|
import ddt
|
|
|
|
from rally.common import validation
|
|
from rally import exceptions
|
|
|
|
from rally_openstack.scenarios.vm import vmtasks
|
|
from tests.unit import test
|
|
|
|
|
|
BASE = "rally_openstack.scenarios.vm.vmtasks"
|
|
|
|
|
|
@ddt.ddt
|
|
class VMTasksTestCase(test.ScenarioTestCase):
|
|
|
|
def setUp(self):
|
|
super(VMTasksTestCase, self).setUp()
|
|
self.context.update({"user": {"keypair": {"name": "keypair_name"},
|
|
"credential": mock.MagicMock()}})
|
|
|
|
cinder_patcher = mock.patch(
|
|
"rally_openstack.services.storage.block.BlockStorage")
|
|
self.cinder = cinder_patcher.start().return_value
|
|
self.cinder.create_volume.return_value = mock.Mock(id="foo_volume")
|
|
self.addCleanup(cinder_patcher.stop)
|
|
|
|
def create_env(self, scenario):
|
|
self.ip = {"id": "foo_id", "ip": "foo_ip", "is_floating": True}
|
|
scenario._boot_server_with_fip = mock.Mock(
|
|
return_value=("foo_server", self.ip))
|
|
scenario._wait_for_ping = mock.Mock()
|
|
scenario._delete_server_with_fip = mock.Mock()
|
|
scenario._run_command = mock.MagicMock(
|
|
return_value=(0, "{\"foo\": 42}", "foo_err"))
|
|
scenario.add_output = mock.Mock()
|
|
return scenario
|
|
|
|
def test_boot_runcommand_delete(self):
|
|
scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))
|
|
scenario._run_command = mock.MagicMock(
|
|
return_value=(0, "{\"foo\": 42}", "foo_err"))
|
|
scenario.run("foo_flavor", image="foo_image",
|
|
command={"script_file": "foo_script",
|
|
"interpreter": "foo_interpreter"},
|
|
username="foo_username",
|
|
password="foo_password",
|
|
use_floating_ip="use_fip",
|
|
floating_network="ext_network",
|
|
force_delete="foo_force",
|
|
volume_args={"size": 16},
|
|
foo_arg="foo_value")
|
|
|
|
self.cinder.create_volume.assert_called_once_with(16, imageRef=None)
|
|
scenario._boot_server_with_fip.assert_called_once_with(
|
|
"foo_image", "foo_flavor", key_name="keypair_name",
|
|
use_floating_ip="use_fip", floating_network="ext_network",
|
|
block_device_mapping={"vdrally": "foo_volume:::1"},
|
|
foo_arg="foo_value")
|
|
|
|
scenario._wait_for_ping.assert_called_once_with("foo_ip")
|
|
scenario._run_command.assert_called_once_with(
|
|
"foo_ip", 22, "foo_username", "foo_password",
|
|
command={"script_file": "foo_script",
|
|
"interpreter": "foo_interpreter"})
|
|
scenario._delete_server_with_fip.assert_called_once_with(
|
|
"foo_server", self.ip, force_delete="foo_force")
|
|
scenario.add_output.assert_called_once_with(
|
|
complete={"chart_plugin": "TextArea",
|
|
"data": [
|
|
"StdErr: foo_err",
|
|
"StdOut:",
|
|
"{\"foo\": 42}"],
|
|
"title": "Script Output"})
|
|
|
|
@ddt.data(
|
|
{"output": (0, "", ""),
|
|
"expected": [{"complete": {"chart_plugin": "TextArea",
|
|
"data": [
|
|
"StdErr: (none)",
|
|
"StdOut:",
|
|
""],
|
|
"title": "Script Output"}}]},
|
|
{"output": (1, "{\"foo\": 42}", ""), "raises": exceptions.ScriptError},
|
|
{"output": ("", 1, ""), "raises": TypeError},
|
|
{"output": (0, "{\"foo\": 42}", ""),
|
|
"expected": [{"complete": {"chart_plugin": "TextArea",
|
|
"data": [
|
|
"StdErr: (none)",
|
|
"StdOut:",
|
|
"{\"foo\": 42}"],
|
|
"title": "Script Output"}}]},
|
|
{"output": (0, "{\"additive\": [1, 2]}", ""),
|
|
"expected": [{"complete": {"chart_plugin": "TextArea",
|
|
"data": [
|
|
"StdErr: (none)",
|
|
"StdOut:", "{\"additive\": [1, 2]}"],
|
|
"title": "Script Output"}}]},
|
|
{"output": (0, "{\"complete\": [3, 4]}", ""),
|
|
"expected": [{"complete": {"chart_plugin": "TextArea",
|
|
"data": [
|
|
"StdErr: (none)",
|
|
"StdOut:",
|
|
"{\"complete\": [3, 4]}"],
|
|
"title": "Script Output"}}]},
|
|
{"output": (0, "{\"additive\": [1, 2], \"complete\": [3, 4]}", ""),
|
|
"expected": [{"additive": 1}, {"additive": 2},
|
|
{"complete": 3}, {"complete": 4}]}
|
|
)
|
|
@ddt.unpack
|
|
def test_boot_runcommand_delete_add_output(self, output,
|
|
expected=None, raises=None):
|
|
scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))
|
|
|
|
scenario._run_command.return_value = output
|
|
kwargs = {"flavor": "foo_flavor",
|
|
"image": "foo_image",
|
|
"command": {"remote_path": "foo"},
|
|
"username": "foo_username",
|
|
"password": "foo_password",
|
|
"use_floating_ip": "use_fip",
|
|
"floating_network": "ext_network",
|
|
"force_delete": "foo_force",
|
|
"volume_args": {"size": 16},
|
|
"foo_arg": "foo_value"}
|
|
if raises:
|
|
self.assertRaises(raises, scenario.run, **kwargs)
|
|
self.assertFalse(scenario.add_output.called)
|
|
else:
|
|
scenario.run(**kwargs)
|
|
calls = [mock.call(**kw) for kw in expected]
|
|
scenario.add_output.assert_has_calls(calls, any_order=True)
|
|
|
|
self.cinder.create_volume.assert_called_once_with(
|
|
16, imageRef=None)
|
|
scenario._boot_server_with_fip.assert_called_once_with(
|
|
"foo_image", "foo_flavor", key_name="keypair_name",
|
|
use_floating_ip="use_fip", floating_network="ext_network",
|
|
block_device_mapping={"vdrally": "foo_volume:::1"},
|
|
foo_arg="foo_value")
|
|
|
|
scenario._run_command.assert_called_once_with(
|
|
"foo_ip", 22, "foo_username", "foo_password",
|
|
command={"remote_path": "foo"})
|
|
scenario._delete_server_with_fip.assert_called_once_with(
|
|
"foo_server", self.ip, force_delete="foo_force")
|
|
|
|
def test_boot_runcommand_delete_command_timeouts(self):
|
|
scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))
|
|
|
|
scenario._run_command.side_effect = exceptions.SSHTimeout()
|
|
self.assertRaises(exceptions.SSHTimeout,
|
|
scenario.run,
|
|
"foo_flavor", "foo_image", "foo_interpreter",
|
|
"foo_script", "foo_username")
|
|
scenario._delete_server_with_fip.assert_called_once_with(
|
|
"foo_server", self.ip, force_delete=False)
|
|
self.assertFalse(scenario.add_output.called)
|
|
|
|
def test_boot_runcommand_delete_ping_wait_timeouts(self):
|
|
scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))
|
|
|
|
scenario._wait_for_ping.side_effect = exceptions.TimeoutException(
|
|
resource_type="foo_resource",
|
|
resource_name="foo_name",
|
|
resource_id="foo_id",
|
|
desired_status="foo_desired_status",
|
|
resource_status="foo_resource_status",
|
|
timeout=2)
|
|
exc = self.assertRaises(exceptions.TimeoutException,
|
|
scenario.run,
|
|
"foo_image", "foo_flavor", "foo_interpreter",
|
|
"foo_script", "foo_username",
|
|
wait_for_ping=True)
|
|
self.assertEqual(exc.kwargs["resource_type"], "foo_resource")
|
|
self.assertEqual(exc.kwargs["resource_name"], "foo_name")
|
|
self.assertEqual(exc.kwargs["resource_id"], "foo_id")
|
|
self.assertEqual(exc.kwargs["desired_status"], "foo_desired_status")
|
|
self.assertEqual(exc.kwargs["resource_status"], "foo_resource_status")
|
|
|
|
scenario._delete_server_with_fip.assert_called_once_with(
|
|
"foo_server", self.ip, force_delete=False)
|
|
self.assertFalse(scenario.add_output.called)
|
|
|
|
@mock.patch("%s.json" % BASE)
|
|
def test_boot_runcommand_delete_json_fails(self, mock_json):
|
|
scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))
|
|
|
|
mock_json.loads.side_effect = ValueError()
|
|
scenario.run("foo_image", "foo_flavor", "foo_interpreter",
|
|
"foo_script", "foo_username")
|
|
scenario.add_output.assert_called_once_with(complete={
|
|
"chart_plugin": "TextArea", "data": ["StdErr: foo_err",
|
|
"StdOut:", "{\"foo\": 42}"],
|
|
"title": "Script Output"})
|
|
scenario._delete_server_with_fip.assert_called_once_with(
|
|
"foo_server", self.ip, force_delete=False)
|
|
|
|
def test_boot_runcommand_delete_custom_image(self):
|
|
context = {
|
|
"user": {
|
|
"tenant_id": "tenant_id",
|
|
"keypair": {"name": "foo_keypair_name"},
|
|
"credential": mock.Mock()
|
|
},
|
|
"tenant": {
|
|
"custom_image": {"id": "image_id"}
|
|
}
|
|
}
|
|
|
|
scenario = self.create_env(vmtasks.BootRuncommandDelete(context))
|
|
scenario._run_command = mock.MagicMock(
|
|
return_value=(0, "{\"foo\": 42}", "foo_err"))
|
|
scenario.run("foo_flavor",
|
|
command={"script_file": "foo_script",
|
|
"interpreter": "foo_interpreter"},
|
|
username="foo_username",
|
|
password="foo_password",
|
|
use_floating_ip="use_fip",
|
|
floating_network="ext_network",
|
|
force_delete="foo_force",
|
|
volume_args={"size": 16},
|
|
foo_arg="foo_value")
|
|
|
|
self.cinder.create_volume.assert_called_once_with(16, imageRef=None)
|
|
scenario._boot_server_with_fip.assert_called_once_with(
|
|
"image_id", "foo_flavor", key_name="foo_keypair_name",
|
|
use_floating_ip="use_fip", floating_network="ext_network",
|
|
block_device_mapping={"vdrally": "foo_volume:::1"},
|
|
foo_arg="foo_value")
|
|
|
|
scenario._wait_for_ping.assert_called_once_with("foo_ip")
|
|
scenario._run_command.assert_called_once_with(
|
|
"foo_ip", 22, "foo_username", "foo_password",
|
|
command={"script_file": "foo_script",
|
|
"interpreter": "foo_interpreter"})
|
|
scenario._delete_server_with_fip.assert_called_once_with(
|
|
"foo_server", self.ip, force_delete="foo_force")
|
|
scenario.add_output.assert_called_once_with(
|
|
complete={"chart_plugin": "TextArea",
|
|
"data": [
|
|
"StdErr: foo_err",
|
|
"StdOut:", "{\"foo\": 42}"],
|
|
"title": "Script Output"})
|
|
|
|
@mock.patch("%s.heat" % BASE)
|
|
@mock.patch("%s.sshutils" % BASE)
|
|
def test_runcommand_heat(self, mock_sshutils, mock_heat):
|
|
fake_ssh = mock.Mock()
|
|
fake_ssh.execute.return_value = [0, "key:val", ""]
|
|
mock_sshutils.SSH.return_value = fake_ssh
|
|
fake_stack = mock.Mock()
|
|
fake_stack.stack.outputs = [{"output_key": "gate_node",
|
|
"output_value": "ok"}]
|
|
mock_heat.main.Stack.return_value = fake_stack
|
|
context = {
|
|
"user": {"keypair": {"name": "name", "private": "pk"},
|
|
"credential": mock.MagicMock()},
|
|
"tenant": {"networks": [{"router_id": "1"}]}
|
|
}
|
|
scenario = vmtasks.RuncommandHeat(context)
|
|
scenario.generate_random_name = mock.Mock(return_value="name")
|
|
scenario.add_output = mock.Mock()
|
|
workload = {"username": "admin",
|
|
"resource": ["foo", "bar"]}
|
|
scenario.run(workload, "template",
|
|
{"file_key": "file_value"},
|
|
{"param_key": "param_value"})
|
|
expected = {"chart_plugin": "Table",
|
|
"data": {"rows": [["key", "val"]],
|
|
"cols": ["key", "value"]},
|
|
"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.context = {"admin": {"credential": mock.MagicMock()},
|
|
"users": [{"credential": mock.MagicMock()}]}
|
|
|
|
@ddt.data({"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):
|
|
validator = vmtasks.ValidCommandValidator(param_name="p",
|
|
required=True)
|
|
self.assertIsNone(validator.check_command_dict(command))
|
|
|
|
@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")})
|
|
@ddt.unpack
|
|
def test_check_command_dict_failed(
|
|
self, command=None, raises_message=None):
|
|
validator = vmtasks.ValidCommandValidator(param_name="p",
|
|
required=True)
|
|
e = self.assertRaises(
|
|
validation.ValidationError,
|
|
validator.check_command_dict, command)
|
|
self.assertIn(raises_message, e.message)
|
|
|
|
@mock.patch("rally.plugins.common.validators.FileExistsValidator"
|
|
"._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(self.context, {"args": {"p": command}},
|
|
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(self.context, {"args": {"p": None}},
|
|
None, None)
|
|
self.assertIsNone(result)
|
|
|
|
def test_valid_command_required(self):
|
|
validator = vmtasks.ValidCommandValidator(param_name="p",
|
|
required=True)
|
|
|
|
e = self.assertRaises(
|
|
validation.ValidationError,
|
|
validator.validate, {"args": {"p": None}},
|
|
self.context, None, None)
|
|
self.assertEqual("Command must be a dictionary", e.message)
|
|
|
|
@mock.patch("rally.plugins.common.validators.FileExistsValidator"
|
|
"._file_access_ok")
|
|
def test_valid_command_unreadable_script_file(self, mock__file_access_ok):
|
|
mock__file_access_ok.side_effect = validation.ValidationError("O_o")
|
|
|
|
validator = vmtasks.ValidCommandValidator(param_name="p",
|
|
required=True)
|
|
|
|
command = {"script_file": "foobar", "interpreter": "foo"}
|
|
e = self.assertRaises(
|
|
validation.ValidationError,
|
|
validator.validate, self.context, {"args": {"p": command}},
|
|
None, None)
|
|
self.assertEqual("O_o", e.message)
|
|
|
|
@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 = validation.ValidationError(
|
|
"foobar")
|
|
e = self.assertRaises(
|
|
validation.ValidationError,
|
|
validator.validate, {"args": {"p": {"foo": "bar"}}},
|
|
self.context, None, None)
|
|
self.assertEqual("foobar", e.message)
|
|
|
|
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(self.context, {"args": {"p": command}},
|
|
None, None)
|
|
self.assertIsNone(result)
|
|
|
|
@mock.patch("rally.plugins.common.validators.FileExistsValidator"
|
|
"._file_access_ok")
|
|
def test_valid_command_local_path(self, mock__file_access_ok):
|
|
mock__file_access_ok.side_effect = validation.ValidationError("")
|
|
|
|
validator = vmtasks.ValidCommandValidator(param_name="p",
|
|
required=True)
|
|
|
|
command = {"remote_path": "bar", "local_path": "foobar"}
|
|
self.assertRaises(
|
|
validation.ValidationError,
|
|
validator.validate, self.context, {"args": {"p": command}},
|
|
None, None)
|
|
mock__file_access_ok.assert_called_once_with(
|
|
filename="foobar", mode=os.R_OK, param_name="p",
|
|
required=True)
|