library/yum
b576e389
 #!/usr/bin/python -tt
7e9e2901
 # -*- coding: utf-8 -*-
 
b576e389
 # (c) 2012, Red Hat, Inc
 # Written by Seth Vidal <skvidal at fedoraproject.org>
 #
 # 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 <http://www.gnu.org/licenses/>.
 #
 
c91befda
 
b576e389
 import traceback
c91befda
 import os
14479e6a
 import yum
 
a040807f
 DOCUMENTATION = '''
 ---
 module: yum
 short_description: Manages packages with the I(yum) package manager
 description:
      - Will install, upgrade, remove, and list packages with the I(yum) package manager.
 options:
   name:
     description:
       - package name, or package specifier with version, like C(name-1.0).
     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"
d89d0755
   enablerepo:
     description:
0a153c67
       - Repoid of repositories to enable for the install/update operation.
         These repos will not persist beyond the transaction
         multiple repos separated with a ','
d89d0755
     required: false
3b832955
     version_added: "0.9"
d89d0755
     default: null
     aliases: []
0a153c67
     
d89d0755
   disablerepo:
     description:
caf003c8
       - I(repoid) of repositories to disable for the install/update operation
0a153c67
         These repos will not persist beyond the transaction
         Multiple repos separated with a ','
d89d0755
     required: false
3b832955
     version_added: "0.9"
d89d0755
     default: null
     aliases: []
     
a040807f
 examples:
d89d0755
    - code: yum name=httpd state=latest
    - code: yum name=httpd state=removed
    - code: yum name=httpd enablerepo=testing state=installed
a040807f
 notes: []
 # informational: requirements for nodes
 requirements: [ yum, rpm ]
 author: Seth Vidal
 '''
 
c91befda
 def_qf = "%{name}-%{version}-%{release}.%{arch}"
ded0c617
 
fd492beb
 repoquery='/usr/bin/repoquery'
14479e6a
 if not os.path.exists(repoquery):
     repoquery = None
c91befda
 
ded0c617
 yumbin='/usr/bin/yum'
14479e6a
 
 def yum_base(conf_file=None, cachedir=False):
ded0c617
 
14479e6a
     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 po_to_nevra(po):
ded0c617
 
14479e6a
     if hasattr(po, 'ui_nevra'):
         return po.ui_nevra
     else:
         return '%s-%s-%s.%s' % (po.name, po.version, po.release, po.arch)
 
d89d0755
 def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]):
ded0c617
 
14479e6a
     if not repoq:
ded0c617
 
14479e6a
         pkgs = []
         try:
             my = yum_base(conf_file)
d89d0755
             for rid in en_repos:
                 my.repos.enableRepo(rid)
             for rid in dis_repos:
                 my.repos.disableRepo(rid)
                 
14479e6a
             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 ]
ded0c617
 
14479e6a
     else:
ded0c617
 
14479e6a
         cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec]
         rc,out,err = run(cmd)
         cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, "--whatprovides", pkgspec]
         rc2,out2,err2 = run(cmd)
         if rc == 0 and rc2 == 0:
             out += out2
             return [ p for p in out.split('\n') if p.strip() ]
         else:
d8337dab
             module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
14479e6a
             
c91befda
     return []
 
d89d0755
 def is_available(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]):
ded0c617
 
14479e6a
     if not repoq:
ded0c617
 
14479e6a
         pkgs = []
         try:
             my = yum_base(conf_file)
d89d0755
             for rid in en_repos:
                 my.repos.enableRepo(rid)
             for rid in dis_repos:
                 my.repos.disableRepo(rid)
 
14479e6a
             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 ]
ded0c617
 
14479e6a
     else:
ded0c617
 
14479e6a
         cmd = repoq + ["--qf", qf, pkgspec]
         rc,out,err = run(cmd)
         if rc == 0:
             return [ p for p in out.split('\n') if p.strip() ]
         else:
d8337dab
             module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
 
14479e6a
             
c91befda
     return []
 
d89d0755
 def is_update(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=[], dis_repos=[]):
ded0c617
 
14479e6a
     if not repoq:
ded0c617
 
14479e6a
         retpkgs = []
         pkgs = []
         updates = []
ded0c617
 
14479e6a
         try:
             my = yum_base(conf_file)
d89d0755
             for rid in en_repos:
                 my.repos.enableRepo(rid)
             for rid in dis_repos:
                 my.repos.disableRepo(rid)
 
14479e6a
             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 ])
faed4b5a
 
14479e6a
     else:
ded0c617
 
14479e6a
         cmd = repoq + ["--pkgnarrow=updates", "--qf", qf, pkgspec]
         rc,out,err = run(cmd)
         
         if rc == 0:
             return set([ p for p in out.split('\n') if p.strip() ])
         else:
d8337dab
             module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
14479e6a
             
c91befda
     return []
 
d89d0755
 def what_provides(module, repoq, req_spec, conf_file,  qf=def_qf, en_repos=[], dis_repos=[]):
ded0c617
 
14479e6a
     if not repoq:
ded0c617
 
14479e6a
         pkgs = []
         try:
             my = yum_base(conf_file)
d89d0755
             for rid in en_repos:
                 my.repos.enableRepo(rid)
             for rid in dis_repos:
                 my.repos.disableRepo(rid)
 
14479e6a
             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 ])
ded0c617
 
14479e6a
     else:
ded0c617
 
14479e6a
         cmd = repoq + ["--qf", qf, "--whatprovides", req_spec]
         rc,out,err = run(cmd)
         cmd = repoq + ["--qf", qf, req_spec]
         rc2,out2,err2 = run(cmd)
         if rc == 0 and rc2 == 0:
             out += out2
326b1602
             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
14479e6a
         else:
d8337dab
             module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
 
14479e6a
     return []
b576e389
 
3175eacf
 def local_nvra(path):
     """return nvra of a local rpm passed in"""
     
     cmd = ['/bin/rpm', '-qp' ,'--qf', 
5dbc85e8
             '%{name}-%{version}-%{release}.%{arch}\n', path ]
3175eacf
     rc, out, err = run(cmd)
     if rc != 0:
         return None
     nvra = out.split('\n')[0]
     return nvra
     
c91befda
 def pkg_to_dict(pkgstr):
ded0c617
 
c91befda
     if pkgstr.strip():
         n,e,v,r,a,repo = pkgstr.split('|')
     else:
         return {'error_parsing': pkgstr}
faed4b5a
 
b576e389
     d = {
c91befda
         'name':n,
         'arch':a,
         'epoch':e,
         'release':r,
         'version':v,
         'repo':repo,
         'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a)
1e4d45af
     }
faed4b5a
 
c91befda
     if repo == 'installed':
209760f8
         d['yumstate'] = 'installed'
b576e389
     else:
209760f8
         d['yumstate'] = 'available'
c91befda
 
     return d
 
 def repolist(repoq, qf="%{repoid}"):
ded0c617
 
5a7d2717
     cmd = repoq + ["--qf", qf, "-a"]
c91befda
     rc,out,err = run(cmd)
     ret = []
     if rc == 0:
         ret = set([ p for p in out.split('\n') if p.strip() ])
     return ret
faed4b5a
 
14479e6a
 def list_stuff(module, conf_file, stuff):
ded0c617
 
c91befda
     qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}"
63641da2
     repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q']
c91befda
     if conf_file and os.path.exists(conf_file):
5a7d2717
         repoq += ['-c', conf_file]
b576e389
 
     if stuff == 'installed':
14479e6a
         return [ pkg_to_dict(p) for p in is_installed(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
b576e389
     elif stuff == 'updates':
14479e6a
         return [ pkg_to_dict(p) for p in is_update(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
b576e389
     elif stuff == 'available':
14479e6a
         return [ pkg_to_dict(p) for p in is_available(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
b576e389
     elif stuff == 'repos':
c91befda
         return [ dict(repoid=name, state='enabled') for name in repolist(repoq) if name.strip() ]
b576e389
     else:
14479e6a
         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() ]
b576e389
 
c91befda
 def run(command):
ded0c617
 
b576e389
     try:
5a7d2717
         cmd = subprocess.Popen(command,
b576e389
             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         out, err = cmd.communicate()
     except (OSError, IOError), e:
         rc = 1
         err = str(e)
         out = ''
     except:
         rc = 1
         err = traceback.format_exc()
         out = ''
         if out is None:
477ca2ed
             out = ''
b576e389
         if err is None:
477ca2ed
             err = ''
b576e389
     else:
         rc = cmd.returncode
faed4b5a
 
b576e389
     return rc, out, err
 
5a7d2717
 
d89d0755
 def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
ded0c617
 
0b94c780
     res = {}
fd492beb
     res['results'] = []
c91befda
     res['msg'] = ''
     res['rc'] = 0
     res['changed'] = False
 
     for spec in items:
         pkg = None
 
         # check if pkgspec is installed (if possible for idempotence)
         # localpkg
         if spec.endswith('.rpm'):
faed4b5a
             # get the pkg name-v-r.arch
5dbc85e8
             if not os.path.exists(spec):
                 res['msg'] += "No Package file matching '%s' found on system" % spec
                 module.fail_json(**res)
 
faed4b5a
             nvra = local_nvra(spec)
c91befda
             # look for them in the rpmdb
d8337dab
             if is_installed(module, repoq, nvra, conf_file, en_repos=en_repos, dis_repos=dis_repos):
c91befda
                 # if they are there, skip it
                 continue
             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 :(
a9a9e3af
         else:
c91befda
             # look up what pkgs provide this
d8337dab
             pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
c91befda
             if not pkglist:
                 res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
de4b8dc5
                 module.fail_json(**res)
faed4b5a
 
c91befda
             # if any of them are installed
             # then nothing to do
faed4b5a
 
c91befda
             found = False
             for this in pkglist:
d8337dab
                 if is_installed(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos):
c91befda
                     found = True
fd492beb
                     res['results'].append('%s providing %s is already installed' % (this, spec))
326b1602
                     break
faed4b5a
 
c91befda
             if found:
                 continue
             # if not - then pass in the spec as what to install
faed4b5a
             # we could get here if nothing provides it but that's not
             # the error we're catching here
c91befda
             pkg = spec
faed4b5a
 
5a7d2717
         cmd = yum_basecmd + ['install', pkg]
c91befda
         rc, out, err = run(cmd)
fe0c70fe
 
         res['rc'] += rc
         res['results'].append(out)
         res['msg'] += err
 
0b94c780
         # 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
fe0c70fe
         if not rc:
c91befda
             res['changed'] = True
faed4b5a
 
c91befda
     module.exit_json(**res)
faed4b5a
 
b576e389
 
d89d0755
 def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
58c975d6
 
c91befda
     res = {}
fd492beb
     res['results'] = []
c91befda
     res['msg'] = ''
     res['changed'] = False
     res['rc'] = 0
faed4b5a
 
49dce05c
     for pkg in items:
         is_group = False
         # group remove - this is doom on a stick
         if pkg.startswith('@'):
             is_group = True
b576e389
         else:
49dce05c
             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)
c91befda
                 continue
b576e389
 
49dce05c
         # run an actual yum transaction
5a7d2717
         cmd = yum_basecmd + ["remove", pkg]
c91befda
         rc, out, err = run(cmd)
faed4b5a
 
fe0c70fe
         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
 
49dce05c
         # 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:
fe0c70fe
                 module.fail_json(**res)
49dce05c
 
c91befda
         if rc != 0:
49dce05c
             module.fail_json(**res)
fe0c70fe
             
c91befda
     module.exit_json(**res)
0b94c780
 
d89d0755
 def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
ded0c617
 
c91befda
     res = {}
fd492beb
     res['results'] = []
c91befda
     res['msg'] = ''
     res['changed'] = False
     res['rc'] = 0
faed4b5a
 
c91befda
     for spec in items:
ded0c617
 
c91befda
         pkg = None
ca63173b
         basecmd = 'update'
c91befda
         # groups, again
         if spec.startswith('@'):
             pkg = spec
         # dep/pkgname  - find it
1d04ec89
         else:
d8337dab
             if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos):
14479e6a
                 basecmd = 'update'
             else:
                 basecmd = 'install'
 
d8337dab
             pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
c91befda
             if not pkglist:
                 res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
fe0c70fe
                 module.fail_json(**res)
14479e6a
             
             nothing_to_do = True
c91befda
             for this in pkglist:
d8337dab
                 if basecmd == 'install' and is_available(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos):
14479e6a
                     nothing_to_do = False
                     break
                     
d8337dab
                 if basecmd == 'update' and is_update(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=en_repos):
14479e6a
                     nothing_to_do = False
                     break
                     
c91befda
             if nothing_to_do:
fd492beb
                 res['results'].append("All packages providing %s are up to date" % spec)
c91befda
                 continue
faed4b5a
 
c91befda
             pkg = spec
 
5a7d2717
         cmd = yum_basecmd + [basecmd, pkg]
c91befda
         rc, out, err = run(cmd)
37f599ef
 
fe0c70fe
         res['rc'] += rc
         res['results'].append(out)
         res['msg'] += err
 
0b94c780
         # 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
 
c91befda
         if rc:
             res['failed'] = True
         else:
             res['changed'] = True
faed4b5a
 
c91befda
     module.exit_json(**res)
0b94c780
 
d89d0755
 def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo):
ded0c617
 
c91befda
     # take multiple args comma separated
5a7d2717
     items = pkgspec.split(',')
2030f82b
 
5a7d2717
     yum_basecmd = [yumbin, '-d', '1', '-y']
ded0c617
 
d89d0755
         
14479e6a
     if not repoquery:
         repoq = None
     else:
         repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q']
ded0c617
 
c91befda
     if conf_file and os.path.exists(conf_file):
5a7d2717
         yum_basecmd += ['-c', conf_file]
14479e6a
         if repoq:
             repoq += ['-c', conf_file]
 
d89d0755
     dis_repos =[]
     en_repos = []
     if disablerepo:
         dis_repos = disablerepo.split(',')
     if enablerepo:
         en_repos = enablerepo.split(',')
 
     for repoid in en_repos:
         r_cmd = ['--enablerepo', repoid]
         yum_basecmd.extend(r_cmd)
         
         if repoq:
             repoq.extend(r_cmd)
     
     for repoid in dis_repos:
         r_cmd = ['--disablerepo', repoid]
         yum_basecmd.extend(r_cmd)
 
         if repoq:
             repoq.extend(r_cmd)
 
14479e6a
     if state in ['installed', 'present']:
d89d0755
         install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos)
14479e6a
     elif state in ['removed', 'absent']:
d89d0755
         remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos)
14479e6a
     elif state == 'latest':
d89d0755
         latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos)
5a7d2717
 
faed4b5a
     # should be caught by AnsibleModule argument_spec
c91befda
     return dict(changed=False, failed=True, results='', errors='unexpected state')
faed4b5a
 
b576e389
 def main():
ded0c617
 
67301c10
     # state=installed name=pkgspec
     # state=removed name=pkgspec
     # state=latest name=pkgspec
b576e389
     #
5764ccdb
     # informational commands:
     #   list=installed
     #   list=updates
     #   list=available
     #   list=repos
     #   list=pkgspec
faed4b5a
 
b47bed96
     module = AnsibleModule(
2030f82b
         argument_spec = dict(
58c975d6
             name=dict(aliases=['pkg']),
2030f82b
             # removed==absent, installed==present, these are accepted as aliases
             state=dict(default='installed', choices=['absent','present','installed','removed','latest']),
d89d0755
             enablerepo=dict(),
             disablerepo=dict(),
5c458b97
             list=dict(),
c91befda
             conf_file=dict(default=None),
1e4d45af
         ),
67301c10
         required_one_of = [['name','list']],
         mutually_exclusive = [['name','list']]
b47bed96
     )
a99b491b
 
b47bed96
     params = module.params
b576e389
 
25acfa81
     if params['list']:
14479e6a
         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']))
2030f82b
         module.exit_json(**results)
faed4b5a
 
ff5d3293
     else:
67301c10
         pkg = params['name']
1e4d45af
         state = params['state']
d89d0755
         enablerepo = params.get('enablerepo', '')
         disablerepo = params.get('disablerepo', '')
         res = ensure(module, state, pkg, params['conf_file'], enablerepo, disablerepo)
1e4d45af
         module.fail_json(msg="we should never get here unless this all failed", **res)
a9a9e3af
 
b47bed96
 # this is magic, see lib/ansible/module_common.py
 #<<INCLUDE_ANSIBLE_MODULE_COMMON>>
 main()