1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2013, Alexander Winkler <mail () winkler-alexander.de>
5# based on svr4pkg by
6#  Boyd Adamson <boyd () boydadamson.com> (2012)
7#
8# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
9
10from __future__ import absolute_import, division, print_function
11__metaclass__ = type
12
13
14DOCUMENTATION = r'''
15---
16module: pkgutil
17short_description: OpenCSW package management on Solaris
18description:
19- This module installs, updates and removes packages from the OpenCSW project for Solaris.
20- Unlike the M(community.general.svr4pkg) module, it will resolve and download dependencies.
21- See U(https://www.opencsw.org/) for more information about the project.
22author:
23- Alexander Winkler (@dermute)
24- David Ponessa (@scathatheworm)
25options:
26  name:
27    description:
28    - The name of the package.
29    - When using C(state=latest), this can be C('*'), which updates all installed packages managed by pkgutil.
30    type: list
31    required: true
32    elements: str
33    aliases: [ pkg ]
34  site:
35    description:
36    - The repository path to install the package from.
37    - Its global definition is in C(/etc/opt/csw/pkgutil.conf).
38    required: false
39    type: str
40  state:
41    description:
42    - Whether to install (C(present)/C(installed)), or remove (C(absent)/C(removed)) packages.
43    - The upgrade (C(latest)) operation will update/install the packages to the latest version available.
44    type: str
45    required: true
46    choices: [ absent, installed, latest, present, removed ]
47  update_catalog:
48    description:
49    - If you always want to refresh your catalog from the mirror, even when it's not stale, set this to C(yes).
50    type: bool
51    default: no
52  force:
53    description:
54    - To allow the update process to downgrade packages to match what is present in the repository, set this to C(yes).
55    - This is useful for rolling back to stable from testing, or similar operations.
56    type: bool
57    default: false
58    version_added: 1.2.0
59notes:
60- In order to check the availability of packages, the catalog cache under C(/var/opt/csw/pkgutil) may be refreshed even in check mode.
61'''
62
63EXAMPLES = r'''
64- name: Install a package
65  community.general.pkgutil:
66    name: CSWcommon
67    state: present
68
69- name: Install a package from a specific repository
70  community.general.pkgutil:
71    name: CSWnrpe
72    site: ftp://myinternal.repo/opencsw/kiel
73    state: latest
74
75- name: Remove a package
76  community.general.pkgutil:
77    name: CSWtop
78    state: absent
79
80- name: Install several packages
81  community.general.pkgutil:
82    name:
83    - CSWsudo
84    - CSWtop
85    state: present
86
87- name: Update all packages
88  community.general.pkgutil:
89    name: '*'
90    state: latest
91
92- name: Update all packages and force versions to match latest in catalog
93  community.general.pkgutil:
94    name: '*'
95    state: latest
96    force: yes
97'''
98
99RETURN = r''' # '''
100
101from ansible.module_utils.basic import AnsibleModule
102
103
104def packages_not_installed(module, names):
105    ''' Check if each package is installed and return list of the ones absent '''
106    pkgs = []
107    for pkg in names:
108        rc, out, err = run_command(module, ['pkginfo', '-q', pkg])
109        if rc != 0:
110            pkgs.append(pkg)
111    return pkgs
112
113
114def packages_installed(module, names):
115    ''' Check if each package is installed and return list of the ones present '''
116    pkgs = []
117    for pkg in names:
118        if not pkg.startswith('CSW'):
119            continue
120        rc, out, err = run_command(module, ['pkginfo', '-q', pkg])
121        if rc == 0:
122            pkgs.append(pkg)
123    return pkgs
124
125
126def packages_not_latest(module, names, site, update_catalog):
127    ''' Check status of each package and return list of the ones with an upgrade available '''
128    cmd = ['pkgutil']
129    if update_catalog:
130        cmd.append('-U')
131    cmd.append('-c')
132    if site is not None:
133        cmd.extend(['-t', site])
134    if names != ['*']:
135        cmd.extend(names)
136    rc, out, err = run_command(module, cmd)
137
138    # Find packages in the catalog which are not up to date
139    packages = []
140    for line in out.split('\n')[1:-1]:
141        if 'catalog' not in line and 'SAME' not in line:
142            packages.append(line.split(' ')[0])
143
144    # Remove duplicates
145    return list(set(packages))
146
147
148def run_command(module, cmd, **kwargs):
149    progname = cmd[0]
150    cmd[0] = module.get_bin_path(progname, True, ['/opt/csw/bin'])
151    return module.run_command(cmd, **kwargs)
152
153
154def package_install(module, state, pkgs, site, update_catalog, force):
155    cmd = ['pkgutil']
156    if module.check_mode:
157        cmd.append('-n')
158    cmd.append('-iy')
159    if update_catalog:
160        cmd.append('-U')
161    if site is not None:
162        cmd.extend(['-t', site])
163    if force:
164        cmd.append('-f')
165    cmd.extend(pkgs)
166    return run_command(module, cmd)
167
168
169def package_upgrade(module, pkgs, site, update_catalog, force):
170    cmd = ['pkgutil']
171    if module.check_mode:
172        cmd.append('-n')
173    cmd.append('-uy')
174    if update_catalog:
175        cmd.append('-U')
176    if site is not None:
177        cmd.extend(['-t', site])
178    if force:
179        cmd.append('-f')
180    cmd += pkgs
181    return run_command(module, cmd)
182
183
184def package_uninstall(module, pkgs):
185    cmd = ['pkgutil']
186    if module.check_mode:
187        cmd.append('-n')
188    cmd.append('-ry')
189    cmd.extend(pkgs)
190    return run_command(module, cmd)
191
192
193def main():
194    module = AnsibleModule(
195        argument_spec=dict(
196            name=dict(type='list', elements='str', required=True, aliases=['pkg']),
197            state=dict(type='str', required=True, choices=['absent', 'installed', 'latest', 'present', 'removed']),
198            site=dict(type='str'),
199            update_catalog=dict(type='bool', default=False),
200            force=dict(type='bool', default=False),
201        ),
202        supports_check_mode=True,
203    )
204    name = module.params['name']
205    state = module.params['state']
206    site = module.params['site']
207    update_catalog = module.params['update_catalog']
208    force = module.params['force']
209
210    rc = None
211    out = ''
212    err = ''
213    result = dict(
214        name=name,
215        state=state,
216    )
217
218    if state in ['installed', 'present']:
219        # Fail with an explicit error when trying to "install" '*'
220        if name == ['*']:
221            module.fail_json(msg="Can not use 'state: present' with name: '*'")
222
223        # Build list of packages that are actually not installed from the ones requested
224        pkgs = packages_not_installed(module, name)
225
226        # If the package list is empty then all packages are already present
227        if pkgs == []:
228            module.exit_json(changed=False)
229
230        (rc, out, err) = package_install(module, state, pkgs, site, update_catalog, force)
231        if rc != 0:
232            module.fail_json(msg=(err or out))
233
234    elif state in ['latest']:
235        # When using latest for *
236        if name == ['*']:
237            # Check for packages that are actually outdated
238            pkgs = packages_not_latest(module, name, site, update_catalog)
239
240            # If the package list comes up empty, everything is already up to date
241            if pkgs == []:
242                module.exit_json(changed=False)
243
244            # If there are packages to update, just empty the list and run the command without it
245            # pkgutil logic is to update all when run without packages names
246            pkgs = []
247            (rc, out, err) = package_upgrade(module, pkgs, site, update_catalog, force)
248            if rc != 0:
249                module.fail_json(msg=(err or out))
250        else:
251            # Build list of packages that are either outdated or not installed
252            pkgs = packages_not_installed(module, name)
253            pkgs += packages_not_latest(module, name, site, update_catalog)
254
255            # If the package list is empty that means all packages are installed and up to date
256            if pkgs == []:
257                module.exit_json(changed=False)
258
259            (rc, out, err) = package_upgrade(module, pkgs, site, update_catalog, force)
260            if rc != 0:
261                module.fail_json(msg=(err or out))
262
263    elif state in ['absent', 'removed']:
264        # Build list of packages requested for removal that are actually present
265        pkgs = packages_installed(module, name)
266
267        # If the list is empty, no packages need to be removed
268        if pkgs == []:
269            module.exit_json(changed=False)
270
271        (rc, out, err) = package_uninstall(module, pkgs)
272        if rc != 0:
273            module.fail_json(msg=(err or out))
274
275    if rc is None:
276        # pkgutil was not executed because the package was already present/absent/up to date
277        result['changed'] = False
278    elif rc == 0:
279        result['changed'] = True
280    else:
281        result['changed'] = False
282        result['failed'] = True
283
284    if out:
285        result['stdout'] = out
286    if err:
287        result['stderr'] = err
288
289    module.exit_json(**result)
290
291
292if __name__ == '__main__':
293    main()
294