1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2012, Red Hat, Inc
5# Written by Seth Vidal <skvidal at fedoraproject.org>
6# Copyright: (c) 2014, Epic Games, Inc.
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 = '''
15---
16module: yum
17version_added: historical
18short_description: Manages packages with the I(yum) package manager
19description:
20     - Installs, upgrade, downgrades, removes, and lists packages and groups with the I(yum) package manager.
21     - This module only works on Python 2. If you require Python 3 support see the M(ansible.builtin.dnf) module.
22options:
23  use_backend:
24    description:
25      - This module supports C(yum) (as it always has), this is known as C(yum3)/C(YUM3)/C(yum-deprecated) by
26        upstream yum developers. As of Ansible 2.7+, this module also supports C(YUM4), which is the
27        "new yum" and it has an C(dnf) backend.
28      - By default, this module will select the backend based on the C(ansible_pkg_mgr) fact.
29    default: "auto"
30    choices: [ auto, yum, yum4, dnf ]
31    type: str
32    version_added: "2.7"
33  name:
34    description:
35      - A package name or package specifier with version, like C(name-1.0).
36      - Comparison operators for package version are valid here C(>), C(<), C(>=), C(<=). Example - C(name>=1.0)
37      - If a previous version is specified, the task also needs to turn C(allow_downgrade) on.
38        See the C(allow_downgrade) documentation for caveats with downgrading packages.
39      - When using state=latest, this can be C('*') which means run C(yum -y update).
40      - You can also pass a url or a local path to a rpm file (using state=present).
41        To operate on several packages this can accept a comma separated string of packages or (as of 2.0) a list of packages.
42    aliases: [ pkg ]
43    type: list
44    elements: str
45  exclude:
46    description:
47      - Package name(s) to exclude when state=present, or latest
48    type: list
49    elements: str
50    version_added: "2.0"
51  list:
52    description:
53      - "Package name to run the equivalent of yum list --show-duplicates <package> against. In addition to listing packages,
54        use can also list the following: C(installed), C(updates), C(available) and C(repos)."
55      - This parameter is mutually exclusive with C(name).
56    type: str
57  state:
58    description:
59      - Whether to install (C(present) or C(installed), C(latest)), or remove (C(absent) or C(removed)) a package.
60      - C(present) and C(installed) will simply ensure that a desired package is installed.
61      - C(latest) will update the specified package if it's not of the latest available version.
62      - C(absent) and C(removed) will remove the specified package.
63      - Default is C(None), however in effect the default action is C(present) unless the C(autoremove) option is
64        enabled for this module, then C(absent) is inferred.
65    type: str
66    choices: [ absent, installed, latest, present, removed ]
67  enablerepo:
68    description:
69      - I(Repoid) of repositories to enable for the install/update operation.
70        These repos will not persist beyond the transaction.
71        When specifying multiple repos, separate them with a C(",").
72      - As of Ansible 2.7, this can alternatively be a list instead of C(",")
73        separated string
74    type: list
75    elements: str
76    version_added: "0.9"
77  disablerepo:
78    description:
79      - I(Repoid) of repositories to disable for the install/update operation.
80        These repos will not persist beyond the transaction.
81        When specifying multiple repos, separate them with a C(",").
82      - As of Ansible 2.7, this can alternatively be a list instead of C(",")
83        separated string
84    type: list
85    elements: str
86    version_added: "0.9"
87  conf_file:
88    description:
89      - The remote yum configuration file to use for the transaction.
90    type: str
91    version_added: "0.6"
92  disable_gpg_check:
93    description:
94      - Whether to disable the GPG checking of signatures of packages being
95        installed. Has an effect only if state is I(present) or I(latest).
96    type: bool
97    default: "no"
98    version_added: "1.2"
99  skip_broken:
100    description:
101      - Skip packages with broken dependencies(devsolve) and are causing problems.
102    type: bool
103    default: "no"
104    version_added: "2.3"
105  update_cache:
106    description:
107      - Force yum to check if cache is out of date and redownload if needed.
108        Has an effect only if state is I(present) or I(latest).
109    type: bool
110    default: "no"
111    aliases: [ expire-cache ]
112    version_added: "1.9"
113  validate_certs:
114    description:
115      - This only applies if using a https url as the source of the rpm. e.g. for localinstall. If set to C(no), the SSL certificates will not be validated.
116      - This should only set to C(no) used on personally controlled sites using self-signed certificates as it avoids verifying the source site.
117      - Prior to 2.1 the code worked as if this was set to C(yes).
118    type: bool
119    default: "yes"
120    version_added: "2.1"
121
122  update_only:
123    description:
124      - When using latest, only update installed packages. Do not install packages.
125      - Has an effect only if state is I(latest)
126    default: "no"
127    type: bool
128    version_added: "2.5"
129
130  installroot:
131    description:
132      - Specifies an alternative installroot, relative to which all packages
133        will be installed.
134    default: "/"
135    type: str
136    version_added: "2.3"
137  security:
138    description:
139      - If set to C(yes), and C(state=latest) then only installs updates that have been marked security related.
140    type: bool
141    default: "no"
142    version_added: "2.4"
143  bugfix:
144    description:
145      - If set to C(yes), and C(state=latest) then only installs updates that have been marked bugfix related.
146    default: "no"
147    type: bool
148    version_added: "2.6"
149  allow_downgrade:
150    description:
151      - Specify if the named package and version is allowed to downgrade
152        a maybe already installed higher version of that package.
153        Note that setting allow_downgrade=True can make this module
154        behave in a non-idempotent way. The task could end up with a set
155        of packages that does not match the complete list of specified
156        packages to install (because dependencies between the downgraded
157        package and others can cause changes to the packages which were
158        in the earlier transaction).
159    type: bool
160    default: "no"
161    version_added: "2.4"
162  enable_plugin:
163    description:
164      - I(Plugin) name to enable for the install/update operation.
165        The enabled plugin will not persist beyond the transaction.
166    type: list
167    elements: str
168    version_added: "2.5"
169  disable_plugin:
170    description:
171      - I(Plugin) name to disable for the install/update operation.
172        The disabled plugins will not persist beyond the transaction.
173    type: list
174    elements: str
175    version_added: "2.5"
176  releasever:
177    description:
178      - Specifies an alternative release from which all packages will be
179        installed.
180    type: str
181    version_added: "2.7"
182  autoremove:
183    description:
184      - If C(yes), removes all "leaf" packages from the system that were originally
185        installed as dependencies of user-installed packages but which are no longer
186        required by any such package. Should be used alone or when state is I(absent)
187      - "NOTE: This feature requires yum >= 3.4.3 (RHEL/CentOS 7+)"
188    type: bool
189    default: "no"
190    version_added: "2.7"
191  disable_excludes:
192    description:
193      - Disable the excludes defined in YUM config files.
194      - If set to C(all), disables all excludes.
195      - If set to C(main), disable excludes defined in [main] in yum.conf.
196      - If set to C(repoid), disable excludes defined for given repo id.
197    type: str
198    version_added: "2.7"
199  download_only:
200    description:
201      - Only download the packages, do not install them.
202    default: "no"
203    type: bool
204    version_added: "2.7"
205  lock_timeout:
206    description:
207      - Amount of time to wait for the yum lockfile to be freed.
208    required: false
209    default: 30
210    type: int
211    version_added: "2.8"
212  install_weak_deps:
213    description:
214      - Will also install all packages linked by a weak dependency relation.
215      - "NOTE: This feature requires yum >= 4 (RHEL/CentOS 8+)"
216    type: bool
217    default: "yes"
218    version_added: "2.8"
219  download_dir:
220    description:
221      - Specifies an alternate directory to store packages.
222      - Has an effect only if I(download_only) is specified.
223    type: str
224    version_added: "2.8"
225  install_repoquery:
226    description:
227      - If repoquery is not available, install yum-utils. If the system is
228        registered to RHN or an RHN Satellite, repoquery allows for querying
229        all channels assigned to the system. It is also required to use the
230        'list' parameter.
231      - "NOTE: This will run and be logged as a separate yum transation which
232        takes place before any other installation or removal."
233      - "NOTE: This will use the system's default enabled repositories without
234        regard for disablerepo/enablerepo given to the module."
235    required: false
236    version_added: "1.5"
237    default: "yes"
238    type: bool
239notes:
240  - When used with a `loop:` each package will be processed individually,
241    it is much more efficient to pass the list directly to the `name` option.
242  - In versions prior to 1.9.2 this module installed and removed each package
243    given to the yum module separately. This caused problems when packages
244    specified by filename or url had to be installed or removed together. In
245    1.9.2 this was fixed so that packages are installed in one yum
246    transaction. However, if one of the packages adds a new yum repository
247    that the other packages come from (such as epel-release) then that package
248    needs to be installed in a separate task. This mimics yum's command line
249    behaviour.
250  - 'Yum itself has two types of groups.  "Package groups" are specified in the
251    rpm itself while "environment groups" are specified in a separate file
252    (usually by the distribution).  Unfortunately, this division becomes
253    apparent to ansible users because ansible needs to operate on the group
254    of packages in a single transaction and yum requires groups to be specified
255    in different ways when used in that way.  Package groups are specified as
256    "@development-tools" and environment groups are "@^gnome-desktop-environment".
257    Use the "yum group list hidden ids" command to see which category of group the group
258    you want to install falls into.'
259  - 'The yum module does not support clearing yum cache in an idempotent way, so it
260    was decided not to implement it, the only method is to use command and call the yum
261    command directly, namely "command: yum clean all"
262    https://github.com/ansible/ansible/pull/31450#issuecomment-352889579'
263# informational: requirements for nodes
264requirements:
265- yum
266author:
267    - Ansible Core Team
268    - Seth Vidal (@skvidal)
269    - Eduard Snesarev (@verm666)
270    - Berend De Schouwer (@berenddeschouwer)
271    - Abhijeet Kasurde (@Akasurde)
272    - Adam Miller (@maxamillion)
273'''
274
275EXAMPLES = '''
276- name: Install the latest version of Apache
277  yum:
278    name: httpd
279    state: latest
280
281- name: Install Apache >= 2.4
282  yum:
283    name: httpd>=2.4
284    state: present
285
286- name: Install a list of packages (suitable replacement for 2.11 loop deprecation warning)
287  yum:
288    name:
289      - nginx
290      - postgresql
291      - postgresql-server
292    state: present
293
294- name: Install a list of packages with a list variable
295  yum:
296    name: "{{ packages }}"
297  vars:
298    packages:
299    - httpd
300    - httpd-tools
301
302- name: Remove the Apache package
303  yum:
304    name: httpd
305    state: absent
306
307- name: Install the latest version of Apache from the testing repo
308  yum:
309    name: httpd
310    enablerepo: testing
311    state: present
312
313- name: Install one specific version of Apache
314  yum:
315    name: httpd-2.2.29-1.4.amzn1
316    state: present
317
318- name: Upgrade all packages
319  yum:
320    name: '*'
321    state: latest
322
323- name: Upgrade all packages, excluding kernel & foo related packages
324  yum:
325    name: '*'
326    state: latest
327    exclude: kernel*,foo*
328
329- name: Install the nginx rpm from a remote repo
330  yum:
331    name: http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
332    state: present
333
334- name: Install nginx rpm from a local file
335  yum:
336    name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm
337    state: present
338
339- name: Install the 'Development tools' package group
340  yum:
341    name: "@Development tools"
342    state: present
343
344- name: Install the 'Gnome desktop' environment group
345  yum:
346    name: "@^gnome-desktop-environment"
347    state: present
348
349- name: List ansible packages and register result to print with debug later
350  yum:
351    list: ansible
352  register: result
353
354- name: Install package with multiple repos enabled
355  yum:
356    name: sos
357    enablerepo: "epel,ol7_latest"
358
359- name: Install package with multiple repos disabled
360  yum:
361    name: sos
362    disablerepo: "epel,ol7_latest"
363
364- name: Download the nginx package but do not install it
365  yum:
366    name:
367      - nginx
368    state: latest
369    download_only: true
370'''
371
372from ansible.module_utils.basic import AnsibleModule
373from ansible.module_utils.common.respawn import has_respawned, respawn_module
374from ansible.module_utils._text import to_native, to_text
375from ansible.module_utils.urls import fetch_url
376from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
377
378import errno
379import os
380import re
381import sys
382import tempfile
383
384try:
385    import rpm
386    HAS_RPM_PYTHON = True
387except ImportError:
388    HAS_RPM_PYTHON = False
389
390try:
391    import yum
392    HAS_YUM_PYTHON = True
393except ImportError:
394    HAS_YUM_PYTHON = False
395
396try:
397    from yum.misc import find_unfinished_transactions, find_ts_remaining
398    from rpmUtils.miscutils import splitFilename, compareEVR
399    transaction_helpers = True
400except ImportError:
401    transaction_helpers = False
402
403from contextlib import contextmanager
404from ansible.module_utils.urls import fetch_file
405
406def_qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}"
407rpmbin = None
408
409
410class YumModule(YumDnf):
411    """
412    Yum Ansible module back-end implementation
413    """
414
415    def __init__(self, module):
416
417        # state=installed name=pkgspec
418        # state=removed name=pkgspec
419        # state=latest name=pkgspec
420        #
421        # informational commands:
422        #   list=installed
423        #   list=updates
424        #   list=available
425        #   list=repos
426        #   list=pkgspec
427
428        # This populates instance vars for all argument spec params
429        super(YumModule, self).__init__(module)
430
431        self.pkg_mgr_name = "yum"
432        self.lockfile = '/var/run/yum.pid'
433        self._yum_base = None
434
435    def _enablerepos_with_error_checking(self):
436        # NOTE: This seems unintuitive, but it mirrors yum's CLI behavior
437        if len(self.enablerepo) == 1:
438            try:
439                self.yum_base.repos.enableRepo(self.enablerepo[0])
440            except yum.Errors.YumBaseError as e:
441                if u'repository not found' in to_text(e):
442                    self.module.fail_json(msg="Repository %s not found." % self.enablerepo[0])
443                else:
444                    raise e
445        else:
446            for rid in self.enablerepo:
447                try:
448                    self.yum_base.repos.enableRepo(rid)
449                except yum.Errors.YumBaseError as e:
450                    if u'repository not found' in to_text(e):
451                        self.module.warn("Repository %s not found." % rid)
452                    else:
453                        raise e
454
455    def is_lockfile_pid_valid(self):
456        try:
457            try:
458                with open(self.lockfile, 'r') as f:
459                    oldpid = int(f.readline())
460            except ValueError:
461                # invalid data
462                os.unlink(self.lockfile)
463                return False
464
465            if oldpid == os.getpid():
466                # that's us?
467                os.unlink(self.lockfile)
468                return False
469
470            try:
471                with open("/proc/%d/stat" % oldpid, 'r') as f:
472                    stat = f.readline()
473
474                if stat.split()[2] == 'Z':
475                    # Zombie
476                    os.unlink(self.lockfile)
477                    return False
478            except IOError:
479                # either /proc is not mounted or the process is already dead
480                try:
481                    # check the state of the process
482                    os.kill(oldpid, 0)
483                except OSError as e:
484                    if e.errno == errno.ESRCH:
485                        # No such process
486                        os.unlink(self.lockfile)
487                        return False
488
489                    self.module.fail_json(msg="Unable to check PID %s in  %s: %s" % (oldpid, self.lockfile, to_native(e)))
490        except (IOError, OSError) as e:
491            # lockfile disappeared?
492            return False
493
494        # another copy seems to be running
495        return True
496
497    @property
498    def yum_base(self):
499        if self._yum_base:
500            return self._yum_base
501        else:
502            # Only init once
503            self._yum_base = yum.YumBase()
504            self._yum_base.preconf.debuglevel = 0
505            self._yum_base.preconf.errorlevel = 0
506            self._yum_base.preconf.plugins = True
507            self._yum_base.preconf.enabled_plugins = self.enable_plugin
508            self._yum_base.preconf.disabled_plugins = self.disable_plugin
509            if self.releasever:
510                self._yum_base.preconf.releasever = self.releasever
511            if self.installroot != '/':
512                # do not setup installroot by default, because of error
513                # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf
514                # in old yum version (like in CentOS 6.6)
515                self._yum_base.preconf.root = self.installroot
516                self._yum_base.conf.installroot = self.installroot
517            if self.conf_file and os.path.exists(self.conf_file):
518                self._yum_base.preconf.fn = self.conf_file
519            if os.geteuid() != 0:
520                if hasattr(self._yum_base, 'setCacheDir'):
521                    self._yum_base.setCacheDir()
522                else:
523                    cachedir = yum.misc.getCacheDir()
524                    self._yum_base.repos.setCacheDir(cachedir)
525                    self._yum_base.conf.cache = 0
526            if self.disable_excludes:
527                self._yum_base.conf.disable_excludes = self.disable_excludes
528
529            # A sideeffect of accessing conf is that the configuration is
530            # loaded and plugins are discovered
531            self.yum_base.conf
532
533            try:
534                for rid in self.disablerepo:
535                    self.yum_base.repos.disableRepo(rid)
536
537                self._enablerepos_with_error_checking()
538
539            except Exception as e:
540                self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e))
541
542        return self._yum_base
543
544    def po_to_envra(self, po):
545        if hasattr(po, 'ui_envra'):
546            return po.ui_envra
547
548        return '%s:%s-%s-%s.%s' % (po.epoch, po.name, po.version, po.release, po.arch)
549
550    def is_group_env_installed(self, name):
551        name_lower = name.lower()
552
553        if yum.__version_info__ >= (3, 4):
554            groups_list = self.yum_base.doGroupLists(return_evgrps=True)
555        else:
556            groups_list = self.yum_base.doGroupLists()
557
558        # list of the installed groups on the first index
559        groups = groups_list[0]
560        for group in groups:
561            if name_lower.endswith(group.name.lower()) or name_lower.endswith(group.groupid.lower()):
562                return True
563
564        if yum.__version_info__ >= (3, 4):
565            # list of the installed env_groups on the third index
566            envs = groups_list[2]
567            for env in envs:
568                if name_lower.endswith(env.name.lower()) or name_lower.endswith(env.environmentid.lower()):
569                    return True
570
571        return False
572
573    def is_installed(self, repoq, pkgspec, qf=None, is_pkg=False):
574        if qf is None:
575            qf = "%{epoch}:%{name}-%{version}-%{release}.%{arch}\n"
576
577        if not repoq:
578            pkgs = []
579            try:
580                e, m, _ = self.yum_base.rpmdb.matchPackageNames([pkgspec])
581                pkgs = e + m
582                if not pkgs and not is_pkg:
583                    pkgs.extend(self.yum_base.returnInstalledPackagesByDep(pkgspec))
584            except Exception as e:
585                self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e))
586
587            return [self.po_to_envra(p) for p in pkgs]
588
589        else:
590            global rpmbin
591            if not rpmbin:
592                rpmbin = self.module.get_bin_path('rpm', required=True)
593
594            cmd = [rpmbin, '-q', '--qf', qf, pkgspec]
595            if self.installroot != '/':
596                cmd.extend(['--root', self.installroot])
597            # rpm localizes messages and we're screen scraping so make sure we use
598            # the C locale
599            lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
600            rc, out, err = self.module.run_command(cmd, environ_update=lang_env)
601            if rc != 0 and 'is not installed' not in out:
602                self.module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err))
603            if 'is not installed' in out:
604                out = ''
605
606            pkgs = [p for p in out.replace('(none)', '0').split('\n') if p.strip()]
607            if not pkgs and not is_pkg:
608                cmd = [rpmbin, '-q', '--qf', qf, '--whatprovides', pkgspec]
609                if self.installroot != '/':
610                    cmd.extend(['--root', self.installroot])
611                rc2, out2, err2 = self.module.run_command(cmd, environ_update=lang_env)
612            else:
613                rc2, out2, err2 = (0, '', '')
614
615            if rc2 != 0 and 'no package provides' not in out2:
616                self.module.fail_json(msg='Error from rpm: %s: %s' % (cmd, err + err2))
617            if 'no package provides' in out2:
618                out2 = ''
619            pkgs += [p for p in out2.replace('(none)', '0').split('\n') if p.strip()]
620            return pkgs
621
622        return []
623
624    def is_available(self, repoq, pkgspec, qf=def_qf):
625        if not repoq:
626
627            pkgs = []
628            try:
629                e, m, _ = self.yum_base.pkgSack.matchPackageNames([pkgspec])
630                pkgs = e + m
631                if not pkgs:
632                    pkgs.extend(self.yum_base.returnPackagesByDep(pkgspec))
633            except Exception as e:
634                self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e))
635
636            return [self.po_to_envra(p) for p in pkgs]
637
638        else:
639            myrepoq = list(repoq)
640
641            r_cmd = ['--disablerepo', ','.join(self.disablerepo)]
642            myrepoq.extend(r_cmd)
643
644            r_cmd = ['--enablerepo', ','.join(self.enablerepo)]
645            myrepoq.extend(r_cmd)
646
647            if self.releasever:
648                myrepoq.extend('--releasever=%s' % self.releasever)
649
650            cmd = myrepoq + ["--qf", qf, pkgspec]
651            rc, out, err = self.module.run_command(cmd)
652            if rc == 0:
653                return [p for p in out.split('\n') if p.strip()]
654            else:
655                self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err))
656
657        return []
658
659    def is_update(self, repoq, pkgspec, qf=def_qf):
660        if not repoq:
661
662            pkgs = []
663            updates = []
664
665            try:
666                pkgs = self.yum_base.returnPackagesByDep(pkgspec) + \
667                    self.yum_base.returnInstalledPackagesByDep(pkgspec)
668                if not pkgs:
669                    e, m, _ = self.yum_base.pkgSack.matchPackageNames([pkgspec])
670                    pkgs = e + m
671                updates = self.yum_base.doPackageLists(pkgnarrow='updates').updates
672            except Exception as e:
673                self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e))
674
675            retpkgs = (pkg for pkg in pkgs if pkg in updates)
676
677            return set(self.po_to_envra(p) for p in retpkgs)
678
679        else:
680            myrepoq = list(repoq)
681            r_cmd = ['--disablerepo', ','.join(self.disablerepo)]
682            myrepoq.extend(r_cmd)
683
684            r_cmd = ['--enablerepo', ','.join(self.enablerepo)]
685            myrepoq.extend(r_cmd)
686
687            if self.releasever:
688                myrepoq.extend('--releasever=%s' % self.releasever)
689
690            cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec]
691            rc, out, err = self.module.run_command(cmd)
692
693            if rc == 0:
694                return set(p for p in out.split('\n') if p.strip())
695            else:
696                self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err))
697
698        return set()
699
700    def what_provides(self, repoq, req_spec, qf=def_qf):
701        if not repoq:
702
703            pkgs = []
704            try:
705                try:
706                    pkgs = self.yum_base.returnPackagesByDep(req_spec) + \
707                        self.yum_base.returnInstalledPackagesByDep(req_spec)
708                except Exception as e:
709                    # If a repo with `repo_gpgcheck=1` is added and the repo GPG
710                    # key was never accepted, querying this repo will throw an
711                    # error: 'repomd.xml signature could not be verified'. In that
712                    # situation we need to run `yum -y makecache` which will accept
713                    # the key and try again.
714                    if 'repomd.xml signature could not be verified' in to_native(e):
715                        if self.releasever:
716                            self.module.run_command(self.yum_basecmd + ['makecache'] + ['--releasever=%s' % self.releasever])
717                        else:
718                            self.module.run_command(self.yum_basecmd + ['makecache'])
719                        pkgs = self.yum_base.returnPackagesByDep(req_spec) + \
720                            self.yum_base.returnInstalledPackagesByDep(req_spec)
721                    else:
722                        raise
723                if not pkgs:
724                    e, m, _ = self.yum_base.pkgSack.matchPackageNames([req_spec])
725                    pkgs.extend(e)
726                    pkgs.extend(m)
727                    e, m, _ = self.yum_base.rpmdb.matchPackageNames([req_spec])
728                    pkgs.extend(e)
729                    pkgs.extend(m)
730            except Exception as e:
731                self.module.fail_json(msg="Failure talking to yum: %s" % to_native(e))
732
733            return set(self.po_to_envra(p) for p in pkgs)
734
735        else:
736            myrepoq = list(repoq)
737            r_cmd = ['--disablerepo', ','.join(self.disablerepo)]
738            myrepoq.extend(r_cmd)
739
740            r_cmd = ['--enablerepo', ','.join(self.enablerepo)]
741            myrepoq.extend(r_cmd)
742
743            if self.releasever:
744                myrepoq.extend('--releasever=%s' % self.releasever)
745
746            cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec]
747            rc, out, err = self.module.run_command(cmd)
748            cmd = myrepoq + ["--qf", qf, req_spec]
749            rc2, out2, err2 = self.module.run_command(cmd)
750            if rc == 0 and rc2 == 0:
751                out += out2
752                pkgs = set([p for p in out.split('\n') if p.strip()])
753                if not pkgs:
754                    pkgs = self.is_installed(repoq, req_spec, qf=qf)
755                return pkgs
756            else:
757                self.module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
758
759        return set()
760
761    def transaction_exists(self, pkglist):
762        """
763        checks the package list to see if any packages are
764        involved in an incomplete transaction
765        """
766
767        conflicts = []
768        if not transaction_helpers:
769            return conflicts
770
771        # first, we create a list of the package 'nvreas'
772        # so we can compare the pieces later more easily
773        pkglist_nvreas = (splitFilename(pkg) for pkg in pkglist)
774
775        # next, we build the list of packages that are
776        # contained within an unfinished transaction
777        unfinished_transactions = find_unfinished_transactions()
778        for trans in unfinished_transactions:
779            steps = find_ts_remaining(trans)
780            for step in steps:
781                # the action is install/erase/etc., but we only
782                # care about the package spec contained in the step
783                (action, step_spec) = step
784                (n, v, r, e, a) = splitFilename(step_spec)
785                # and see if that spec is in the list of packages
786                # requested for installation/updating
787                for pkg in pkglist_nvreas:
788                    # if the name and arch match, we're going to assume
789                    # this package is part of a pending transaction
790                    # the label is just for display purposes
791                    label = "%s-%s" % (n, a)
792                    if n == pkg[0] and a == pkg[4]:
793                        if label not in conflicts:
794                            conflicts.append("%s-%s" % (n, a))
795                        break
796        return conflicts
797
798    def local_envra(self, path):
799        """return envra of a local rpm passed in"""
800
801        ts = rpm.TransactionSet()
802        ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
803        fd = os.open(path, os.O_RDONLY)
804        try:
805            header = ts.hdrFromFdno(fd)
806        except rpm.error as e:
807            return None
808        finally:
809            os.close(fd)
810
811        return '%s:%s-%s-%s.%s' % (
812            header[rpm.RPMTAG_EPOCH] or '0',
813            header[rpm.RPMTAG_NAME],
814            header[rpm.RPMTAG_VERSION],
815            header[rpm.RPMTAG_RELEASE],
816            header[rpm.RPMTAG_ARCH]
817        )
818
819    @contextmanager
820    def set_env_proxy(self):
821        # setting system proxy environment and saving old, if exists
822        namepass = ""
823        scheme = ["http", "https"]
824        old_proxy_env = [os.getenv("http_proxy"), os.getenv("https_proxy")]
825        try:
826            # "_none_" is a special value to disable proxy in yum.conf/*.repo
827            if self.yum_base.conf.proxy and self.yum_base.conf.proxy not in ("_none_",):
828                if self.yum_base.conf.proxy_username:
829                    namepass = namepass + self.yum_base.conf.proxy_username
830                    proxy_url = self.yum_base.conf.proxy
831                    if self.yum_base.conf.proxy_password:
832                        namepass = namepass + ":" + self.yum_base.conf.proxy_password
833                elif '@' in self.yum_base.conf.proxy:
834                    namepass = self.yum_base.conf.proxy.split('@')[0].split('//')[-1]
835                    proxy_url = self.yum_base.conf.proxy.replace("{0}@".format(namepass), "")
836
837                if namepass:
838                    namepass = namepass + '@'
839                    for item in scheme:
840                        os.environ[item + "_proxy"] = re.sub(
841                            r"(http://)",
842                            r"\g<1>" + namepass, proxy_url
843                        )
844                else:
845                    for item in scheme:
846                        os.environ[item + "_proxy"] = self.yum_base.conf.proxy
847            yield
848        except yum.Errors.YumBaseError:
849            raise
850        finally:
851            # revert back to previously system configuration
852            for item in scheme:
853                if os.getenv("{0}_proxy".format(item)):
854                    del os.environ["{0}_proxy".format(item)]
855            if old_proxy_env[0]:
856                os.environ["http_proxy"] = old_proxy_env[0]
857            if old_proxy_env[1]:
858                os.environ["https_proxy"] = old_proxy_env[1]
859
860    def pkg_to_dict(self, pkgstr):
861        if pkgstr.strip() and pkgstr.count('|') == 5:
862            n, e, v, r, a, repo = pkgstr.split('|')
863        else:
864            return {'error_parsing': pkgstr}
865
866        d = {
867            'name': n,
868            'arch': a,
869            'epoch': e,
870            'release': r,
871            'version': v,
872            'repo': repo,
873            'envra': '%s:%s-%s-%s.%s' % (e, n, v, r, a)
874        }
875
876        if repo == 'installed':
877            d['yumstate'] = 'installed'
878        else:
879            d['yumstate'] = 'available'
880
881        return d
882
883    def repolist(self, repoq, qf="%{repoid}"):
884        cmd = repoq + ["--qf", qf, "-a"]
885        if self.releasever:
886            cmd.extend(['--releasever=%s' % self.releasever])
887        rc, out, _ = self.module.run_command(cmd)
888        if rc == 0:
889            return set(p for p in out.split('\n') if p.strip())
890        else:
891            return []
892
893    def list_stuff(self, repoquerybin, stuff):
894
895        qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}"
896        # is_installed goes through rpm instead of repoquery so it needs a slightly different format
897        is_installed_qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|installed\n"
898        repoq = [repoquerybin, '--show-duplicates', '--plugins', '--quiet']
899        if self.disablerepo:
900            repoq.extend(['--disablerepo', ','.join(self.disablerepo)])
901        if self.enablerepo:
902            repoq.extend(['--enablerepo', ','.join(self.enablerepo)])
903        if self.installroot != '/':
904            repoq.extend(['--installroot', self.installroot])
905        if self.conf_file and os.path.exists(self.conf_file):
906            repoq += ['-c', self.conf_file]
907
908        if stuff == 'installed':
909            return [self.pkg_to_dict(p) for p in sorted(self.is_installed(repoq, '-a', qf=is_installed_qf)) if p.strip()]
910
911        if stuff == 'updates':
912            return [self.pkg_to_dict(p) for p in sorted(self.is_update(repoq, '-a', qf=qf)) if p.strip()]
913
914        if stuff == 'available':
915            return [self.pkg_to_dict(p) for p in sorted(self.is_available(repoq, '-a', qf=qf)) if p.strip()]
916
917        if stuff == 'repos':
918            return [dict(repoid=name, state='enabled') for name in sorted(self.repolist(repoq)) if name.strip()]
919
920        return [
921            self.pkg_to_dict(p) for p in
922            sorted(self.is_installed(repoq, stuff, qf=is_installed_qf) + self.is_available(repoq, stuff, qf=qf))
923            if p.strip()
924        ]
925
926    def exec_install(self, items, action, pkgs, res):
927        cmd = self.yum_basecmd + [action] + pkgs
928        if self.releasever:
929            cmd.extend(['--releasever=%s' % self.releasever])
930
931        if self.module.check_mode:
932            self.module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs))
933        else:
934            res['changes'] = dict(installed=pkgs)
935
936        lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
937        rc, out, err = self.module.run_command(cmd, environ_update=lang_env)
938
939        if rc == 1:
940            for spec in items:
941                # Fail on invalid urls:
942                if ('://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)):
943                    err = 'Package at %s could not be installed' % spec
944                    self.module.fail_json(changed=False, msg=err, rc=rc)
945
946        res['rc'] = rc
947        res['results'].append(out)
948        res['msg'] += err
949        res['changed'] = True
950
951        if ('Nothing to do' in out and rc == 0) or ('does not have any packages' in err):
952            res['changed'] = False
953
954        if rc != 0:
955            res['changed'] = False
956            self.module.fail_json(**res)
957
958        # Fail if yum prints 'No space left on device' because that means some
959        # packages failed executing their post install scripts because of lack of
960        # free space (e.g. kernel package couldn't generate initramfs). Note that
961        # yum can still exit with rc=0 even if some post scripts didn't execute
962        # correctly.
963        if 'No space left on device' in (out or err):
964            res['changed'] = False
965            res['msg'] = 'No space left on device'
966            self.module.fail_json(**res)
967
968        # FIXME - if we did an install - go and check the rpmdb to see if it actually installed
969        # look for each pkg in rpmdb
970        # look for each pkg via obsoletes
971
972        return res
973
974    def install(self, items, repoq):
975
976        pkgs = []
977        downgrade_pkgs = []
978        res = {}
979        res['results'] = []
980        res['msg'] = ''
981        res['rc'] = 0
982        res['changed'] = False
983
984        for spec in items:
985            pkg = None
986            downgrade_candidate = False
987
988            # check if pkgspec is installed (if possible for idempotence)
989            if spec.endswith('.rpm') or '://' in spec:
990                if '://' not in spec and not os.path.exists(spec):
991                    res['msg'] += "No RPM file matching '%s' found on system" % spec
992                    res['results'].append("No RPM file matching '%s' found on system" % spec)
993                    res['rc'] = 127  # Ensure the task fails in with-loop
994                    self.module.fail_json(**res)
995
996                if '://' in spec:
997                    with self.set_env_proxy():
998                        package = fetch_file(self.module, spec)
999                        if not package.endswith('.rpm'):
1000                            # yum requires a local file to have the extension of .rpm and we
1001                            # can not guarantee that from an URL (redirects, proxies, etc)
1002                            new_package_path = '%s.rpm' % package
1003                            os.rename(package, new_package_path)
1004                            package = new_package_path
1005                else:
1006                    package = spec
1007
1008                # most common case is the pkg is already installed
1009                envra = self.local_envra(package)
1010                if envra is None:
1011                    self.module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec)
1012                installed_pkgs = self.is_installed(repoq, envra)
1013                if installed_pkgs:
1014                    res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], package))
1015                    continue
1016
1017                (name, ver, rel, epoch, arch) = splitFilename(envra)
1018                installed_pkgs = self.is_installed(repoq, name)
1019
1020                # case for two same envr but different archs like x86_64 and i686
1021                if len(installed_pkgs) == 2:
1022                    (cur_name0, cur_ver0, cur_rel0, cur_epoch0, cur_arch0) = splitFilename(installed_pkgs[0])
1023                    (cur_name1, cur_ver1, cur_rel1, cur_epoch1, cur_arch1) = splitFilename(installed_pkgs[1])
1024                    cur_epoch0 = cur_epoch0 or '0'
1025                    cur_epoch1 = cur_epoch1 or '0'
1026                    compare = compareEVR((cur_epoch0, cur_ver0, cur_rel0), (cur_epoch1, cur_ver1, cur_rel1))
1027                    if compare == 0 and cur_arch0 != cur_arch1:
1028                        for installed_pkg in installed_pkgs:
1029                            if installed_pkg.endswith(arch):
1030                                installed_pkgs = [installed_pkg]
1031
1032                if len(installed_pkgs) == 1:
1033                    installed_pkg = installed_pkgs[0]
1034                    (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(installed_pkg)
1035                    cur_epoch = cur_epoch or '0'
1036                    compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel))
1037
1038                    # compare > 0 -> higher version is installed
1039                    # compare == 0 -> exact version is installed
1040                    # compare < 0 -> lower version is installed
1041                    if compare > 0 and self.allow_downgrade:
1042                        downgrade_candidate = True
1043                    elif compare >= 0:
1044                        continue
1045
1046                # else: if there are more installed packages with the same name, that would mean
1047                # kernel, gpg-pubkey or like, so just let yum deal with it and try to install it
1048
1049                pkg = package
1050
1051            # groups
1052            elif spec.startswith('@'):
1053                if self.is_group_env_installed(spec):
1054                    continue
1055
1056                pkg = spec
1057
1058            # range requires or file-requires or pkgname :(
1059            else:
1060                # most common case is the pkg is already installed and done
1061                # short circuit all the bs - and search for it as a pkg in is_installed
1062                # if you find it then we're done
1063                if not set(['*', '?']).intersection(set(spec)):
1064                    installed_pkgs = self.is_installed(repoq, spec, is_pkg=True)
1065                    if installed_pkgs:
1066                        res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], spec))
1067                        continue
1068
1069                # look up what pkgs provide this
1070                pkglist = self.what_provides(repoq, spec)
1071                if not pkglist:
1072                    res['msg'] += "No package matching '%s' found available, installed or updated" % spec
1073                    res['results'].append("No package matching '%s' found available, installed or updated" % spec)
1074                    res['rc'] = 126  # Ensure the task fails in with-loop
1075                    self.module.fail_json(**res)
1076
1077                # if any of the packages are involved in a transaction, fail now
1078                # so that we don't hang on the yum operation later
1079                conflicts = self.transaction_exists(pkglist)
1080                if conflicts:
1081                    res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
1082                    res['rc'] = 125  # Ensure the task fails in with-loop
1083                    self.module.fail_json(**res)
1084
1085                # if any of them are installed
1086                # then nothing to do
1087
1088                found = False
1089                for this in pkglist:
1090                    if self.is_installed(repoq, this, is_pkg=True):
1091                        found = True
1092                        res['results'].append('%s providing %s is already installed' % (this, spec))
1093                        break
1094
1095                # if the version of the pkg you have installed is not in ANY repo, but there are
1096                # other versions in the repos (both higher and lower) then the previous checks won't work.
1097                # so we check one more time. This really only works for pkgname - not for file provides or virt provides
1098                # but virt provides should be all caught in what_provides on its own.
1099                # highly irritating
1100                if not found:
1101                    if self.is_installed(repoq, spec):
1102                        found = True
1103                        res['results'].append('package providing %s is already installed' % (spec))
1104
1105                if found:
1106                    continue
1107
1108                # Downgrade - The yum install command will only install or upgrade to a spec version, it will
1109                # not install an older version of an RPM even if specified by the install spec. So we need to
1110                # determine if this is a downgrade, and then use the yum downgrade command to install the RPM.
1111                if self.allow_downgrade:
1112                    for package in pkglist:
1113                        # Get the NEVRA of the requested package using pkglist instead of spec because pkglist
1114                        #  contains consistently-formatted package names returned by yum, rather than user input
1115                        #  that is often not parsed correctly by splitFilename().
1116                        (name, ver, rel, epoch, arch) = splitFilename(package)
1117
1118                        # Check if any version of the requested package is installed
1119                        inst_pkgs = self.is_installed(repoq, name, is_pkg=True)
1120                        if inst_pkgs:
1121                            (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(inst_pkgs[0])
1122                            compare = compareEVR((cur_epoch, cur_ver, cur_rel), (epoch, ver, rel))
1123                            if compare > 0:
1124                                downgrade_candidate = True
1125                            else:
1126                                downgrade_candidate = False
1127                                break
1128
1129                # If package needs to be installed/upgraded/downgraded, then pass in the spec
1130                # we could get here if nothing provides it but that's not
1131                # the error we're catching here
1132                pkg = spec
1133
1134            if downgrade_candidate and self.allow_downgrade:
1135                downgrade_pkgs.append(pkg)
1136            else:
1137                pkgs.append(pkg)
1138
1139        if downgrade_pkgs:
1140            res = self.exec_install(items, 'downgrade', downgrade_pkgs, res)
1141
1142        if pkgs:
1143            res = self.exec_install(items, 'install', pkgs, res)
1144
1145        return res
1146
1147    def remove(self, items, repoq):
1148
1149        pkgs = []
1150        res = {}
1151        res['results'] = []
1152        res['msg'] = ''
1153        res['changed'] = False
1154        res['rc'] = 0
1155
1156        for pkg in items:
1157            if pkg.startswith('@'):
1158                installed = self.is_group_env_installed(pkg)
1159            else:
1160                installed = self.is_installed(repoq, pkg)
1161
1162            if installed:
1163                pkgs.append(pkg)
1164            else:
1165                res['results'].append('%s is not installed' % pkg)
1166
1167        if pkgs:
1168            if self.module.check_mode:
1169                self.module.exit_json(changed=True, results=res['results'], changes=dict(removed=pkgs))
1170            else:
1171                res['changes'] = dict(removed=pkgs)
1172
1173            # run an actual yum transaction
1174            if self.autoremove:
1175                cmd = self.yum_basecmd + ["autoremove"] + pkgs
1176            else:
1177                cmd = self.yum_basecmd + ["remove"] + pkgs
1178            rc, out, err = self.module.run_command(cmd)
1179
1180            res['rc'] = rc
1181            res['results'].append(out)
1182            res['msg'] = err
1183
1184            if rc != 0:
1185                if self.autoremove and 'No such command' in out:
1186                    self.module.fail_json(msg='Version of YUM too old for autoremove: Requires yum 3.4.3 (RHEL/CentOS 7+)')
1187                else:
1188                    self.module.fail_json(**res)
1189
1190            # compile the results into one batch. If anything is changed
1191            # then mark changed
1192            # at the end - if we've end up failed then fail out of the rest
1193            # of the process
1194
1195            # at this point we check to see if the pkg is no longer present
1196            self._yum_base = None  # previous YumBase package index is now invalid
1197            for pkg in pkgs:
1198                if pkg.startswith('@'):
1199                    installed = self.is_group_env_installed(pkg)
1200                else:
1201                    installed = self.is_installed(repoq, pkg, is_pkg=True)
1202
1203                if installed:
1204                    # Return a message so it's obvious to the user why yum failed
1205                    # and which package couldn't be removed. More details:
1206                    # https://github.com/ansible/ansible/issues/35672
1207                    res['msg'] = "Package '%s' couldn't be removed!" % pkg
1208                    self.module.fail_json(**res)
1209
1210            res['changed'] = True
1211
1212        return res
1213
1214    def run_check_update(self):
1215        # run check-update to see if we have packages pending
1216        if self.releasever:
1217            rc, out, err = self.module.run_command(self.yum_basecmd + ['check-update'] + ['--releasever=%s' % self.releasever])
1218        else:
1219            rc, out, err = self.module.run_command(self.yum_basecmd + ['check-update'])
1220        return rc, out, err
1221
1222    @staticmethod
1223    def parse_check_update(check_update_output):
1224        updates = {}
1225        obsoletes = {}
1226
1227        # remove incorrect new lines in longer columns in output from yum check-update
1228        # yum line wrapping can move the repo to the next line
1229        #
1230        # Meant to filter out sets of lines like:
1231        #  some_looooooooooooooooooooooooooooooooooooong_package_name   1:1.2.3-1.el7
1232        #                                                                    some-repo-label
1233        #
1234        # But it also needs to avoid catching lines like:
1235        # Loading mirror speeds from cached hostfile
1236        #
1237        # ceph.x86_64                               1:11.2.0-0.el7                    ceph
1238
1239        # preprocess string and filter out empty lines so the regex below works
1240        out = re.sub(r'\n[^\w]\W+(.*)', r' \1', check_update_output)
1241
1242        available_updates = out.split('\n')
1243
1244        # build update dictionary
1245        for line in available_updates:
1246            line = line.split()
1247            # ignore irrelevant lines
1248            # '*' in line matches lines like mirror lists:
1249            #      * base: mirror.corbina.net
1250            # len(line) != 3 or 6 could be junk or a continuation
1251            # len(line) = 6 is package obsoletes
1252            #
1253            # FIXME: what is  the '.' not in line  conditional for?
1254
1255            if '*' in line or len(line) not in [3, 6] or '.' not in line[0]:
1256                continue
1257
1258            pkg, version, repo = line[0], line[1], line[2]
1259            name, dist = pkg.rsplit('.', 1)
1260            updates.update({name: {'version': version, 'dist': dist, 'repo': repo}})
1261
1262            if len(line) == 6:
1263                obsolete_pkg, obsolete_version, obsolete_repo = line[3], line[4], line[5]
1264                obsolete_name, obsolete_dist = obsolete_pkg.rsplit('.', 1)
1265                obsoletes.update({obsolete_name: {'version': obsolete_version, 'dist': obsolete_dist, 'repo': obsolete_repo}})
1266
1267        return updates, obsoletes
1268
1269    def latest(self, items, repoq):
1270
1271        res = {}
1272        res['results'] = []
1273        res['msg'] = ''
1274        res['changed'] = False
1275        res['rc'] = 0
1276        pkgs = {}
1277        pkgs['update'] = []
1278        pkgs['install'] = []
1279        updates = {}
1280        obsoletes = {}
1281        update_all = False
1282        cmd = None
1283
1284        # determine if we're doing an update all
1285        if '*' in items:
1286            update_all = True
1287
1288        rc, out, err = self.run_check_update()
1289
1290        if rc == 0 and update_all:
1291            res['results'].append('Nothing to do here, all packages are up to date')
1292            return res
1293        elif rc == 100:
1294            updates, obsoletes = self.parse_check_update(out)
1295        elif rc == 1:
1296            res['msg'] = err
1297            res['rc'] = rc
1298            self.module.fail_json(**res)
1299
1300        if update_all:
1301            cmd = self.yum_basecmd + ['update']
1302            will_update = set(updates.keys())
1303            will_update_from_other_package = dict()
1304        else:
1305            will_update = set()
1306            will_update_from_other_package = dict()
1307            for spec in items:
1308                # some guess work involved with groups. update @<group> will install the group if missing
1309                if spec.startswith('@'):
1310                    pkgs['update'].append(spec)
1311                    will_update.add(spec)
1312                    continue
1313
1314                # check if pkgspec is installed (if possible for idempotence)
1315                # localpkg
1316                if spec.endswith('.rpm') and '://' not in spec:
1317                    if not os.path.exists(spec):
1318                        res['msg'] += "No RPM file matching '%s' found on system" % spec
1319                        res['results'].append("No RPM file matching '%s' found on system" % spec)
1320                        res['rc'] = 127  # Ensure the task fails in with-loop
1321                        self.module.fail_json(**res)
1322
1323                    # get the pkg e:name-v-r.arch
1324                    envra = self.local_envra(spec)
1325
1326                    if envra is None:
1327                        self.module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec)
1328
1329                    # local rpm files can't be updated
1330                    if self.is_installed(repoq, envra):
1331                        pkgs['update'].append(spec)
1332                    else:
1333                        pkgs['install'].append(spec)
1334                    continue
1335
1336                # URL
1337                if '://' in spec:
1338                    # download package so that we can check if it's already installed
1339                    with self.set_env_proxy():
1340                        package = fetch_file(self.module, spec)
1341                    envra = self.local_envra(package)
1342
1343                    if envra is None:
1344                        self.module.fail_json(msg="Failed to get nevra information from RPM package: %s" % spec)
1345
1346                    # local rpm files can't be updated
1347                    if self.is_installed(repoq, envra):
1348                        pkgs['update'].append(spec)
1349                    else:
1350                        pkgs['install'].append(spec)
1351                    continue
1352
1353                # dep/pkgname  - find it
1354                if self.is_installed(repoq, spec):
1355                    pkgs['update'].append(spec)
1356                else:
1357                    pkgs['install'].append(spec)
1358                pkglist = self.what_provides(repoq, spec)
1359                # FIXME..? may not be desirable to throw an exception here if a single package is missing
1360                if not pkglist:
1361                    res['msg'] += "No package matching '%s' found available, installed or updated" % spec
1362                    res['results'].append("No package matching '%s' found available, installed or updated" % spec)
1363                    res['rc'] = 126  # Ensure the task fails in with-loop
1364                    self.module.fail_json(**res)
1365
1366                nothing_to_do = True
1367                for pkg in pkglist:
1368                    if spec in pkgs['install'] and self.is_available(repoq, pkg):
1369                        nothing_to_do = False
1370                        break
1371
1372                    # this contains the full NVR and spec could contain wildcards
1373                    # or virtual provides (like "python-*" or "smtp-daemon") while
1374                    # updates contains name only.
1375                    pkgname, _, _, _, _ = splitFilename(pkg)
1376                    if spec in pkgs['update'] and pkgname in updates:
1377                        nothing_to_do = False
1378                        will_update.add(spec)
1379                        # Massage the updates list
1380                        if spec != pkgname:
1381                            # For reporting what packages would be updated more
1382                            # succinctly
1383                            will_update_from_other_package[spec] = pkgname
1384                        break
1385
1386                if not self.is_installed(repoq, spec) and self.update_only:
1387                    res['results'].append("Packages providing %s not installed due to update_only specified" % spec)
1388                    continue
1389                if nothing_to_do:
1390                    res['results'].append("All packages providing %s are up to date" % spec)
1391                    continue
1392
1393                # if any of the packages are involved in a transaction, fail now
1394                # so that we don't hang on the yum operation later
1395                conflicts = self.transaction_exists(pkglist)
1396                if conflicts:
1397                    res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
1398                    res['results'].append("The following packages have pending transactions: %s" % ", ".join(conflicts))
1399                    res['rc'] = 128  # Ensure the task fails in with-loop
1400                    self.module.fail_json(**res)
1401
1402        # check_mode output
1403        to_update = []
1404        for w in will_update:
1405            if w.startswith('@'):
1406                to_update.append((w, None))
1407            elif w not in updates:
1408                other_pkg = will_update_from_other_package[w]
1409                to_update.append(
1410                    (
1411                        w,
1412                        'because of (at least) %s-%s.%s from %s' % (
1413                            other_pkg,
1414                            updates[other_pkg]['version'],
1415                            updates[other_pkg]['dist'],
1416                            updates[other_pkg]['repo']
1417                        )
1418                    )
1419                )
1420            else:
1421                to_update.append((w, '%s.%s from %s' % (updates[w]['version'], updates[w]['dist'], updates[w]['repo'])))
1422
1423        if self.update_only:
1424            res['changes'] = dict(installed=[], updated=to_update)
1425        else:
1426            res['changes'] = dict(installed=pkgs['install'], updated=to_update)
1427
1428        if obsoletes:
1429            res['obsoletes'] = obsoletes
1430
1431        # return results before we actually execute stuff
1432        if self.module.check_mode:
1433            if will_update or pkgs['install']:
1434                res['changed'] = True
1435            return res
1436
1437        if self.releasever:
1438            cmd.extend(['--releasever=%s' % self.releasever])
1439
1440        # run commands
1441        if cmd:     # update all
1442            rc, out, err = self.module.run_command(cmd)
1443            res['changed'] = True
1444        elif self.update_only:
1445            if pkgs['update']:
1446                cmd = self.yum_basecmd + ['update'] + pkgs['update']
1447                lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
1448                rc, out, err = self.module.run_command(cmd, environ_update=lang_env)
1449                out_lower = out.strip().lower()
1450                if not out_lower.endswith("no packages marked for update") and \
1451                        not out_lower.endswith("nothing to do"):
1452                    res['changed'] = True
1453            else:
1454                rc, out, err = [0, '', '']
1455        elif pkgs['install'] or will_update and not self.update_only:
1456            cmd = self.yum_basecmd + ['install'] + pkgs['install'] + pkgs['update']
1457            lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
1458            rc, out, err = self.module.run_command(cmd, environ_update=lang_env)
1459            out_lower = out.strip().lower()
1460            if not out_lower.endswith("no packages marked for update") and \
1461                    not out_lower.endswith("nothing to do"):
1462                res['changed'] = True
1463        else:
1464            rc, out, err = [0, '', '']
1465
1466        res['rc'] = rc
1467        res['msg'] += err
1468        res['results'].append(out)
1469
1470        if rc:
1471            res['failed'] = True
1472
1473        return res
1474
1475    def ensure(self, repoq):
1476        pkgs = self.names
1477
1478        # autoremove was provided without `name`
1479        if not self.names and self.autoremove:
1480            pkgs = []
1481            self.state = 'absent'
1482
1483        if self.conf_file and os.path.exists(self.conf_file):
1484            self.yum_basecmd += ['-c', self.conf_file]
1485
1486            if repoq:
1487                repoq += ['-c', self.conf_file]
1488
1489        if self.skip_broken:
1490            self.yum_basecmd.extend(['--skip-broken'])
1491
1492        if self.disablerepo:
1493            self.yum_basecmd.extend(['--disablerepo=%s' % ','.join(self.disablerepo)])
1494
1495        if self.enablerepo:
1496            self.yum_basecmd.extend(['--enablerepo=%s' % ','.join(self.enablerepo)])
1497
1498        if self.enable_plugin:
1499            self.yum_basecmd.extend(['--enableplugin', ','.join(self.enable_plugin)])
1500
1501        if self.disable_plugin:
1502            self.yum_basecmd.extend(['--disableplugin', ','.join(self.disable_plugin)])
1503
1504        if self.exclude:
1505            e_cmd = ['--exclude=%s' % ','.join(self.exclude)]
1506            self.yum_basecmd.extend(e_cmd)
1507
1508        if self.disable_excludes:
1509            self.yum_basecmd.extend(['--disableexcludes=%s' % self.disable_excludes])
1510
1511        if self.download_only:
1512            self.yum_basecmd.extend(['--downloadonly'])
1513
1514            if self.download_dir:
1515                self.yum_basecmd.extend(['--downloaddir=%s' % self.download_dir])
1516
1517        if self.releasever:
1518            self.yum_basecmd.extend(['--releasever=%s' % self.releasever])
1519
1520        if self.installroot != '/':
1521            # do not setup installroot by default, because of error
1522            # CRITICAL:yum.cli:Config Error: Error accessing file for config file:////etc/yum.conf
1523            # in old yum version (like in CentOS 6.6)
1524            e_cmd = ['--installroot=%s' % self.installroot]
1525            self.yum_basecmd.extend(e_cmd)
1526
1527        if self.state in ('installed', 'present', 'latest'):
1528            """ The need of this entire if conditional has to be changed
1529                this function is the ensure function that is called
1530                in the main section.
1531
1532                This conditional tends to disable/enable repo for
1533                install present latest action, same actually
1534                can be done for remove and absent action
1535
1536                As solution I would advice to cal
1537                try: self.yum_base.repos.disableRepo(disablerepo)
1538                and
1539                try: self.yum_base.repos.enableRepo(enablerepo)
1540                right before any yum_cmd is actually called regardless
1541                of yum action.
1542
1543                Please note that enable/disablerepo options are general
1544                options, this means that we can call those with any action
1545                option.  https://linux.die.net/man/8/yum
1546
1547                This docstring will be removed together when issue: #21619
1548                will be solved.
1549
1550                This has been triggered by: #19587
1551            """
1552
1553            if self.update_cache:
1554                self.module.run_command(self.yum_basecmd + ['clean', 'expire-cache'])
1555
1556            try:
1557                current_repos = self.yum_base.repos.repos.keys()
1558                if self.enablerepo:
1559                    try:
1560                        new_repos = self.yum_base.repos.repos.keys()
1561                        for i in new_repos:
1562                            if i not in current_repos:
1563                                rid = self.yum_base.repos.getRepo(i)
1564                                a = rid.repoXML.repoid  # nopep8 - https://github.com/ansible/ansible/pull/21475#pullrequestreview-22404868
1565                        current_repos = new_repos
1566                    except yum.Errors.YumBaseError as e:
1567                        self.module.fail_json(msg="Error setting/accessing repos: %s" % to_native(e))
1568            except yum.Errors.YumBaseError as e:
1569                self.module.fail_json(msg="Error accessing repos: %s" % to_native(e))
1570        if self.state == 'latest' or self.update_only:
1571            if self.disable_gpg_check:
1572                self.yum_basecmd.append('--nogpgcheck')
1573            if self.security:
1574                self.yum_basecmd.append('--security')
1575            if self.bugfix:
1576                self.yum_basecmd.append('--bugfix')
1577            res = self.latest(pkgs, repoq)
1578        elif self.state in ('installed', 'present'):
1579            if self.disable_gpg_check:
1580                self.yum_basecmd.append('--nogpgcheck')
1581            res = self.install(pkgs, repoq)
1582        elif self.state in ('removed', 'absent'):
1583            res = self.remove(pkgs, repoq)
1584        else:
1585            # should be caught by AnsibleModule argument_spec
1586            self.module.fail_json(
1587                msg="we should never get here unless this all failed",
1588                changed=False,
1589                results='',
1590                errors='unexpected state'
1591            )
1592        return res
1593
1594    @staticmethod
1595    def has_yum():
1596        return HAS_YUM_PYTHON
1597
1598    def run(self):
1599        """
1600        actually execute the module code backend
1601        """
1602
1603        if (not HAS_RPM_PYTHON or not HAS_YUM_PYTHON) and sys.executable != '/usr/bin/python' and not has_respawned():
1604            respawn_module('/usr/bin/python')
1605            # end of the line for this process; we'll exit here once the respawned module has completed
1606
1607        error_msgs = []
1608        if not HAS_RPM_PYTHON:
1609            error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.')
1610        if not HAS_YUM_PYTHON:
1611            error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.')
1612
1613        self.wait_for_lock()
1614
1615        if error_msgs:
1616            self.module.fail_json(msg='. '.join(error_msgs))
1617
1618        # fedora will redirect yum to dnf, which has incompatibilities
1619        # with how this module expects yum to operate. If yum-deprecated
1620        # is available, use that instead to emulate the old behaviors.
1621        if self.module.get_bin_path('yum-deprecated'):
1622            yumbin = self.module.get_bin_path('yum-deprecated')
1623        else:
1624            yumbin = self.module.get_bin_path('yum')
1625
1626        # need debug level 2 to get 'Nothing to do' for groupinstall.
1627        self.yum_basecmd = [yumbin, '-d', '2', '-y']
1628
1629        if self.update_cache and not self.names and not self.list:
1630            rc, stdout, stderr = self.module.run_command(self.yum_basecmd + ['clean', 'expire-cache'])
1631            if rc == 0:
1632                self.module.exit_json(
1633                    changed=False,
1634                    msg="Cache updated",
1635                    rc=rc,
1636                    results=[]
1637                )
1638            else:
1639                self.module.exit_json(
1640                    changed=False,
1641                    msg="Failed to update cache",
1642                    rc=rc,
1643                    results=[stderr],
1644                )
1645
1646        repoquerybin = self.module.get_bin_path('repoquery', required=False)
1647
1648        if self.install_repoquery and not repoquerybin and not self.module.check_mode:
1649            yum_path = self.module.get_bin_path('yum')
1650            if yum_path:
1651                if self.releasever:
1652                    self.module.run_command('%s -y install yum-utils --releasever %s' % (yum_path, self.releasever))
1653                else:
1654                    self.module.run_command('%s -y install yum-utils' % yum_path)
1655            repoquerybin = self.module.get_bin_path('repoquery', required=False)
1656
1657        if self.list:
1658            if not repoquerybin:
1659                self.module.fail_json(msg="repoquery is required to use list= with this module. Please install the yum-utils package.")
1660            results = {'results': self.list_stuff(repoquerybin, self.list)}
1661        else:
1662            # If rhn-plugin is installed and no rhn-certificate is available on
1663            # the system then users will see an error message using the yum API.
1664            # Use repoquery in those cases.
1665
1666            repoquery = None
1667            try:
1668                yum_plugins = self.yum_base.plugins._plugins
1669            except AttributeError:
1670                pass
1671            else:
1672                if 'rhnplugin' in yum_plugins:
1673                    if repoquerybin:
1674                        repoquery = [repoquerybin, '--show-duplicates', '--plugins', '--quiet']
1675                        if self.installroot != '/':
1676                            repoquery.extend(['--installroot', self.installroot])
1677
1678                        if self.disable_excludes:
1679                            # repoquery does not support --disableexcludes,
1680                            # so make a temp copy of yum.conf and get rid of the 'exclude=' line there
1681                            try:
1682                                with open('/etc/yum.conf', 'r') as f:
1683                                    content = f.readlines()
1684
1685                                tmp_conf_file = tempfile.NamedTemporaryFile(dir=self.module.tmpdir, delete=False)
1686                                self.module.add_cleanup_file(tmp_conf_file.name)
1687
1688                                tmp_conf_file.writelines([c for c in content if not c.startswith("exclude=")])
1689                                tmp_conf_file.close()
1690                            except Exception as e:
1691                                self.module.fail_json(msg="Failure setting up repoquery: %s" % to_native(e))
1692
1693                            repoquery.extend(['-c', tmp_conf_file.name])
1694
1695            results = self.ensure(repoquery)
1696            if repoquery:
1697                results['msg'] = '%s %s' % (
1698                    results.get('msg', ''),
1699                    'Warning: Due to potential bad behaviour with rhnplugin and certificates, used slower repoquery calls instead of Yum API.'
1700                )
1701
1702        self.module.exit_json(**results)
1703
1704
1705def main():
1706    # state=installed name=pkgspec
1707    # state=removed name=pkgspec
1708    # state=latest name=pkgspec
1709    #
1710    # informational commands:
1711    #   list=installed
1712    #   list=updates
1713    #   list=available
1714    #   list=repos
1715    #   list=pkgspec
1716
1717    yumdnf_argument_spec['argument_spec']['use_backend'] = dict(default='auto', choices=['auto', 'yum', 'yum4', 'dnf'])
1718
1719    module = AnsibleModule(
1720        **yumdnf_argument_spec
1721    )
1722
1723    module_implementation = YumModule(module)
1724    module_implementation.run()
1725
1726
1727if __name__ == '__main__':
1728    main()
1729