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