From 19df26016f232c1fe03ebde87e8330d1b2b7a546 Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Mon, 19 Aug 2013 19:53:34 +1000 Subject: [PATCH] Load deployer/global environment files at startup This allows for a global environment that the deployer can use to set their own defaults. An example /etc/heat/environment.d/x.yaml resource_registry: "OS::Quantum*": "OS::Neutron*" "AWS::RDS::DBInstance": "file:///etc/heat/templates/AWS_RDS_DBInstance.template" Change-Id: Iee647a109e5a41d41cd4a79909c6edaca0d17b87 --- etc/heat/heat.conf.sample | 3 ++ heat/common/config.py | 5 ++- heat/engine/resources/__init__.py | 42 ++++++++++++++++--- heat/tests/test_environment.py | 70 +++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/etc/heat/heat.conf.sample b/etc/heat/heat.conf.sample index 8a31cd20..8b8b47f0 100644 --- a/etc/heat/heat.conf.sample +++ b/etc/heat/heat.conf.sample @@ -21,6 +21,9 @@ # List of directories to search for Plugins (list value) #plugin_dirs=/usr/lib64/heat,/usr/lib/heat +# The directory to search for environment files (string value) +#environment_dir=/etc/heat/environment.d + # Name of the engine node. This can be an opaque identifier.It # is not necessarily a hostname, FQDN, or IP address. (string # value) diff --git a/heat/common/config.py b/heat/common/config.py index 8feeacb3..b19dc159 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -101,7 +101,10 @@ engine_opts = [ help='Driver to use for controlling instances'), cfg.ListOpt('plugin_dirs', default=['/usr/lib64/heat', '/usr/lib/heat'], - help='List of directories to search for Plugins')] + help='List of directories to search for Plugins'), + cfg.StrOpt('environment_dir', + default='/etc/heat/environment.d', + help='The directory to search for environment files')] rpc_opts = [ cfg.StrOpt('host', diff --git a/heat/engine/resources/__init__.py b/heat/engine/resources/__init__.py index 535e707e..94665faf 100644 --- a/heat/engine/resources/__init__.py +++ b/heat/engine/resources/__init__.py @@ -12,12 +12,17 @@ # 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 heat.openstack.common import log as logging + +import os +import os.path + +from heat.common import environment_format +from heat.openstack.common import log from heat.openstack.common.gettextutils import _ from heat.engine import environment -logger = logging.getLogger(__name__) +LOG = log.getLogger(__name__) def _register_resources(type_pairs): @@ -31,7 +36,7 @@ def _get_module_resources(module): try: return module.resource_mapping().iteritems() except Exception as ex: - logger.error(_('Failed to load resources from %s') % str(module)) + LOG.error(_('Failed to load resources from %s') % str(module)) else: return [] @@ -53,18 +58,45 @@ def global_env(): return _environment +def _list_environment_files(env_dir): + try: + return os.listdir(env_dir) + except OSError as osex: + LOG.error('Failed to read %s' % (env_dir)) + LOG.exception(osex) + return [] + + +def _load_global_environment(env_dir): + for env_name in _list_environment_files(env_dir): + try: + file_path = os.path.join(env_dir, env_name) + with open(file_path) as env_fd: + LOG.info('Loading %s' % file_path) + env_body = environment_format.parse(env_fd.read()) + environment_format.default_for_missing(env_body) + _environment.load(env_body) + except ValueError as vex: + LOG.error('Failed to parse %s/%s' % (env_dir, env_name)) + LOG.exception(vex) + except IOError as ioex: + LOG.error('Failed to read %s/%s' % (env_dir, env_name)) + LOG.exception(ioex) + + def initialise(): global _environment if _environment is not None: return import sys + from oslo.config import cfg from heat.common import plugin_loader _environment = environment.Environment({}, user_env=False) + cfg.CONF.import_opt('environment_dir', 'heat.common.config') + _load_global_environment(cfg.CONF.environment_dir) _register_modules(plugin_loader.load_modules(sys.modules[__name__])) - from oslo.config import cfg - cfg.CONF.import_opt('plugin_dirs', 'heat.common.config') plugin_pkg = plugin_loader.create_subpackage(cfg.CONF.plugin_dirs, diff --git a/heat/tests/test_environment.py b/heat/tests/test_environment.py index 487ded6f..32be6793 100644 --- a/heat/tests/test_environment.py +++ b/heat/tests/test_environment.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + from heat.engine import environment from heat.engine import resources @@ -96,3 +98,71 @@ class EnvironmentTest(common.HeatTestCase): self.assertEqual('ip.yaml', env.get_resource_info('OS::Networking::FloatingIP', 'my_fip').value) + + +class GlobalEnvLoadingTest(common.HeatTestCase): + + def test_happy_path(self): + list_dir = 'heat.engine.resources._list_environment_files' + with mock.patch(list_dir) as m_ldir: + m_ldir.return_value = ['a.yaml'] + env_dir = '/etc_etc/heat/enviroment.d' + env_content = '{"resource_registry": {}}' + + with mock.patch('heat.engine.resources.open', + mock.mock_open(read_data=env_content), + create=True) as m_open: + resources._load_global_environment(env_dir) + + m_ldir.assert_called_once_with(env_dir) + m_open.assert_called_once_with('%s/a.yaml' % env_dir) + + def test_empty_env_dir(self): + list_dir = 'heat.engine.resources._list_environment_files' + with mock.patch(list_dir) as m_ldir: + m_ldir.return_value = [] + env_dir = '/etc_etc/heat/enviroment.d' + resources._load_global_environment(env_dir) + + m_ldir.assert_called_once_with(env_dir) + + def test_continue_on_ioerror(self): + """assert we get all files processed even if there are + processing exceptions. + """ + list_dir = 'heat.engine.resources._list_environment_files' + with mock.patch(list_dir) as m_ldir: + m_ldir.return_value = ['a.yaml', 'b.yaml'] + env_dir = '/etc_etc/heat/enviroment.d' + env_content = '{}' + + with mock.patch('heat.engine.resources.open', + mock.mock_open(read_data=env_content), + create=True) as m_open: + m_open.side_effect = IOError + resources._load_global_environment(env_dir) + + m_ldir.assert_called_once_with(env_dir) + expected = [mock.call('%s/a.yaml' % env_dir), + mock.call('%s/b.yaml' % env_dir)] + self.assertEqual(expected, m_open.call_args_list) + + def test_continue_on_parse_error(self): + """assert we get all files processed even if there are + processing exceptions. + """ + list_dir = 'heat.engine.resources._list_environment_files' + with mock.patch(list_dir) as m_ldir: + m_ldir.return_value = ['a.yaml', 'b.yaml'] + env_dir = '/etc_etc/heat/enviroment.d' + env_content = '{@$%#$%' + + with mock.patch('heat.engine.resources.open', + mock.mock_open(read_data=env_content), + create=True) as m_open: + resources._load_global_environment(env_dir) + + m_ldir.assert_called_once_with(env_dir) + expected = [mock.call('%s/a.yaml' % env_dir), + mock.call('%s/b.yaml' % env_dir)] + self.assertEqual(expected, m_open.call_args_list) -- 2.45.2