From 1a8e838bb58c0a8a7ab75f35a34095a5654a17d0 Mon Sep 17 00:00:00 2001 From: Pavel Boldin Date: Thu, 14 May 2015 14:32:38 +0300 Subject: [PATCH] Add image_command_customizer context Add `image_command_customizer' context that allows image customization using side effects of a command execution. E.g. one can install an application to the image and use these image for `boot_runcommand_delete' scenario afterwards. Change-Id: Ib283ca470b3d0e8e0f3c5b8f953dfd00c8718b35 Implements: blueprint vm-workloads-framework --- .../context/vm/image_command_customizer.py | 104 ++++++++++++++++++ .../vm/test_image_command_customizer.py | 94 ++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 rally/plugins/openstack/context/vm/image_command_customizer.py create mode 100644 tests/unit/plugins/openstack/context/vm/test_image_command_customizer.py diff --git a/rally/plugins/openstack/context/vm/image_command_customizer.py b/rally/plugins/openstack/context/vm/image_command_customizer.py new file mode 100644 index 0000000000..2db387e388 --- /dev/null +++ b/rally/plugins/openstack/context/vm/image_command_customizer.py @@ -0,0 +1,104 @@ +# Copyright 2015: Mirantis Inc. +# 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 copy + +from rally import exceptions +from rally.plugins.openstack.context.vm import custom_image +from rally.plugins.openstack.scenarios.vm import utils as vm_utils +import rally.task.context as context + + +@context.context(name="image_command_customizer", order=501) +class ImageCommandCustomizerContext(custom_image.BaseCustomImageGenerator): + """Context class for generating image customized by a command execution. + + Run a command specified by configuration to prepare image. + + Use this script e.g. to download and install something. + """ + + CONFIG_SCHEMA = copy.deepcopy( + custom_image.BaseCustomImageGenerator.CONFIG_SCHEMA) + CONFIG_SCHEMA["definitions"] = { + "stringOrStringList": { + "anyOf": [ + {"type": "string"}, + { + "type": "array", + "items": {"type": "string"} + } + ] + }, + "scriptFile": { + "properties": { + "script_file": {"$ref": "#/definitions/stringOrStringList"}, + "interpreter": {"$ref": "#/definitions/stringOrStringList"}, + "command_args": {"$ref": "#/definitions/stringOrStringList"} + }, + "required": ["script_file", "interpreter"], + "additionalProperties": False, + }, + "scriptInline": { + "properties": { + "script_inline": {"type": "string"}, + "interpreter": {"$ref": "#/definitions/stringOrStringList"}, + "command_args": {"$ref": "#/definitions/stringOrStringList"} + }, + "required": ["script_inline", "interpreter"], + "additionalProperties": False, + }, + "commandPath": { + "properties": { + "remote_path": {"$ref": "#/definitions/stringOrStringList"}, + "local_path": {"type": "string"}, + "command_args": {"$ref": "#/definitions/stringOrStringList"} + }, + "required": ["remote_path"], + "additionalProperties": False, + }, + "commandDict": { + "type": "object", + "oneOf": [ + {"$ref": "#/definitions/scriptFile"}, + {"$ref": "#/definitions/scriptInline"}, + {"$ref": "#/definitions/commandPath"}, + ], + } + } + CONFIG_SCHEMA["properties"]["command"] = { + "$ref": "#/definitions/commandDict" + } + + def _customize_image(self, server, fip, user): + code, out, err = vm_utils.VMScenario(self.context)._run_command( + fip["ip"], self.config["port"], + self.config["username"], self.config.get("password"), + command=self.config["command"], + pkey=user["keypair"]["private"]) + + if code: + raise exceptions.ScriptError( + message="Command `%(command)s' execution failed," + " code %(code)d:\n" + "STDOUT:\n============================\n" + "%(out)s\n" + "STDERR:\n============================\n" + "%(err)s\n" + "============================\n" + % {"command": self.config["command"], "code": code, + "out": out, "err": err}) + + return code, out, err diff --git a/tests/unit/plugins/openstack/context/vm/test_image_command_customizer.py b/tests/unit/plugins/openstack/context/vm/test_image_command_customizer.py new file mode 100644 index 0000000000..3c04b9c6aa --- /dev/null +++ b/tests/unit/plugins/openstack/context/vm/test_image_command_customizer.py @@ -0,0 +1,94 @@ +# Copyright 2015: Mirantis Inc. +# 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. + +"""Tests for the image customizer using a command execution.""" + +import mock + +from rally import exceptions +from rally.plugins.openstack.context.vm import image_command_customizer +from tests.unit import test + +BASE = "rally.plugins.openstack.context.vm.image_command_customizer" + + +class ImageCommandCustomizerContextVMTestCase(test.TestCase): + + def setUp(self): + super(ImageCommandCustomizerContextVMTestCase, self).setUp() + + self.context = { + "task": mock.MagicMock(), + "config": { + "image_command_customizer": { + "image": {"name": "image"}, + "flavor": {"name": "flavor"}, + "username": "fedora", + "password": "foo_password", + "floating_network": "floating", + "port": 1022, + "command": { + "interpreter": "foo_interpreter", + "script_file": "foo_script" + } + } + }, + "admin": { + "endpoint": "endpoint", + } + } + + self.user = {"keypair": {"private": "foo_private"}} + self.fip = {"ip": "foo_ip"} + + @mock.patch("%s.vm_utils.VMScenario" % BASE) + def test_customize_image(self, mock_vm_scenario): + mock_vm_scenario.return_value._run_command.return_value = ( + 0, "foo_stdout", "foo_stderr") + + customizer = image_command_customizer.ImageCommandCustomizerContext( + self.context) + + retval = customizer.customize_image(server=None, ip=self.fip, + user=self.user) + + mock_vm_scenario.assert_called_once_with(customizer.context) + mock_vm_scenario.return_value._run_command.assert_called_once_with( + "foo_ip", 1022, "fedora", "foo_password", pkey="foo_private", + command={"interpreter": "foo_interpreter", + "script_file": "foo_script"}) + + self.assertEqual((0, "foo_stdout", "foo_stderr"), retval) + + @mock.patch("%s.vm_utils.VMScenario" % BASE) + def test_customize_image_fail(self, mock_vm_scenario): + mock_vm_scenario.return_value._run_command.return_value = ( + 1, "foo_stdout", "foo_stderr") + + customizer = image_command_customizer.ImageCommandCustomizerContext( + self.context) + + exc = self.assertRaises( + exceptions.ScriptError, customizer.customize_image, + server=None, ip=self.fip, user=self.user) + + str_exc = str(exc) + self.assertIn("foo_stdout", str_exc) + self.assertIn("foo_stderr", str_exc) + + mock_vm_scenario.return_value._run_command.assert_called_once_with( + "foo_ip", 1022, "fedora", "foo_password", pkey="foo_private", + command={"interpreter": "foo_interpreter", + "script_file": "foo_script"})