ansible/packaging/os/dnf.py

350 lines
11 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2015 Cristian van Ee <cristian at cvee.org>
# Copyright 2015 Igor Gnatenko <i.gnatenko.brain@gmail.com>
#
# 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/>.
#
DOCUMENTATION = '''
---
module: dnf
version_added: 1.9
short_description: Manages packages with the I(dnf) package manager
description:
- Installs, upgrade, removes, and lists packages and groups with the I(dnf) package manager.
options:
name:
description:
- "Package name, or package specifier with version, like C(name-1.0). When using state=latest, this can be '*' which means run: dnf -y update. You can also pass a url or a local path to a rpm file."
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.
These repos will not persist beyond the transaction.
When specifying multiple repos, separate them with a ",".
required: false
default: null
aliases: []
disablerepo:
description:
- I(Repoid) of repositories to disable for the install/update operation.
These repos will not persist beyond the transaction.
When specifying multiple repos, separate them with a ",".
required: false
default: null
aliases: []
conf_file:
description:
- The remote dnf configuration file to use for the transaction.
required: false
default: null
aliases: []
disable_gpg_check:
description:
- Whether to disable the GPG checking of signatures of packages being
installed. Has an effect only if state is I(present) or I(latest).
required: false
default: "no"
choices: ["yes", "no"]
aliases: []
notes: []
# informational: requirements for nodes
requirements:
- "python >= 2.6"
- python-dnf
author:
- '"Igor Gnatenko (@ignatenkobrain)" <i.gnatenko.brain@gmail.com>'
- '"Cristian van Ee (@DJMuggs)" <cristian at cvee.org>'
'''
EXAMPLES = '''
- name: install the latest version of Apache
dnf: name=httpd state=latest
- name: remove the Apache package
dnf: name=httpd state=absent
- name: install the latest version of Apache from the testing repo
dnf: name=httpd enablerepo=testing state=present
- name: upgrade all packages
dnf: name=* state=latest
- name: install the nginx rpm from a remote repo
dnf: name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present
- name: install nginx rpm from a local file
dnf: name=/usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present
- name: install the 'Development tools' package group
dnf: name="@Development tools" state=present
'''
import os
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:
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(module, base, pkg_spec):
"""Mark the package for install."""
try:
base.install(pkg_spec)
except exceptions.MarkingError:
module.fail_json(msg="No package {} available.".format(pkg_spec))
def ensure(module, base, state, names):
if names == ['*'] and state == 'latest':
base.upgrade_all()
else:
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:
base.package_install(base.add_remote_rpm(filename))
# Install groups.
for group in groups:
base.group_install(group, const.GROUP_PACKAGE_TYPES)
# Install packages.
for pkg_spec in pkg_specs:
_mark_package_install(module, base, pkg_spec)
elif state == 'latest':
# "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:
base.upgrade(pkg_spec)
except dnf.exceptions.MarkingError:
# If not already installed, try to install.
_mark_package_install(module, base, pkg_spec)
else:
if filenames:
module.fail_json(
msg="Cannot remove paths -- please specify package name.")
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)
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))
module.exit_json(**response)
def main():
"""The main function."""
module = AnsibleModule(
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(default=False, type='bool'),
),
required_one_of=[['name', 'list']],
mutually_exclusive=[['name', 'list']],
supports_check_mode=True)
params = module.params
if params['list']:
base = _base(
module, params['conf_file'], params['disable_gpg_check'],
params['disablerepo'], params['enablerepo'])
list_items(module, base, params['list'])
else:
# Note: base takes a long time to run so we want to check for failure
# before running it.
if not util.am_i_root():
module.fail_json(msg="This command has to be run under the root user.")
base = _base(
module, params['conf_file'], params['disable_gpg_check'],
params['disablerepo'], params['enablerepo'])
ensure(module, base, params['state'], params['name'])
# import module snippets
from ansible.module_utils.basic import *
if __name__ == '__main__':
main()