diff --git a/rally-jobs/extra/instance_test.sh b/rally-jobs/extra/instance_test.sh new file mode 100644 index 00000000..4d212383 --- /dev/null +++ b/rally-jobs/extra/instance_test.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# Load server and output JSON results ready to be processed +# by Rally scenario + +get_used_cpu_percent() { + echo 100 $(top -b -n 1 | grep -i CPU | head -n 1 | awk '{print $8}' | tr -d %) - p | dc +} + +get_used_ram_percent() { + local total=$(free | grep Mem: | awk '{print $2}') + local used=$(free | grep -- -/+\ buffers | awk '{print $3}') + echo ${used} 100 \* ${total} / p | dc +} + +get_used_disk_percent() { + df -P / | grep -v Filesystem | awk '{print $5}' | tr -d % +} + +get_seconds() { + (time -p ${1}) 2>&1 | awk '/real/{print $2}' +} + +complete_load() { + local script_file=${LOAD_SCRIPT_FILE:-/tmp/load.sh} + local stop_file=${LOAD_STOP_FILE:-/tmp/load.stop} + local processes_num=${LOAD_PROCESSES_COUNT:-20} + local size=${LOAD_SIZE_MB:-5} + + cat << EOF > ${script_file} +until test -e ${stop_file} +do dd if=/dev/urandom bs=1M count=${size} 2>/dev/null | gzip >/dev/null ; done +EOF + + local sep + local cpu + local ram + local dis + rm -f ${stop_file} + for i in $(seq ${processes_num}) + do + i=$((i-1)) + sh ${script_file} & + cpu="${cpu}${sep}[${i}, $(get_used_cpu_percent)]" + ram="${ram}${sep}[${i}, $(get_used_ram_percent)]" + dis="${dis}${sep}[${i}, $(get_used_disk_percent)]" + sep=", " + done + > ${stop_file} + cat << EOF + { + "title": "Generate load by spawning processes", + "description": "Each process runs gzip for ${size}M urandom data in a loop", + "chart_plugin": "Lines", + "axis_label": "Number of processes", + "label": "Usage, %", + "data": [ + ["CPU", [${cpu}]], + ["Memory", [${ram}]], + ["Disk", [${dis}]]] + } +EOF +} + +additive_dd() { + local c=${1:-50} # Megabytes + local file=/tmp/dd_test.img + local write=$(get_seconds "dd if=/dev/urandom of=${file} bs=1M count=${c}") + local read=$(get_seconds "dd if=${file} of=/dev/null bs=1M count=${c}") + local gzip=$(get_seconds "gzip ${file}") + rm ${file}.gz + cat << EOF + { + "title": "Write, read and gzip file", + "description": "Using file '${file}', size ${c}Mb.", + "chart_plugin": "StackedArea", + "data": [ + ["write_${c}M", ${write}], + ["read_${c}M", ${read}], + ["gzip_${c}M", ${gzip}]] + }, + { + "title": "Statistics for write/read/gzip", + "chart_plugin": "StatsTable", + "data": [ + ["write_${c}M", ${write}], + ["read_${c}M", ${read}], + ["gzip_${c}M", ${gzip}]] + } + +EOF +} + +cat << EOF +{ + "additive": [$(additive_dd)], + "complete": [$(complete_load)] +} +EOF diff --git a/rally-jobs/rally-neutron.yaml b/rally-jobs/rally-neutron.yaml index 68f5d54a..96409f19 100644 --- a/rally-jobs/rally-neutron.yaml +++ b/rally-jobs/rally-neutron.yaml @@ -670,6 +670,28 @@ max: 0 VMTasks.boot_runcommand_delete: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{image_name}} + command: + script_file: "~/.rally/extra/instance_test.sh" + interpreter: "/bin/sh" + username: "cirros" + runner: + type: "constant" + times: {{smoke or 4}} + concurrency: {{smoke or 2}} + context: + users: + tenants: {{smoke or 2}} + users_per_tenant: {{smoke or 2}} + network: {} + sla: + failure_rate: + max: 0 - args: flavor: @@ -689,6 +711,9 @@ tenants: {{smoke or 2}} users_per_tenant: {{smoke or 2}} network: {} + sla: + failure_rate: + max: 0 VMTasks.boot_runcommand_delete_custom_image: - diff --git a/rally/plugins/openstack/scenarios/vm/vmtasks.py b/rally/plugins/openstack/scenarios/vm/vmtasks.py index c4152794..ce613619 100644 --- a/rally/plugins/openstack/scenarios/vm/vmtasks.py +++ b/rally/plugins/openstack/scenarios/vm/vmtasks.py @@ -190,21 +190,38 @@ class VMTasks(vm_utils.VMScenario): self._delete_server_with_fip(server, fip, force_delete=force_delete) - # NOTE(amaretskiy): command output should be in format: - # {"key1": numeric_value, "key2": numeric_value, ...} - output = None - if type(data) == dict: + if type(data) != dict: + raise exceptions.ScriptError( + "Command has returned data in unexpected format.\n" + "Expected format: {" + "\"additive\": [{chart data}, {chart data}, ...], " + "\"complete\": [{chart data}, {chart data}, ...]}\n" + "Actual data: %s" % data) + + if set(data) - {"additive", "complete"}: + LOG.warning( + "Deprecated since Rally release 0.4.1: command has " + "returned data in format {\"key\": , ...}\n" + "Expected format: {" + "\"additive\": [{chart data}, {chart data}, ...], " + "\"complete\": [{chart data}, {chart data}, ...]}") + output = None try: output = [[str(k), float(v)] for k, v in data.items()] except (TypeError, ValueError): - LOG.error(("Command has returned data in unexpected format.\n" - "Expected format: {key1: numeric_value, " - "key2: numeric_value, ...}.\n" - "Actual data: %s" % data)) - if output: - self.add_output(additive={"title": "Command output", - "chart_plugin": "Lines", - "data": output}) + raise exceptions.ScriptError( + "Command has returned data in unexpected format.\n" + "Expected format: {key1: , " + "key2: , ...}.\n" + "Actual data: %s" % data) + if output: + self.add_output(additive={"title": "Command output", + "chart_plugin": "Lines", + "data": output}) + else: + for chart_type, charts in data.items(): + for chart in charts: + self.add_output(**{chart_type: chart}) @types.convert(image={"type": "glance_image"}, flavor={"type": "nova_flavor"}) diff --git a/samples/tasks/scenarios/vm/boot-runcommand-delete.json b/samples/tasks/scenarios/vm/boot-runcommand-delete.json index 5ed0bb44..8ccec797 100644 --- a/samples/tasks/scenarios/vm/boot-runcommand-delete.json +++ b/samples/tasks/scenarios/vm/boot-runcommand-delete.json @@ -13,7 +13,7 @@ "force_delete": false, "command": { "interpreter": "/bin/sh", - "script_file": "samples/tasks/support/instance_dd_test.sh" + "script_file": "samples/tasks/support/instance_test.sh" }, "username": "cirros" }, diff --git a/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml b/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml index c0496acc..dd47d403 100644 --- a/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml +++ b/samples/tasks/scenarios/vm/boot-runcommand-delete.yaml @@ -11,7 +11,7 @@ force_delete: false command: interpreter: "/bin/sh" - script_file: "samples/tasks/support/instance_dd_test.sh" + script_file: "samples/tasks/support/instance_test.sh" username: "cirros" runner: type: "constant" diff --git a/samples/tasks/support/instance_test.sh b/samples/tasks/support/instance_test.sh new file mode 100644 index 00000000..4d212383 --- /dev/null +++ b/samples/tasks/support/instance_test.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# Load server and output JSON results ready to be processed +# by Rally scenario + +get_used_cpu_percent() { + echo 100 $(top -b -n 1 | grep -i CPU | head -n 1 | awk '{print $8}' | tr -d %) - p | dc +} + +get_used_ram_percent() { + local total=$(free | grep Mem: | awk '{print $2}') + local used=$(free | grep -- -/+\ buffers | awk '{print $3}') + echo ${used} 100 \* ${total} / p | dc +} + +get_used_disk_percent() { + df -P / | grep -v Filesystem | awk '{print $5}' | tr -d % +} + +get_seconds() { + (time -p ${1}) 2>&1 | awk '/real/{print $2}' +} + +complete_load() { + local script_file=${LOAD_SCRIPT_FILE:-/tmp/load.sh} + local stop_file=${LOAD_STOP_FILE:-/tmp/load.stop} + local processes_num=${LOAD_PROCESSES_COUNT:-20} + local size=${LOAD_SIZE_MB:-5} + + cat << EOF > ${script_file} +until test -e ${stop_file} +do dd if=/dev/urandom bs=1M count=${size} 2>/dev/null | gzip >/dev/null ; done +EOF + + local sep + local cpu + local ram + local dis + rm -f ${stop_file} + for i in $(seq ${processes_num}) + do + i=$((i-1)) + sh ${script_file} & + cpu="${cpu}${sep}[${i}, $(get_used_cpu_percent)]" + ram="${ram}${sep}[${i}, $(get_used_ram_percent)]" + dis="${dis}${sep}[${i}, $(get_used_disk_percent)]" + sep=", " + done + > ${stop_file} + cat << EOF + { + "title": "Generate load by spawning processes", + "description": "Each process runs gzip for ${size}M urandom data in a loop", + "chart_plugin": "Lines", + "axis_label": "Number of processes", + "label": "Usage, %", + "data": [ + ["CPU", [${cpu}]], + ["Memory", [${ram}]], + ["Disk", [${dis}]]] + } +EOF +} + +additive_dd() { + local c=${1:-50} # Megabytes + local file=/tmp/dd_test.img + local write=$(get_seconds "dd if=/dev/urandom of=${file} bs=1M count=${c}") + local read=$(get_seconds "dd if=${file} of=/dev/null bs=1M count=${c}") + local gzip=$(get_seconds "gzip ${file}") + rm ${file}.gz + cat << EOF + { + "title": "Write, read and gzip file", + "description": "Using file '${file}', size ${c}Mb.", + "chart_plugin": "StackedArea", + "data": [ + ["write_${c}M", ${write}], + ["read_${c}M", ${read}], + ["gzip_${c}M", ${gzip}]] + }, + { + "title": "Statistics for write/read/gzip", + "chart_plugin": "StatsTable", + "data": [ + ["write_${c}M", ${write}], + ["read_${c}M", ${read}], + ["gzip_${c}M", ${gzip}]] + } + +EOF +} + +cat << EOF +{ + "additive": [$(additive_dd)], + "complete": [$(complete_load)] +} +EOF diff --git a/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py b/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py index 0c352545..15d86bb7 100644 --- a/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py +++ b/tests/unit/plugins/openstack/scenarios/vm/test_vmtasks.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from rally import exceptions @@ -20,6 +21,7 @@ from rally.plugins.openstack.scenarios.vm import vmtasks from tests.unit import test +@ddt.ddt class VMTasksTestCase(test.ScenarioTestCase): def setUp(self): @@ -35,7 +37,7 @@ class VMTasksTestCase(test.ScenarioTestCase): self.scenario._create_volume = mock.Mock( return_value=mock.Mock(id="foo_volume")) self.scenario._run_command = mock.MagicMock( - return_value=(0, "\"foo_out\"", "foo_err")) + return_value=(0, "{\"foo\": 42}", "foo_err")) self.scenario.add_output = mock.Mock() def test_boot_runcommand_delete(self): @@ -72,42 +74,57 @@ class VMTasksTestCase(test.ScenarioTestCase): additive={"title": "Command output", "chart_plugin": "Lines", "data": [["foo", 42.0]]}) - def test_boot_runcommand_delete_command(self): - self.scenario.boot_runcommand_delete( - "foo_image", "foo_flavor", - 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") + @ddt.data( + {"output": (0, "", ""), "raises": exceptions.ScriptError}, + {"output": (0, "{\"foo\": 42}", ""), + "expected": [{"additive": {"chart_plugin": "Lines", + "data": [["foo", 42.0]], + "title": "Command output"}}]}, + {"output": (1, "{\"foo\": 42}", ""), "raises": exceptions.ScriptError}, + {"output": ("", 1, ""), "raises": TypeError}, + {"output": (0, "{\"additive\": [1, 2]}", ""), + "expected": [{"additive": 1}, {"additive": 2}]}, + {"output": (0, "{\"complete\": [3, 4]}", ""), + "expected": [{"complete": 3}, {"complete": 4}]}, + {"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): + self.scenario._run_command.return_value = output + kwargs = {"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, self.scenario.boot_runcommand_delete, + "foo_image", "foo_flavor", **kwargs) + self.assertFalse(self.scenario.add_output.called) + else: + self.scenario.boot_runcommand_delete("foo_image", "foo_flavor", + **kwargs) + calls = [mock.call(**kw) for kw in expected] + self.scenario.add_output.assert_has_calls(calls, any_order=True) - self.scenario._create_volume.assert_called_once_with( - 16, imageRef=None) - self.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") + self.scenario._create_volume.assert_called_once_with( + 16, imageRef=None) + self.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") - self.scenario._run_command.assert_called_once_with( - "foo_ip", 22, "foo_username", "foo_password", - command={"remote_path": "foo"}) - self.scenario._delete_server_with_fip.assert_called_once_with( - "foo_server", self.ip, force_delete="foo_force") - - def test_boot_runcommand_delete_script_fails(self): - self.scenario._run_command = mock.MagicMock( - return_value=(1, "\"foo_out\"", "foo_err")) - self.assertRaises(exceptions.ScriptError, - self.scenario.boot_runcommand_delete, - "foo_image", "foo_flavor", "foo_interpreter", - "foo_script", "foo_username") - self.scenario._delete_server_with_fip.assert_called_once_with( - "foo_server", self.ip, force_delete=False) - self.assertFalse(self.scenario.add_output.called) + self.scenario._run_command.assert_called_once_with( + "foo_ip", 22, "foo_username", "foo_password", + command={"remote_path": "foo"}) + self.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): self.scenario._run_command.side_effect = exceptions.SSHTimeout()