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.
pull/18777/head
ToBeReplaced 9 years ago committed by Matt Clay
parent cdf603448a
commit ad54ce6125

@ -1,4 +1,4 @@
#!/usr/bin/python -tt #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2015 Cristian van Ee <cristian at cvee.org> # Copyright 2015 Cristian van Ee <cristian at cvee.org>
@ -20,15 +20,6 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# #
import traceback
import os
import operator
import functools
import dnf
import dnf.cli
import dnf.util
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: dnf module: dnf
@ -44,12 +35,14 @@ options:
version_added: "1.8" version_added: "1.8"
default: null default: null
aliases: [] aliases: []
list: list:
description: description:
- Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks. See examples. - Various (non-idempotent) commands for usage with C(/usr/bin/ansible) and I(not) playbooks. See examples.
required: false required: false
version_added: "1.8" version_added: "1.8"
default: null default: null
state: state:
description: description:
- Whether to install (C(present), C(latest)), or remove (C(absent)) a package. - Whether to install (C(present), C(latest)), or remove (C(absent)) a package.
@ -57,6 +50,7 @@ options:
choices: [ "present", "latest", "absent" ] choices: [ "present", "latest", "absent" ]
version_added: "1.8" version_added: "1.8"
default: "present" default: "present"
enablerepo: enablerepo:
description: description:
- I(Repoid) of repositories to enable for the install/update operation. - I(Repoid) of repositories to enable for the install/update operation.
@ -101,7 +95,7 @@ requirements:
- "python >= 2.6" - "python >= 2.6"
- dnf - dnf
author: author:
- '"Igor Gnatenko (@ignatenkobrain) " <i.gnatenko.brain@gmail.com>' - '"Igor Gnatenko (@ignatenkobrain)" <i.gnatenko.brain@gmail.com>'
- '"Cristian van Ee (@DJMuggs)" <cristian at cvee.org>' - '"Cristian van Ee (@DJMuggs)" <cristian at cvee.org>'
''' '''
@ -128,178 +122,229 @@ EXAMPLES = '''
dnf: name="@Development tools" state=present dnf: name="@Development tools" state=present
''' '''
import os
import syslog try:
import dnf
def log(msg): from dnf import cli, const, exceptions, subject, util
syslog.openlog('ansible-dnf', 0, syslog.LOG_USER) HAS_DNF = True
syslog.syslog(syslog.LOG_NOTICE, msg) except ImportError:
HAS_DNF = False
def dnf_base(conf_file=None):
"""Return a dnf Base object. You must call fill_sack."""
my = dnf.Base() def _fail_if_no_dnf(module):
my.conf.debuglevel = 0 """Fail if unable to import dnf."""
if conf_file and os.path.exists(conf_file): if not HAS_DNF:
my.conf.config_file_path = conf_file module.fail_json(
my.conf.read() msg="`python-dnf` is not installed, but it is required for the Ansible dnf module.")
my.read_all_repos()
return my def _configure_base(module, base, conf_file, disable_gpg_check):
"""Configure the dnf Base object."""
def pkg_to_dict(pkg): conf = base.conf
"""
Args: # Turn off debug messages in the output
pkg (hawkey.Package): The package conf.debuglevel = 0
"""
# Set whether to check gpg signatures
d = { conf.gpgcheck = not disable_gpg_check
'name': pkg.name,
'arch': pkg.arch, # Don't prompt for user confirmations
'epoch': str(pkg.epoch), conf.assumeyes = True
'release': pkg.release,
'version': pkg.version, # Change the configuration file path if provided
'repo': pkg.repoid, if conf_file:
} # Fail if we can't read the configuration file.
d['nevra'] = '{epoch}:{name}-{version}-{release}.{arch}'.format(**d) if not os.access(conf_file, os.R_OK):
module.fail_json(
if pkg.installed: msg="cannot read configuration file", conf_file=conf_file)
d['dnfstate'] = 'installed' 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: else:
d['dnfstate'] = 'available' packages = subject.Subject(command).get_best_query(base.sack)
results = [_package_dict(package) for package in packages]
return d
module.exit_json(results=results)
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)]
def _mark_package_install(my, res, pkg_spec): def _mark_package_install(module, base, pkg_spec):
"""Mark the package for install.""" """Mark the package for install."""
try: try:
my.install(pkg_spec) base.install(pkg_spec)
except dnf.exceptions.MarkingError: except exceptions.MarkingError:
res['results'].append('No package %s available.' % pkg_spec) module.fail(msg="No package {} available.".format(pkg_spec))
res['rc'] = 1
def ensure(module, base, state, names):
def ensure(module, state, pkgspec, conf_file, enablerepo, disablerepo, disable_gpg_check): if not util.am_i_root():
my = dnf_base(conf_file) module.fail_json(msg="This command has to be run under the root user.")
if disablerepo:
for repo in disablerepo.split(','): if names == ['*'] and state == 'latest':
[r.disable() for r in my.repos.get_matching(repo)] base.upgrade_all()
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()
else: else:
items = pkgspec.split(',') pkg_specs, group_specs, filenames = cli.commands.parse_spec_group_file(
pkg_specs, grp_specs, filenames = dnf.cli.commands.parse_spec_group_file(items) 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']: if state in ['installed', 'present']:
# Install files. # Install files.
for filename in filenames: for filename in filenames:
my.package_install(my.add_remote_rpm(filename)) base.package_install(base.add_remote_rpm(filename))
# Install groups. # Install groups.
if grp_specs: for group in groups:
my.read_comps() base.group_install(group, const.GROUP_PACKAGE_TYPES)
my.env_group_install(grp_specs, dnf.const.GROUP_PACKAGE_TYPES)
# Install packages. # Install packages.
for pkg_spec in pkg_specs: for pkg_spec in pkg_specs:
_mark_package_install(my, res, pkg_spec) _mark_package_install(module, base, pkg_spec)
elif state == 'latest': elif state == 'latest':
# These aren't implemented yet, so assert them out. # "latest" is same as "installed" for filenames.
assert not filenames for filename in filenames:
assert not grp_specs 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: for pkg_spec in pkg_specs:
try: try:
my.upgrade(pkg_spec) base.upgrade(pkg_spec)
except dnf.exceptions.MarkingError: except dnf.exceptions.MarkingError:
# If not already installed, try to install. # 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: else:
res['msg'] += 'Nothing to do' if filenames:
res['changed'] = False module.fail_json(
else: msg="Cannot remove paths -- please specify package name.")
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]
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 module.exit_json(**response)
# state=removed name=pkgspec
# state=latest name=pkgspec
#
# informational commands:
# list=installed
# list=updates
# list=available
# list=repos
# list=pkgspec
def main():
"""The main function."""
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( argument_spec=dict(
name=dict(aliases=['pkg']), name=dict(aliases=['pkg'], type='list'),
# removed==absent, installed==present, these are accepted as aliases state=dict(
state=dict(default='installed', choices=['absent', 'present', 'installed', 'removed', 'latest']), default='installed',
enablerepo=dict(), choices=[
disablerepo=dict(), 'absent', 'present', 'installed', 'removed', 'latest']),
enablerepo=dict(type='list', default=[]),
disablerepo=dict(type='list', default=[]),
list=dict(), list=dict(),
conf_file=dict(default=None), 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']], required_one_of=[['name', 'list']],
mutually_exclusive = [['name','list']], mutually_exclusive=[['name', 'list']],
supports_check_mode = True supports_check_mode=True)
)
params = module.params params = module.params
base = _base(
if not repoquery: module, params['conf_file'], params['disable_gpg_check'],
module.fail_json(msg="repoquery is required to use this module at this time. Please install the yum-utils package.") params['disablerepo'], params['enablerepo'])
if params['list']: if params['list']:
results = dict(results=list_stuff(module, params['conf_file'], params['list'])) list_items(module, base, params['list'])
module.exit_json(**results)
else: else:
pkg = params['name'] ensure(module, base, params['state'], params['name'])
state = params['state']
enablerepo = params.get('enablerepo', '')
disablerepo = params.get('disablerepo', '')
disable_gpg_check = params['disable_gpg_check']
res = ensure(module, state, pkg, params['conf_file'], enablerepo,
disablerepo, disable_gpg_check)
module.fail_json(msg="we should never get here unless this all failed", **res)
# import module snippets # import module snippets
from ansible.module_utils.basic import * from ansible.module_utils.basic import *

Loading…
Cancel
Save