#!/usr/bin/python -tt # -*- coding: utf-8 -*- # (c) 2012, Red Hat, Inc # Written by Seth Vidal # # 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 subprocess def_qf = "%{name}-%{version}-%{release}.%{arch}" repoquery='/usr/bin/repoquery' yumbin='/usr/bin/yum' rpmbin = '/bin/rpm' def is_installed(repoq, pkgspec, qf=def_qf): cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec] rc,out,err = run(cmd) if rc == 0: return [ p for p in out.split('\n') if p.strip() ] return [] def is_available(repoq, pkgspec, qf=def_qf): cmd = repoq + ["--qf", qf, pkgspec] rc,out,err = run(cmd) if rc == 0: return [ p for p in out.split('\n') if p.strip() ] return [] def is_update(repoq, pkgspec, qf=def_qf): 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() ]) return [] def what_provides(repoq, req_spec, qf=def_qf): cmd = repoq + ["--qf", qf, "--whatprovides", req_spec] rc,out,err = run(cmd) ret = [] if rc == 0: ret = set([ p for p in out.split('\n') if p.strip() ]) return ret 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(repoq, qf="%{repoid}"): cmd = repoq + ["--qf", qf, "-a"] rc,out,err = run(cmd) ret = [] if rc == 0: ret = set([ p for p in out.split('\n') if p.strip() ]) return ret def list_stuff(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(repoq, '-a', qf=qf) if p.strip() ] elif stuff == 'updates': return [ pkg_to_dict(p) for p in is_update(repoq, '-a', qf=qf) if p.strip() ] elif stuff == 'available': return [ pkg_to_dict(p) for p in is_available(repoq, '-a', qf=qf) if p.strip() ] elif stuff == 'repos': return [ dict(repoid=name, state='enabled') for name in repolist(repoq) if name.strip() ] else: return [ pkg_to_dict(p) for p in is_installed(repoq, stuff, qf=qf) + is_available(repoq, stuff, qf=qf) if p.strip() ] def run(command): try: cmd = subprocess.Popen(command, 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: out = '' if err is None: err = '' else: rc = cmd.returncode return rc, out, err def install_no_repoq(module, items, yum_basecmd, latest=False): res = {'changed': False} to_install = [] if not latest: for item in items: rc, out, err = run([rpmbin, "-q", "--whatprovides", item]) if rc != 0: to_install.append(item) if len(to_install) > 0: res['changed'] = True else: cmd = yum_basecmd + ["check-update"] + items rc, out, err = run(cmd) if rc == 100: res['changed'] = True to_install = items elif rc != 0: module.fail_json(msg=err) if len(to_install) > 0: rc, out, err = run(yum_basecmd + ["--obsoletes", "install"] + to_install) if rc != 0: module.fail_json(msg=err) for item in to_install: rc, out, err = run([rpmbin, "-q", "--whatprovides", item]) if rc != 0: module.fail_json(msg="%s could not be installed" % item) module.exit_json(**res) def remove_no_repoq(module, items, yum_basecmd): res = {'changed': False} to_remove = [] for item in items: rc, out, err = run([rpmbin, "-q", "--whatprovides", "--qf", "%{NAME}\n", item]) if rc == 0: to_remove.append(out.strip()) if len(to_remove) > 0: res['changed'] = True rc, out, err = run(yum_basecmd + ["remove"] + to_remove) if rc != 0: module.fail_json(msg=err) res['out'] = out res['err'] = err for item in to_remove: rc, out, err = run([rpmbin, "-q", item]) if rc == 0: module.fail_json(msg="%s was not removed" % item) module.exit_json(**res) def install(module, items, repoq, yum_basecmd): res = {} res['results'] = [] 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'): # get the pkg name-v-r.arch nvra = local_nvra(spec) # look for them in the rpmdb if is_installed(repoq, nvra): # 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 :( else: # look up what pkgs provide this pkglist = what_provides(repoq, spec) if not pkglist: res['msg'] += "No Package matching '%s' found available, installed or updated" % spec res['failed'] = True module.exit_json(**res) # if any of them are installed # then nothing to do found = False for this in pkglist: if is_installed(repoq, this): found = True res['results'].append('%s providing %s is already installed' % (this, spec)) if found: continue # 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 cmd = yum_basecmd + ['install', pkg] rc, out, err = run(cmd) # 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 if rc: res['changed'] = False res['rc'] = rc res['results'].append(out) res['msg'] += err else: res['changed'] = True res['rc'] = 0 res['results'].append(out) res['msg'] += err module.exit_json(**res) def remove(module, items, repoq, yum_basecmd): res = {} res['results'] = [] res['msg'] = '' res['changed'] = False res['rc'] = 0 for spec in items: pkg = None # group remove - hope you like things dying! if spec.startswith('@'): pkg = spec # req or pkgname remove else: pkglist = what_provides(repoq, spec) if not pkglist: res['msg'] += "No Package matching '%s' found available, installed or updated" % spec res['failed']=True module.exit_json(**res) found = False for this in pkglist: if is_installed(repoq, this): found = True if not found: res['results'].append('%s is not installed' % spec) continue pkg = spec cmd = yum_basecmd + ["remove", pkg] rc, out, err = run(cmd) # FIXME if we ran the remove - check to make sure it actually removed :( # look for the pkg in the rpmdb - this is notoriously hard for groups :( if rc != 0: res['changed'] = False res['failed'] = True res['rc'] = rc res['results'].append(out) res['msg'] += err else: res['changed'] = True res['rc'] = 0 res['results'].append(out) res['msg'] += err module.exit_json(**res) def latest(module, items, repoq, yum_basecmd): res = {} res['results'] = [] res['msg'] = '' res['changed'] = False res['rc'] = 0 for spec in items: pkg = None basecmd = 'update' # groups, again if spec.startswith('@'): pkg = spec # dep/pkgname - find it else: pkglist = what_provides(repoq, spec) if not pkglist: res['msg'] += "No Package matching '%s' found available, installed or updated" % spec res['failed']=True module.exit_json(**res) found = False nothing_to_do = False can_be_installed = True for this in pkglist: if is_installed(repoq, this): if is_update(repoq, this): found = True else: nothing_to_do = True if nothing_to_do: res['results'].append("All packages providing %s are up to date" % spec) continue if not found: basecmd = 'install' else: basecmd = 'update' pkg = spec cmd = yum_basecmd + [basecmd, pkg] rc, out, err = run(cmd) # 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: changed = False failed = True else: changed = True failed = False if rc: res['changed'] = False res['failed'] = True res['rc'] = rc res['results'].append(out) res['msg'] += err else: res['changed'] = True res['rc'] = 0 res['results'].append(out) res['msg'] += err module.exit_json(**res) def ensure(module, state, pkgspec, conf_file): # take multiple args comma separated items = pkgspec.split(',') yum_basecmd = [yumbin, '-d', '1', '-y'] repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q'] if conf_file and os.path.exists(conf_file): yum_basecmd += ['-c', conf_file] repoq += ['-c', conf_file] if os.path.exists(repoquery): if state in ['installed', 'present']: install(module, items, repoq, yum_basecmd) elif state in ['removed', 'absent']: remove(module, items, repoq, yum_basecmd) elif state == 'latest': latest(module, items, repoq, yum_basecmd) else: if len(filter(lambda x: x.find('>') != -1 or x.find('<') != -1 or x.find('=') != -1, items)) > 0: module.fail_json(msg="%s is required to use yum equality comparisons. Please install the yum-utils package." % repoquery) if state in ['installed', 'present']: install_no_repoq(module, items, yum_basecmd) elif state in ['removed', 'absent']: remove_no_repoq(module, items, yum_basecmd) elif state == 'latest': install_no_repoq(module, items, yum_basecmd, latest=True) # should be caught by AnsibleModule argument_spec return dict(changed=False, failed=True, results='', errors='unexpected state') def main(): # state=installed pkg=pkgspec # state=removed pkg=pkgspec # state=latest pkg=pkgspec # # informational commands: # list=installed # list=updates # list=available # list=repos # list=pkgspec module = AnsibleModule( argument_spec = dict( pkg=dict(aliases=['name']), # removed==absent, installed==present, these are accepted as aliases state=dict(default='installed', choices=['absent','present','installed','removed','latest']), list=dict(), conf_file=dict(default=None), ), required_one_of = [['pkg','list']], mutually_exclusive = [['pkg','list']] ) params = module.params if params['list']: if not os.path.exists(repoquery): module.fail_json(msg="%s is required to use list= with this module. Please install the yum-utils package." % repoquery) results = dict(results=list_stuff(params['conf_file'], params['list'])) module.exit_json(**results) else: pkg = params['pkg'] state = params['state'] res = ensure(module, state, pkg, params['conf_file']) module.fail_json(msg="we should never get here unless this all failed", **res) # this is magic, see lib/ansible/module_common.py #<> main()