From 25d61b7d01d5da493a4cc3cebaec8a4e5148d07e Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 22 May 2015 21:09:01 +0300 Subject: [PATCH 1/7] remove all and start from scratch Signed-off-by: Igor Gnatenko --- packaging/os/dnf.py | 652 ++------------------------------------------ 1 file changed, 25 insertions(+), 627 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 7afbee44c54..b3fa60bef42 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -1,7 +1,7 @@ #!/usr/bin/python -tt # -*- coding: utf-8 -*- -# Written by Cristian van Ee +# Written by Igor Gnatenko # # This file is part of Ansible # @@ -92,10 +92,8 @@ options: notes: [] # informational: requirements for nodes -requirements: - - dnf - - yum-utils (for repoquery) -author: "Cristian van Ee (@DJMuggs)" +requirements: [ dnf ] +author: '"Igor Gnatenko" ' ''' EXAMPLES = ''' @@ -122,24 +120,15 @@ EXAMPLES = ''' ''' -def_qf = "%{name}-%{version}-%{release}.%{arch}" - -repoquery='/usr/bin/repoquery' -if not os.path.exists(repoquery): - repoquery = None - -dnfbin='/usr/bin/dnf' - import syslog def log(msg): syslog.openlog('ansible-dnf', 0, syslog.LOG_USER) syslog.syslog(syslog.LOG_NOTICE, msg) -def dnf_base(conf_file=None, cachedir=False): - +def dnf_base(conf_file=None): my = dnf.Base() - my.conf.debuglevel=0 + my.conf.debuglevel = 0 if conf_file and os.path.exists(conf_file): my.conf.config_file_path = conf_file my.conf.read() @@ -148,629 +137,42 @@ def dnf_base(conf_file=None, cachedir=False): return my -def install_dnf_utils(module): - - if not module.check_mode: - dnf_path = module.get_bin_path('dnf') - if dnf_path: - rc, so, se = module.run_command('%s -y install yum-utils' % dnf_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 = dnf_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 dnf: %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 = dnf_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 dnf: %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 = dnf_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 dnf: %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 = dnf_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 dnf: %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 +def pkg_to_dict(pkg): + """ + Args: + pkg (hawkey.Package): The package """ - - 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) + 'name': pkg.name, + 'arch': pkg.arch, + 'epoch': str(pkg.epoch), + 'release': pkg.release, + 'version': pkg.version, + 'repo': pkg.repoid, + 'nevra': str(pkg) } - if repo == 'installed': + if pkg.installed: d['dnfstate'] = 'installed' else: d['dnfstate'] = '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] + my = dnf_base(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() ] + return [pkg_to_dict(p) for p in my.sack.query().installed()] elif stuff == 'updates': - return [ pkg_to_dict(p) for p in is_update(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] + return [pkg_to_dict(p) for p in my.sack.query().upgrades()] elif stuff == 'available': - return [ pkg_to_dict(p) for p in is_available(module, repoq, '-a', conf_file, qf=qf) if p.strip() ] + return [pkg_to_dict(p) for p in my.sack.query().available()] elif stuff == 'repos': - return [ dict(repoid=name, state='enabled') for name in repolist(module, repoq) if name.strip() ] + return [dict(repoid=repo.id, state='enabled') for repo in my.repos.iter_enabled()] 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, dnf_basecmd, conf_file, en_repos, dis_repos): - - 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') 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 dnf 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 - - # 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 = dnf_basecmd + ['install', 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, dnf_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 dnf transaction - cmd = dnf_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, dnf_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(dnf_basecmd + ['check-update']) - if rc == 100: - cmd = dnf_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 dnf 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 = dnf_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. - dnf_basecmd = [dnfbin, '-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): - dnf_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] - dnf_basecmd.extend(r_cmd) - - for repoid in en_repos: - r_cmd = ['--enablerepo=%s' % repoid] - dnf_basecmd.extend(r_cmd) - - if state in ['installed', 'present', 'latest']: - my = dnf_base(conf_file) - try: - for r in dis_repos: - my.repos.disableRepo(r) - - current_repos = dnf.yum.config.RepoConf() - 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 dnf.exceptions.Error, e: - module.fail_json(msg="Error setting/accessing repo %s: %s" % (r, e)) - except dnf.exceptions.Error, e: - module.fail_json(msg="Error accessing repos: %s" % e) - - if state in ['installed', 'present']: - if disable_gpg_check: - dnf_basecmd.append('--nogpgcheck') - install(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos) - elif state in ['removed', 'absent']: - remove(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos) - elif state == 'latest': - if disable_gpg_check: - dnf_basecmd.append('--nogpgcheck') - latest(module, items, repoq, dnf_basecmd, conf_file, en_repos, dis_repos) - - # should be caught by AnsibleModule argument_spec - return dict(changed=False, failed=True, results='', errors='unexpected state') + return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] def main(): @@ -789,24 +191,19 @@ def main(): 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']), + 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_dnf_utils(module) if not repoquery: module.fail_json(msg="repoquery is required to use this module at this time. Please install the yum-utils package.") @@ -815,6 +212,7 @@ def main(): module.exit_json(**results) else: + return pkg = params['name'] state = params['state'] enablerepo = params.get('enablerepo', '') From bbc8dae00694386240cd5ff95ac02373be9110c6 Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 22 May 2015 23:04:16 +0300 Subject: [PATCH 2/7] add ability to install packages Signed-off-by: Igor Gnatenko --- packaging/os/dnf.py | 60 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index b3fa60bef42..32ae442042c 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -22,14 +22,11 @@ import traceback import os +import operator +import functools import dnf - -try: - from dnf import find_unfinished_transactions, find_ts_remaining - from rpmUtils.miscutils import splitFilename - transaction_helpers = True -except: - transaction_helpers = False +import dnf.cli +import dnf.util DOCUMENTATION = ''' --- @@ -174,6 +171,54 @@ def list_stuff(module, conf_file, stuff): else: return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] +def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): + my = dnf_base(conf_file) + items = pkgspec.split(',') + if disablerepo: + for repo in disablerepo.split(','): + [r.disable() for r in b.repos.get_matching(repo)] + if enablerepo: + for repo in enablerepo.split(','): + [r.enable() for r in b.repos.get_matching(repo)] + my.conf.gpgcheck = disable_gpg_check + + res = {} + res['results'] = [] + res['msg'] = '' + res['rc'] = 0 + res['changed'] = False + + if not dnf.util.am_i_root(): + res['msg'] = 'This command has to be run under the root user.' + res['rc'] = 1 + + pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) + if state in ['installed', 'present']: + # Install files. + local_pkgs = map(my.add_remote_rpm, filenames) + map(my.package_install, local_pkgs) + # Install groups. + if grp_specs: + my.read_comps() + my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) + # Install packages. + for pkg_spec in pkg_specs: + try: + my.install(pkg_spec) + except dnf.exceptions.MarkingError: + res['results'].append('No package %s available.' % pkg_spec) + res['rc'] = 1 + if not my.resolve() and res['rc'] == 0: + res['msg'] += 'Nothing to do' + res['changed'] = False + else: + my.download_packages(my.transaction.install_set) + my.do_transaction() + [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set) + [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set) + + module.exit_json(**res) + def main(): # state=installed name=pkgspec @@ -212,7 +257,6 @@ def main(): module.exit_json(**results) else: - return pkg = params['name'] state = params['state'] enablerepo = params.get('enablerepo', '') From c798019c739279618ba661bb5552d434559fd123 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 22 May 2015 12:44:32 -0700 Subject: [PATCH 3/7] Fix nevra, fixes to docs and copyright info --- packaging/os/dnf.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 32ae442042c..8fea0ac89a8 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -1,7 +1,8 @@ #!/usr/bin/python -tt # -*- coding: utf-8 -*- -# Written by Igor Gnatenko +# Copyright 2015 Cristian van Ee +# Copyright 2015 Igor Gnatenko # # This file is part of Ansible # @@ -89,8 +90,12 @@ options: notes: [] # informational: requirements for nodes -requirements: [ dnf ] -author: '"Igor Gnatenko" ' +requirements: + - "python >= 2.6" + - dnf +author: + - '"Igor Gnatenko (@ignatenkobrain) " ' + - '"Cristian van Ee (@DJMuggs)" ' ''' EXAMPLES = ''' @@ -147,8 +152,8 @@ def pkg_to_dict(pkg): 'release': pkg.release, 'version': pkg.version, 'repo': pkg.repoid, - 'nevra': str(pkg) } + d['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format(**d) if pkg.installed: d['dnfstate'] = 'installed' @@ -268,5 +273,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 5d99dcfe4f1e77d59b7c4c7441bdde7240a5c665 Mon Sep 17 00:00:00 2001 From: Igor Gnatenko Date: Fri, 22 May 2015 23:11:13 +0300 Subject: [PATCH 4/7] typo fix in oneliners Signed-off-by: Igor Gnatenko --- packaging/os/dnf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 8fea0ac89a8..60b991f8cd7 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -219,8 +219,8 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g else: my.download_packages(my.transaction.install_set) my.do_transaction() - [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set) - [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set) + [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set] + [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set] module.exit_json(**res) From 050e619e7b40df3a5a63caa46e1ec4d4d272b2ff Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Tue, 7 Jul 2015 11:47:34 -0600 Subject: [PATCH 5/7] Add change reporting, enablerepo support, and gpgcheck. disable_gpg_check was configured backwards, so it was toggled. Typos in enablerepo/disablerepo are removed. fill_sack() calls are relocated to occur after repo decisions. The "changed" key is now set for new installations. --- packaging/os/dnf.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 60b991f8cd7..63e3e4a62d9 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -129,13 +129,13 @@ def log(msg): syslog.syslog(syslog.LOG_NOTICE, msg) def dnf_base(conf_file=None): + """Return a dnf Base object. You must call fill_sack.""" my = dnf.Base() my.conf.debuglevel = 0 if conf_file and os.path.exists(conf_file): my.conf.config_file_path = conf_file my.conf.read() my.read_all_repos() - my.fill_sack() return my @@ -164,6 +164,7 @@ def pkg_to_dict(pkg): def list_stuff(module, conf_file, stuff): my = dnf_base(conf_file) + my.fill_sack() if stuff == 'installed': return [pkg_to_dict(p) for p in my.sack.query().installed()] @@ -181,11 +182,12 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g items = pkgspec.split(',') if disablerepo: for repo in disablerepo.split(','): - [r.disable() for r in b.repos.get_matching(repo)] + [r.disable() for r in my.repos.get_matching(repo)] if enablerepo: for repo in enablerepo.split(','): - [r.enable() for r in b.repos.get_matching(repo)] - my.conf.gpgcheck = disable_gpg_check + [r.enable() for r in my.repos.get_matching(repo)] + my.fill_sack() + my.conf.gpgcheck = not disable_gpg_check res = {} res['results'] = [] @@ -219,6 +221,7 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g else: my.download_packages(my.transaction.install_set) my.do_transaction() + res['changed'] = True [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set] [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set] @@ -275,4 +278,3 @@ def main(): from ansible.module_utils.basic import * if __name__ == '__main__': main() - From 495af842fcee94b4e07d8af77cfd8753b3799bd2 Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Thu, 9 Jul 2015 15:00:22 -0600 Subject: [PATCH 6/7] Add support for state=latest and * --- packaging/os/dnf.py | 54 +++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 63e3e4a62d9..107895bddb6 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -177,9 +177,18 @@ def list_stuff(module, conf_file, stuff): else: return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] + +def _mark_package_install(my, res, pkg_spec): + """Mark the package for install.""" + try: + my.install(pkg_spec) + except dnf.exceptions.MarkingError: + res['results'].append('No package %s available.' % pkg_spec) + res['rc'] = 1 + + def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): my = dnf_base(conf_file) - items = pkgspec.split(',') if disablerepo: for repo in disablerepo.split(','): [r.disable() for r in my.repos.get_matching(repo)] @@ -199,22 +208,33 @@ def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_g res['msg'] = 'This command has to be run under the root user.' res['rc'] = 1 - pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) - if state in ['installed', 'present']: - # Install files. - local_pkgs = map(my.add_remote_rpm, filenames) - map(my.package_install, local_pkgs) - # Install groups. - if grp_specs: - my.read_comps() - my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) - # Install packages. - for pkg_spec in pkg_specs: - try: - my.install(pkg_spec) - except dnf.exceptions.MarkingError: - res['results'].append('No package %s available.' % pkg_spec) - res['rc'] = 1 + if pkgspec == '*' and state == 'latest': + my.upgrade_all() + else: + items = pkgspec.split(',') + pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) + if state in ['installed', 'present']: + # Install files. + for filename in filenames: + my.package_install(my.add_remote_rpm(filename)) + # Install groups. + if grp_specs: + my.read_comps() + my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) + # Install packages. + for pkg_spec in pkg_specs: + _mark_package_install(my, res, pkg_spec) + elif state == 'latest': + # These aren't implemented yet, so assert them out. + assert not filenames + assert not grp_specs + for pkg_spec in pkg_specs: + try: + my.upgrade(pkg_spec) + except dnf.exceptions.MarkingError: + # If not already installed, try to install. + _mark_package_install(my, res, pkg_spec) + if not my.resolve() and res['rc'] == 0: res['msg'] += 'Nothing to do' res['changed'] = False From e82f6e9463d512cfe698155bfed683f89ba45a4d Mon Sep 17 00:00:00 2001 From: ToBeReplaced Date: Sat, 11 Jul 2015 01:29:18 -0600 Subject: [PATCH 7/7] Rewrite dnf module. This fully implements all expected functionality of the dnf module. Group removal may behave oddly due to hiccups in tagging groups as being installed. A pkg_types option could be added to specify the group package types. --- packaging/os/dnf.py | 351 +++++++++++++++++++++++++------------------- 1 file changed, 198 insertions(+), 153 deletions(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 107895bddb6..bb6a2c9d495 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -1,4 +1,4 @@ -#!/usr/bin/python -tt +#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2015 Cristian van Ee @@ -20,15 +20,6 @@ # along with Ansible. If not, see . # - -import traceback -import os -import operator -import functools -import dnf -import dnf.cli -import dnf.util - DOCUMENTATION = ''' --- module: dnf @@ -43,17 +34,20 @@ options: 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: - I(Repoid) of repositories to enable for the install/update operation. @@ -94,7 +88,7 @@ requirements: - "python >= 2.6" - dnf author: - - '"Igor Gnatenko (@ignatenkobrain) " ' + - '"Igor Gnatenko (@ignatenkobrain)" ' - '"Cristian van Ee (@DJMuggs)" ' ''' @@ -121,178 +115,229 @@ EXAMPLES = ''' dnf: name="@Development tools" state=present ''' +import os -import syslog - -def log(msg): - syslog.openlog('ansible-dnf', 0, syslog.LOG_USER) - syslog.syslog(syslog.LOG_NOTICE, msg) - -def dnf_base(conf_file=None): - """Return a dnf Base object. You must call fill_sack.""" - my = dnf.Base() - my.conf.debuglevel = 0 - if conf_file and os.path.exists(conf_file): - my.conf.config_file_path = conf_file - my.conf.read() - my.read_all_repos() - - return my - -def pkg_to_dict(pkg): - """ - Args: - pkg (hawkey.Package): The package - """ - - d = { - 'name': pkg.name, - 'arch': pkg.arch, - 'epoch': str(pkg.epoch), - 'release': pkg.release, - 'version': pkg.version, - 'repo': pkg.repoid, - } - d['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format(**d) - - if pkg.installed: - d['dnfstate'] = 'installed' +try: + import dnf + from dnf import cli, const, exceptions, subject, util + HAS_DNF = True +except ImportError: + HAS_DNF = False + + +def _fail_if_no_dnf(module): + """Fail if unable to import dnf.""" + if not HAS_DNF: + module.fail_json( + msg="`python-dnf` is not installed, but it is required for the Ansible dnf module.") + + +def _configure_base(module, base, conf_file, disable_gpg_check): + """Configure the dnf Base object.""" + conf = base.conf + + # Turn off debug messages in the output + conf.debuglevel = 0 + + # Set whether to check gpg signatures + conf.gpgcheck = not disable_gpg_check + + # Don't prompt for user confirmations + conf.assumeyes = True + + # Change the configuration file path if provided + if conf_file: + # Fail if we can't read the configuration file. + if not os.access(conf_file, os.R_OK): + module.fail_json( + msg="cannot read configuration file", conf_file=conf_file) + else: + conf.config_file_path = conf_file + + # Read the configuration file + conf.read() + + +def _specify_repositories(base, disablerepo, enablerepo): + """Enable and disable repositories matching the provided patterns.""" + base.read_all_repos() + repos = base.repos + + # Disable repositories + for repo_pattern in disablerepo: + for repo in repos.get_matching(repo_pattern): + repo.disable() + + # Enable repositories + for repo_pattern in enablerepo: + for repo in repos.get_matching(repo_pattern): + repo.enable() + + +def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo): + """Return a fully configured dnf Base object.""" + _fail_if_no_dnf(module) + base = dnf.Base() + _configure_base(module, base, conf_file, disable_gpg_check) + _specify_repositories(base, disablerepo, enablerepo) + base.fill_sack() + return base + + +def _package_dict(package): + """Return a dictionary of information for the package.""" + # NOTE: This no longer contains the 'dnfstate' field because it is + # already known based on the query type. + result = { + 'name': package.name, + 'arch': package.arch, + 'epoch': str(package.epoch), + 'release': package.release, + 'version': package.version, + 'repo': package.repoid} + result['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format( + **result) + + return result + + +def list_items(module, base, command): + """List package info based on the command.""" + # Rename updates to upgrades + if command == 'updates': + command = 'upgrades' + + # Return the corresponding packages + if command in ['installed', 'upgrades', 'available']: + results = [ + _package_dict(package) + for package in getattr(base.sack.query(), command)()] + # Return the enabled repository ids + elif command in ['repos', 'repositories']: + results = [ + {'repoid': repo.id, 'state': 'enabled'} + for repo in base.repos.iter_enabled()] + # Return any matching packages else: - d['dnfstate'] = 'available' - - return d - -def list_stuff(module, conf_file, stuff): - my = dnf_base(conf_file) - my.fill_sack() - - if stuff == 'installed': - return [pkg_to_dict(p) for p in my.sack.query().installed()] - elif stuff == 'updates': - return [pkg_to_dict(p) for p in my.sack.query().upgrades()] - elif stuff == 'available': - return [pkg_to_dict(p) for p in my.sack.query().available()] - elif stuff == 'repos': - return [dict(repoid=repo.id, state='enabled') for repo in my.repos.iter_enabled()] - else: - return [pkg_to_dict(p) for p in dnf.subject.Subject(stuff).get_best_query(my.sack)] + packages = subject.Subject(command).get_best_query(base.sack) + results = [_package_dict(package) for package in packages] + + module.exit_json(results=results) -def _mark_package_install(my, res, pkg_spec): +def _mark_package_install(module, base, pkg_spec): """Mark the package for install.""" try: - my.install(pkg_spec) - except dnf.exceptions.MarkingError: - res['results'].append('No package %s available.' % pkg_spec) - res['rc'] = 1 - - -def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): - my = dnf_base(conf_file) - if disablerepo: - for repo in disablerepo.split(','): - [r.disable() for r in my.repos.get_matching(repo)] - if enablerepo: - for repo in enablerepo.split(','): - [r.enable() for r in my.repos.get_matching(repo)] - my.fill_sack() - my.conf.gpgcheck = not disable_gpg_check - - res = {} - res['results'] = [] - res['msg'] = '' - res['rc'] = 0 - res['changed'] = False - - if not dnf.util.am_i_root(): - res['msg'] = 'This command has to be run under the root user.' - res['rc'] = 1 - - if pkgspec == '*' and state == 'latest': - my.upgrade_all() + base.install(pkg_spec) + except exceptions.MarkingError: + module.fail(msg="No package {} available.".format(pkg_spec)) + + +def ensure(module, base, state, names): + if not util.am_i_root(): + module.fail_json(msg="This command has to be run under the root user.") + + if names == ['*'] and state == 'latest': + base.upgrade_all() else: - items = pkgspec.split(',') - pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) + pkg_specs, group_specs, filenames = cli.commands.parse_spec_group_file( + names) + if group_specs: + base.read_comps() + + groups = [] + for group_spec in group_specs: + group = base.comps.group_by_pattern(group_spec) + if group: + groups.append(group) + else: + module.fail_json( + msg="No group {} available.".format(group_spec)) + if state in ['installed', 'present']: # Install files. for filename in filenames: - my.package_install(my.add_remote_rpm(filename)) + base.package_install(base.add_remote_rpm(filename)) # Install groups. - if grp_specs: - my.read_comps() - my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES) + for group in groups: + base.group_install(group, const.GROUP_PACKAGE_TYPES) # Install packages. for pkg_spec in pkg_specs: - _mark_package_install(my, res, pkg_spec) + _mark_package_install(module, base, pkg_spec) + elif state == 'latest': - # These aren't implemented yet, so assert them out. - assert not filenames - assert not grp_specs + # "latest" is same as "installed" for filenames. + for filename in filenames: + base.package_install(base.add_remote_rpm(filename)) + for group in groups: + try: + base.group_upgrade(group) + except exceptions.CompsError: + # If not already installed, try to install. + base.group_install(group, const.GROUP_PACKAGE_TYPES) for pkg_spec in pkg_specs: try: - my.upgrade(pkg_spec) + base.upgrade(pkg_spec) except dnf.exceptions.MarkingError: # If not already installed, try to install. - _mark_package_install(my, res, pkg_spec) + _mark_package_install(module, base, pkg_spec) - if not my.resolve() and res['rc'] == 0: - res['msg'] += 'Nothing to do' - res['changed'] = False - else: - my.download_packages(my.transaction.install_set) - my.do_transaction() - res['changed'] = True - [res['results'].append('Installed: %s' % pkg) for pkg in my.transaction.install_set] - [res['results'].append('Removed: %s' % pkg) for pkg in my.transaction.remove_set] + else: + if filenames: + module.fail_json( + msg="Cannot remove paths -- please specify package name.") - module.exit_json(**res) + installed = base.sack.query().installed() + for group in groups: + if installed.filter(name=group.name): + base.group_remove(group) + for pkg_spec in pkg_specs: + if installed.filter(name=pkg_spec): + base.remove(pkg_spec) -def main(): + if not base.resolve(): + module.exit_json(msg="Nothing to do") + else: + if module.check_mode: + module.exit_json(changed=True) + base.download_packages(base.transaction.install_set) + base.do_transaction() + response = {'changed': True, 'results': []} + for package in base.transaction.install_set: + response['results'].append("Installed: {}".format(package)) + for package in base.transaction.remove_set: + response['results'].append("Removed: {}".format(package)) - # 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.exit_json(**response) + +def main(): + """The main function.""" 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(), + argument_spec=dict( + name=dict(aliases=['pkg'], type='list'), + state=dict( + default='installed', + choices=[ + 'absent', 'present', 'installed', 'removed', 'latest']), + enablerepo=dict(type='list', default=[]), + disablerepo=dict(type='list', default=[]), list=dict(), conf_file=dict(default=None), - disable_gpg_check=dict(required=False, default="no", type='bool'), + disable_gpg_check=dict(default=False, type='bool'), ), - required_one_of = [['name','list']], - mutually_exclusive = [['name','list']], - supports_check_mode = True - ) - + required_one_of=[['name', 'list']], + mutually_exclusive=[['name', 'list']], + supports_check_mode=True) params = module.params - - if not repoquery: - module.fail_json(msg="repoquery is required to use this module at this time. Please install the yum-utils package.") + base = _base( + module, params['conf_file'], params['disable_gpg_check'], + params['disablerepo'], params['enablerepo']) if params['list']: - results = dict(results=list_stuff(module, params['conf_file'], params['list'])) - module.exit_json(**results) - + list_items(module, base, params['list']) 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) + ensure(module, base, params['state'], params['name']) + # import module snippets from ansible.module_utils.basic import *