Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add decompress module #9175

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
29705f0
adds simple implementation of `decompress` module
shamilovstas Nov 5, 2024
ecac887
adds simple test, fixes src and dest arg types
shamilovstas Nov 5, 2024
0473131
minor refactoring
shamilovstas Nov 10, 2024
f9ff4cd
adds support for common file operations
shamilovstas Nov 12, 2024
b17d344
makes tests parametrized to test all supported compression formats
shamilovstas Nov 12, 2024
a2c85c6
checks that target file exists
shamilovstas Nov 13, 2024
a578fba
writes to decompressed file now uses atomic_move
shamilovstas Nov 13, 2024
9580084
adds idempotency for decompression
shamilovstas Nov 14, 2024
8db0256
refactoring, removed classes
shamilovstas Nov 14, 2024
21f301d
adds support for check mode
shamilovstas Nov 17, 2024
a5bfe90
adds check for destination file. If it exists and it is a directory, …
shamilovstas Nov 18, 2024
36bd421
refactoring, moves code to a class. Also, simplifies tests (now only …
shamilovstas Nov 18, 2024
c7b6e48
adds 'remove' parameter that deletes original compressed file after d…
shamilovstas Nov 19, 2024
c5a2214
adds documentation
shamilovstas Nov 19, 2024
4816b8f
fixes bug with 'remove' parameter in check mode
shamilovstas Nov 19, 2024
203312b
makes dest argument not required. Dest filename now can be produced f…
shamilovstas Nov 20, 2024
89cd527
adds dest to output
shamilovstas Nov 20, 2024
3e4a174
updates the documentation, adds "RETURN" block
shamilovstas Nov 20, 2024
ee9f58c
fixes test
shamilovstas Nov 22, 2024
c6e7961
adds support for python2
shamilovstas Nov 22, 2024
eba3564
removes some of the test files that can be generated during testing. …
shamilovstas Nov 22, 2024
7774900
adds maintainer
shamilovstas Nov 22, 2024
9e34fb1
apply minor suggestions from code review
shamilovstas Nov 23, 2024
ef683c1
fixes code review comments (idempotency issue with non existing src, …
shamilovstas Nov 23, 2024
85a0765
refactors the module to use ModuleHelper
shamilovstas Nov 25, 2024
52b3f57
refactors lzma dependency manual check to use 'deps.validate'
shamilovstas Nov 25, 2024
51b31ee
minor fix
shamilovstas Nov 25, 2024
cbcd66c
removes registered handlers check
shamilovstas Nov 25, 2024
3fbcbd4
minor refactoring
shamilovstas Nov 25, 2024
8210a7c
adds aliases
shamilovstas Nov 25, 2024
e5917cc
changes setup for tests
shamilovstas Nov 25, 2024
e2c94ab
tests: ignores macos and fixes tests for FreeBSD
shamilovstas Nov 25, 2024
d2d9159
tests: reverts ignore for macos and fixes issue with centos7
shamilovstas Nov 25, 2024
28752e3
tests: adds liblzma dependency for python2
shamilovstas Nov 25, 2024
ae8db2b
tests: adds backports.lzma
shamilovstas Nov 26, 2024
e68936b
fixes bz2 decompression for python2
shamilovstas Nov 26, 2024
893b92a
tests: install xz for osx
shamilovstas Nov 26, 2024
bab6620
tests: install xz for osx (2)
shamilovstas Nov 26, 2024
6e7fed0
fixes code review comments
shamilovstas Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/BOTMETA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ files:
ignore: skornehl
$modules/dconf.py:
maintainers: azaghal
$modules/decompress.py:
maintainers: shamilovstas
$modules/deploy_helper.py:
maintainers: ramondelafuente
$modules/dimensiondata_network.py:
Expand Down
238 changes: 238 additions & 0 deletions plugins/modules/decompress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2024, Stanislav Shamilov <[email protected]>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

DOCUMENTATION = r'''
---
module: decompress
short_description: Decompresses compressed files
version_added: 10.1.0
description:
- Decompresses compressed files.
- The source (compressed) file and destination (decompressed) files are on the remote host.
- Source file can be deleted after decompression.
extends_documentation_fragment:
- ansible.builtin.files
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
src:
description:
- Remote absolute path for the file to decompress.
type: path
required: true
dest:
description:
- The file name of the destination file where the compressed file will be decompressed.
- If the destination file exists, it will be truncated and overwritten.
- If not specified, the destination filename will be derived from O(src) by removing the compression format
extension. For example, if O(src) is V(/path/to/file.txt.gz) and O(format) is V(gz), O(dest) will be
V(/path/to/file.txt). If the O(src) file does not have an extension for the current O(format), the O(dest)
filename will be made by appending C(_decompressed) to the O(src) filename. For instance, if O(src) is
V(/path/to/file.myextension), the (dest) filename will be V(/path/to/file.myextension_decompressed).
type: path
format:
description:
- The type of compression to use to decompress.
type: str
choices: [ gz, bz2, xz ]
default: gz
remove:
description:
- Remove original compressed file after decompression.
type: bool
default: false
requirements:
- Requires C(lzma) (standard library of Python 3) or L(backports.lzma, https://pypi.org/project/backports.lzma/) (Python 2) if using C(xz) format.
author:
- Stanislav Shamilov (@shamilovstas)
'''

EXAMPLES = r'''
- name: Decompress file /path/to/file.txt.gz into /path/to/file.txt (gz compression is used by default)
community.general.decompress:
src: /path/to/file.txt.gz
dest: /path/to/file.txt

- name: Decompress file /path/to/file.txt.gz into /path/to/file.txt
community.general.decompress:
src: /path/to/file.txt.gz

- name: Decompress file compressed with bzip2
community.general.decompress:
src: /path/to/file.txt.bz2
dest: /path/to/file.bz2
format: bz2

- name: Decompress file and delete the compressed file afterwards
community.general.decompress:
src: /path/to/file.txt.gz
dest: /path/to/file.txt
remove: true
'''

RETURN = r'''
dest:
description: Path to decompressed file
type: str
returned: success
sample: /path/to/file.txt
'''

import bz2
import filecmp
import gzip
import os
import shutil
import tempfile

from traceback import format_exc
from ansible.module_utils import six
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_bytes

LZMA_IMP_ERR = None
if six.PY3:
try:
import lzma

HAS_LZMA = True
except ImportError:
LZMA_IMP_ERR = format_exc()
HAS_LZMA = False
else:
try:
from backports import lzma

HAS_LZMA = True
except ImportError:
LZMA_IMP_ERR = format_exc()
HAS_LZMA = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to consider using module_utils.deps, see https://docs.ansible.com/ansible/latest/collections/community/general/docsite/guide_deps.html for guidance on how to use it.



def lzma_decompress(src):
return lzma.open(src, "rb")


def bz2_decompress(src):
return bz2.open(src, "rb")


def gzip_decompress(src):
return gzip.open(src, "rb")


def decompress(b_src, b_dest, handler):
with handler(b_src) as src_file:
with open(b_dest, "wb") as dest_file:
shutil.copyfileobj(src_file, dest_file)


class Decompress(object):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destination_filename_template = "%s_decompressed"

def __init__(self, module):
self.src = module.params['src']
self.fmt = module.params['format']
self.remove = module.params['remove']
self.check_mode = module.check_mode
self.module = module
self.changed = False
self.handlers = {"gz": gzip_decompress, "bz2": bz2_decompress, "xz": lzma_decompress}

dest = module.params['dest']
if dest is None:
self.dest = self.get_destination_filename()
else:
self.dest = dest
self.b_dest = to_bytes(self.dest, errors='surrogate_or_strict')
self.b_src = to_bytes(self.src, errors='surrogate_or_strict')

def configure(self):
if not os.path.exists(self.b_src):
msg = "Path does not exist: '%s'" % self.b_src
if self.remove and os.path.exists(self.b_dest):
self.module.warn(msg)
self.module.exit_json(changed=False)
else:
self.module.fail_json(msg=msg)
if os.path.isdir(self.b_src):
self.module.fail_json(msg="Cannot decompress directory '%s'" % self.b_src)
if os.path.isdir(self.b_dest):
self.module.fail_json(msg="Destination is a directory, cannot decompress: '%s'" % self.b_src)
self.fmt = self.module.params['format']
if self.fmt not in self.handlers:
self.module.fail_json(msg="Could not decompress '%s' format" % self.fmt)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format is controlled by the module parameter, so it is always supposed to have a handler. This code is redundant.


def run(self):
self.configure()
file_args = self.module.load_file_common_arguments(self.module.params, path=self.dest)
handler = self.handlers[self.fmt]
try:
tempfd, temppath = tempfile.mkstemp(dir=self.module.tmpdir)
russoz marked this conversation as resolved.
Show resolved Hide resolved
self.module.add_cleanup_file(temppath)
b_temppath = to_bytes(temppath, errors='surrogate_or_strict')
decompress(self.b_src, b_temppath, handler)
except OSError as e:
self.module.fail_json(msg="Unable to create temporary file '%s'" % to_native(e))

if os.path.exists(self.b_dest):
self.changed = not filecmp.cmp(b_temppath, self.b_dest, shallow=False)
else:
self.changed = True

if self.changed and not self.module.check_mode:
try:
self.module.atomic_move(b_temppath, self.b_dest)
except OSError:
self.module.fail_json(msg="Unable to move temporary file '%s' to '%s'" % (b_temppath, self.dest))

if self.remove and not self.check_mode:
os.remove(self.b_src)
self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)

def get_destination_filename(self):
src = self.src
fmt_extension = ".%s" % self.fmt
if src.endswith(fmt_extension) and len(src) > len(fmt_extension):
filename = src[:-len(fmt_extension)]
else:
filename = Decompress.destination_filename_template % src
return filename


def main():
module = AnsibleModule(
argument_spec=dict(
src=dict(type='path', required=True),
dest=dict(type='path'),
format=dict(type='str', default='gz', choices=['gz', 'bz2', 'xz']),
remove=dict(type='bool', default=False)
),
add_file_common_args=True,
supports_check_mode=True
)
if not HAS_LZMA and module.params['format'] == 'xz':
module.fail_json(
msg=missing_required_lib("lzma or backports.lzma", reason="when using xz format"), exception=LZMA_IMP_ERR
)

d = Decompress(module)
d.run()

module.exit_json(changed=d.changed, dest=d.dest)


if __name__ == '__main__':
main()
5 changes: 5 additions & 0 deletions tests/integration/targets/decompress/files/file.txt
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

This is sample file
5 changes: 5 additions & 0 deletions tests/integration/targets/decompress/files/second_file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

Content of this file must differ from the 'file.txt'
7 changes: 7 additions & 0 deletions tests/integration/targets/decompress/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

dependencies:
- setup_remote_tmp_dir
12 changes: 12 additions & 0 deletions tests/integration/targets/decompress/tasks/cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

- name: Delete decompressed files
file:
path: "{{ remote_tmp_dir }}/file_from_{{ format }}.txt"
state: absent
loop: "{{ formats }}"
loop_control:
loop_var: format
29 changes: 29 additions & 0 deletions tests/integration/targets/decompress/tasks/core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

- name: Set mode for decompressed file ({{ format }} test)
set_fact:
decompressed_mode: "0640"

- name: Simple decompress ({{ format }} test)
community.general.decompress:
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
src: "{{ remote_tmp_dir }}/file.txt.{{ format }}"
dest: "{{ remote_tmp_dir }}/file_from_{{ format }}.txt"
format: "{{ format }}"
mode: "{{ decompressed_mode }}"
register: first_decompression

- name: Stat decompressed file ({{ format }} test)
stat:
path: "{{ remote_tmp_dir }}/file_from_{{ format }}.txt"
register: decompressed_file_stat

- name: Check that file was decompressed correctly ({{ format }} test)
assert:
that:
- first_decompression.changed
- decompressed_file_stat.stat.exists
- decompressed_file_stat.stat.mode == decompressed_mode
- orig_file_stat.stat.checksum == decompressed_file_stat.stat.checksum
51 changes: 51 additions & 0 deletions tests/integration/targets/decompress/tasks/dest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

- name: Copy a compressed file
copy:
src: "{{ item.orig }}"
dest: "{{ item.new }}"
remote_src: true
loop:
- { orig: "{{ remote_tmp_dir }}/file.txt.gz", new: "{{ remote_tmp_dir }}/dest.txt.gz" }
- { orig: "{{ remote_tmp_dir }}/file.txt.gz", new: "{{ remote_tmp_dir }}/dest" }

- name: Decompress a file without specifying destination
community.general.decompress:
src: "{{ remote_tmp_dir }}/dest.txt.gz"
remove: true

- name: Decompress a file which lacks extension without specifying destination
community.general.decompress:
src: "{{ remote_tmp_dir }}/dest"
remove: true

- name: Stat result files
stat:
path: "{{ remote_tmp_dir }}/{{ filename }}"
loop:
- dest.txt
- dest_decompressed
loop_control:
loop_var: filename
register: result_files_stat

- name: Test that file exists
assert:
that: "{{ item.stat.exists }}"
quiet: true
loop: "{{ result_files_stat.results }}"
loop_control:
label: "{{ item.stat.path }}"

- name: Delete test files
file:
path: "{{ filename }}"
state: absent
loop:
- "dest.txt"
- "dest_decompressed"
loop_control:
loop_var: filename
Loading