diff --git a/plugins/modules/tripleo_shell_script.py b/plugins/modules/tripleo_shell_script.py new file mode 100644 index 0000000..b3bd517 --- /dev/null +++ b/plugins/modules/tripleo_shell_script.py @@ -0,0 +1,144 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat, 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. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + +import os +import yaml + +try: # py3 + from shlex import quote +except ImportError: # py2 + from pipes import quote + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: tripleo_shell_script +author: + - Alex Schultz +version_added: '2.8' +short_description: Writes out a shell script with environment vars and command +notes: [] +description: + - This module will write out a bash script from a shell environment dict + and a commandline string. +options: + dest: + description: + - Destination file path for the output shell script. + required: True + type: str + shell_command: + description: + - Shell command that will be run + required: True + type: str + shell_environment: + description: + - Environment dictionary for the shell execution. + default: {} + required: False + type: dict +""" + +EXAMPLES = """ +- name: Undercloud install + tripleo_shell_script: + dest: /home/stack/undercloud_install.sh + shell_command: "/usr/bin/openstack undercloud install" + shell_environment: {} +- name: Image upload + tripleo_shell_script: + dest: /home/stack/overcloud_image_upload.sh + shell_command: "/usr/bin/openstack overcloud image upload" + shell_environment: + OS_CLOUD: undercloud +""" + +RETURN = """ +""" + +_SHELL_HEADER = """#!/bin/bash +# This file is managed by ansible +set -ex + +""" + + +class TripleoShellScript(object): + """Notes about this module. + + This module will write out a bash script from the provided parameters. + """ + + def __init__(self, module, results): + + self.module = module + self.results = results + + # parse args + args = self.module.params + + # Set parameters + dest = args['dest'] + shell_command = args['shell_command'] + shell_environment = args.get('shell_environment', {}) + + if os.path.exists(dest): + self.module.debug('File exists, truncating %s' % dest) + + try: + with open(dest, 'w') as fh: + fh.write(_SHELL_HEADER) + for k, v in shell_environment.items(): + fh.write("export %(key)s=%(val)s\n" % {'key': k, + 'val': quote(v)}) + fh.write(shell_command) + fh.write("\n") + os.chmod(dest, 0o755) + self.results['changed'] = True + except Exception as e: + self.results['failed'] = True + self.results['error'] = str(e) + self.results['msg'] = ("Unable to output shell script %s: %s" % ( + dest, e)) + + self.module.exit_json(**self.results) + + +def main(): + module = AnsibleModule( + argument_spec=yaml.safe_load(DOCUMENTATION)['options'], + supports_check_mode=False + ) + results = dict( + changed=False + ) + TripleoShellScript(module, results) + + +if __name__ == '__main__': + main() diff --git a/tests/plugins/modules/__init__.py b/tests/plugins/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/plugins/modules/test_tripleo_shell_script.py b/tests/plugins/modules/test_tripleo_shell_script.py new file mode 100644 index 0000000..144aa8f --- /dev/null +++ b/tests/plugins/modules/test_tripleo_shell_script.py @@ -0,0 +1,124 @@ +# Copyright 2019 Red Hat, 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 plugins.modules import tripleo_shell_script +from tests import base as tests_base + + +class TestTripleoShellScript(tests_base.TestCase): + + @mock.patch('os.chmod') + def test_run(self, mock_chmod): + mock_module = mock.Mock() + mock_exit_json = mock.Mock() + mock_open = mock.mock_open() + mock_module.exit_json = mock_exit_json + params = {'dest': '/tmo/foo.sh', + 'shell_command': 'foo'} + mock_module.params = params + results = {} + + with mock.patch('plugins.modules.tripleo_shell_script.open', + mock_open): + tripleo_shell_script.TripleoShellScript(mock_module, results) + + mock_calls = [ + mock.call().write(tripleo_shell_script._SHELL_HEADER), + mock.call().write('foo'), + mock.call().write("\n") + ] + mock_open.assert_has_calls(mock_calls) + mock_chmod.assert_called_once_with('/tmo/foo.sh', 0o755) + mock_exit_json.assert_called_once_with(changed=True) + + @mock.patch('os.chmod') + def test_run_env(self, mock_chmod): + mock_module = mock.Mock() + mock_exit_json = mock.Mock() + mock_open = mock.mock_open() + mock_module.exit_json = mock_exit_json + params = {'dest': '/tmo/foo.sh', + 'shell_command': 'foo', + 'shell_environment': { + 'OS_CLOUD': 'undercloud'} + } + mock_module.params = params + results = {} + + with mock.patch('plugins.modules.tripleo_shell_script.open', + mock_open): + tripleo_shell_script.TripleoShellScript(mock_module, results) + + mock_calls = [ + mock.call().write(tripleo_shell_script._SHELL_HEADER), + mock.call().write('export OS_CLOUD=undercloud\n'), + mock.call().write('foo'), + mock.call().write("\n") + ] + mock_open.assert_has_calls(mock_calls) + mock_chmod.assert_called_once_with('/tmo/foo.sh', 0o755) + mock_exit_json.assert_called_once_with(changed=True) + + @mock.patch('os.chmod') + def test_run_env_quoted(self, mock_chmod): + mock_module = mock.Mock() + mock_exit_json = mock.Mock() + mock_open = mock.mock_open() + mock_module.exit_json = mock_exit_json + params = {'dest': '/tmo/foo.sh', + 'shell_command': 'foo', + 'shell_environment': { + 'OS_CLOUD': 'undercloud', + 'FILES': 'a.yaml b.yaml'} + } + mock_module.params = params + results = {} + + with mock.patch('plugins.modules.tripleo_shell_script.open', + mock_open): + tripleo_shell_script.TripleoShellScript(mock_module, results) + + mock_calls = [ + mock.call().write(tripleo_shell_script._SHELL_HEADER), + mock.call().write('export OS_CLOUD=undercloud\n'), + mock.call().write('export FILES=\'a.yaml b.yaml\'\n'), + mock.call().write('foo'), + mock.call().write("\n") + ] + mock_open.assert_has_calls(mock_calls) + mock_chmod.assert_called_once_with('/tmo/foo.sh', 0o755) + mock_exit_json.assert_called_once_with(changed=True) + + @mock.patch('os.chmod') + def test_run_fail(self, mock_chmod): + mock_module = mock.Mock() + mock_exit_json = mock.Mock() + mock_open = mock.mock_open() + mock_open.side_effect = Exception('err') + mock_module.exit_json = mock_exit_json + params = {'dest': '/tmo/foo.sh', + 'shell_command': 'foo'} + mock_module.params = params + results = {} + + with mock.patch('plugins.modules.tripleo_shell_script.open', + mock_open): + tripleo_shell_script.TripleoShellScript(mock_module, results) + mock_exit_json.assert_called_once_with( + error='err', + failed=True, + msg='Unable to output shell script /tmo/foo.sh: err')