#!/usr/bin/python -tt # -*- coding: utf-8 -*- # (c) 2012, Red Hat, Inc # Written by Seth Vidal # (c) 2014, Epic Games, Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # import traceback import os import yum try: from yum.misc import find_unfinished_transactions, find_ts_remaining from rpmUtils.miscutils import splitFilename from rpmUtils.miscutils import compareEVR transaction_helpers = True except: transaction_helpers = False DOCUMENTATION = ''' --- module: yum version_added: historical short_description: Manages packages with the I(yum) package manager description: - Installs, upgrades, downgrades, removes and lists packages and groups with the I(yum) package manager. options: name: description: - "Package name, or package specifier with version, like C(name-1.0). When using state=latest, this can be '*' which means run: yum -y update. You can also pass a url or a local path to a rpm file." required: true default: null aliases: [] list: description: - Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks. See examples. required: false default: null state: description: - Whether to install (C(present), C(latest)), or remove (C(absent)) a package. required: false choices: [ "present", "latest", "absent" ] default: "present" enablerepo: description: - Repoid of repositories to enable for the install/update operation. These repos will not persist beyond the transaction multiple repos separated with a ',' required: false version_added: "0.9" default: null aliases: [] disablerepo: description: - I(repoid) of repositories to disable for the install/update operation These repos will not persist beyond the transaction Multiple repos separated with a ',' required: false version_added: "0.9" default: null aliases: [] conf_file: description: - The remote yum configuration file to use for the transaction. required: false version_added: "0.6" default: null aliases: [] disable_gpg_check: description: - Whether to disable the GPG checking of signatures of packages being installed. Has an effect only if state is I(present) or I(latest). required: false version_added: "1.2" default: "no" choices: ["yes", "no"] aliases: [] notes: [] # informational: requirements for nodes requirements: [ yum, rpm ] author: Seth Vidal ''' EXAMPLES = ''' - name: install the latest version of Apache yum: name=httpd state=latest - name: remove the Apache package yum: name=httpd state=removed - name: install the latest version of Apache from the testing repo yum: name=httpd enablerepo=testing state=installed - name: upgrade all packages yum: name=* state=latest - name: install the nginx rpm from a remote repo yum: name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present - name: install nginx rpm from a local file yum: name=/usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present - name: install the 'Development tools' package group yum: name="@Development tools" state=present ''' def_qf = "%{name}-%{version}-%{release}.%{arch}" repoquery='/usr/bin/repoquery' if not os.path.exists(repoquery): repoquery = None yumbin='/usr/bin/yum' import syslog def log(msg): syslog.openlog('ansible-yum', 0, syslog.LOG_USER) syslog.syslog(syslog.LOG_NOTICE, msg) def yum_base(conf_file=None, cachedir=False): my = yum.YumBase() my.preconf.debuglevel=0 my.preconf.errorlevel=0 if conf_file and os.path.exists(conf_file): my.preconf.fn = conf_file if cachedir or os.geteuid() != 0: if hasattr(my, 'setCacheDir'): my.setCacheDir() else: cachedir = yum.misc.getCacheDir() my.repos.setCacheDir(cachedir) my.conf.cache = 0 return my def install_yum_utils(module): if not module.check_mode: yum_path = module.get_bin_path('yum') if yum_path: rc, so, se = module.run_command('%s -y install yum-utils' % yum_path) if rc == 0: this_path = module.get_bin_path('repoquery') global repoquery repoquery = this_path def po_to_nevra(po): if hasattr(po, 'ui_nevra'): return po.ui_nevra else: return '%s-%s-%s.%s' % (po.name, po.version, po.release, po.arch) def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[], is_pkg=False): if not repoq: pkgs = [] try: my = yum_base(conf_file) for rid in en_repos: my.repos.enableRepo(rid) for rid in dis_repos: my.repos.disableRepo(rid) e,m,u = my.rpmdb.matchPackageNames([pkgspec]) pkgs = e + m if not pkgs: pkgs.extend(my.returnInstalledPackagesByDep(pkgspec)) except Exception, e: module.fail_json(msg="Failure talking to yum: %s" % e) return [ po_to_nevra(p) for p in pkgs ] else: cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec] rc,out,err = module.run_command(cmd) if not is_pkg: cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, "--whatprovides", pkgspec] rc2,out2,err2 = module.run_command(cmd) else: rc2,out2,err2 = (0, '', '') if rc == 0 and rc2 == 0: out += out2 return [ p for p in out.split('\n') if p.strip() ] else: module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) return [] def is_available(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]): if not repoq: pkgs = [] try: my = yum_base(conf_file) for rid in en_repos: my.repos.enableRepo(rid) for rid in dis_repos: my.repos.disableRepo(rid) e,m,u = my.pkgSack.matchPackageNames([pkgspec]) pkgs = e + m if not pkgs: pkgs.extend(my.returnPackagesByDep(pkgspec)) except Exception, e: module.fail_json(msg="Failure talking to yum: %s" % e) return [ po_to_nevra(p) for p in pkgs ] else: myrepoq = list(repoq) for repoid in dis_repos: r_cmd = ['--disablerepo', repoid] myrepoq.extend(r_cmd) for repoid in en_repos: r_cmd = ['--enablerepo', repoid] myrepoq.extend(r_cmd) cmd = myrepoq + ["--qf", qf, pkgspec] rc,out,err = module.run_command(cmd) if rc == 0: return [ p for p in out.split('\n') if p.strip() ] else: module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) return [] def is_update(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]): if not repoq: retpkgs = [] pkgs = [] updates = [] try: my = yum_base(conf_file) for rid in en_repos: my.repos.enableRepo(rid) for rid in dis_repos: my.repos.disableRepo(rid) pkgs = my.returnPackagesByDep(pkgspec) + my.returnInstalledPackagesByDep(pkgspec) if not pkgs: e,m,u = my.pkgSack.matchPackageNames([pkgspec]) pkgs = e + m updates = my.doPackageLists(pkgnarrow='updates').updates except Exception, e: module.fail_json(msg="Failure talking to yum: %s" % e) for pkg in pkgs: if pkg in updates: retpkgs.append(pkg) return set([ po_to_nevra(p) for p in retpkgs ]) else: myrepoq = list(repoq) for repoid in dis_repos: r_cmd = ['--disablerepo', repoid] myrepoq.extend(r_cmd) for repoid in en_repos: r_cmd = ['--enablerepo', repoid] myrepoq.extend(r_cmd) cmd = myrepoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec] rc,out,err = module.run_command(cmd) if rc == 0: return set([ p for p in out.split('\n') if p.strip() ]) else: module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err)) return [] def what_provides(module, repoq, req_spec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]): if not repoq: pkgs = [] try: my = yum_base(conf_file) for rid in en_repos: my.repos.enableRepo(rid) for rid in dis_repos: my.repos.disableRepo(rid) pkgs = my.returnPackagesByDep(req_spec) + my.returnInstalledPackagesByDep(req_spec) if not pkgs: e,m,u = my.pkgSack.matchPackageNames([req_spec]) pkgs.extend(e) pkgs.extend(m) e,m,u = my.rpmdb.matchPackageNames([req_spec]) pkgs.extend(e) pkgs.extend(m) except Exception, e: module.fail_json(msg="Failure talking to yum: %s" % e) return set([ po_to_nevra(p) for p in pkgs ]) else: myrepoq = list(repoq) for repoid in dis_repos: r_cmd = ['--disablerepo', repoid] myrepoq.extend(r_cmd) for repoid in en_repos: r_cmd = ['--enablerepo', repoid] myrepoq.extend(r_cmd) cmd = myrepoq + ["--qf", qf, "--whatprovides", req_spec] rc,out,err = module.run_command(cmd) cmd = myrepoq + ["--qf", qf, req_spec] rc2,out2,err2 = module.run_command(cmd) if rc == 0 and rc2 == 0: out += out2 pkgs = set([ p for p in out.split('\n') if p.strip() ]) if not pkgs: pkgs = is_installed(module, repoq, req_spec, conf_file, qf=qf) return pkgs else: module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2)) return [] def transaction_exists(pkglist): """ checks the package list to see if any packages are involved in an incomplete transaction """ conflicts = [] if not transaction_helpers: return conflicts # first, we create a list of the package 'nvreas' # so we can compare the pieces later more easily pkglist_nvreas = [] for pkg in pkglist: pkglist_nvreas.append(splitFilename(pkg)) # next, we build the list of packages that are # contained within an unfinished transaction unfinished_transactions = find_unfinished_transactions() for trans in unfinished_transactions: steps = find_ts_remaining(trans) for step in steps: # the action is install/erase/etc., but we only # care about the package spec contained in the step (action, step_spec) = step (n,v,r,e,a) = splitFilename(step_spec) # and see if that spec is in the list of packages # requested for installation/updating for pkg in pkglist_nvreas: # if the name and arch match, we're going to assume # this package is part of a pending transaction # the label is just for display purposes label = "%s-%s" % (n,a) if n == pkg[0] and a == pkg[4]: if label not in conflicts: conflicts.append("%s-%s" % (n,a)) break return conflicts def local_nvra(module, path): """return nvra of a local rpm passed in""" cmd = ['/bin/rpm', '-qp' ,'--qf', '%{name}-%{version}-%{release}.%{arch}\n', path ] rc, out, err = module.run_command(cmd) if rc != 0: return None nvra = out.split('\n')[0] return nvra def pkg_to_dict(pkgstr): if pkgstr.strip(): n,e,v,r,a,repo = pkgstr.split('|') else: return {'error_parsing': pkgstr} d = { 'name':n, 'arch':a, 'epoch':e, 'release':r, 'version':v, 'repo':repo, 'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a) } if repo == 'installed': d['yumstate'] = 'installed' else: d['yumstate'] = 'available' return d def repolist(module, repoq, qf="%{repoid}"): cmd = repoq + ["--qf", qf, "-a"] rc,out,err = module.run_command(cmd) ret = [] if rc == 0: ret = set([ p for p in out.split('\n') if p.strip() ]) return ret def list_stuff(module, conf_file, stuff): qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}" repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q'] if conf_file and os.path.exists(conf_file): repoq += ['-c', conf_file] if stuff == 'installed': return [ pkg_to_dict(p) for p in is_installed(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] elif stuff == 'updates': return [ pkg_to_dict(p) for p in is_update(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] elif stuff == 'available': return [ pkg_to_dict(p) for p in is_available(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] elif stuff == 'repos': return [ dict(repoid=name, state='enabled') for name in repolist(module, repoq) if name.strip() ] else: return [ pkg_to_dict(p) for p in is_installed(module, repoq, stuff, conf_file, qf=qf) + is_available(module, repoq, stuff, conf_file, qf=qf) if p.strip() ] def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): res = {} res['results'] = [] res['msg'] = '' res['rc'] = 0 res['changed'] = False downgrade = False for spec in items: pkg = None # check if pkgspec is installed (if possible for idempotence) # localpkg if spec.endswith('.rpm') and '://' not in spec: # get the pkg name-v-r.arch if not os.path.exists(spec): res['msg'] += "No Package file matching '%s' found on system" % spec module.fail_json(**res) nvra = local_nvra(module, spec) # look for them in the rpmdb if is_installed(module, repoq, nvra, conf_file, en_repos=en_repos, dis_repos=dis_repos): # if they are there, skip it continue pkg = spec # URL elif '://' in spec: pkg = spec #groups :( elif spec.startswith('@'): # complete wild ass guess b/c it's a group pkg = spec # range requires or file-requires or pkgname :( else: # most common case is the pkg is already installed and done # short circuit all the bs - and search for it as a pkg in is_installed # if you find it then we're done if not set(['*','?']).intersection(set(spec)): pkgs = is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True) if pkgs: res['results'].append('%s providing %s is already installed' % (pkgs[0], spec)) continue # look up what pkgs provide this pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos) if not pkglist: res['msg'] += "No Package matching '%s' found available, installed or updated" % spec module.fail_json(**res) # if any of the packages are involved in a transaction, fail now # so that we don't hang on the yum operation later conflicts = transaction_exists(pkglist) if len(conflicts) > 0: res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) module.fail_json(**res) # if any of them are installed # then nothing to do found = False for this in pkglist: if is_installed(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True): found = True res['results'].append('%s providing %s is already installed' % (this, spec)) break # if the version of the pkg you have installed is not in ANY repo, but there are # other versions in the repos (both higher and lower) then the previous checks won't work. # so we check one more time. This really only works for pkgname - not for file provides or virt provides # but virt provides should be all caught in what_provides on its own. # highly irritating if not found: if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos): found = True res['results'].append('package providing %s is already installed' % (spec)) if found: continue # downgrade - the yum install command will only install or upgrade to a spec version, it will # not install an older version of an RPM even if specified by the install spec. So we need to # determine if this is a downgrade, and then use the yum downgrade command to install the RPM. split_pkg_name = splitFilename(spec) # if the Name and Version match a version was not provided and this is not a downgrade. if split_pkg_name[0] != split_pkg_name[1]: pkg_name = split_pkg_name[0] pkgs = is_installed(module, repoq, pkg_name, conf_file, en_repos=en_repos, dis_repos=dis_repos, is_pkg=True) if pkgs: (cur_name, cur_ver, cur_rel, cur_epoch, cur_arch) = splitFilename(pkgs[0]) (new_name, new_ver, new_rel, new_epoch, new_arch) = splitFilename(spec) compare = compareEVR((cur_epoch, cur_ver, cur_rel), (new_epoch, new_ver, new_rel)) if compare > 0: downgrade = True # if not - then pass in the spec as what to install # we could get here if nothing provides it but that's not # the error we're catching here pkg = spec operation = 'install' if downgrade: operation = 'downgrade' cmd = yum_basecmd + [operation, pkg] if module.check_mode: module.exit_json(changed=True) changed = True rc, out, err = module.run_command(cmd) # Fail on invalid urls: if (rc == 1 and '://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)): err = 'Package at %s could not be installed' % spec module.fail_json(changed=False,msg=err,rc=1) elif (rc != 0 and 'Nothing to do' in err) or 'Nothing to do' in out: # avoid failing in the 'Nothing To Do' case # this may happen with an URL spec. # for an already installed group, # we get rc = 0 and 'Nothing to do' in out, not in err. rc = 0 err = '' out = '%s: Nothing to do' % spec changed = False res['rc'] += rc res['results'].append(out) res['msg'] += err # FIXME - if we did an install - go and check the rpmdb to see if it actually installed # look for the pkg in rpmdb # look for the pkg via obsoletes # accumulate any changes res['changed'] |= changed module.exit_json(**res) def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): res = {} res['results'] = [] res['msg'] = '' res['changed'] = False res['rc'] = 0 for pkg in items: is_group = False # group remove - this is doom on a stick if pkg.startswith('@'): is_group = True else: if not is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos): res['results'].append('%s is not installed' % pkg) continue # run an actual yum transaction cmd = yum_basecmd + ["remove", pkg] if module.check_mode: module.exit_json(changed=True) rc, out, err = module.run_command(cmd) res['rc'] += rc res['results'].append(out) res['msg'] += err # compile the results into one batch. If anything is changed # then mark changed # at the end - if we've end up failed then fail out of the rest # of the process # at this point we should check to see if the pkg is no longer present if not is_group: # we can't sensibly check for a group being uninstalled reliably # look to see if the pkg shows up from is_installed. If it doesn't if not is_installed(module, repoq, pkg, conf_file, en_repos=en_repos, dis_repos=dis_repos): res['changed'] = True else: module.fail_json(**res) if rc != 0: module.fail_json(**res) module.exit_json(**res) def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): res = {} res['results'] = [] res['msg'] = '' res['changed'] = False res['rc'] = 0 for spec in items: pkg = None basecmd = 'update' cmd = '' # groups, again if spec.startswith('@'): pkg = spec elif spec == '*': #update all # use check-update to see if there is any need rc,out,err = module.run_command(yum_basecmd + ['check-update']) if rc == 100: cmd = yum_basecmd + [basecmd] else: res['results'].append('All packages up to date') continue # dep/pkgname - find it else: if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos): basecmd = 'update' else: basecmd = 'install' pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos) if not pkglist: res['msg'] += "No Package matching '%s' found available, installed or updated" % spec module.fail_json(**res) nothing_to_do = True for this in pkglist: if basecmd == 'install' and is_available(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos): nothing_to_do = False break if basecmd == 'update' and is_update(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=en_repos): nothing_to_do = False break if nothing_to_do: res['results'].append("All packages providing %s are up to date" % spec) continue # if any of the packages are involved in a transaction, fail now # so that we don't hang on the yum operation later conflicts = transaction_exists(pkglist) if len(conflicts) > 0: res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) module.fail_json(**res) pkg = spec if not cmd: cmd = yum_basecmd + [basecmd, pkg] if module.check_mode: return module.exit_json(changed=True) rc, out, err = module.run_command(cmd) res['rc'] += rc res['results'].append(out) res['msg'] += err # FIXME if it is - update it and check to see if it applied # check to see if there is no longer an update available for the pkgspec if rc: res['failed'] = True else: res['changed'] = True module.exit_json(**res) def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): # take multiple args comma separated items = pkgspec.split(',') # need debug level 2 to get 'Nothing to do' for groupinstall. yum_basecmd = [yumbin, '-d', '2', '-y'] if not repoquery: repoq = None else: repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q'] if conf_file and os.path.exists(conf_file): yum_basecmd += ['-c', conf_file] if repoq: repoq += ['-c', conf_file] dis_repos =[] en_repos = [] if disablerepo: dis_repos = disablerepo.split(',') if enablerepo: en_repos = enablerepo.split(',') for repoid in dis_repos: r_cmd = ['--disablerepo=%s' % repoid] yum_basecmd.extend(r_cmd) for repoid in en_repos: r_cmd = ['--enablerepo=%s' % repoid] yum_basecmd.extend(r_cmd) if state in ['installed', 'present', 'latest']: my = yum_base(conf_file) try: for r in dis_repos: my.repos.disableRepo(r) current_repos = my.repos.repos.keys() for r in en_repos: try: my.repos.enableRepo(r) new_repos = my.repos.repos.keys() for i in new_repos: if not i in current_repos: rid = my.repos.getRepo(i) a = rid.repoXML.repoid current_repos = new_repos except yum.Errors.YumBaseError, e: module.fail_json(msg="Error setting/accessing repo %s: %s" % (r, e)) except yum.Errors.YumBaseError, e: module.fail_json(msg="Error accessing repos: %s" % e) if state in ['installed', 'present']: if disable_gpg_check: yum_basecmd.append('--nogpgcheck') install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos) elif state in ['removed', 'absent']: remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos) elif state == 'latest': if disable_gpg_check: yum_basecmd.append('--nogpgcheck') latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos) # should be caught by AnsibleModule argument_spec return dict(changed=False, failed=True, results='', errors='unexpected state') def main(): # state=installed name=pkgspec # state=removed name=pkgspec # state=latest name=pkgspec # # informational commands: # list=installed # list=updates # list=available # list=repos # list=pkgspec module = AnsibleModule( argument_spec = dict( name=dict(aliases=['pkg']), # removed==absent, installed==present, these are accepted as aliases state=dict(default='installed', choices=['absent','present','installed','removed','latest']), enablerepo=dict(), disablerepo=dict(), list=dict(), conf_file=dict(default=None), disable_gpg_check=dict(required=False, default="no", type='bool'), # this should not be needed, but exists as a failsafe install_repoquery=dict(required=False, default="yes", type='bool'), ), required_one_of = [['name','list']], mutually_exclusive = [['name','list']], supports_check_mode = True ) # this should not be needed, but exists as a failsafe params = module.params if params['install_repoquery'] and not repoquery and not module.check_mode: install_yum_utils(module) if params['list']: if not repoquery: module.fail_json(msg="repoquery is required to use list= with this module. Please install the yum-utils package.") results = dict(results=list_stuff(module, params['conf_file'], params['list'])) module.exit_json(**results) else: pkg = params['name'] state = params['state'] enablerepo = params.get('enablerepo', '') disablerepo = params.get('disablerepo', '') disable_gpg_check = params['disable_gpg_check'] res = ensure(module, state, pkg, params['conf_file'], enablerepo, disablerepo, disable_gpg_check) module.fail_json(msg="we should never get here unless this all failed", **res) # import module snippets from ansible.module_utils.basic import * main()