From d812db9f7f8e5d0988ca4ab347772a4004242e40 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sat, 18 Jul 2015 11:37:29 -0400 Subject: [PATCH 1/6] Adding apk module --- packaging/os/apk.py | 196 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 packaging/os/apk.py diff --git a/packaging/os/apk.py b/packaging/os/apk.py new file mode 100644 index 00000000000..53672ca4489 --- /dev/null +++ b/packaging/os/apk.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Kevin Brebanov +# Based on pacman (Afterburn , Aaron Bull Schaefer ) +# and apt (Matthew Williams >) modules. +# +# 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 . + +DOCUMENTATION = ''' +--- +module: apk +short_description: Manages apk packages +description: + - Manages I(apk) packages for Alpine Linux. +options: + name: + description: + - A package name, like C(foo). + required: false + default: null + state: + description: + - Indicates the desired package state. + - C(present) ensures the package is present. + - C(absent) ensures the package is absent. + - C(latest) ensures the package is present and the latest version. + required: false + default: present + choices: [ "present", "absent", "latest" ] + update_cache: + description: + - Update repository indexes. Can be run with other steps or on it's own. + required: false + default: no + choices: [ "yes", "no" ] + upgrade: + description: + - Upgrade all installed packages to their latest version. + required: false + default: no + choices: [ "yes", "no" ] +''' + +EXAMPLES = ''' +# Update repositories and install "foo" package +- apk: name=foo update_cache=yes + +# Remove "foo" package +- apk: name=foo state=absent + +# Install the package "foo" +- apk: name=foo state=present + +# Update repositories and update package "foo" to latest version +- apk: name=foo state=latest update_cache=yes + +# Update all installed packages to the latest versions +- apk: upgrade=yes + +# Update repositories as a separate step +- apk: update_cache=yes +''' + +import os +import re + +APK_PATH="/sbin/apk" + +def update_package_db(module): + cmd = "apk update" + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc == 0: + return True + else: + module.fail_json(msg="could not update package db") + +def query_package(module, name): + cmd = "apk -v info --installed %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc == 0: + return True + else: + return False + +def query_latest(module, name): + cmd = "apk version %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + search_pattern = "(%s)-[\d\.\w]+-[\d\w]+\s+(.)\s+[\d\.\w]+-[\d\w]+\s+" % (name) + match = re.search(search_pattern, stdout) + if match and match.group(2) == "<": + return False + return True + +def upgrade_packages(module): + if module.check_mode: + cmd = "apk upgrade --simulate" + else: + cmd = "apk upgrade" + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc != 0: + module.fail_json(msg="failed to upgrade packages") + if re.search('^OK', stdout): + module.exit_json(changed=False, msg="packages already upgraded") + module.exit_json(changed=True, msg="upgraded packages") + +def install_package(module, name, state): + upgrade = False + installed = query_package(module, name) + latest = query_latest(module, name) + if state == 'latest' and not latest: + upgrade = True + if installed and not upgrade: + module.exit_json(changed=False, msg="package already installed") + if upgrade: + if module.check_mode: + cmd = "apk add --upgrade --simulate %s" % (name) + else: + cmd = "apk add --upgrade %s" % (name) + else: + if module.check_mode: + cmd = "apk add --simulate %s" % (name) + else: + cmd = "apk add %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc != 0: + module.fail_json(msg="failed to install %s" % (name)) + module.exit_json(changed=True, msg="installed %s package" % (name)) + +def remove_package(module, name): + installed = query_package(module, name) + if not installed: + module.exit_json(changed=False, msg="package already removed") + if module.check_mode: + cmd = "apk del --purge --simulate %s" % (name) + else: + cmd = "apk del --purge %s" % (name) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + if rc != 0: + module.fail_json(msg="failed to remove %s" % (name)) + module.exit_json(changed=True, msg="removed %s package" % (name)) + +# ========================================== +# Main control flow. + +def main(): + module = AnsibleModule( + argument_spec = dict( + state = dict(default='present', choices=['present', 'installed', 'absent', 'removed', 'latest']), + name = dict(type='str'), + update_cache = dict(default='no', choices=BOOLEANS, type='bool'), + upgrade = dict(default='no', choices=BOOLEANS, type='bool'), + ), + required_one_of = [['name', 'update_cache', 'upgrade']], + supports_check_mode = True + ) + + if not os.path.exists(APK_PATH): + module.fail_json(msg="cannot find apk, looking for %s" % (APK_PATH)) + + p = module.params + + # normalize the state parameter + if p['state'] in ['present', 'installed']: + p['state'] = 'present' + if p['state'] in ['absent', 'removed']: + p['state'] = 'absent' + + if p['update_cache']: + update_package_db(module) + if not p['name']: + module.exit_json(changed=True, msg='updated repository indexes') + + if p['upgrade']: + upgrade_packages(module) + + if p['state'] in ['present', 'latest']: + install_package(module, p['name'], p['state']) + elif p['state'] == 'absent': + remove_package(module, p['name']) + +# Import module snippets. +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() From c4c65b6c9164abe73f615e95425e46da20aeb7ab Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 19 Jul 2015 13:47:17 -0400 Subject: [PATCH 2/6] Allow multiple packages to removed at the same time --- packaging/os/apk.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 53672ca4489..858d4ad7450 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -60,6 +60,9 @@ EXAMPLES = ''' # Remove "foo" package - apk: name=foo state=absent +# Remove "foo" and "bar" packages +- apk: name=foo,bar state=absent + # Install the package "foo" - apk: name=foo state=present @@ -138,19 +141,23 @@ def install_package(module, name, state): module.fail_json(msg="failed to install %s" % (name)) module.exit_json(changed=True, msg="installed %s package" % (name)) -def remove_package(module, name): - installed = query_package(module, name) +def remove_packages(module, names): + installed = [] + for name in names: + if query_package(module, name): + installed.append(name) if not installed: - module.exit_json(changed=False, msg="package already removed") + module.exit_json(changed=False, msg="package(s) already removed") + names = " ".join(installed) if module.check_mode: - cmd = "apk del --purge --simulate %s" % (name) + cmd = "apk del --purge --simulate %s" % (names) else: - cmd = "apk del --purge %s" % (name) + cmd = "apk del --purge %s" % (names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: - module.fail_json(msg="failed to remove %s" % (name)) - module.exit_json(changed=True, msg="removed %s package" % (name)) - + module.fail_json(msg="failed to remove %s package(s)" % (names)) + module.exit_json(changed=True, msg="removed %s package(s)" % (names)) + # ========================================== # Main control flow. @@ -185,10 +192,14 @@ def main(): if p['upgrade']: upgrade_packages(module) + # Create a list of package names + # Removing empty strings that may have been created by a trailing ',' + names = filter((lambda x: x != ''), p['name'].split(',')) + if p['state'] in ['present', 'latest']: install_package(module, p['name'], p['state']) elif p['state'] == 'absent': - remove_package(module, p['name']) + remove_packages(module, names) # Import module snippets. from ansible.module_utils.basic import * From dd2d35c888f28f9b9f6d7b5a16b95be6dbef4c7d Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 19 Jul 2015 14:33:35 -0400 Subject: [PATCH 3/6] Allow multiple packages to be installed at the same time --- packaging/os/apk.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 858d4ad7450..55d79f3686d 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -57,6 +57,9 @@ EXAMPLES = ''' # Update repositories and install "foo" package - apk: name=foo update_cache=yes +# Update repositories and install "foo" and "bar" packages +- apk: name=foo,bar update_cache=yes + # Remove "foo" package - apk: name=foo state=absent @@ -66,9 +69,15 @@ EXAMPLES = ''' # Install the package "foo" - apk: name=foo state=present +# Install the packages "foo" and "bar" +- apk: name=foo,bar state=present + # Update repositories and update package "foo" to latest version - apk: name=foo state=latest update_cache=yes +# Update repositories and update packages "foo" and "bar" to latest versions +- apk: name=foo,bar state=latest update_cache=yes + # Update all installed packages to the latest versions - apk: upgrade=yes @@ -118,28 +127,31 @@ def upgrade_packages(module): module.exit_json(changed=False, msg="packages already upgraded") module.exit_json(changed=True, msg="upgraded packages") -def install_package(module, name, state): +def install_packages(module, names, state): upgrade = False - installed = query_package(module, name) - latest = query_latest(module, name) - if state == 'latest' and not latest: - upgrade = True - if installed and not upgrade: - module.exit_json(changed=False, msg="package already installed") + uninstalled = [] + for name in names: + if not query_package(module, name): + uninstalled.append(name) + elif state == 'latest' and not query_latest(module, name): + upgrade = True + if not uninstalled and not upgrade: + module.exit_json(changed=False, msg="package(s) already installed") + names = " ".join(uninstalled) if upgrade: if module.check_mode: - cmd = "apk add --upgrade --simulate %s" % (name) + cmd = "apk add --upgrade --simulate %s" % (names) else: - cmd = "apk add --upgrade %s" % (name) + cmd = "apk add --upgrade %s" % (names) else: if module.check_mode: - cmd = "apk add --simulate %s" % (name) + cmd = "apk add --simulate %s" % (names) else: - cmd = "apk add %s" % (name) + cmd = "apk add %s" % (names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: - module.fail_json(msg="failed to install %s" % (name)) - module.exit_json(changed=True, msg="installed %s package" % (name)) + module.fail_json(msg="failed to install %s" % (names)) + module.exit_json(changed=True, msg="installed %s package(s)" % (names)) def remove_packages(module, names): installed = [] @@ -197,7 +209,7 @@ def main(): names = filter((lambda x: x != ''), p['name'].split(',')) if p['state'] in ['present', 'latest']: - install_package(module, p['name'], p['state']) + install_packages(module, names, p['state']) elif p['state'] == 'absent': remove_packages(module, names) From 91e3d2afd550466b821554f534472d213542aaa4 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Sun, 19 Jul 2015 14:36:16 -0400 Subject: [PATCH 4/6] Update documentation --- packaging/os/apk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 55d79f3686d..4b78a898901 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -27,15 +27,15 @@ description: options: name: description: - - A package name, like C(foo). + - A package name, like C(foo), or mutliple packages, like C(foo, bar). required: false default: null state: description: - - Indicates the desired package state. - - C(present) ensures the package is present. - - C(absent) ensures the package is absent. - - C(latest) ensures the package is present and the latest version. + - Indicates the desired package(s) state. + - C(present) ensures the package(s) is/are present. + - C(absent) ensures the package(s) is/are absent. + - C(latest) ensures the package(s) is/are present and the latest version(s). required: false default: present choices: [ "present", "absent", "latest" ] From 6aaae617cae1067af8fc66bde77f9f195cf83d46 Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Wed, 29 Jul 2015 16:22:32 -0400 Subject: [PATCH 5/6] Modify 'name' argument to be of type 'list' in order to support 'with_items' looping --- packaging/os/apk.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index 4b78a898901..f8ddbc2aa04 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -177,7 +177,7 @@ def main(): module = AnsibleModule( argument_spec = dict( state = dict(default='present', choices=['present', 'installed', 'absent', 'removed', 'latest']), - name = dict(type='str'), + name = dict(type='list'), update_cache = dict(default='no', choices=BOOLEANS, type='bool'), upgrade = dict(default='no', choices=BOOLEANS, type='bool'), ), @@ -204,14 +204,10 @@ def main(): if p['upgrade']: upgrade_packages(module) - # Create a list of package names - # Removing empty strings that may have been created by a trailing ',' - names = filter((lambda x: x != ''), p['name'].split(',')) - if p['state'] in ['present', 'latest']: - install_packages(module, names, p['state']) + install_packages(module, p['name'], p['state']) elif p['state'] == 'absent': - remove_packages(module, names) + remove_packages(module, p['name']) # Import module snippets. from ansible.module_utils.basic import * From 5d6f0d153cfb55d8f0f48f1a3e021270e1d711cb Mon Sep 17 00:00:00 2001 From: Kevin Brebanov Date: Mon, 3 Aug 2015 16:14:09 -0400 Subject: [PATCH 6/6] Use the module's get_bin_path function to find 'apk' and reuse the return value in all functions --- packaging/os/apk.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/packaging/os/apk.py b/packaging/os/apk.py index f8ddbc2aa04..f14d5593443 100644 --- a/packaging/os/apk.py +++ b/packaging/os/apk.py @@ -88,10 +88,8 @@ EXAMPLES = ''' import os import re -APK_PATH="/sbin/apk" - def update_package_db(module): - cmd = "apk update" + cmd = "%s update" % (APK_PATH) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: return True @@ -99,7 +97,7 @@ def update_package_db(module): module.fail_json(msg="could not update package db") def query_package(module, name): - cmd = "apk -v info --installed %s" % (name) + cmd = "%s -v info --installed %s" % (APK_PATH, name) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc == 0: return True @@ -107,7 +105,7 @@ def query_package(module, name): return False def query_latest(module, name): - cmd = "apk version %s" % (name) + cmd = "%s version %s" % (APK_PATH, name) rc, stdout, stderr = module.run_command(cmd, check_rc=False) search_pattern = "(%s)-[\d\.\w]+-[\d\w]+\s+(.)\s+[\d\.\w]+-[\d\w]+\s+" % (name) match = re.search(search_pattern, stdout) @@ -117,9 +115,9 @@ def query_latest(module, name): def upgrade_packages(module): if module.check_mode: - cmd = "apk upgrade --simulate" + cmd = "%s upgrade --simulate" % (APK_PATH) else: - cmd = "apk upgrade" + cmd = "%s upgrade" % (APK_PATH) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: module.fail_json(msg="failed to upgrade packages") @@ -140,14 +138,14 @@ def install_packages(module, names, state): names = " ".join(uninstalled) if upgrade: if module.check_mode: - cmd = "apk add --upgrade --simulate %s" % (names) + cmd = "%s add --upgrade --simulate %s" % (APK_PATH, names) else: - cmd = "apk add --upgrade %s" % (names) + cmd = "%s add --upgrade %s" % (APK_PATH, names) else: if module.check_mode: - cmd = "apk add --simulate %s" % (names) + cmd = "%s add --simulate %s" % (APK_PATH, names) else: - cmd = "apk add %s" % (names) + cmd = "%s add %s" % (APK_PATH, names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: module.fail_json(msg="failed to install %s" % (names)) @@ -162,9 +160,9 @@ def remove_packages(module, names): module.exit_json(changed=False, msg="package(s) already removed") names = " ".join(installed) if module.check_mode: - cmd = "apk del --purge --simulate %s" % (names) + cmd = "%s del --purge --simulate %s" % (APK_PATH, names) else: - cmd = "apk del --purge %s" % (names) + cmd = "%s del --purge %s" % (APK_PATH, names) rc, stdout, stderr = module.run_command(cmd, check_rc=False) if rc != 0: module.fail_json(msg="failed to remove %s package(s)" % (names)) @@ -185,8 +183,8 @@ def main(): supports_check_mode = True ) - if not os.path.exists(APK_PATH): - module.fail_json(msg="cannot find apk, looking for %s" % (APK_PATH)) + global APK_PATH + APK_PATH = module.get_bin_path('apk', required=True) p = module.params