yum 16.7 KB
Newer Older
1
#!/usr/bin/python -tt
2 3
# -*- coding: utf-8 -*-

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# (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/>.
#

23

24
import traceback
25
import os
26 27
import yum

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
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"
examples:
   - code: yum name=httpd state=latest
   - code: yum name=httpd state=removed
   - code: yum name=httpd state=installed
notes: []
# informational: requirements for nodes
requirements: [ yum, rpm ]
author: Seth Vidal
'''

62
def_qf = "%{name}-%{version}-%{release}.%{arch}"
63

64
repoquery='/usr/bin/repoquery'
65 66
if not os.path.exists(repoquery):
    repoquery = None
67

68
yumbin='/usr/bin/yum'
69 70

def yum_base(conf_file=None, cachedir=False):
71

72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
    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):
88

89 90 91 92 93 94
    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):
95

96
    if not repoq:
97

98 99 100 101 102 103 104 105 106 107 108
        pkgs = []
        try:
            my = yum_base(conf_file)
            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 ]
109

110
    else:
111

112 113 114 115 116 117 118 119 120 121
        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:
            module.fail_json(msg='Error from repoquery: %s' % err + err2)
            
122 123
    return []

124
def is_available(module, repoq, pkgspec, conf_file, qf=def_qf):
125

126
    if not repoq:
127

128 129 130 131 132 133 134 135 136 137 138
        pkgs = []
        try:
            my = yum_base(conf_file)
            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 ]
139

140
    else:
141

142 143 144 145 146 147 148
        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:
            module.fail_json(msg='Error from repoquery: %s' % err)
            
149 150
    return []

151
def is_update(module, repoq, pkgspec, conf_file, qf=def_qf):
152

153
    if not repoq:
154

155 156 157
        retpkgs = []
        pkgs = []
        updates = []
158

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
        try:
            my = yum_base(conf_file)
            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 ])
174

175
    else:
176

177 178 179 180 181 182 183 184
        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:
            module.fail_json(msg='Error from repoquery: %s' % err)
            
185 186
    return []

187
def what_provides(module, repoq, req_spec, conf_file,  qf=def_qf):
188

189
    if not repoq:
190

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
        pkgs = []
        try:
            my = yum_base(conf_file)
            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 ])
206

207
    else:
208

209 210 211 212 213 214 215 216 217 218 219
        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
            return set([ p for p in out.split('\n') if p.strip() ])
        else:
            module.fail_json(msg='Error from repoquery: %s' % err + err2)
            
    return []
220

Seth Vidal committed
221 222 223 224
def local_nvra(path):
    """return nvra of a local rpm passed in"""
    
    cmd = ['/bin/rpm', '-qp' ,'--qf', 
Seth Vidal committed
225
            '%{name}-%{version}-%{release}.%{arch}\n', path ]
Seth Vidal committed
226 227 228 229 230 231
    rc, out, err = run(cmd)
    if rc != 0:
        return None
    nvra = out.split('\n')[0]
    return nvra
    
232
def pkg_to_dict(pkgstr):
233

234 235 236 237
    if pkgstr.strip():
        n,e,v,r,a,repo = pkgstr.split('|')
    else:
        return {'error_parsing': pkgstr}
238

239
    d = {
240 241 242 243 244 245 246
        'name':n,
        'arch':a,
        'epoch':e,
        'release':r,
        'version':v,
        'repo':repo,
        'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a)
247
    }
248

249
    if repo == 'installed':
250
        d['yumstate'] = 'installed'
251
    else:
252
        d['yumstate'] = 'available'
253 254 255 256

    return d

def repolist(repoq, qf="%{repoid}"):
257

258
    cmd = repoq + ["--qf", qf, "-a"]
259 260 261 262 263
    rc,out,err = run(cmd)
    ret = []
    if rc == 0:
        ret = set([ p for p in out.split('\n') if p.strip() ])
    return ret
264

265
def list_stuff(module, conf_file, stuff):
266

267
    qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}"
268
    repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q']
269
    if conf_file and os.path.exists(conf_file):
270
        repoq += ['-c', conf_file]
271 272

    if stuff == 'installed':
273
        return [ pkg_to_dict(p) for p in is_installed(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
274
    elif stuff == 'updates':
275
        return [ pkg_to_dict(p) for p in is_update(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
276
    elif stuff == 'available':
277
        return [ pkg_to_dict(p) for p in is_available(module, repoq, '-a', conf_file, qf=qf) if p.strip() ]
278
    elif stuff == 'repos':
279
        return [ dict(repoid=name, state='enabled') for name in repolist(repoq) if name.strip() ]
280
    else:
281
        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() ]
282

283
def run(command):
284

285
    try:
286
        cmd = subprocess.Popen(command,
287 288 289 290 291 292 293 294 295 296 297
            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:
298
            out = ''
299
        if err is None:
300
            err = ''
301 302
    else:
        rc = cmd.returncode
303

304 305
    return rc, out, err

306

307
def install(module, items, repoq, yum_basecmd, conf_file):
308

Seth Vidal committed
309
    res = {}
310
    res['results'] = []
311 312 313 314 315 316 317 318 319 320
    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'):
321
            # get the pkg name-v-r.arch
Seth Vidal committed
322 323 324 325
            if not os.path.exists(spec):
                res['msg'] += "No Package file matching '%s' found on system" % spec
                module.fail_json(**res)

326
            nvra = local_nvra(spec)
327
            # look for them in the rpmdb
328
            if is_installed(module, repoq, nvra, conf_file):
329 330 331 332 333 334 335 336 337
                # 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 :(
338
        else:
339
            # look up what pkgs provide this
340
            pkglist = what_provides(module, repoq, spec, conf_file)
341 342
            if not pkglist:
                res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
343
                module.fail_json(**res)
344

345 346
            # if any of them are installed
            # then nothing to do
347

348 349
            found = False
            for this in pkglist:
350
                if is_installed(module, repoq, this, conf_file):
351
                    found = True
352
                    res['results'].append('%s providing %s is already installed' % (this, spec))
353

354 355 356
            if found:
                continue
            # if not - then pass in the spec as what to install
357 358
            # we could get here if nothing provides it but that's not
            # the error we're catching here
359
            pkg = spec
360

361
        cmd = yum_basecmd + ['install', pkg]
362
        rc, out, err = run(cmd)
Seth Vidal committed
363 364 365
        # 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
366
        if rc:
367 368
            res['changed'] = False
            res['rc'] = rc
369
            res['results'].append(out)
370
            res['msg'] += err
371
        else:
372 373
            res['changed'] = True
            res['rc'] = 0
374
            res['results'].append(out)
375
            res['msg'] += err
376

377
    module.exit_json(**res)
378

379

380
def remove(module, items, repoq, yum_basecmd, conf_file):
381

382
    res = {}
383
    res['results'] = []
384 385 386
    res['msg'] = ''
    res['changed'] = False
    res['rc'] = 0
387

388 389 390 391 392 393 394
    for spec in items:
        pkg = None

        # group remove - hope you like things dying!
        if spec.startswith('@'):
            pkg = spec
        # req or pkgname remove
395
        else:
396
            pkglist = is_installed(module, repoq, spec, conf_file)
397
            if not pkglist:
398
                res['msg'] += "No Package matching '%s' found installed" % spec
399
                module.exit_json(**res)
400

401 402
            found = False
            for this in pkglist:
403
                if is_installed(module, repoq, this, conf_file):
404
                    found = True
405

406
            if not found:
407
                res['results'].append('%s is not installed' % spec)
408 409
                continue
            pkg = spec
410

411
        cmd = yum_basecmd + ["remove", pkg]
412
        rc, out, err = run(cmd)
413

Seth Vidal committed
414
        # FIXME if we ran the remove - check to make sure it actually removed :(
415 416 417 418 419
        # 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
420
            res['results'].append(out)
421
            res['msg'] += err
422
        else:
423 424
            res['changed'] = True
            res['rc'] = 0
425
            res['results'].append(out)
426
            res['msg'] += err
427

428
    module.exit_json(**res)
Seth Vidal committed
429

430
def latest(module, items, repoq, yum_basecmd, conf_file):
431

432
    res = {}
433
    res['results'] = []
434 435 436
    res['msg'] = ''
    res['changed'] = False
    res['rc'] = 0
437

438
    for spec in items:
439

440
        pkg = None
Seth Vidal committed
441
        basecmd = 'update'
442 443 444 445
        # groups, again
        if spec.startswith('@'):
            pkg = spec
        # dep/pkgname  - find it
446
        else:
447 448 449 450 451 452
            if is_installed(module, repoq, spec, conf_file):
                basecmd = 'update'
            else:
                basecmd = 'install'

            pkglist = what_provides(module, repoq, spec, conf_file)
453 454 455 456
            if not pkglist:
                res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
                res['failed']=True
                module.exit_json(**res)
457 458
            
            nothing_to_do = True
459
            for this in pkglist:
460 461 462 463 464 465 466 467
                if basecmd == 'install' and is_available(module, repoq, this, conf_file):
                    nothing_to_do = False
                    break
                    
                if basecmd == 'update' and is_update(module, repoq, this, conf_file):
                    nothing_to_do = False
                    break
                    
468
            if nothing_to_do:
469
                res['results'].append("All packages providing %s are up to date" % spec)
470
                continue
471

472 473
            pkg = spec

474
        cmd = yum_basecmd + [basecmd, pkg]
475
        rc, out, err = run(cmd)
476

Seth Vidal committed
477 478 479 480 481 482 483 484 485
        # 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

486 487 488 489
        if rc:
            res['changed'] = False
            res['failed'] = True
            res['rc'] = rc
490
            res['results'].append(out)
491 492 493 494
            res['msg'] += err
        else:
            res['changed'] = True
            res['rc'] = 0
495
            res['results'].append(out)
496
            res['msg'] += err
497

498
    module.exit_json(**res)
Seth Vidal committed
499

500
def ensure(module, state, pkgspec, conf_file):
501

502
    # take multiple args comma separated
503
    items = pkgspec.split(',')
504

505
    yum_basecmd = [yumbin, '-d', '1', '-y']
506

507 508 509 510
    if not repoquery:
        repoq = None
    else:
        repoq = [repoquery, '--show-duplicates', '--plugins', '--quiet', '-q']
511

512
    if conf_file and os.path.exists(conf_file):
513
        yum_basecmd += ['-c', conf_file]
514 515 516 517 518 519 520 521 522
        if repoq:
            repoq += ['-c', conf_file]

    if state in ['installed', 'present']:
        install(module, items, repoq, yum_basecmd, conf_file)
    elif state in ['removed', 'absent']:
        remove(module, items, repoq, yum_basecmd, conf_file)
    elif state == 'latest':
        latest(module, items, repoq, yum_basecmd, conf_file)
523

524
    # should be caught by AnsibleModule argument_spec
525
    return dict(changed=False, failed=True, results='', errors='unexpected state')
526

527
def main():
528

529 530 531
    # state=installed name=pkgspec
    # state=removed name=pkgspec
    # state=latest name=pkgspec
532
    #
533 534 535 536 537 538
    # informational commands:
    #   list=installed
    #   list=updates
    #   list=available
    #   list=repos
    #   list=pkgspec
539

540
    module = AnsibleModule(
541
        argument_spec = dict(
542
            name=dict(aliases=['pkg']),
543 544
            # removed==absent, installed==present, these are accepted as aliases
            state=dict(default='installed', choices=['absent','present','installed','removed','latest']),
545
            list=dict(),
546
            conf_file=dict(default=None),
547
        ),
548 549
        required_one_of = [['name','list']],
        mutually_exclusive = [['name','list']]
550
    )
Seth Vidal committed
551

552
    params = module.params
553

554
    if params['list']:
555 556 557
        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']))
558
        module.exit_json(**results)
559

560
    else:
561
        pkg = params['name']
562 563 564
        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)
565

566 567 568 569
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()