3a8c9f04da
If one pins a package and does a 'apt-get dist-upgrade' then the output looks like: # apt-get dist-upgrade Reading package lists... Done Building dependency tree Reading state information... Done Calculating upgrade... Done The following packages have been kept back: cassandra 0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded. The check for any changes made should only be on the 'upgraded' and 'newly installed' values and not include the 'to remove' and 'not upgraded' values.
408 lines
15 KiB
Python
408 lines
15 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2012, Flowroute LLC
|
|
# Written by Matthew Williams <matthew@flowroute.com>
|
|
# Based on yum module written by Seth Vidal <skvidal at fedoraproject.org>
|
|
#
|
|
# This module 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.
|
|
#
|
|
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: apt
|
|
short_description: Manages apt-packages
|
|
description:
|
|
- Manages I(apt) packages (such as for Debian/Ubuntu).
|
|
version_added: "0.0.2"
|
|
options:
|
|
pkg:
|
|
description:
|
|
- A package name or package specifier with version, like C(foo) or C(foo=1.0). Shell like wildcards (fnmatch) like apt* are also supported.
|
|
required: false
|
|
default: null
|
|
state:
|
|
description:
|
|
- Indicates the desired package state
|
|
required: false
|
|
default: present
|
|
choices: [ "latest", "absent", "present" ]
|
|
update_cache:
|
|
description:
|
|
- Run the equivalent of C(apt-get update) before the operation. Can be run as part of the package installation or as a separate step
|
|
required: false
|
|
default: "no"
|
|
choices: [ "yes", "no" ]
|
|
cache_valid_time:
|
|
description:
|
|
- If C(update_cache) is specified and the last run is less or equal than I(cache_valid_time) seconds ago, the C(update_cache) gets skipped.
|
|
required: false
|
|
default: "no"
|
|
purge:
|
|
description:
|
|
- Will force purging of configuration files if the module state is set to I(absent).
|
|
required: false
|
|
default: "no"
|
|
choices: [ "yes", "no" ]
|
|
default_release:
|
|
description:
|
|
- Corresponds to the C(-t) option for I(apt) and sets pin priorities
|
|
required: false
|
|
default: null
|
|
install_recommends:
|
|
description:
|
|
- Corresponds to the C(--no-install-recommends) option for I(apt), default behavior works as apt's default behavior, C(no) does not install recommended packages. Suggested packages are never installed.
|
|
required: false
|
|
default: "yes"
|
|
choices: [ "yes", "no" ]
|
|
force:
|
|
description:
|
|
- If C(yes), force installs/removes.
|
|
required: false
|
|
default: "no"
|
|
choices: [ "yes", "no" ]
|
|
upgrade:
|
|
description:
|
|
- 'If yes or safe, performs an aptitude safe-upgrade.'
|
|
- 'If full, performs an aptitude full-upgrade.'
|
|
- 'If dist, performs an apt-get dist-upgrade.'
|
|
- 'Note: This does not upgrade a specific package, use state=latest for that.'
|
|
version_added: "1.1"
|
|
required: false
|
|
default: "yes"
|
|
choices: [ "yes", "safe", "full", "dist"]
|
|
requirements: [ python-apt, aptitude ]
|
|
author: Matthew Williams
|
|
notes:
|
|
- Three of the upgrade modes (C(full), C(safe) and its alias C(yes)) require C(aptitude), otherwise
|
|
C(apt-get) suffices.
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# Update repositories cache and install "foo" package
|
|
- apt: pkg=foo update_cache=yes
|
|
|
|
# Remove "foo" package
|
|
- apt: pkg=foo state=absent
|
|
|
|
# Install the package "foo"
|
|
- apt: pkg=foo state=present
|
|
|
|
# Install the version '1.00' of package "foo"
|
|
- apt: pkg=foo=1.00 state=present
|
|
|
|
# Update the repository cache and update package "nginx" to latest version using default release squeeze-backport
|
|
- apt: pkg=nginx state=latest default_release=squeeze-backports update_cache=yes
|
|
|
|
# Install latest version of "openjdk-6-jdk" ignoring "install-reccomends"
|
|
- apt: pkg=openjdk-6-jdk state=latest install_recommends=no
|
|
|
|
# Update all packages to the latest version
|
|
- apt: upgrade=dist
|
|
|
|
# Run the equivalent of "apt-get update" as a separate step
|
|
- apt: update_cache=yes
|
|
|
|
# Only run "update_cache=yes" if the last one is more than more than 3600 seconds ago
|
|
- apt: update_cache=yes cache_valid_time=3600
|
|
'''
|
|
|
|
|
|
import traceback
|
|
# added to stave off future warnings about apt api
|
|
import warnings
|
|
warnings.filterwarnings('ignore', "apt API not stable yet", FutureWarning)
|
|
|
|
import os
|
|
import datetime
|
|
import fnmatch
|
|
|
|
# APT related constants
|
|
APT_ENVVARS = "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical"
|
|
DPKG_OPTIONS = '-o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold"'
|
|
APT_GET_ZERO = "0 upgraded, 0 newly installed"
|
|
APTITUDE_ZERO = "0 packages upgraded, 0 newly installed"
|
|
APT_LISTS_PATH = "/var/lib/apt/lists"
|
|
APT_UPDATE_SUCCESS_STAMP_PATH = "/var/lib/apt/periodic/update-success-stamp"
|
|
|
|
HAS_PYTHON_APT = True
|
|
try:
|
|
import apt
|
|
import apt_pkg
|
|
except:
|
|
HAS_PYTHON_APT = False
|
|
|
|
def package_split(pkgspec):
|
|
parts = pkgspec.split('=')
|
|
if len(parts) > 1:
|
|
return parts[0], parts[1]
|
|
else:
|
|
return parts[0], None
|
|
|
|
def package_status(m, pkgname, version, cache, state):
|
|
try:
|
|
# get the package from the cache, as well as the
|
|
# the low-level apt_pkg.Package object which contains
|
|
# state fields not directly acccesible from the
|
|
# higher-level apt.package.Package object.
|
|
pkg = cache[pkgname]
|
|
ll_pkg = cache._cache[pkgname] # the low-level package object
|
|
except KeyError:
|
|
if state == 'install':
|
|
m.fail_json(msg="No package matching '%s' is available" % pkgname)
|
|
else:
|
|
return False, False, False
|
|
try:
|
|
has_files = len(pkg.installed_files) > 0
|
|
except AttributeError:
|
|
has_files = False # older python-apt cannot be used to determine non-purged
|
|
|
|
if version:
|
|
try :
|
|
return ll_pkg.current_state == apt_pkg.CURSTATE_INSTALLED and \
|
|
fnmatch.fnmatch(pkg.installed.version, version), False, has_files
|
|
except AttributeError:
|
|
#assume older version of python-apt is installed
|
|
return ll_pkg.current_state == apt_pkg.CURSTATE_INSTALLED and \
|
|
fnmatch.fnmatch(pkg.installedVersion, version), False, has_files
|
|
else:
|
|
try :
|
|
return ll_pkg.current_state == apt_pkg.CURSTATE_INSTALLED, pkg.is_upgradable, has_files
|
|
except AttributeError:
|
|
#assume older version of python-apt is installed
|
|
return ll_pkg.current_state == apt_pkg.CURSTATE_INSTALLED, pkg.isUpgradable, has_files
|
|
|
|
def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
|
|
new_pkgspec = []
|
|
for pkgname_or_fnmatch_pattern in pkgspec:
|
|
# note that any of these chars is not allowed in a (debian) pkgname
|
|
if [c for c in pkgname_or_fnmatch_pattern if c in "*?[]!"]:
|
|
if "=" in pkgname_or_fnmatch_pattern:
|
|
m.fail_json(msg="pkgname wildcard and version can not be mixed")
|
|
# handle multiarch pkgnames, the idea is that "apt*" should
|
|
# only select native packages. But "apt*:i386" should still work
|
|
if not ":" in pkgname_or_fnmatch_pattern:
|
|
matches = fnmatch.filter(
|
|
[pkg.name for pkg in cache
|
|
if not ":" in pkg.name], pkgname_or_fnmatch_pattern)
|
|
else:
|
|
matches = fnmatch.filter(
|
|
[pkg.name for pkg in cache], pkgname_or_fnmatch_pattern)
|
|
|
|
if len(matches) == 0:
|
|
m.fail_json(msg="No package(s) matching '%s' available" % str(pkgname_or_fnmatch_pattern))
|
|
else:
|
|
new_pkgspec.extend(matches)
|
|
else:
|
|
new_pkgspec.append(pkgname_or_fnmatch_pattern)
|
|
return new_pkgspec
|
|
|
|
def install(m, pkgspec, cache, upgrade=False, default_release=None, install_recommends=True, force=False):
|
|
packages = ""
|
|
pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache)
|
|
for package in pkgspec:
|
|
name, version = package_split(package)
|
|
installed, upgradable, has_files = package_status(m, name, version, cache, state='install')
|
|
if not installed or (upgrade and upgradable):
|
|
packages += "'%s' " % package
|
|
|
|
if len(packages) != 0:
|
|
if force:
|
|
force_yes = '--force-yes'
|
|
else:
|
|
force_yes = ''
|
|
|
|
if m.check_mode:
|
|
check_arg = '--simulate'
|
|
else:
|
|
check_arg = ''
|
|
|
|
cmd = "%s %s -y %s %s %s install %s" % (APT_ENVVARS, APT_GET_CMD, DPKG_OPTIONS, force_yes, check_arg, packages)
|
|
|
|
if default_release:
|
|
cmd += " -t '%s'" % (default_release,)
|
|
if not install_recommends:
|
|
cmd += " --no-install-recommends"
|
|
|
|
rc, out, err = m.run_command(cmd)
|
|
if rc:
|
|
m.fail_json(msg="'apt-get install %s' failed: %s" % (packages, err))
|
|
else:
|
|
m.exit_json(changed=True)
|
|
else:
|
|
m.exit_json(changed=False)
|
|
|
|
def remove(m, pkgspec, cache, purge=False):
|
|
packages = ""
|
|
for package in pkgspec:
|
|
name, version = package_split(package)
|
|
installed, upgradable, has_files = package_status(m, name, version, cache, state='remove')
|
|
if installed or (has_files and purge):
|
|
packages += "'%s' " % package
|
|
|
|
if len(packages) == 0:
|
|
m.exit_json(changed=False)
|
|
else:
|
|
if purge:
|
|
purge = '--purge'
|
|
else:
|
|
purge = ''
|
|
cmd = "%s %s -q -y %s %s remove %s" % (APT_ENVVARS, APT_GET_CMD, DPKG_OPTIONS, purge, packages)
|
|
|
|
if m.check_mode:
|
|
m.exit_json(changed=True)
|
|
|
|
rc, out, err = m.run_command(cmd)
|
|
if rc:
|
|
m.fail_json(msg="'apt-get remove %s' failed: %s" % (packages, err))
|
|
m.exit_json(changed=True)
|
|
|
|
def upgrade(m, mode="yes", force=False):
|
|
if m.check_mode:
|
|
check_arg = '--simulate'
|
|
else:
|
|
check_arg = ''
|
|
|
|
apt_cmd = None
|
|
if mode == "dist":
|
|
# apt-get dist-upgrade
|
|
apt_cmd = APT_GET_CMD
|
|
upgrade_command = "dist-upgrade"
|
|
elif mode == "full":
|
|
# aptitude full-upgrade
|
|
apt_cmd = APTITUDE_CMD
|
|
upgrade_command = "full-upgrade"
|
|
else:
|
|
# aptitude safe-upgrade # mode=yes # default
|
|
apt_cmd = APTITUDE_CMD
|
|
upgrade_command = "safe-upgrade"
|
|
|
|
if force:
|
|
force_yes = '--force-yes'
|
|
else:
|
|
force_yes = ''
|
|
|
|
apt_cmd_path = m.get_bin_path(apt_cmd, required=True)
|
|
cmd = '%s %s -y %s %s %s %s' % (APT_ENVVARS, apt_cmd_path, DPKG_OPTIONS,
|
|
force_yes, check_arg, upgrade_command)
|
|
rc, out, err = m.run_command(cmd)
|
|
if rc:
|
|
m.fail_json(msg="'%s %s' failed: %s" % (apt_cmd, upgrade_command, err))
|
|
if (apt_cmd == APT_GET_CMD and APT_GET_ZERO in out) or (apt_cmd == APTITUDE_CMD and APTITUDE_ZERO in out):
|
|
m.exit_json(changed=False, msg=out)
|
|
m.exit_json(changed=True, msg=out)
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec = dict(
|
|
state = dict(default='installed', choices=['installed', 'latest', 'removed', 'absent', 'present']),
|
|
update_cache = dict(aliases=['update-cache'], type='bool'),
|
|
cache_valid_time = dict(type='int'),
|
|
purge = dict(default='no', type='bool'),
|
|
package = dict(default=None, aliases=['pkg', 'name']),
|
|
default_release = dict(default=None, aliases=['default-release']),
|
|
install_recommends = dict(default='yes', aliases=['install-recommends'], type='bool'),
|
|
force = dict(default='no', type='bool'),
|
|
upgrade = dict(choices=['yes', 'safe', 'full', 'dist'])
|
|
),
|
|
mutually_exclusive = [['package', 'upgrade']],
|
|
required_one_of = [['package', 'upgrade', 'update_cache']],
|
|
supports_check_mode = True
|
|
)
|
|
|
|
if not HAS_PYTHON_APT:
|
|
module.fail_json(msg="Could not import python modules: apt, apt_pkg. Please install python-apt package.")
|
|
|
|
global APTITUDE_CMD
|
|
APTITUDE_CMD = module.get_bin_path("aptitude", False)
|
|
global APT_GET_CMD
|
|
APT_GET_CMD = module.get_bin_path("apt-get")
|
|
|
|
p = module.params
|
|
if not APTITUDE_CMD and p.get('upgrade', None) in [ 'full', 'safe', 'yes' ]:
|
|
module.fail_json(msg="Could not find aptitude. Please ensure it is installed.")
|
|
|
|
install_recommends = p['install_recommends']
|
|
|
|
try:
|
|
cache = apt.Cache()
|
|
if p['default_release']:
|
|
apt_pkg.config['APT::Default-Release'] = p['default_release']
|
|
# reopen cache w/ modified config
|
|
cache.open(progress=None)
|
|
|
|
if p['update_cache']:
|
|
# Default is: always update the cache
|
|
cache_valid = False
|
|
if p['cache_valid_time']:
|
|
tdelta = datetime.timedelta(seconds=p['cache_valid_time'])
|
|
try:
|
|
mtime = os.stat(APT_UPDATE_SUCCESS_STAMP_PATH).st_mtime
|
|
except:
|
|
mtime = False
|
|
if mtime is False:
|
|
# Looks like the update-success-stamp is not available
|
|
# Fallback: Checking the mtime of the lists
|
|
try:
|
|
mtime = os.stat(APT_LISTS_PATH).st_mtime
|
|
except:
|
|
mtime = False
|
|
if mtime is False:
|
|
# No mtime could be read - looks like lists are not there
|
|
# We update the cache to be safe
|
|
cache_valid = False
|
|
else:
|
|
mtimestamp = datetime.datetime.fromtimestamp(mtime)
|
|
if mtimestamp + tdelta >= datetime.datetime.now():
|
|
# dont update the cache
|
|
# the old cache is less than cache_valid_time seconds old - so still valid
|
|
cache_valid = True
|
|
|
|
if cache_valid is not True:
|
|
cache.update()
|
|
cache.open(progress=None)
|
|
if not p['package'] and not p['upgrade']:
|
|
module.exit_json(changed=False)
|
|
|
|
force_yes = p['force']
|
|
|
|
if p['upgrade']:
|
|
upgrade(module, p['upgrade'], force_yes)
|
|
|
|
packages = p['package'].split(',')
|
|
latest = p['state'] == 'latest'
|
|
for package in packages:
|
|
if package.count('=') > 1:
|
|
module.fail_json(msg="invalid package spec: %s" % package)
|
|
if latest and '=' in package:
|
|
module.fail_json(msg='version number inconsistent with state=latest: %s' % package)
|
|
|
|
if p['state'] == 'latest':
|
|
install(module, packages, cache, upgrade=True,
|
|
default_release=p['default_release'],
|
|
install_recommends=install_recommends,
|
|
force=force_yes)
|
|
elif p['state'] in [ 'installed', 'present' ]:
|
|
install(module, packages, cache, default_release=p['default_release'],
|
|
install_recommends=install_recommends,force=force_yes)
|
|
elif p['state'] in [ 'removed', 'absent' ]:
|
|
remove(module, packages, cache, p['purge'])
|
|
|
|
except apt.cache.LockFailedException:
|
|
module.fail_json(msg="Failed to lock apt for exclusive operation")
|
|
|
|
# this is magic, see lib/ansible/module_common.py
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
|
|
main()
|