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