mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-22 12:50:22 -07:00
New module: dpkg_divert (#417)
* feature: module dpkg_divert + tests * try to skip non-deb linux distrib * use collection namespace in EXAMPLES * skip unsupported OS/distrib in tasks instead * tests: remove unskipped distribs * apply changes suggested by Andersson007 * Remove ANSIBLE_METADATA (no more needed). * Normalize docstrings (capitalize descriptions, fix styling, use yes/no booleans). * fix descriptions * update DOCUMENTATION * Drop field 'version_added' (no more needed). * Add a note about check_mode support. * use list comprehension * support diff mode * Move 'before'/'after' dicts into 'diff' dictionary. * Set 'diversion' as the actual state (or the state that would be reached, when in check mode). * Always return 'diversion' on handled exits (exit_json & fail_json). * enable 'diff' mode in tests, add missing 'become'
This commit is contained in:
parent
e47da0f512
commit
8635cd84d4
7 changed files with 1085 additions and 0 deletions
1
plugins/modules/dpkg_divert.py
Symbolic link
1
plugins/modules/dpkg_divert.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
./system/dpkg_divert.py
|
369
plugins/modules/system/dpkg_divert.py
Normal file
369
plugins/modules/system/dpkg_divert.py
Normal file
|
@ -0,0 +1,369 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017-2020, Yann Amar <quidame@poivron.org>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: dpkg_divert
|
||||
short_description: Override a debian package's version of a file
|
||||
author:
|
||||
- quidame (@quidame)
|
||||
description:
|
||||
- A diversion is for C(dpkg) the knowledge that only a given package
|
||||
(or the local administrator) is allowed to install a file at a given
|
||||
location. Other packages shipping their own version of this file will
|
||||
be forced to I(divert) it, i.e. to install it at another location. It
|
||||
allows one to keep changes in a file provided by a debian package by
|
||||
preventing its overwrite at package upgrade.
|
||||
- This module manages diversions of debian packages files using the
|
||||
C(dpkg-divert) commandline tool. It can either create or remove a
|
||||
diversion for a given file, but also update an existing diversion
|
||||
to modify its I(holder) and/or its I(divert) location.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The original and absolute path of the file to be diverted or
|
||||
undiverted. This path is unique, i.e. it is not possible to get
|
||||
two diversions for the same I(path).
|
||||
required: true
|
||||
type: path
|
||||
state:
|
||||
description:
|
||||
- When I(state=absent), remove the diversion of the specified
|
||||
I(path); when I(state=present), create the diversion if it does
|
||||
not exist, or update its package I(holder) or I(divert) location,
|
||||
if it already exists.
|
||||
type: str
|
||||
default: present
|
||||
choices: [absent, present]
|
||||
holder:
|
||||
description:
|
||||
- The name of the package whose copy of file is not diverted, also
|
||||
known as the diversion holder or the package the diversion belongs
|
||||
to.
|
||||
- The actual package does not have to be installed or even to exist
|
||||
for its name to be valid. If not specified, the diversion is hold
|
||||
by 'LOCAL', that is reserved by/for dpkg for local diversions.
|
||||
- This parameter is ignored when I(state=absent).
|
||||
type: str
|
||||
divert:
|
||||
description:
|
||||
- The location where the versions of file will be diverted.
|
||||
- Default is to add suffix C(.distrib) to the file path.
|
||||
- This parameter is ignored when I(state=absent).
|
||||
type: path
|
||||
rename:
|
||||
description:
|
||||
- Actually move the file aside (when I(state=present)) or back (when
|
||||
I(state=absent)), but only when changing the state of the diversion.
|
||||
This parameter has no effect when attempting to add a diversion that
|
||||
already exists or when removing an unexisting one.
|
||||
- Unless I(force=true), renaming fails if the destination file already
|
||||
exists (this lock being a dpkg-divert feature, and bypassing it being
|
||||
a module feature).
|
||||
type: bool
|
||||
default: no
|
||||
force:
|
||||
description:
|
||||
- When I(rename=true) and I(force=true), renaming is performed even if
|
||||
the target of the renaming exists, i.e. the existing contents of the
|
||||
file at this location will be lost.
|
||||
- This parameter is ignored when I(rename=false).
|
||||
type: bool
|
||||
default: no
|
||||
notes:
|
||||
- This module supports I(check_mode) and I(diff).
|
||||
requirements:
|
||||
- dpkg-divert >= 1.15.0 (Debian family)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Divert /usr/bin/busybox to /usr/bin/busybox.distrib and keep file in place
|
||||
community.general.dpkg_divert:
|
||||
path: /usr/bin/busybox
|
||||
|
||||
- name: Divert /usr/bin/busybox by package 'branding'
|
||||
community.general.dpkg_divert:
|
||||
path: /usr/bin/busybox
|
||||
holder: branding
|
||||
|
||||
- name: Divert and rename busybox to busybox.dpkg-divert
|
||||
community.general.dpkg_divert:
|
||||
path: /usr/bin/busybox
|
||||
divert: /usr/bin/busybox.dpkg-divert
|
||||
rename: yes
|
||||
|
||||
- name: Remove the busybox diversion and move the diverted file back
|
||||
community.general.dpkg_divert:
|
||||
path: /usr/bin/busybox
|
||||
state: absent
|
||||
rename: yes
|
||||
force: yes
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
commands:
|
||||
description: The dpkg-divert commands ran internally by the module.
|
||||
type: list
|
||||
returned: on_success
|
||||
elements: str
|
||||
sample: |-
|
||||
[
|
||||
"/usr/bin/dpkg-divert --no-rename --remove /etc/foobarrc",
|
||||
"/usr/bin/dpkg-divert --package ansible --no-rename --add /etc/foobarrc"
|
||||
]
|
||||
messages:
|
||||
description: The dpkg-divert relevant messages (stdout or stderr).
|
||||
type: list
|
||||
returned: on_success
|
||||
elements: str
|
||||
sample: |-
|
||||
[
|
||||
"Removing 'local diversion of /etc/foobarrc to /etc/foobarrc.distrib'",
|
||||
"Adding 'diversion of /etc/foobarrc to /etc/foobarrc.distrib by ansible'"
|
||||
]
|
||||
diversion:
|
||||
description: The status of the diversion after task execution.
|
||||
type: dict
|
||||
returned: always
|
||||
contains:
|
||||
divert:
|
||||
description: The location of the diverted file.
|
||||
type: str
|
||||
holder:
|
||||
description: The package holding the diversion.
|
||||
type: str
|
||||
path:
|
||||
description: The path of the file to divert/undivert.
|
||||
type: str
|
||||
state:
|
||||
description: The state of the diversion.
|
||||
type: str
|
||||
sample: |-
|
||||
{
|
||||
"divert": "/etc/foobarrc.distrib",
|
||||
"holder": "LOCAL",
|
||||
"path": "/etc/foobarrc"
|
||||
"state": "present"
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
import re
|
||||
import os
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_bytes, to_native
|
||||
|
||||
|
||||
def diversion_state(module, command, path):
|
||||
diversion = dict(path=path, state='absent', divert=None, holder=None)
|
||||
rc, out, err = module.run_command([command, '--listpackage', path], check_rc=True)
|
||||
if out:
|
||||
diversion['state'] = 'present'
|
||||
diversion['holder'] = out.rstrip()
|
||||
rc, out, err = module.run_command([command, '--truename', path], check_rc=True)
|
||||
diversion['divert'] = out.rstrip()
|
||||
return diversion
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
path=dict(required=True, type='path'),
|
||||
state=dict(required=False, type='str', default='present', choices=['absent', 'present']),
|
||||
holder=dict(required=False, type='str'),
|
||||
divert=dict(required=False, type='path'),
|
||||
rename=dict(required=False, type='bool', default=False),
|
||||
force=dict(required=False, type='bool', default=False),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
path = module.params['path']
|
||||
state = module.params['state']
|
||||
holder = module.params['holder']
|
||||
divert = module.params['divert']
|
||||
rename = module.params['rename']
|
||||
force = module.params['force']
|
||||
|
||||
diversion_wanted = dict(path=path, state=state)
|
||||
changed = False
|
||||
|
||||
DPKG_DIVERT = module.get_bin_path('dpkg-divert', required=True)
|
||||
MAINCOMMAND = [DPKG_DIVERT]
|
||||
|
||||
# Option --listpackage is needed and comes with 1.15.0
|
||||
rc, stdout, stderr = module.run_command([DPKG_DIVERT, '--version'], check_rc=True)
|
||||
[current_version] = [x for x in stdout.splitlines()[0].split() if re.match('^[0-9]+[.][0-9]', x)]
|
||||
if LooseVersion(current_version) < LooseVersion("1.15.0"):
|
||||
module.fail_json(msg="Unsupported dpkg version (<1.15.0).")
|
||||
no_rename_is_supported = (LooseVersion(current_version) >= LooseVersion("1.19.1"))
|
||||
|
||||
b_path = to_bytes(path, errors='surrogate_or_strict')
|
||||
path_exists = os.path.exists(b_path)
|
||||
# Used for things not doable with a single dpkg-divert command (as forced
|
||||
# renaming of files, and diversion's 'holder' or 'divert' updates).
|
||||
target_exists = False
|
||||
truename_exists = False
|
||||
|
||||
diversion_before = diversion_state(module, DPKG_DIVERT, path)
|
||||
if diversion_before['state'] == 'present':
|
||||
b_divert = to_bytes(diversion_before['divert'], errors='surrogate_or_strict')
|
||||
truename_exists = os.path.exists(b_divert)
|
||||
|
||||
# Append options as requested in the task parameters, but ignore some of
|
||||
# them when removing the diversion.
|
||||
if rename:
|
||||
MAINCOMMAND.append('--rename')
|
||||
elif no_rename_is_supported:
|
||||
MAINCOMMAND.append('--no-rename')
|
||||
|
||||
if state == 'present':
|
||||
if holder and holder != 'LOCAL':
|
||||
MAINCOMMAND.extend(['--package', holder])
|
||||
diversion_wanted['holder'] = holder
|
||||
else:
|
||||
MAINCOMMAND.append('--local')
|
||||
diversion_wanted['holder'] = 'LOCAL'
|
||||
|
||||
if divert:
|
||||
MAINCOMMAND.extend(['--divert', divert])
|
||||
target = divert
|
||||
else:
|
||||
target = '%s.distrib' % path
|
||||
|
||||
MAINCOMMAND.extend(['--add', path])
|
||||
diversion_wanted['divert'] = target
|
||||
b_target = to_bytes(target, errors='surrogate_or_strict')
|
||||
target_exists = os.path.exists(b_target)
|
||||
|
||||
else:
|
||||
MAINCOMMAND.extend(['--remove', path])
|
||||
diversion_wanted['divert'] = None
|
||||
diversion_wanted['holder'] = None
|
||||
|
||||
# Start to populate the returned objects.
|
||||
diversion = diversion_before.copy()
|
||||
maincommand = ' '.join(MAINCOMMAND)
|
||||
commands = [maincommand]
|
||||
|
||||
if module.check_mode or diversion_wanted == diversion_before:
|
||||
MAINCOMMAND.insert(1, '--test')
|
||||
diversion_after = diversion_wanted
|
||||
|
||||
# Just try and see
|
||||
rc, stdout, stderr = module.run_command(MAINCOMMAND)
|
||||
|
||||
if rc == 0:
|
||||
messages = [stdout.rstrip()]
|
||||
|
||||
# else... cases of failure with dpkg-divert are:
|
||||
# - The diversion does not belong to the same package (or LOCAL)
|
||||
# - The divert filename is not the same (e.g. path.distrib != path.divert)
|
||||
# - The renaming is forbidden by dpkg-divert (i.e. both the file and the
|
||||
# diverted file exist)
|
||||
|
||||
elif state != diversion_before['state']:
|
||||
# There should be no case with 'divert' and 'holder' when creating the
|
||||
# diversion from none, and they're ignored when removing the diversion.
|
||||
# So this is all about renaming...
|
||||
if rename and path_exists and (
|
||||
(state == 'absent' and truename_exists) or
|
||||
(state == 'present' and target_exists)):
|
||||
if not force:
|
||||
msg = "Set 'force' param to True to force renaming of files."
|
||||
module.fail_json(changed=changed, cmd=maincommand, rc=rc, msg=msg,
|
||||
stderr=stderr, stdout=stdout, diversion=diversion)
|
||||
else:
|
||||
msg = "Unexpected error while changing state of the diversion."
|
||||
module.fail_json(changed=changed, cmd=maincommand, rc=rc, msg=msg,
|
||||
stderr=stderr, stdout=stdout, diversion=diversion)
|
||||
|
||||
to_remove = path
|
||||
if state == 'present':
|
||||
to_remove = target
|
||||
|
||||
if not module.check_mode:
|
||||
try:
|
||||
b_remove = to_bytes(to_remove, errors='surrogate_or_strict')
|
||||
os.unlink(b_remove)
|
||||
except OSError as e:
|
||||
msg = 'Failed to remove %s: %s' % (to_remove, to_native(e))
|
||||
module.fail_json(changed=changed, cmd=maincommand, rc=rc, msg=msg,
|
||||
stderr=stderr, stdout=stdout, diversion=diversion)
|
||||
rc, stdout, stderr = module.run_command(MAINCOMMAND, check_rc=True)
|
||||
|
||||
messages = [stdout.rstrip()]
|
||||
|
||||
# The situation is that we want to modify the settings (holder or divert)
|
||||
# of an existing diversion. dpkg-divert does not handle this, and we have
|
||||
# to remove the existing diversion first, and then set a new one.
|
||||
else:
|
||||
RMDIVERSION = [DPKG_DIVERT, '--remove', path]
|
||||
if no_rename_is_supported:
|
||||
RMDIVERSION.insert(1, '--no-rename')
|
||||
rmdiversion = ' '.join(RMDIVERSION)
|
||||
|
||||
if module.check_mode:
|
||||
RMDIVERSION.insert(1, '--test')
|
||||
|
||||
if rename:
|
||||
MAINCOMMAND.remove('--rename')
|
||||
if no_rename_is_supported:
|
||||
MAINCOMMAND.insert(1, '--no-rename')
|
||||
maincommand = ' '.join(MAINCOMMAND)
|
||||
|
||||
commands = [rmdiversion, maincommand]
|
||||
rc, rmdout, rmderr = module.run_command(RMDIVERSION, check_rc=True)
|
||||
|
||||
if module.check_mode:
|
||||
messages = [rmdout.rstrip(), 'Running in check mode']
|
||||
else:
|
||||
rc, stdout, stderr = module.run_command(MAINCOMMAND, check_rc=True)
|
||||
messages = [rmdout.rstrip(), stdout.rstrip()]
|
||||
|
||||
# Avoid if possible to orphan files (i.e. to dereference them in diversion
|
||||
# database but let them in place), but do not make renaming issues fatal.
|
||||
# BTW, this module is not about state of files involved in the diversion.
|
||||
old = diversion_before['divert']
|
||||
new = diversion_wanted['divert']
|
||||
if new != old:
|
||||
b_old = to_bytes(old, errors='surrogate_or_strict')
|
||||
b_new = to_bytes(new, errors='surrogate_or_strict')
|
||||
if os.path.exists(b_old) and not os.path.exists(b_new):
|
||||
try:
|
||||
os.rename(b_old, b_new)
|
||||
except OSError as e:
|
||||
pass
|
||||
|
||||
if not module.check_mode:
|
||||
diversion_after = diversion_state(module, DPKG_DIVERT, path)
|
||||
|
||||
diversion = diversion_after.copy()
|
||||
diff = dict()
|
||||
if module._diff:
|
||||
diff['before'] = diversion_before
|
||||
diff['after'] = diversion_after
|
||||
|
||||
if diversion_after != diversion_before:
|
||||
changed = True
|
||||
|
||||
if diversion_after == diversion_wanted:
|
||||
module.exit_json(changed=changed, diversion=diversion,
|
||||
commands=commands, messages=messages, diff=diff)
|
||||
else:
|
||||
msg = "Unexpected error: see stdout and stderr for details."
|
||||
module.fail_json(changed=changed, cmd=maincommand, rc=rc, msg=msg,
|
||||
stderr=stderr, stdout=stdout, diversion=diversion)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue