From cf604be22ecc75e5f4d1e368c2a05c162b199a6e Mon Sep 17 00:00:00 2001 From: marios Date: Fri, 20 Mar 2015 14:01:21 +0200 Subject: [PATCH] Adds tuskar-load-role to load a single role and associated extra-data This adds a stand-alone tuskar-load-role command which takes a name and path to the main file defining this role. If no name is provided this is deduced from the path (filename) as currently occurs with tuskar-load-roles. If a path is not specified then a SystemExit occurs. This also wires up the relative_path into the role creation (this is added to the model by the parent commit) Optionally you may specify the extra-data files that the given role uses as multiple '--extra-data' arguments to tuskar-load-role. Change-Id: I9c143afb52c43e4258c3a6797a9707c08d8dcdaf --- setup.cfg | 1 + tuskar/cmd/delete_roles.py | 2 +- tuskar/cmd/load_role.py | 58 ++++++++++++++++++++++++++++ tuskar/storage/drivers/base.py | 4 +- tuskar/storage/drivers/sqlalchemy.py | 22 +++++++---- tuskar/storage/load_roles.py | 11 ++++++ tuskar/storage/load_utils.py | 14 +++---- tuskar/storage/stores.py | 8 ++-- tuskar/tests/cmd/test_load_roles.py | 38 ++++++++++++++++++ tuskar/tests/storage/test_stores.py | 33 ++++++++++------ 10 files changed, 158 insertions(+), 33 deletions(-) create mode 100644 tuskar/cmd/load_role.py diff --git a/setup.cfg b/setup.cfg index c210efb4..2a82e913 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ console_scripts = tuskar-load-roles = tuskar.cmd.load_roles:main tuskar-load-seed = tuskar.cmd.load_seed:main tuskar-delete-roles = tuskar.cmd.delete_roles:main + tuskar-load-role = tuskar.cmd.load_role:main [build_sphinx] all_files = 1 diff --git a/tuskar/cmd/delete_roles.py b/tuskar/cmd/delete_roles.py index 778f4d8f..46c49397 100644 --- a/tuskar/cmd/delete_roles.py +++ b/tuskar/cmd/delete_roles.py @@ -27,7 +27,7 @@ from tuskar.storage.delete_roles import delete_roles def _print_names(message, names): print("{0}: \n {1}".format(message, '\n '.join(names))) -cfg.CONF.register_cli_opt(cfg.BoolOpt('dryrun', short='n', default=False)) +cfg.CONF.register_cli_opt(cfg.BoolOpt('dryrun', default=False)) cfg.CONF.register_cli_opt(cfg.ListOpt( 'uuids', help='List of role uuid to delete')) diff --git a/tuskar/cmd/load_role.py b/tuskar/cmd/load_role.py new file mode 100644 index 00000000..c8f31925 --- /dev/null +++ b/tuskar/cmd/load_role.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Copyright 2015 Red Hat +# 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. + +from __future__ import print_function + +import sys + +from oslo.config import cfg + +from tuskar.common import service +from tuskar.storage.load_roles import load_role + + +def _print_names(message, names): + print("{0}: \n {1}".format(message, '\n '.join(names))) + +cfg.CONF.register_cli_opt(cfg.StrOpt('name', short='n', dest='name')) +cfg.CONF.register_cli_opt(cfg.StrOpt( + 'filepath', dest='file_path', short='f')) +cfg.CONF.register_cli_opt(cfg.StrOpt('relative-path', dest='relative_path')) +cfg.CONF.register_cli_opt(cfg.MultiStrOpt('extra-data', short='e')) + + +def main(argv=None): + if argv is None: + argv = sys.argv + + service.prepare_service(argv) + if not cfg.CONF.file_path: + sys.stderr.write("You must specify the path to the main template " + "which defines this role.") + sys.exit(1) + + name = cfg.CONF.name if cfg.CONF.name else '' + relative_path = cfg.CONF.relative_path if cfg.CONF.relative_path else None + created, updated = load_role(name, cfg.CONF.file_path, + extra_data=cfg.CONF.extra_data, + relative_path=relative_path) + + if len(created): + _print_names("Created", created) + + if len(updated): + _print_names("Updated", updated) diff --git a/tuskar/storage/drivers/base.py b/tuskar/storage/drivers/base.py index ede17257..0fe41c6e 100644 --- a/tuskar/storage/drivers/base.py +++ b/tuskar/storage/drivers/base.py @@ -34,7 +34,7 @@ class BaseDriver(object): """ @abstractmethod - def create(self, store, name, contents): + def create(self, store, name, contents, relative_path): """Given the store, name and contents create a new file and return a `StoredFile` instance representing it. @@ -77,7 +77,7 @@ class BaseDriver(object): """ @abstractmethod - def update(self, store, uuid, contents): + def update(self, store, uuid, contents, relative_path): """Given the store, uuid, name and contents update the existing stored file and return an instance of StoredFile that reflects the updates. Either name and/or contents can be provided. If they are not then they diff --git a/tuskar/storage/drivers/sqlalchemy.py b/tuskar/storage/drivers/sqlalchemy.py index db8b14c1..9eef97a8 100644 --- a/tuskar/storage/drivers/sqlalchemy.py +++ b/tuskar/storage/drivers/sqlalchemy.py @@ -87,21 +87,23 @@ class SQLAlchemyDriver(BaseDriver): finally: session.close() - def _create(self, store, name, contents, version): + def _create(self, store, name, contents, version, relative_path=''): stored_file = StoredFile( uuid=self._generate_uuid(), contents=contents, object_type=store.object_type, name=name, - version=version + version=version, + relative_path=relative_path ) return self._upsert(store, stored_file) - def create(self, store, name, contents): + def create(self, store, name, contents, relative_path=''): """Given the store, name and contents create a new file and return a - `StoredFile` instance representing it. + `StoredFile` instance representing it. The optional relative_path + is appended to the generated template directory structure. Some of the stored items such as environment files do not have names. When working with these, name must be passed explicitly as None. This @@ -116,6 +118,9 @@ class SQLAlchemyDriver(BaseDriver): :param contents: String containing the file contents :type contents: str + :param relative_path: String relative path to place the template under + : type relative_path: str + :return: StoredFile instance containing the file metadata and contents :rtype: tuskar.storage.models.StoredFile """ @@ -136,7 +141,7 @@ class SQLAlchemyDriver(BaseDriver): except UnknownName: pass - return self._create(store, name, contents, version) + return self._create(store, name, contents, version, relative_path) def _retrieve(self, object_type, uuid): @@ -172,7 +177,7 @@ class SQLAlchemyDriver(BaseDriver): stored_file = self._retrieve(store.object_type, uuid) return self._to_storage_model(store, stored_file) - def update(self, store, uuid, contents): + def update(self, store, uuid, contents, relative_path=''): """Given the store, uuid, name and contents update the existing stored file and return an instance of StoredFile that reflects the updates. Either name and/or contents can be provided. If they are not then they @@ -201,10 +206,13 @@ class SQLAlchemyDriver(BaseDriver): stored_file.contents = contents + stored_file.relative_path = relative_path if relative_path else None + if store.versioned: version = self._get_latest_version(store, stored_file.name) + 1 return self._create( - store, stored_file.name, stored_file.contents, version) + store, stored_file.name, stored_file.contents, version, + relative_path) return self._upsert(store, stored_file) diff --git a/tuskar/storage/load_roles.py b/tuskar/storage/load_roles.py index bc82c534..5a279bfd 100644 --- a/tuskar/storage/load_roles.py +++ b/tuskar/storage/load_roles.py @@ -26,6 +26,7 @@ from tuskar.storage.stores import MasterSeedStore from tuskar.storage.stores import ResourceRegistryMappingStore from tuskar.storage.stores import ResourceRegistryStore from tuskar.storage.stores import TemplateExtraStore +from tuskar.storage.stores import TemplateStore from tuskar.templates import parser MASTER_SEED_NAME = '_master_seed' @@ -47,6 +48,16 @@ def load_seed(seed_file, resource_registry_path): return created, updated +def load_role(name, file_path, extra_data=None, relative_path=''): + name = role_name_from_path(file_path) if (name == '') else name + all_roles, created, updated = load_roles( + roles=[], seed_file=None, + resource_registry_path=None, role_extra=extra_data) + process_role(file_path, name, TemplateStore(), all_roles, created, + updated, relative_path) + return created, updated + + def load_roles(roles, seed_file=None, resource_registry_path=None, role_extra=None): """Given a list of roles files import them into the diff --git a/tuskar/storage/load_utils.py b/tuskar/storage/load_utils.py index 6f398d56..3c17690f 100644 --- a/tuskar/storage/load_utils.py +++ b/tuskar/storage/load_utils.py @@ -21,24 +21,24 @@ def load_file(role_path): return role_file.read() -def _create_or_update(name, contents, store=None): +def _create_or_update(name, contents, store=None, relative_path=''): if store is None: store = TemplateStore() - try: role = store.retrieve_by_name(name) - if role.contents != contents: - role = store.update(role.uuid, contents) + role = store.update(role.uuid, contents, relative_path) return False, role except UnknownName: - return True, store.create(name, contents) + return True, store.create(name, contents, relative_path) -def process_role(role_path, role_name, store, all_roles, created, updated): +def process_role(role_path, role_name, store, all_roles, created, updated, + relative_path=''): contents = load_file(role_path) - role_created, _ = _create_or_update(role_name, contents, store) + role_created, _ = _create_or_update(role_name, contents, store, + relative_path) if all_roles is not None: all_roles.append(role_name) diff --git a/tuskar/storage/stores.py b/tuskar/storage/stores.py index f6fc99a8..b10a2761 100644 --- a/tuskar/storage/stores.py +++ b/tuskar/storage/stores.py @@ -75,7 +75,7 @@ class _BaseStore(object): """ return self._driver.retrieve(self, uuid) - def update(self, uuid, contents): + def update(self, uuid, contents, relative_path=''): """Given the uuid and contents update the existing stored file and return an instance of StoredFile that reflects the updates. @@ -91,7 +91,7 @@ class _BaseStore(object): :raises: tuskar.storage.exceptions.UnknownUUID if the UUID can't be found """ - return self._driver.update(self, uuid, contents) + return self._driver.update(self, uuid, contents, relative_path) def delete(self, uuid): """Delete the file in this store with the matching uuid. @@ -120,7 +120,7 @@ class _NamedStore(_BaseStore): where required. """ - def create(self, name, contents): + def create(self, name, contents, relative_path=''): """Given the name and contents create a new file and return a `StoredFile` instance representing it. @@ -139,7 +139,7 @@ class _NamedStore(_BaseStore): :raises: tuskar.storage.exceptions.NameAlreadyUsed if the name is already in use """ - return self._driver.create(self, name, contents) + return self._driver.create(self, name, contents, relative_path) def retrieve_by_name(self, name): """Returns the stored file for a given store that matches the provided diff --git a/tuskar/tests/cmd/test_load_roles.py b/tuskar/tests/cmd/test_load_roles.py index 8371be21..ff598e92 100644 --- a/tuskar/tests/cmd/test_load_roles.py +++ b/tuskar/tests/cmd/test_load_roles.py @@ -15,6 +15,7 @@ from mock import call from mock import patch +from tuskar.cmd import load_role from tuskar.cmd import load_roles from tuskar.cmd import load_seed from tuskar.tests.base import TestCase @@ -70,3 +71,40 @@ resource_registry: self.assertEqual([call('Created', expected_created)], mock_print.call_args_list) + + @patch('tuskar.storage.load_utils.load_file', return_value="YAML") + @patch('tuskar.cmd.load_role._print_names') + def test_load_role(self, mock_print, mock_read): + main_args = (" tuskar-load-role -n Compute" + " --filepath /path/to/puppet/compute-puppet.yaml " + " --extra-data /path/to/puppet/hieradata/compute.yaml " + " --extra-data /path/to/puppet/hieradata/common.yaml ") + expected_res = ['extra_compute_yaml', 'extra_common_yaml', 'Compute'] + + load_role.main(argv=(main_args).split()) + + self.assertEqual([call('Created', expected_res)], + mock_print.call_args_list) + + @patch('tuskar.storage.load_utils.load_file', return_value="YAML") + @patch('tuskar.cmd.load_role._print_names') + def test_load_role_no_name(self, mock_print, mock_read): + main_args = (" tuskar-load-role" + " -f /path/to/puppet/compute-puppet.yaml " + " --extra-data /path/to/puppet/hieradata/compute.yaml " + " --extra-data /path/to/puppet/hieradata/common.yaml ") + expected_res = ['extra_compute_yaml', 'extra_common_yaml', + 'compute-puppet'] + + load_role.main(argv=(main_args).split()) + + self.assertEqual([call('Created', expected_res)], + mock_print.call_args_list) + + @patch('tuskar.storage.load_utils.load_file', return_value="YAML") + @patch('tuskar.cmd.load_role._print_names') + def test_load_role_no_path(self, mock_print, mock_read): + main_args = (" tuskar-load-role" + " --extra-data /path/to/puppet/hieradata/compute.yaml " + " --extra-data /path/to/puppet/hieradata/common.yaml ") + self.assertRaises(SystemExit, load_role.main, (main_args.split())) diff --git a/tuskar/tests/storage/test_stores.py b/tuskar/tests/storage/test_stores.py index 1fc29a80..d639e87f 100644 --- a/tuskar/tests/storage/test_stores.py +++ b/tuskar/tests/storage/test_stores.py @@ -41,7 +41,8 @@ class BaseStoreTests(TestCase): uuid = "d131dd02c5e6eec4" contents = "Stored contents" self.store.update(uuid, contents) - self.driver.update.assert_called_once_with(self.store, uuid, contents) + self.driver.update.assert_called_once_with(self.store, uuid, + contents, "") def test_retrieve(self): uuid = "d131dd02c5e6eec5" @@ -70,13 +71,14 @@ class NamedStoreTests(TestCase): name = "Object name" self.store.create(name, "My contents") self.driver.create.assert_called_once_with( - self.store, name, "My contents") + self.store, name, "My contents", '') def test_update(self): uuid = "d131dd02c5e6eec4" contents = "Stored contents" self.store.update(uuid, contents) - self.driver.update.assert_called_once_with(self.store, uuid, contents) + self.driver.update.assert_called_once_with(self.store, uuid, + contents, '') def test_retrieve(self): uuid = "d131dd02c5e6eec5" @@ -110,13 +112,14 @@ class VersionedStoreTests(TestCase): name = "Object name" self.store.create(name, "My contents") self.driver.create.assert_called_once_with( - self.store, name, "My contents") + self.store, name, "My contents", "") def test_update(self): uuid = "d131dd02c5e6eec4" contents = "Stored contents" self.store.update(uuid, contents) - self.driver.update.assert_called_once_with(self.store, uuid, contents) + self.driver.update.assert_called_once_with(self.store, uuid, + contents, "") def test_retrieve(self): uuid = "d131dd02c5e6eec5" @@ -156,8 +159,9 @@ class TemplateStoreTests(TestCase): def test_create(self): name = "template name" contents = "template contents" - self.store.create(name, contents) - self.driver.create.assert_called_once_with(self.store, name, contents) + self.store.create(name, contents, "") + self.driver.create.assert_called_once_with(self.store, name, + contents, "") class TemplateExtraStoreTests(TestCase): @@ -172,7 +176,8 @@ class TemplateExtraStoreTests(TestCase): name = "template_name_name" contents = "template extra contents" self.store.create(name, contents) - self.driver.create.assert_called_once_with(self.store, name, contents) + self.driver.create.assert_called_once_with(self.store, name, + contents, "") class MasterSeedStoreTests(TestCase): @@ -187,7 +192,8 @@ class MasterSeedStoreTests(TestCase): name = "master seed" contents = "seed contents" self.store.create(name, contents) - self.driver.create.assert_called_once_with(self.store, name, contents) + self.driver.create.assert_called_once_with(self.store, name, + contents, "") def test_object_type(self): self.assertEqual(stores.MasterSeedStore.object_type, "master_seed") @@ -261,7 +267,8 @@ class DeploymentPlanMockedTests(TestCase): self.driver.create.return_value = self._stored_file(name, contents) result = self.store.create(name, 'Template UUID', 'Environment UUID') - self.driver.create.assert_called_once_with(self.store, name, contents) + self.driver.create.assert_called_once_with(self.store, name, + contents, "") self.assertEqual(result.name, name) @@ -285,7 +292,8 @@ class DeploymentPlanMockedTests(TestCase): 'deployment_plan name', '') self.assertItemsEqual(self.environment_store.create.call_args_list, []) - self.driver.create.assert_called_once_with(self.store, name, contents) + self.driver.create.assert_called_once_with(self.store, name, + contents, "") self.assertEqual(result.name, name) self.master_template_store.retrieve.assert_called_once_with('UUID1') @@ -307,7 +315,8 @@ class DeploymentPlanMockedTests(TestCase): self.assertItemsEqual( self.master_template_store.create.call_args_list, []) - self.driver.create.assert_called_once_with(self.store, name, contents) + self.driver.create.assert_called_once_with(self.store, name, + contents, "") self.assertEqual(result.name, name) self.master_template_store.retrieve.assert_called_once_with(