diff --git a/roles/stage-output/__init__.py b/roles/stage-output/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roles/stage-output/library/__init__.py b/roles/stage-output/library/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roles/stage-output/library/stage_output_renames.py b/roles/stage-output/library/stage_output_renames.py new file mode 100644 index 000000000..7e318f1b3 --- /dev/null +++ b/roles/stage-output/library/stage_output_renames.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# +# 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. + + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community' +} + + +DOCUMENTATION = ''' +--- +module: stage_output_renames + +short_description: Rename file extensions for stage-output + +description: + - "Renames file extensions for stage-output quickly." + +options: + extensions: + description: + - Dict of file extensions to be replaced with .txt when staging. + key is extension and value is boolean determining if it should + be replaced. + required: true + rename_dir: + description: + - The base dir to rename files in. + required: true + +author: + - Clark Boylan +''' + + +EXAMPLES = ''' +- name: Stage output rename extensions + stage_output_renames: + extensions: + conf: true + log: true + py: false + rename_dir: '/home/zuul/logs' +''' + + +RETURN = ''' +msg: + description: The output message from the module. +renamed: + description: Dict of old: new file names. +errors: + description: Dict of old: error strings. +''' + + +from ansible.module_utils.basic import AnsibleModule # noqa + +import os +import os.path + + +def rename_file(old_path): + # Only rename the file if it is a normal file + if os.path.isfile(old_path) and not os.path.islink(old_path): + path, ext = old_path.rsplit('.', 1) + new_path = path + '_' + ext + '.txt' + # Only rename if the target doesn't already exist + if not os.path.isfile(new_path) and \ + not os.path.isdir(new_path) and \ + not os.path.islink(new_path): + try: + os.rename(old_path, new_path) + except Exception as e: + # TODO Do we need to distinguish between these cases? + return None, str(e) + return new_path, None + return None, "Target path exists." + return None, "Source path does not exist." + + +def rename_exts(path, exts): + results = dict( + renamed=dict(), + errors=dict(), + ) + for root, dirs, files in os.walk(path): + for f in files: + for e in exts: + if f.endswith(e): + full_path = os.path.join(root, f) + new_path, error = rename_file(full_path) + if error: + results["errors"][full_path] = error + elif new_path: + results["renamed"][full_path] = new_path + break + return results + + +def run_module(): + module_args = dict( + extensions=dict(type='dict', required=True), + rename_dir=dict(type='str', required=True), + ) + + result = dict( + changed=False, + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=False + ) + + rename_dir = module.params['rename_dir'] + + if not os.path.isdir(rename_dir): + module.fail_json(msg='Rename dir %s is not a dir or does not exist' % + rename_dir, **result) + + extensions_dict = module.params['extensions'] + extensions_list = ['.' + k for k, v in extensions_dict.items() + if module.boolean(v)] + + rename_result = rename_exts(rename_dir, extensions_list) + result.update(rename_result) + + if result['renamed']: + result['changed'] = True + if result['errors']: + module.fail_json(msg='Failed to rename all requested files', **result) + else: + module.exit_json(msg='Renamed files for staging.', **result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/roles/stage-output/library/test_stage_output_renames.py b/roles/stage-output/library/test_stage_output_renames.py new file mode 100644 index 000000000..67b414e8e --- /dev/null +++ b/roles/stage-output/library/test_stage_output_renames.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# 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 +import os.path + +import testtools +import fixtures + +from .stage_output_renames import rename_exts + + +class TestStageOutputRenames(testtools.TestCase): + def test_rename_file(self): + rename_root = self.useFixture(fixtures.TempDir()).path + subdir = os.path.join(rename_root, 'subdir') + os.mkdir(subdir) + + renamed_file1 = os.path.join(rename_root, 'foo.log') + renamed_file2 = os.path.join(subdir, 'bar.something.log') + ignored_file = os.path.join(subdir, 'foo.txt') + + with open(renamed_file1, 'w') as f: + f.write("This is a test file: renamed 1") + with open(renamed_file2, 'w') as f: + f.write("This is a test file: renamed 2") + with open(ignored_file, 'w') as f: + f.write("This is a test file: ignored") + + results = rename_exts(rename_root, ['.log', '.notused']) + # Check the files we expect are present + self.assertTrue( + os.path.isfile('_'.join(renamed_file1.rsplit('.', 1)) + '.txt')) + self.assertTrue( + os.path.isfile('_'.join(renamed_file2.rsplit('.', 1)) + '.txt')) + self.assertTrue(os.path.isfile(ignored_file)) + # Check the files we don't expect are gone + self.assertFalse(os.path.isfile(renamed_file1)) + self.assertFalse(os.path.isfile(renamed_file2)) + self.assertFalse( + os.path.isfile('_'.join(ignored_file.rsplit('.', 1)) + '.txt')) + + self.assertFalse(results['errors']) + self.assertEqual(len(results['renamed'].items()), 2) diff --git a/roles/stage-output/tasks/main.yaml b/roles/stage-output/tasks/main.yaml index b5cbd30b0..ec07404f5 100644 --- a/roles/stage-output/tasks/main.yaml +++ b/roles/stage-output/tasks/main.yaml @@ -15,25 +15,6 @@ failed_when: false register: sudo_result -- name: Build the extensions list from a dict (or empty) - set_fact: - extension_list: > - {% set extensions = ['__do_not_match__'] -%} - {% if extensions_to_txt -%} - {% for extension, extension_bool in extensions_to_txt.items() -%} - {% if extension_bool -%} - {% set _ = extensions.append(extension) -%} - {% endif -%} - {% endfor -%} - {% endif -%} - {{- extensions -}} - -- name: Build the extensions regular expression - # https://github.com/ansible/ansible-lint/issues/2241 - # noqa var-spacing - set_fact: - extensions_regex: "^(.*)\\.({{ extension_list | join('|') }})$" - # TODO(andreaf) We might want to enforce that zj_source.value is a valid value # in docs, artifacts, logs. Null case already handled. # NOTE(andreaf) Files or folders that start with a '.' are renamed to starting @@ -90,24 +71,13 @@ recurse: yes become: "{{ sudo_result.rc == 0 }}" -- name: Discover log files that match extension_list - find: - paths: "{{ stage_dir }}/logs" - patterns: "{{ extensions_regex }}" - use_regex: true - recurse: true - file_type: 'file' - register: log_files_to_rename - -- name: Rename log files that match extension_list - shell: "mv {{ zj_log_file.path }} {{ zj_log_file.path | regex_replace(extensions_regex, '\\1_\\2.txt') }}" - with_items: "{{ log_files_to_rename.files }}" - loop_control: - loop_var: zj_log_file - args: - chdir: "{{ stage_dir }}/logs" - tags: - - skip_ansible_lint +# Do this in a module as looping tasks with ansible is very slow +# with large lists of files. Up to three seconds per file has been observed. +- name: Rename log files that match extensions_to_txt + stage_output_renames: + extensions: "{{ extensions_to_txt }}" + rename_dir: "{{ stage_dir }}/logs" + when: extensions_to_txt is defined and extensions_to_txt is not none - name: Collect log files block: