diff --git a/changelogs/fragments/pkgutil-check-mode-etc.yaml b/changelogs/fragments/pkgutil-check-mode-etc.yaml new file mode 100644 index 0000000000..9c659ba110 --- /dev/null +++ b/changelogs/fragments/pkgutil-check-mode-etc.yaml @@ -0,0 +1,4 @@ +minor_changes: + - pkgutil - module now supports check mode (https://github.com/ansible-collections/community.general/pull/799). + - pkgutil - module can now accept a list of packages (https://github.com/ansible-collections/community.general/pull/799). + - pkgutil - module has a new option, ``force``, equivalent to the ``-f`` option to the `pkgutil `_ command (https://github.com/ansible-collections/community.general/pull/799). diff --git a/plugins/modules/packaging/os/pkgutil.py b/plugins/modules/packaging/os/pkgutil.py index a17e2cc180..7e8e9a5de2 100644 --- a/plugins/modules/packaging/os/pkgutil.py +++ b/plugins/modules/packaging/os/pkgutil.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2013, Alexander Winkler +# Copyright: (c) 2013, Alexander Winkler # based on svr4pkg by # Boyd Adamson (2012) # @@ -11,46 +11,55 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: pkgutil -short_description: Manage CSW-Packages on Solaris +short_description: OpenCSW package management on Solaris description: - - Manages CSW packages (SVR4 format) on Solaris 10 and 11. - - These were the native packages on Solaris <= 10 and are available - as a legacy feature in Solaris 11. - - Pkgutil is an advanced packaging system, which resolves dependency on installation. - It is designed for CSW packages. -author: "Alexander Winkler (@dermute)" +- This module installs, updates and removes packages from the OpenCSW project for Solaris. +- Unlike the M(community.general.svr4pkg) module, it will resolve and download dependencies. +- See U(https://www.opencsw.org/) for more information about the project. +author: +- Alexander Winkler (@dermute) +- David Ponessa (@scathatheworm) options: name: description: - - Package name, e.g. (C(CSWnrpe)) + - The name of the package. + - When using C(state=latest), this can be C('*'), which updates all installed packages managed by pkgutil. + type: list required: true - type: str + elements: str + aliases: [ pkg ] site: description: - - Specifies the repository path to install the package from. - - Its global definition is done in C(/etc/opt/csw/pkgutil.conf). + - The repository path to install the package from. + - Its global definition is in C(/etc/opt/csw/pkgutil.conf). required: false type: str state: description: - - Whether to install (C(present)), or remove (C(absent)) a package. - - The upgrade (C(latest)) operation will update/install the package to the latest version available. - - "Note: The module has a limitation that (C(latest)) only works for one package, not lists of them." - required: true - choices: ["present", "absent", "latest"] + - Whether to install (C(present)/C(installed)), or remove (C(absent)/C(removed)) packages. + - The upgrade (C(latest)) operation will update/install the packages to the latest version available. type: str + required: true + choices: [ absent, installed, latest, present, removed ] update_catalog: description: - - If you want to refresh your catalog from the mirror, set this to (C(yes)). - required: false - default: no + - If you always want to refresh your catalog from the mirror, even when it's not stale, set this to C(yes). type: bool + default: no + force: + description: + - To allow the update process to downgrade packages to match what is present in the repository, set this to C(yes). + - This is useful for rolling back to stable from testing, or similar operations. + type: bool + version_added: 1.2.0 +notes: +- In order to check the availability of packages, the catalog cache under C(/var/opt/csw/pkgutil) may be refreshed even in check mode. ''' -EXAMPLES = ''' +EXAMPLES = r''' - name: Install a package community.general.pkgutil: name: CSWcommon @@ -59,35 +68,80 @@ EXAMPLES = ''' - name: Install a package from a specific repository community.general.pkgutil: name: CSWnrpe - site: 'ftp://myinternal.repo/opencsw/kiel' + site: ftp://myinternal.repo/opencsw/kiel state: latest + +- name: Remove a package + community.general.pkgutil: + name: CSWtop + state: absent + +- name: Install several packages + community.general.pkgutil: + name: + - CSWsudo + - CSWtop + state: present + +- name: Update all packages + community.general.pkgutil: + name: '*' + state: latest + +- name: Update all packages and force versions to match latest in catalog + community.general.pkgutil: + name: '*' + state: latest + force: yes ''' +RETURN = r''' # ''' + from ansible.module_utils.basic import AnsibleModule -def package_installed(module, name): - cmd = ['pkginfo'] - cmd.append('-q') - cmd.append(name) - rc, out, err = run_command(module, cmd) - if rc == 0: - return True - else: - return False +def packages_not_installed(module, names): + ''' Check if each package is installed and return list of the ones absent ''' + pkgs = [] + for pkg in names: + rc, out, err = run_command(module, ['pkginfo', '-q', pkg]) + if rc != 0: + pkgs.append(pkg) + return pkgs -def package_latest(module, name, site): - # Only supports one package - cmd = ['pkgutil', '-U', '--single', '-c'] +def packages_installed(module, names): + ''' Check if each package is installed and return list of the ones present ''' + pkgs = [] + for pkg in names: + if not pkg.startswith('CSW'): + continue + rc, out, err = run_command(module, ['pkginfo', '-q', pkg]) + if rc == 0: + pkgs.append(pkg) + return pkgs + + +def packages_not_latest(module, names, site, update_catalog): + ''' Check status of each package and return list of the ones with an upgrade available ''' + cmd = ['pkgutil'] + if update_catalog: + cmd.append('-U') + cmd.append('-c') if site is not None: - cmd += ['-t', site] - cmd.append(name) + cmd.extend('-t', site) + if names != ['*']: + cmd.extend(names) rc, out, err = run_command(module, cmd) - # replace | tail -1 |grep -v SAME - # use -2, because splitting on \n create a empty line - # at the end of the list - return 'SAME' in out.split('\n')[-2] + + # Find packages in the catalog which are not up to date + packages = [] + for line in out.split('\n')[1:-1]: + if 'catalog' not in line and 'SAME' not in line: + packages.append(line.split(' ')[0]) + + # Remove duplicates + return list(set(packages)) def run_command(module, cmd, **kwargs): @@ -96,117 +150,129 @@ def run_command(module, cmd, **kwargs): return module.run_command(cmd, **kwargs) -def package_install(module, state, name, site, update_catalog): - cmd = ['pkgutil', '-iy'] +def package_install(module, state, pkgs, site, update_catalog, force): + cmd = ['pkgutil'] + if module.check_mode: + cmd.append('-n') + cmd.append('-iy') if update_catalog: - cmd += ['-U'] + cmd.append('-U') if site is not None: - cmd += ['-t', site] - if state == 'latest': - cmd += ['-f'] - cmd.append(name) - (rc, out, err) = run_command(module, cmd) - return (rc, out, err) + cmd.extend('-t', site) + if force: + cmd.append('-f') + cmd.extend(pkgs) + return run_command(module, cmd) -def package_upgrade(module, name, site, update_catalog): - cmd = ['pkgutil', '-ufy'] +def package_upgrade(module, pkgs, site, update_catalog, force): + cmd = ['pkgutil'] + if module.check_mode: + cmd.append('-n') + cmd.append('-uy') if update_catalog: - cmd += ['-U'] + cmd.append('-U') if site is not None: - cmd += ['-t', site] - cmd.append(name) - (rc, out, err) = run_command(module, cmd) - return (rc, out, err) + cmd.extend('-t', site) + if force: + cmd.append('-f') + cmd += pkgs + return run_command(module, cmd) -def package_uninstall(module, name): - cmd = ['pkgutil', '-ry', name] - (rc, out, err) = run_command(module, cmd) - return (rc, out, err) +def package_uninstall(module, pkgs): + cmd = ['pkgutil'] + if module.check_mode: + cmd.append('-n') + cmd.append('-ry') + cmd.extend(pkgs) + return run_command(module, cmd) def main(): module = AnsibleModule( argument_spec=dict( - name=dict(required=True), - state=dict(required=True, choices=['present', 'absent', 'latest']), - site=dict(default=None), - update_catalog=dict(required=False, default=False, type='bool'), + name=dict(type='list', elements='str', required=True, aliases=['pkg']), + state=dict(type='str', required=True, choices=['absent', 'installed', 'latest', 'present', 'removed']), + site=dict(type='str'), + update_catalog=dict(type='bool', default=False), + force=dict(type='bool', default=False), ), - supports_check_mode=True + supports_check_mode=True, ) name = module.params['name'] state = module.params['state'] site = module.params['site'] update_catalog = module.params['update_catalog'] + force = module.params['force'] + rc = None out = '' err = '' - result = {} - result['name'] = name - result['state'] = state + result = dict( + name=name, + state=state, + ) - if state == 'present': - if not package_installed(module, name): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = package_install(module, state, name, site, update_catalog) - # Stdout is normally empty but for some packages can be - # very long and is not often useful - if len(out) > 75: - out = out[:75] + '...' + if state in ['installed', 'present']: + # Fail with an explicit error when trying to "install" '*' + if name == ['*']: + module.fail_json(msg="Can not use 'state: present' with name: '*'") + + # Build list of packages that are actually not installed from the ones requested + pkgs = packages_not_installed(module, name) + + # If the package list is empty then all packages are already present + if pkgs == []: + module.exit_json(changed=False) + + (rc, out, err) = package_install(module, state, pkgs, site, update_catalog, force) + if rc != 0: + module.fail_json(msg=(err or out)) + + elif state in ['latest']: + # When using latest for * + if name == ['*']: + # Check for packages that are actually outdated + pkgs = packages_not_latest(module, name, site, update_catalog) + + # If the package list comes up empty, everything is already up to date + if pkgs == []: + module.exit_json(changed=False) + + # If there are packages to update, just empty the list and run the command without it + # pkgutil logic is to update all when run without packages names + pkgs = [] + (rc, out, err) = package_upgrade(module, pkgs, site, update_catalog, force) if rc != 0: - if err: - msg = err - else: - msg = out - module.fail_json(msg=msg) - - elif state == 'latest': - if not package_installed(module, name): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = package_install(module, state, name, site, update_catalog) - if len(out) > 75: - out = out[:75] + '...' - if rc != 0: - if err: - msg = err - else: - msg = out - module.fail_json(msg=msg) - + module.fail_json(msg=(err or out)) else: - if not package_latest(module, name, site): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = package_upgrade(module, name, site, update_catalog) - if len(out) > 75: - out = out[:75] + '...' - if rc != 0: - if err: - msg = err - else: - msg = out - module.fail_json(msg=msg) + # Build list of packages that are either outdated or not installed + pkgs = packages_not_installed(module, name) + pkgs += packages_not_latest(module, name, site, update_catalog) - elif state == 'absent': - if package_installed(module, name): - if module.check_mode: - module.exit_json(changed=True) - (rc, out, err) = package_uninstall(module, name) - if len(out) > 75: - out = out[:75] + '...' + # If the package list is empty that means all packages are installed and up to date + if pkgs == []: + module.exit_json(changed=False) + + (rc, out, err) = package_upgrade(module, pkgs, site, update_catalog, force) if rc != 0: - if err: - msg = err - else: - msg = out - module.fail_json(msg=msg) + module.fail_json(msg=(err or out)) + + elif state in ['absent', 'removed']: + # Build list of packages requested for removal that are actually present + pkgs = packages_installed(module, name) + + # If the list is empty, no packages need to be removed + if pkgs == []: + module.exit_json(changed=False) + + (rc, out, err) = package_uninstall(module, pkgs) + if rc != 0: + module.fail_json(msg=(err or out)) if rc is None: - # pkgutil was not executed because the package was already present/absent + # pkgutil was not executed because the package was already present/absent/up to date result['changed'] = False elif rc == 0: result['changed'] = True diff --git a/tests/integration/targets/pkgutil/aliases b/tests/integration/targets/pkgutil/aliases new file mode 100644 index 0000000000..5e163ed765 --- /dev/null +++ b/tests/integration/targets/pkgutil/aliases @@ -0,0 +1,2 @@ +destructive +unsupported diff --git a/tests/integration/targets/pkgutil/tasks/main.yml b/tests/integration/targets/pkgutil/tasks/main.yml new file mode 100644 index 0000000000..f2bf44e88c --- /dev/null +++ b/tests/integration/targets/pkgutil/tasks/main.yml @@ -0,0 +1,116 @@ +# Test code for the pkgutil module + +# Copyright: (c) 2019, Dag Wieers (@dagwieers) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +# CLEAN ENVIRONMENT +- name: Remove CSWtop + pkgutil: + name: CSWtop + state: absent + register: originally_installed + + +# ADD PACKAGE +- name: Add package (check_mode) + pkgutil: + name: CSWtop + state: present + check_mode: yes + register: cm_add_package + +- name: Verify cm_add_package + assert: + that: + - cm_add_package is changed + +- name: Add package (normal mode) + pkgutil: + name: CSWtop + state: present + register: nm_add_package + +- name: Verify nm_add_package + assert: + that: + - nm_add_package is changed + +- name: Add package again (check_mode) + pkgutil: + name: CSWtop + state: present + check_mode: yes + register: cm_add_package_again + +- name: Verify cm_add_package_again + assert: + that: + - cm_add_package_again is not changed + +- name: Add package again (normal mode) + pkgutil: + name: CSWtop + state: present + register: nm_add_package_again + +- name: Verify nm_add_package_again + assert: + that: + - nm_add_package_again is not changed + + +# REMOVE PACKAGE +- name: Remove package (check_mode) + pkgutil: + name: CSWtop + state: absent + check_mode: yes + register: cm_remove_package + +- name: Verify cm_remove_package + assert: + that: + - cm_remove_package is changed + +- name: Remove package (normal mode) + pkgutil: + name: CSWtop + state: absent + register: nm_remove_package + +- name: Verify nm_remove_package + assert: + that: + - nm_remove_package is changed + +- name: Remove package again (check_mode) + pkgutil: + name: CSWtop + state: absent + check_mode: yes + register: cm_remove_package_again + +- name: Verify cm_remove_package_again + assert: + that: + - cm_remove_package_again is not changed + +- name: Remove package again (normal mode) + pkgutil: + name: CSWtop + state: absent + register: nm_remove_package_again + +- name: Verify nm_remove_package_again + assert: + that: + - nm_remove_package_again is not changed + + +# RESTORE ENVIRONMENT +- name: Reinstall CSWtop + pkgutil: + name: CSWtop + state: present + when: originally_installed is changed