From a04841e71f965e5b0ef97de1e56b8c57450be923 Mon Sep 17 00:00:00 2001 From: Anastasia Kuznetsova Date: Thu, 9 Jul 2015 20:19:23 +0300 Subject: [PATCH] Add new Murano scenarios - added four new Murano scenarios which check performance of commands which work with packages, like 'import-package', 'list-packages', 'delete-package'. - added appropriate methods into the murano/utils.py - added unit tests Change-Id: I655bb0c8a0980c59cd4ab576f97ee94c2c3cd0f7 --- .../manifest.yaml | 2 +- rally-jobs/rally-murano.yaml | 78 +++++++++ .../openstack/scenarios/murano/packages.py | 142 ++++++++++++++++ .../openstack/scenarios/murano/utils.py | 155 +++++++++++++++++- .../scenarios/murano/test_packages.py | 79 +++++++++ .../openstack/scenarios/murano/test_utils.py | 126 ++++++++++++++ 6 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 rally/plugins/openstack/scenarios/murano/packages.py create mode 100644 tests/unit/plugins/openstack/scenarios/murano/test_packages.py diff --git a/rally-jobs/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/manifest.yaml b/rally-jobs/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/manifest.yaml index 47e4d510..58075461 100644 --- a/rally-jobs/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/manifest.yaml +++ b/rally-jobs/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/manifest.yaml @@ -5,6 +5,6 @@ Name: HelloReporter Description: | HelloReporter test app. Author: 'Mirantis, Inc' -Tags: [App, Test, HelloWorld] +Tags: [] Classes: io.murano.apps.HelloReporter: HelloReporter.yaml diff --git a/rally-jobs/rally-murano.yaml b/rally-jobs/rally-murano.yaml index b69431fe..3e6a1b9d 100644 --- a/rally-jobs/rally-murano.yaml +++ b/rally-jobs/rally-murano.yaml @@ -61,6 +61,84 @@ app_package: "/home/jenkins/.rally/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/" roles: - "admin" + + MuranoPackages.import_and_list_packages: + - + args: + package: "/home/jenkins/.rally/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/" + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + sla: + failure_rate: + max: 0 + - + args: + package: "/home/jenkins/.rally/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter.zip" + runner: + type: "constant" + times: 1 + concurrency: 1 + context: + users: + tenants: 1 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 + + MuranoPackages.import_and_delete_package: + - + args: + package: "/home/jenkins/.rally/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/" + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + sla: + failure_rate: + max: 0 + + MuranoPackages.import_and_filter_applications: + - + args: + package: "/home/jenkins/.rally/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/" + filter_query: {"category" : "Web"} + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + sla: + failure_rate: + max: 0 + + MuranoPackages.package_lifecycle: + - + args: + package: "/home/jenkins/.rally/extra/murano/applications/HelloReporter/io.murano.apps.HelloReporter/" + body: {"categories": ["Web"]} + operation: "add" + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 sla: failure_rate: max: 0 diff --git a/rally/plugins/openstack/scenarios/murano/packages.py b/rally/plugins/openstack/scenarios/murano/packages.py new file mode 100644 index 00000000..26283d59 --- /dev/null +++ b/rally/plugins/openstack/scenarios/murano/packages.py @@ -0,0 +1,142 @@ +# 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 os + +from rally import consts +from rally.plugins.openstack.scenarios.murano import utils +from rally.task import scenario +from rally.task import validation + + +class MuranoPackages(utils.MuranoScenario): + """Benchmark scenarios for Murano packages.""" + + @validation.required_parameters("package") + @validation.file_exists(param_name="package", mode=os.F_OK) + @validation.required_clients("murano") + @validation.required_services(consts.Service.MURANO) + @validation.required_openstack(users=True) + @scenario.configure(context={"cleanup": ["murano.packages"]}) + def import_and_list_packages(self, package, include_disabled=False): + """Import Murano package and get list of packages. + + Measure the "murano import-package" and "murano package-list" commands + performance. + It imports Murano package from "package" (if it is not a zip archive + then zip archive will be prepared) and gets list of imported packages. + + :param package: path to zip archive that represents Murano + application package or absolute path to folder with + package components + :param include_disabled: specifies whether the disabled packages will + be included in a the result or not. + Default value is False. + """ + package_path = self._zip_package(package) + try: + self._import_package(package_path) + self._list_packages(include_disabled=include_disabled) + finally: + os.remove(package_path) + + @validation.required_parameters("package") + @validation.file_exists(param_name="package", mode=os.F_OK) + @validation.required_clients("murano") + @validation.required_services(consts.Service.MURANO) + @validation.required_openstack(users=True) + @scenario.configure(context={"cleanup": ["murano.packages"]}) + def import_and_delete_package(self, package): + """Import Murano package and then delete it. + + Measure the "murano import-package" and "murano package-delete" + commands performance. + It imports Murano package from "package" (if it is not a zip archive + then zip archive will be prepared) and deletes it. + + :param package: path to zip archive that represents Murano + application package or absolute path to folder with + package components + """ + package_path = self._zip_package(package) + try: + package = self._import_package(package_path) + self._delete_package(package) + finally: + os.remove(package_path) + + @validation.required_parameters("package", "body") + @validation.file_exists(param_name="package", mode=os.F_OK) + @validation.required_clients("murano") + @validation.required_services(consts.Service.MURANO) + @validation.required_openstack(users=True) + @scenario.configure(context={"cleanup": ["murano.packages"]}) + def package_lifecycle(self, package, body, operation="replace"): + """Import Murano package, modify it and then delete it. + + Measure the Murano import, update and delete package + commands performance. + It imports Murano package from "package" (if it is not a zip archive + then zip archive will be prepared), modifies it (using data from + "body") and deletes. + + :param package: path to zip archive that represents Murano + application package or absolute path to folder with + package components + :param body: dict object that defines what package property will be + updated, e.g {"tags": ["tag"]} or {"enabled": "true"} + :param operation: string object that defines the way of how package + property will be updated, allowed operations are + "add", "replace" or "delete". + Default value is "replace". + + """ + package_path = self._zip_package(package) + try: + package = self._import_package(package_path) + self._update_package(package, body, operation) + self._delete_package(package) + finally: + os.remove(package_path) + + @validation.required_parameters("package", "filter_query") + @validation.file_exists(param_name="package", mode=os.F_OK) + @validation.required_clients("murano") + @validation.required_services(consts.Service.MURANO) + @validation.required_openstack(users=True) + @scenario.configure(context={"cleanup": ["murano.packages"]}) + def import_and_filter_applications(self, package, filter_query): + """Import Murano package and then filter packages by some criteria. + + Measure the performance of package import and package + filtering commands. + It imports Murano package from "package" (if it is not a zip archive + then zip archive will be prepared) and filters packages by some + criteria. + + :param package: path to zip archive that represents Murano + application package or absolute path to folder with + package components + :param filter_query: dict that contains filter criteria, lately it + will be passed as **kwargs to filter method + e.g. {"category": "Web"} + """ + package_path = self._zip_package(package) + try: + self._import_package(package_path) + self._filter_applications(filter_query) + finally: + os.remove(package_path) diff --git a/rally/plugins/openstack/scenarios/murano/utils.py b/rally/plugins/openstack/scenarios/murano/utils.py index ad7f3e19..41b85bf8 100644 --- a/rally/plugins/openstack/scenarios/murano/utils.py +++ b/rally/plugins/openstack/scenarios/murano/utils.py @@ -13,10 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import shutil +import tempfile import uuid +import zipfile from oslo_config import cfg +import yaml +from rally.common import fileutils +from rally.common import utils as common_utils from rally.plugins.openstack import scenario from rally.task import atomic from rally.task import utils @@ -31,7 +38,7 @@ MURANO_TIMEOUT_OPTS = [ cfg.IntOpt("delete_environment_check_interval", default=2, help="Delete environment check interval in seconds"), cfg.IntOpt("deploy_environment_check_interval", default=5, - help="Deploy environment check interval in seconds") + help="Deploy environment check interval in seconds"), ] benchmark_group = cfg.OptGroup(name="benchmark", title="benchmark options") @@ -125,3 +132,149 @@ class MuranoScenario(scenario.OpenStackScenario): timeout=CONF.benchmark.deploy_environment_timeout, check_interval=CONF.benchmark.deploy_environment_check_interval ) + + @atomic.action_timer("murano.list_packages") + def _list_packages(self, include_disabled=False): + """Returns packages list. + + :param include_disabled: if "True" then disabled packages will be + included in a the result. + Default value is False. + :returns: list of imported packages + """ + return self.clients("murano").packages.list( + include_disabled=include_disabled) + + @atomic.action_timer("murano.import_package") + def _import_package(self, package): + """Import package to the Murano. + + :param package: path to zip archive with Murano application + :returns: imported package + """ + + package = self.clients("murano").packages.create( + {}, {"file": open(package)} + ) + + return package + + @atomic.action_timer("murano.delete_package") + def _delete_package(self, package): + """Delete specified package. + + :param package: package that will be deleted + """ + + self.clients("murano").packages.delete(package.id) + + @atomic.action_timer("murano.update_package") + def _update_package(self, package, body, operation="replace"): + """Update specified package. + + :param package: package that will be updated + :param body: dict object that defines what package property will be + updated, e.g {"tags": ["tag"]} or {"enabled": "true"} + :param operation: string object that defines the way of how package + property will be updated, allowed operations are + "add", "replace" or "delete". + Default value is "replace". + :returns: updated package + """ + + return self.clients("murano").packages.update( + package.id, body, operation) + + @atomic.action_timer("murano.filter_applications") + def _filter_applications(self, filter_query): + """Filter list of uploaded application by specified criteria. + + :param filter_query: dict that contains filter criteria, it + will be passed as **kwargs to filter method + e.g. {"category": "Web"} + :returns: filtered list of packages + """ + + return self.clients("murano").packages.filter(**filter_query) + + def _zip_package(self, package_path): + """Call _prepare_package method that returns path to zip archive.""" + return MuranoPackageManager()._prepare_package(package_path) + + +class MuranoPackageManager(object): + + @staticmethod + def _read_from_file(filename): + with open(filename, "r") as f: + read_data = f.read() + return yaml.safe_load(read_data) + + @staticmethod + def _write_to_file(data, filename): + with open(filename, "w") as f: + yaml.safe_dump(data, f) + + def _change_app_fullname(self, app_dir): + """Change application full name. + + To avoid name conflict error during package import (when user + tries to import a few packages into the same tenant) need to change the + application name. For doing this need to replace following parts + in manifest.yaml + from + ... + FullName: app.name + ... + Classes: + app.name: app_class.yaml + to: + ... + FullName: + ... + Classes: + : app_class.yaml + + :param app_dir: path to directory with Murano application context + """ + + new_fullname = common_utils.generate_random_name("app.") + + manifest_file = os.path.join(app_dir, "manifest.yaml") + manifest = self._read_from_file(manifest_file) + + class_file_name = manifest["Classes"][manifest["FullName"]] + + # update manifest.yaml file + del manifest["Classes"][manifest["FullName"]] + manifest["FullName"] = new_fullname + manifest["Classes"][new_fullname] = class_file_name + self._write_to_file(manifest, manifest_file) + + def _prepare_package(self, package_path): + """Check whether the package path is path to zip archive or not. + + If package_path is not a path to zip archive but path to Murano + application folder, than method prepares zip archive with Murano + application. It copies directory with Murano app files to temporary + folder, changes manifest.yaml and class file (to avoid '409 Conflict' + errors in Murano) and prepares zip package. + + :param package_path: path to zip archive or directory with package + components + :returns: path to zip archive with Murano application + """ + + if not zipfile.is_zipfile(package_path): + tmp_dir = tempfile.mkdtemp() + pkg_dir = os.path.join(tmp_dir, "package/") + try: + shutil.copytree(package_path, pkg_dir) + + self._change_app_fullname(pkg_dir) + package_path = fileutils.pack_dir(pkg_dir) + + finally: + shutil.rmtree(tmp_dir) + + return package_path diff --git a/tests/unit/plugins/openstack/scenarios/murano/test_packages.py b/tests/unit/plugins/openstack/scenarios/murano/test_packages.py new file mode 100644 index 00000000..86282119 --- /dev/null +++ b/tests/unit/plugins/openstack/scenarios/murano/test_packages.py @@ -0,0 +1,79 @@ +# 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 mock + +from rally.plugins.openstack.scenarios.murano import packages +from tests.unit import test + +MURANO_SCENARIO = ("rally.plugins.openstack.scenarios.murano." + "packages.MuranoPackages") + + +class MuranoPackagesTestCase(test.TestCase): + + def setUp(self): + super(MuranoPackagesTestCase, self).setUp() + self.scenario = packages.MuranoPackages() + self.scenario._import_package = mock.Mock() + self.scenario._zip_package = mock.Mock() + self.scenario._list_packages = mock.Mock() + self.scenario._delete_package = mock.Mock() + self.scenario._update_package = mock.Mock() + self.scenario._filter_applications = mock.Mock() + self.mock_remove = mock.patch("os.remove") + self.mock_remove.start() + + def tearDown(self): + super(MuranoPackagesTestCase, self).tearDown() + self.mock_remove.stop() + + def test_make_zip_import_and_list_packages(self): + self.scenario.import_and_list_packages("foo_package.zip") + self.scenario._import_package.assert_called_once_with( + self.scenario._zip_package.return_value) + self.scenario._zip_package.assert_called_once_with("foo_package.zip") + self.scenario._list_packages.assert_called_once_with( + include_disabled=False) + + def test_import_and_delete_package(self): + fake_package = mock.Mock() + self.scenario._import_package.return_value = fake_package + self.scenario.import_and_delete_package("foo_package.zip") + self.scenario._import_package.assert_called_once_with( + self.scenario._zip_package.return_value) + self.scenario._delete_package.assert_called_once_with(fake_package) + + def test_package_lifecycle(self): + fake_package = mock.Mock() + self.scenario._import_package.return_value = fake_package + self.scenario.package_lifecycle( + "foo_package.zip", {"category": "Web"}, "add") + self.scenario._import_package.assert_called_once_with( + self.scenario._zip_package.return_value) + self.scenario._update_package.assert_called_once_with( + fake_package, {"category": "Web"}, "add") + self.scenario._delete_package.assert_called_once_with(fake_package) + + def test_import_and_filter_applications(self): + fake_package = mock.Mock() + self.scenario._import_package.return_value = fake_package + self.scenario.import_and_filter_applications( + "foo_package.zip", {"category": "Web"}) + self.scenario._import_package.assert_called_once_with( + self.scenario._zip_package.return_value) + self.scenario._filter_applications.assert_called_once_with( + {"category": "Web"} + ) diff --git a/tests/unit/plugins/openstack/scenarios/murano/test_utils.py b/tests/unit/plugins/openstack/scenarios/murano/test_utils.py index 60656abf..5a625ded 100644 --- a/tests/unit/plugins/openstack/scenarios/murano/test_utils.py +++ b/tests/unit/plugins/openstack/scenarios/murano/test_utils.py @@ -105,3 +105,129 @@ class MuranoScenarioTestCase(test.ScenarioTestCase): self.mock_resource_is.mock.assert_called_once_with("READY") self._test_atomic_action_timer(scenario.atomic_actions(), "murano.deploy_environment") + + @mock.patch(MRN_UTILS + ".open", + side_effect=mock.mock_open(read_data="Key: value"), + create=True) + def test_read_from_file(self, mock_open): + utility = utils.MuranoPackageManager() + data = utility._read_from_file("filename") + expected_data = {"Key": "value"} + self.assertEqual(expected_data, data) + + @mock.patch(MRN_UTILS + ".MuranoPackageManager._read_from_file") + @mock.patch(MRN_UTILS + ".MuranoPackageManager._write_to_file") + def test_change_app_fullname( + self, mock_murano_package_manager__write_to_file, + mock_murano_package_manager__read_from_file): + manifest = {"FullName": "app.name_abc", + "Classes": {"app.name_abc": "app_class.yaml"}} + mock_murano_package_manager__read_from_file.side_effect = ( + [manifest]) + utility = utils.MuranoPackageManager() + utility._change_app_fullname("tmp/tmpfile/") + mock_murano_package_manager__read_from_file.assert_has_calls( + [mock.call("tmp/tmpfile/manifest.yaml")] + ) + mock_murano_package_manager__write_to_file.assert_has_calls( + [mock.call(manifest, "tmp/tmpfile/manifest.yaml")] + ) + + @mock.patch("zipfile.is_zipfile") + @mock.patch("tempfile.mkdtemp") + @mock.patch("shutil.copytree") + @mock.patch(MRN_UTILS + ".MuranoPackageManager._change_app_fullname") + @mock.patch("rally.common.fileutils.pack_dir") + @mock.patch("shutil.rmtree") + def test_prepare_zip_if_not_zip( + self, mock_shutil_rmtree, mock_pack_dir, + mock_murano_package_manager__change_app_fullname, + mock_shutil_copytree, mock_tempfile_mkdtemp, + mock_zipfile_is_zipfile): + utility = utils.MuranoPackageManager() + package_path = "tmp/tmpfile" + + mock_zipfile_is_zipfile.return_value = False + mock_tempfile_mkdtemp.return_value = "tmp/tmpfile" + mock_pack_dir.return_value = "tmp/tmpzipfile" + + zip_file = utility._prepare_package(package_path) + + self.assertEqual("tmp/tmpzipfile", zip_file) + mock_tempfile_mkdtemp.assert_called_once_with() + mock_shutil_copytree.assert_called_once_with( + "tmp/tmpfile", + "tmp/tmpfile/package/" + ) + (mock_murano_package_manager__change_app_fullname. + assert_called_once_with("tmp/tmpfile/package/")) + mock_shutil_rmtree.assert_called_once_with("tmp/tmpfile") + + @mock.patch("zipfile.is_zipfile") + def test_prepare_zip_if_zip(self, mock_zipfile_is_zipfile): + utility = utils.MuranoPackageManager() + package_path = "tmp/tmpfile.zip" + mock_zipfile_is_zipfile.return_value = True + zip_file = utility._prepare_package(package_path) + self.assertEqual("tmp/tmpfile.zip", zip_file) + + def test_list_packages(self): + scenario = utils.MuranoScenario() + self.assertEqual(self.clients("murano").packages.list.return_value, + scenario._list_packages()) + self._test_atomic_action_timer(scenario.atomic_actions(), + "murano.list_packages") + + @mock.patch(MRN_UTILS + ".open", create=True) + def test_import_package(self, mock_open): + self.clients("murano").packages.create.return_value = ( + "created_foo_package" + ) + scenario = utils.MuranoScenario() + mock_open.return_value = "opened_foo_package.zip" + imp_package = scenario._import_package("foo_package.zip") + self.assertEqual("created_foo_package", imp_package) + self.clients("murano").packages.create.assert_called_once_with( + {}, {"file": "opened_foo_package.zip"}) + mock_open.assert_called_once_with("foo_package.zip") + self._test_atomic_action_timer(scenario.atomic_actions(), + "murano.import_package") + + def test_delete_package(self): + package = mock.Mock(id="package_id") + scenario = utils.MuranoScenario() + scenario._delete_package(package) + self.clients("murano").packages.delete.assert_called_once_with( + "package_id" + ) + self._test_atomic_action_timer(scenario.atomic_actions(), + "murano.delete_package") + + def test_update_package(self): + package = mock.Mock(id="package_id") + self.clients("murano").packages.update.return_value = "updated_package" + scenario = utils.MuranoScenario() + upd_package = scenario._update_package( + package, {"tags": ["tag"]}, "add" + ) + self.assertEqual("updated_package", upd_package) + self.clients("murano").packages.update.assert_called_once_with( + "package_id", + {"tags": ["tag"]}, + "add" + ) + self._test_atomic_action_timer(scenario.atomic_actions(), + "murano.update_package") + + def test_filter_packages(self): + self.clients("murano").packages.filter.return_value = [] + scenario = utils.MuranoScenario() + return_apps_list = scenario._filter_applications( + {"category": "Web"} + ) + self.assertEqual([], return_apps_list) + self.clients("murano").packages.filter.assert_called_once_with( + category="Web" + ) + self._test_atomic_action_timer(scenario.atomic_actions(), + "murano.filter_applications")