From 0b51476c0ac82983e7ba48dfb675ddc706d68246 Mon Sep 17 00:00:00 2001
From: Yegor Minin <yegor@limbo.im>
Date: Thu, 3 Oct 2013 20:14:52 +0300
Subject: [PATCH] apt: allow specifying dpkg options

This will allow specifying dpkg options as a string passed over to apt
command. dpkg_options expects a comma-separated string of options to be
passed as dpkg options which will be further expanded. For example
dpkg_options='force-confdef,force-confold' will end up as
-o \"Dpkg::Options::=--force-confold\" when passed to apt
Example usage would be:
-m apt -u ubuntu -s \
 -a "upgrade=dist update_cache=yes dpkg_options='force-confold'"
or
apt: upgrade=dist update_cache=yes dpkg_options='force-confold'
---
 packaging/apt | 58 ++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 41 insertions(+), 17 deletions(-)

diff --git a/packaging/apt b/packaging/apt
index 6ab20171866..0ef16c81cdb 100644
--- a/packaging/apt
+++ b/packaging/apt
@@ -82,6 +82,12 @@ options:
     required: false
     default: "yes"
     choices: [ "yes", "safe", "full", "dist"]
+  dpkg_options:
+    description:
+      - Add dpkg options to apt command. Defaults to '-o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold"'
+      - Options should be supplied as comma separated list
+    required: false
+    default: 'force-confdef,force-confold'
 requirements: [ python-apt, aptitude ]
 author: Matthew Williams
 notes:
@@ -105,7 +111,7 @@ EXAMPLES = '''
 # 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"
+# Install latest version of "openjdk-6-jdk" ignoring "install-recommends"
 - apt: pkg=openjdk-6-jdk state=latest install_recommends=no
 
 # Update all packages to the latest version
@@ -116,6 +122,9 @@ EXAMPLES = '''
 
 # 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
+
+# Pass options to dpkg on run
+- apt: upgrade=dist update_cache=yes dpkg_options='force-confold,force-confdef'
 '''
 
 
@@ -130,7 +139,7 @@ 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"'
+DPKG_OPTIONS = 'force-confdef,force-confold'
 APT_GET_ZERO = "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
 APTITUDE_ZERO = "0 packages upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
 APT_LISTS_PATH = "/var/lib/apt/lists"
@@ -183,6 +192,14 @@ def package_status(m, pkgname, version, cache, state):
             #assume older version of python-apt is installed
             return ll_pkg.current_state == apt_pkg.CURSTATE_INSTALLED, pkg.isUpgradable, has_files
 
+def expand_dpkg_options(dpkg_options_compressed):
+    options_list = dpkg_options_compressed.split(',')
+    dpkg_options = ""
+    for dpkg_option in options_list:
+        dpkg_options = '%s -o "Dpkg::Options::=--%s"' \
+                       % (dpkg_options, dpkg_option)
+    return dpkg_options.strip()
+
 def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
     new_pkgspec = []
     for pkgname_or_fnmatch_pattern in pkgspec:
@@ -190,7 +207,7 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
         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 
+            # 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(
@@ -208,7 +225,9 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
             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):
+def install(m, pkgspec, cache, upgrade=False, default_release=None,
+            install_recommends=True, force=False,
+            dpkg_options=expand_dpkg_options(DPKG_OPTIONS)):
     packages = ""
     pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache)
     for package in pkgspec:
@@ -228,7 +247,7 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None, install_reco
         else:
             check_arg = ''
 
-        cmd = "%s %s -y %s %s %s install %s" % (APT_ENVVARS, APT_GET_CMD, DPKG_OPTIONS, force_yes, check_arg, packages)
+        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,)
@@ -243,7 +262,8 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None, install_reco
     else:
         m.exit_json(changed=False)
 
-def remove(m, pkgspec, cache, purge=False):
+def remove(m, pkgspec, cache, purge=False,
+           dpkg_options=expand_dpkg_options(DPKG_OPTIONS)):
     packages = ""
     for package in pkgspec:
         name, version = package_split(package)
@@ -258,7 +278,7 @@ def remove(m, pkgspec, cache, purge=False):
             purge = '--purge'
         else:
             purge = ''
-        cmd = "%s %s -q -y %s %s remove %s" % (APT_ENVVARS, APT_GET_CMD, DPKG_OPTIONS, purge, packages)
+        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)
@@ -268,7 +288,8 @@ def remove(m, pkgspec, cache, purge=False):
             m.fail_json(msg="'apt-get remove %s' failed: %s" % (packages, err))
         m.exit_json(changed=True)
 
-def upgrade(m, mode="yes", force=False):
+def upgrade(m, mode="yes", force=False,
+            dpkg_options=expand_dpkg_options(DPKG_OPTIONS)):
     if m.check_mode:
         check_arg = '--simulate'
     else:
@@ -279,7 +300,7 @@ def upgrade(m, mode="yes", force=False):
         # apt-get dist-upgrade
         apt_cmd = APT_GET_CMD
         upgrade_command = "dist-upgrade"
-    elif mode == "full": 
+    elif mode == "full":
         # aptitude full-upgrade
         apt_cmd = APTITUDE_CMD
         upgrade_command = "full-upgrade"
@@ -294,7 +315,7 @@ def upgrade(m, mode="yes", force=False):
         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,
+    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:
@@ -312,9 +333,10 @@ def main():
             purge = dict(default=False, type='bool'),
             package = dict(default=None, aliases=['pkg', 'name']),
             default_release = dict(default=None, aliases=['default-release']),
-            install_recommends = dict(default=True, aliases=['install-recommends'], type='bool'),
-            force = dict(default=False, type='bool'),
-            upgrade = dict(choices=['yes', 'safe', 'full', 'dist'])
+            install_recommends = dict(default='yes', aliases=['install-recommends'], type='bool'),
+            force = dict(default='no', type='bool'),
+            upgrade = dict(choices=['yes', 'safe', 'full', 'dist']),
+            dpkg_options = dict(default=DPKG_OPTIONS)
         ),
         mutually_exclusive = [['package', 'upgrade']],
         required_one_of = [['package', 'upgrade', 'update_cache']],
@@ -334,6 +356,7 @@ def main():
         module.fail_json(msg="Could not find aptitude. Please ensure it is installed.")
 
     install_recommends = p['install_recommends']
+    dpkg_options = expand_dpkg_options(p['dpkg_options'])
 
     try:
         cache = apt.Cache()
@@ -378,7 +401,7 @@ def main():
         force_yes = p['force']
 
         if p['upgrade']:
-            upgrade(module, p['upgrade'], force_yes)
+            upgrade(module, p['upgrade'], force_yes, dpkg_options)
 
         packages = p['package'].split(',')
         latest = p['state'] == 'latest'
@@ -392,12 +415,13 @@ def main():
             install(module, packages, cache, upgrade=True,
                     default_release=p['default_release'],
                     install_recommends=install_recommends,
-                    force=force_yes)
+                    force=force_yes, dpkg_options=dpkg_options)
         elif p['state'] in [ 'installed', 'present' ]:
             install(module, packages, cache, default_release=p['default_release'],
-                      install_recommends=install_recommends,force=force_yes)
+                      install_recommends=install_recommends,force=force_yes,
+                      dpkg_options=dpkg_options)
         elif p['state'] in [ 'removed', 'absent' ]:
-            remove(module, packages, cache, p['purge'])
+            remove(module, packages, cache, p['purge'], dpkg_options)
 
     except apt.cache.LockFailedException:
         module.fail_json(msg="Failed to lock apt for exclusive operation")