371 lines
14 KiB
Python
371 lines
14 KiB
Python
#!/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
|
|
version_added: '0.2.0'
|
|
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 ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
|
|
|
|
|
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()
|