diff options
Diffstat (limited to 'lib/ansible/modules/extras/packaging/language')
8 files changed, 1779 insertions, 0 deletions
diff --git a/lib/ansible/modules/extras/packaging/language/__init__.py b/lib/ansible/modules/extras/packaging/language/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/__init__.py diff --git a/lib/ansible/modules/extras/packaging/language/bower.py b/lib/ansible/modules/extras/packaging/language/bower.py new file mode 100644 index 0000000000..2b58b1ce1f --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/bower.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Michael Warkentin <mwarkentin@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: bower +short_description: Manage bower packages with bower +description: + - Manage bower packages with bower +version_added: 1.9 +author: "Michael Warkentin (@mwarkentin)" +options: + name: + description: + - The name of a bower package to install + required: false + offline: + description: + - Install packages from local cache, if the packages were installed before + required: false + default: no + choices: [ "yes", "no" ] + production: + description: + - Install with --production flag + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.0" + path: + description: + - The base path where to install the bower packages + required: true + relative_execpath: + description: + - Relative path to bower executable from install path + default: null + required: false + version_added: "2.1" + state: + description: + - The state of the bower package + required: false + default: present + choices: [ "present", "absent", "latest" ] + version: + description: + - The version to be installed + required: false +''' + +EXAMPLES = ''' +description: Install "bootstrap" bower package. +- bower: name=bootstrap + +description: Install "bootstrap" bower package on version 3.1.1. +- bower: name=bootstrap version=3.1.1 + +description: Remove the "bootstrap" bower package. +- bower: name=bootstrap state=absent + +description: Install packages based on bower.json. +- bower: path=/app/location + +description: Update packages based on bower.json to their latest version. +- bower: path=/app/location state=latest + +description: install bower locally and run from there +- npm: path=/app/location name=bower global=no +- bower: path=/app/location relative_execpath=node_modules/.bin +''' + + +class Bower(object): + def __init__(self, module, **kwargs): + self.module = module + self.name = kwargs['name'] + self.offline = kwargs['offline'] + self.production = kwargs['production'] + self.path = kwargs['path'] + self.relative_execpath = kwargs['relative_execpath'] + self.version = kwargs['version'] + + if kwargs['version']: + self.name_version = self.name + '#' + self.version + else: + self.name_version = self.name + + def _exec(self, args, run_in_check_mode=False, check_rc=True): + if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): + cmd = [] + + if self.relative_execpath: + cmd.append(os.path.join(self.path, self.relative_execpath, "bower")) + if not os.path.isfile(cmd[-1]): + self.module.fail_json(msg="bower not found at relative path %s" % self.relative_execpath) + else: + cmd.append("bower") + + cmd.extend(args) + cmd.extend(['--config.interactive=false', '--allow-root']) + + if self.name: + cmd.append(self.name_version) + + if self.offline: + cmd.append('--offline') + + if self.production: + cmd.append('--production') + + # If path is specified, cd into that path and run the command. + cwd = None + if self.path: + if not os.path.exists(self.path): + os.makedirs(self.path) + if not os.path.isdir(self.path): + self.module.fail_json(msg="path %s is not a directory" % self.path) + cwd = self.path + + rc, out, err = self.module.run_command(cmd, check_rc=check_rc, cwd=cwd) + return out + return '' + + def list(self): + cmd = ['list', '--json'] + + installed = list() + missing = list() + outdated = list() + data = json.loads(self._exec(cmd, True, False)) + if 'dependencies' in data: + for dep in data['dependencies']: + dep_data = data['dependencies'][dep] + if dep_data.get('missing', False): + missing.append(dep) + elif ('version' in dep_data['pkgMeta'] and + 'update' in dep_data and + dep_data['pkgMeta']['version'] != dep_data['update']['latest']): + outdated.append(dep) + elif dep_data.get('incompatible', False): + outdated.append(dep) + else: + installed.append(dep) + # Named dependency not installed + else: + missing.append(self.name) + + return installed, missing, outdated + + def install(self): + return self._exec(['install']) + + def update(self): + return self._exec(['update']) + + def uninstall(self): + return self._exec(['uninstall']) + + +def main(): + arg_spec = dict( + name=dict(default=None), + offline=dict(default='no', type='bool'), + production=dict(default='no', type='bool'), + path=dict(required=True, type='path'), + relative_execpath=dict(default=None, required=False, type='path'), + state=dict(default='present', choices=['present', 'absent', 'latest', ]), + version=dict(default=None), + ) + module = AnsibleModule( + argument_spec=arg_spec + ) + + name = module.params['name'] + offline = module.params['offline'] + production = module.params['production'] + path = os.path.expanduser(module.params['path']) + relative_execpath = module.params['relative_execpath'] + state = module.params['state'] + version = module.params['version'] + + if state == 'absent' and not name: + module.fail_json(msg='uninstalling a package is only available for named packages') + + bower = Bower(module, name=name, offline=offline, production=production, path=path, relative_execpath=relative_execpath, version=version) + + changed = False + if state == 'present': + installed, missing, outdated = bower.list() + if len(missing): + changed = True + bower.install() + elif state == 'latest': + installed, missing, outdated = bower.list() + if len(missing) or len(outdated): + changed = True + bower.update() + else: # Absent + installed, missing, outdated = bower.list() + if name in installed: + changed = True + bower.uninstall() + + module.exit_json(changed=changed) + +# Import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/packaging/language/bundler.py b/lib/ansible/modules/extras/packaging/language/bundler.py new file mode 100644 index 0000000000..152b51810a --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/bundler.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Tim Hoiberg <tim.hoiberg@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: bundler +short_description: Manage Ruby Gem dependencies with Bundler +description: + - Manage installation and Gem version dependencies for Ruby using the Bundler gem +version_added: "2.0.0" +options: + executable: + description: + - The path to the bundler executable + required: false + default: null + state: + description: + - The desired state of the Gem bundle. C(latest) updates gems to the most recent, acceptable version + required: false + choices: [present, latest] + default: present + chdir: + description: + - The directory to execute the bundler commands from. This directoy + needs to contain a valid Gemfile or .bundle/ directory + required: false + default: temporary working directory + exclude_groups: + description: + - A list of Gemfile groups to exclude during operations. This only + applies when state is C(present). Bundler considers this + a 'remembered' property for the Gemfile and will automatically exclude + groups in future operations even if C(exclude_groups) is not set + required: false + default: null + clean: + description: + - Only applies if state is C(present). If set removes any gems on the + target host that are not in the gemfile + required: false + choices: [yes, no] + default: "no" + gemfile: + description: + - Only applies if state is C(present). The path to the gemfile to use to install gems. + required: false + default: Gemfile in current directory + local: + description: + - If set only installs gems from the cache on the target host + required: false + choices: [yes, no] + default: "no" + deployment_mode: + description: + - Only applies if state is C(present). If set it will only install gems + that are in the default or production groups. Requires a Gemfile.lock + file to have been created prior + required: false + choices: [yes, no] + default: "no" + user_install: + description: + - Only applies if state is C(present). Installs gems in the local user's cache or for all users + required: false + choices: [yes, no] + default: "yes" + gem_path: + description: + - Only applies if state is C(present). Specifies the directory to + install the gems into. If C(chdir) is set then this path is relative to + C(chdir) + required: false + default: RubyGems gem paths + binstub_directory: + description: + - Only applies if state is C(present). Specifies the directory to + install any gem bins files to. When executed the bin files will run + within the context of the Gemfile and fail if any required gem + dependencies are not installed. If C(chdir) is set then this path is + relative to C(chdir) + required: false + default: null + extra_args: + description: + - A space separated string of additional commands that can be applied to + the Bundler command. Refer to the Bundler documentation for more + information + required: false + default: null +author: "Tim Hoiberg (@thoiberg)" +''' + +EXAMPLES=''' +# Installs gems from a Gemfile in the current directory +- bundler: state=present executable=~/.rvm/gems/2.1.5/bin/bundle + +# Excludes the production group from installing +- bundler: state=present exclude_groups=production + +# Only install gems from the default and production groups +- bundler: state=present deployment_mode=yes + +# Installs gems using a Gemfile in another directory +- bundler: state=present gemfile=../rails_project/Gemfile + +# Updates Gemfile in another directory +- bundler: state=latest chdir=~/rails_project +''' + + +def get_bundler_executable(module): + if module.params.get('executable'): + return module.params.get('executable').split(' ') + else: + return [ module.get_bin_path('bundle', True) ] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + executable=dict(default=None, required=False), + state=dict(default='present', required=False, choices=['present', 'latest']), + chdir=dict(default=None, required=False, type='path'), + exclude_groups=dict(default=None, required=False, type='list'), + clean=dict(default=False, required=False, type='bool'), + gemfile=dict(default=None, required=False, type='path'), + local=dict(default=False, required=False, type='bool'), + deployment_mode=dict(default=False, required=False, type='bool'), + user_install=dict(default=True, required=False, type='bool'), + gem_path=dict(default=None, required=False, type='path'), + binstub_directory=dict(default=None, required=False, type='path'), + extra_args=dict(default=None, required=False), + ), + supports_check_mode=True + ) + + executable = module.params.get('executable') + state = module.params.get('state') + chdir = module.params.get('chdir') + exclude_groups = module.params.get('exclude_groups') + clean = module.params.get('clean') + gemfile = module.params.get('gemfile') + local = module.params.get('local') + deployment_mode = module.params.get('deployment_mode') + user_install = module.params.get('user_install') + gem_path = module.params.get('gem_path') + binstub_directory = module.params.get('binstub_directory') + extra_args = module.params.get('extra_args') + + cmd = get_bundler_executable(module) + + if module.check_mode: + cmd.append('check') + rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=False) + + module.exit_json(changed=rc != 0, state=state, stdout=out, stderr=err) + + if state == 'present': + cmd.append('install') + if exclude_groups: + cmd.extend(['--without', ':'.join(exclude_groups)]) + if clean: + cmd.append('--clean') + if gemfile: + cmd.extend(['--gemfile', gemfile]) + if local: + cmd.append('--local') + if deployment_mode: + cmd.append('--deployment') + if not user_install: + cmd.append('--system') + if gem_path: + cmd.extend(['--path', gem_path]) + if binstub_directory: + cmd.extend(['--binstubs', binstub_directory]) + else: + cmd.append('update') + if local: + cmd.append('--local') + + if extra_args: + cmd.extend(extra_args.split(' ')) + + rc, out, err = module.run_command(cmd, cwd=chdir, check_rc=True) + + module.exit_json(changed='Installing' in out, state=state, stdout=out, stderr=err) + + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/packaging/language/composer.py b/lib/ansible/modules/extras/packaging/language/composer.py new file mode 100644 index 0000000000..4c5f8518be --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/composer.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Dimitrios Tydeas Mengidis <tydeas.dr@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: composer +author: + - "Dimitrios Tydeas Mengidis (@dmtrs)" + - "René Moser (@resmo)" +short_description: Dependency Manager for PHP +version_added: "1.6" +description: + - Composer is a tool for dependency management in PHP. It allows you to declare the dependent libraries your project needs and it will install them in your project for you +options: + command: + version_added: "1.8" + description: + - Composer command like "install", "update" and so on + required: false + default: install + arguments: + version_added: "2.0" + description: + - Composer arguments like required package, version and so on + required: false + default: null + working_dir: + description: + - Directory of your project ( see --working-dir ) + required: true + default: null + aliases: [ "working-dir" ] + prefer_source: + description: + - Forces installation from package sources when possible ( see --prefer-source ) + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "prefer-source" ] + prefer_dist: + description: + - Forces installation from package dist even for dev versions ( see --prefer-dist ) + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "prefer-dist" ] + no_dev: + description: + - Disables installation of require-dev packages ( see --no-dev ) + required: false + default: "yes" + choices: [ "yes", "no" ] + aliases: [ "no-dev" ] + no_scripts: + description: + - Skips the execution of all scripts defined in composer.json ( see --no-scripts ) + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "no-scripts" ] + no_plugins: + description: + - Disables all plugins ( see --no-plugins ) + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "no-plugins" ] + optimize_autoloader: + description: + - Optimize autoloader during autoloader dump ( see --optimize-autoloader ). Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. + required: false + default: "yes" + choices: [ "yes", "no" ] + aliases: [ "optimize-autoloader" ] + ignore_platform_reqs: + version_added: "2.0" + description: + - Ignore php, hhvm, lib-* and ext-* requirements and force the installation even if the local machine does not fulfill these. + required: false + default: "no" + choices: [ "yes", "no" ] + aliases: [ "ignore-platform-reqs" ] +requirements: + - php + - composer installed in bin path (recommended /usr/local/bin) +notes: + - Default options that are always appended in each execution are --no-ansi, --no-interaction and --no-progress if available. + - We received reports about issues on macOS if composer was installed by Homebrew. Please use the official install method to avoid it. +''' + +EXAMPLES = ''' +# Downloads and installs all the libs and dependencies outlined in the /path/to/project/composer.lock +- composer: command=install working_dir=/path/to/project + +- composer: + command: "require" + arguments: "my/package" + working_dir: "/path/to/project" + +# Clone project and install with all dependencies +- composer: + command: "create-project" + arguments: "package/package /path/to/project ~1.0" + working_dir: "/path/to/project" + prefer_dist: "yes" +''' + +import os +import re + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + + +def parse_out(string): + return re.sub("\s+", " ", string).strip() + +def has_changed(string): + return "Nothing to install or update" not in string + +def get_available_options(module, command='install'): + # get all availabe options from a composer command using composer help to json + rc, out, err = composer_command(module, "help %s --format=json" % command) + if rc != 0: + output = parse_out(err) + module.fail_json(msg=output) + + command_help_json = json.loads(out) + return command_help_json['definition']['options'] + +def composer_command(module, command, arguments = "", options=[]): + php_path = module.get_bin_path("php", True, ["/usr/local/bin"]) + composer_path = module.get_bin_path("composer", True, ["/usr/local/bin"]) + cmd = "%s %s %s %s %s" % (php_path, composer_path, command, " ".join(options), arguments) + return module.run_command(cmd) + +def main(): + module = AnsibleModule( + argument_spec = dict( + command = dict(default="install", type="str", required=False), + arguments = dict(default="", type="str", required=False), + working_dir = dict(aliases=["working-dir"], required=True), + prefer_source = dict(default="no", type="bool", aliases=["prefer-source"]), + prefer_dist = dict(default="no", type="bool", aliases=["prefer-dist"]), + no_dev = dict(default="yes", type="bool", aliases=["no-dev"]), + no_scripts = dict(default="no", type="bool", aliases=["no-scripts"]), + no_plugins = dict(default="no", type="bool", aliases=["no-plugins"]), + optimize_autoloader = dict(default="yes", type="bool", aliases=["optimize-autoloader"]), + ignore_platform_reqs = dict(default="no", type="bool", aliases=["ignore-platform-reqs"]), + ), + supports_check_mode=True + ) + + # Get composer command with fallback to default + command = module.params['command'] + if re.search(r"\s", command): + module.fail_json(msg="Use the 'arguments' param for passing arguments with the 'command'") + + arguments = module.params['arguments'] + available_options = get_available_options(module=module, command=command) + + options = [] + + # Default options + default_options = [ + 'no-ansi', + 'no-interaction', + 'no-progress', + ] + + for option in default_options: + if option in available_options: + option = "--%s" % option + options.append(option) + + options.extend(['--working-dir', os.path.abspath(module.params['working_dir'])]) + + option_params = { + 'prefer_source': 'prefer-source', + 'prefer_dist': 'prefer-dist', + 'no_dev': 'no-dev', + 'no_scripts': 'no-scripts', + 'no_plugins': 'no_plugins', + 'optimize_autoloader': 'optimize-autoloader', + 'ignore_platform_reqs': 'ignore-platform-reqs', + } + + for param, option in option_params.iteritems(): + if module.params.get(param) and option in available_options: + option = "--%s" % option + options.append(option) + + if module.check_mode: + options.append('--dry-run') + + rc, out, err = composer_command(module, command, arguments, options) + + if rc != 0: + output = parse_out(err) + module.fail_json(msg=output, stdout=err) + else: + # Composer version > 1.0.0-alpha9 now use stderr for standard notification messages + output = parse_out(out + err) + module.exit_json(changed=has_changed(output), msg=output, stdout=out+err) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/packaging/language/cpanm.py b/lib/ansible/modules/extras/packaging/language/cpanm.py new file mode 100644 index 0000000000..790a493915 --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/cpanm.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2012, Franck Cuny <franck@lumberjaph.net> +# +# 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: cpanm +short_description: Manages Perl library dependencies. +description: + - Manage Perl library dependencies. +version_added: "1.6" +options: + name: + description: + - The name of the Perl library to install. You may use the "full distribution path", e.g. MIYAGAWA/Plack-0.99_05.tar.gz + required: false + default: null + aliases: ["pkg"] + from_path: + description: + - The local directory from where to install + required: false + default: null + notest: + description: + - Do not run unit tests + required: false + default: false + locallib: + description: + - Specify the install base to install modules + required: false + default: false + mirror: + description: + - Specifies the base URL for the CPAN mirror to use + required: false + default: false + mirror_only: + description: + - Use the mirror's index file instead of the CPAN Meta DB + required: false + default: false + installdeps: + description: + - Only install dependencies + required: false + default: false + version_added: "2.0" + version: + description: + - minimum version of perl module to consider acceptable + required: false + default: false + version_added: "2.1" + system_lib: + description: + - Use this if you want to install modules to the system perl include path. You must be root or have "passwordless" sudo for this to work. + - This uses the cpanm commandline option '--sudo', which has nothing to do with ansible privilege escalation. + required: false + default: false + version_added: "2.0" + aliases: ['use_sudo'] + executable: + description: + - Override the path to the cpanm executable + required: false + default: null + version_added: "2.1" +notes: + - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. +author: "Franck Cuny (@franckcuny)" +''' + +EXAMPLES = ''' +# install Dancer perl package +- cpanm: name=Dancer + +# install version 0.99_05 of the Plack perl package +- cpanm: name=MIYAGAWA/Plack-0.99_05.tar.gz + +# install Dancer into the specified locallib +- cpanm: name=Dancer locallib=/srv/webapps/my_app/extlib + +# install perl dependencies from local directory +- cpanm: from_path=/srv/webapps/my_app/src/ + +# install Dancer perl package without running the unit tests in indicated locallib +- cpanm: name=Dancer notest=True locallib=/srv/webapps/my_app/extlib + +# install Dancer perl package from a specific mirror +- cpanm: name=Dancer mirror=http://cpan.cpantesters.org/ + +# install Dancer perl package into the system root path +- cpanm: name=Dancer system_lib=yes + +# install Dancer if it's not already installed +# OR the installed version is older than version 1.0 +- cpanm: name=Dancer version=1.0 +''' + +def _is_package_installed(module, name, locallib, cpanm, version): + cmd = "" + if locallib: + os.environ["PERL5LIB"] = "%s/lib/perl5" % locallib + cmd = "%s perl -e ' use %s" % (cmd, name) + if version: + cmd = "%s %s;'" % (cmd, version) + else: + cmd = "%s;'" % cmd + res, stdout, stderr = module.run_command(cmd, check_rc=False) + if res == 0: + return True + else: + return False + +def _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, installdeps, cpanm, use_sudo): + # this code should use "%s" like everything else and just return early but not fixing all of it now. + # don't copy stuff like this + if from_path: + cmd = cpanm + " " + from_path + else: + cmd = cpanm + " " + name + + if notest is True: + cmd = cmd + " -n" + + if locallib is not None: + cmd = cmd + " -l " + locallib + + if mirror is not None: + cmd = cmd + " --mirror " + mirror + + if mirror_only is True: + cmd = cmd + " --mirror-only" + + if installdeps is True: + cmd = cmd + " --installdeps" + + if use_sudo is True: + cmd = cmd + " --sudo" + + return cmd + + +def _get_cpanm_path(module): + if module.params['executable']: + return module.params['executable'] + else: + return module.get_bin_path('cpanm', True) + + +def main(): + arg_spec = dict( + name=dict(default=None, required=False, aliases=['pkg']), + from_path=dict(default=None, required=False, type='path'), + notest=dict(default=False, type='bool'), + locallib=dict(default=None, required=False, type='path'), + mirror=dict(default=None, required=False), + mirror_only=dict(default=False, type='bool'), + installdeps=dict(default=False, type='bool'), + system_lib=dict(default=False, type='bool', aliases=['use_sudo']), + version=dict(default=None, required=False), + executable=dict(required=False, type='path'), + ) + + module = AnsibleModule( + argument_spec=arg_spec, + required_one_of=[['name', 'from_path']], + ) + + cpanm = _get_cpanm_path(module) + name = module.params['name'] + from_path = module.params['from_path'] + notest = module.boolean(module.params.get('notest', False)) + locallib = module.params['locallib'] + mirror = module.params['mirror'] + mirror_only = module.params['mirror_only'] + installdeps = module.params['installdeps'] + use_sudo = module.params['system_lib'] + version = module.params['version'] + + changed = False + + installed = _is_package_installed(module, name, locallib, cpanm, version) + + if not installed: + cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, installdeps, cpanm, use_sudo) + + rc_cpanm, out_cpanm, err_cpanm = module.run_command(cmd, check_rc=False) + + if rc_cpanm != 0: + module.fail_json(msg=err_cpanm, cmd=cmd) + + if (err_cpanm.find('is up to date') == -1 and out_cpanm.find('is up to date') == -1): + changed = True + + module.exit_json(changed=changed, binary=cpanm, name=name) + +# import module snippets +from ansible.module_utils.basic import * + +main() diff --git a/lib/ansible/modules/extras/packaging/language/maven_artifact.py b/lib/ansible/modules/extras/packaging/language/maven_artifact.py new file mode 100644 index 0000000000..1136f7aaaf --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/maven_artifact.py @@ -0,0 +1,390 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014, Chris Schmidt <chris.schmidt () contrastsecurity.com> +# +# Built using https://github.com/hamnis/useful-scripts/blob/master/python/download-maven-artifact +# as a reference and starting point. +# +# 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/>. + +__author__ = 'cschmidt' + +from lxml import etree +import os +import hashlib +import sys +import posixpath +import urlparse +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +try: + import boto3 + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +DOCUMENTATION = ''' +--- +module: maven_artifact +short_description: Downloads an Artifact from a Maven Repository +version_added: "2.0" +description: + - Downloads an artifact from a maven repository given the maven coordinates provided to the module. Can retrieve + - snapshots or release versions of the artifact and will resolve the latest available version if one is not + - available. +author: "Chris Schmidt (@chrisisbeef)" +requirements: + - "python >= 2.6" + - lxml + - boto if using a S3 repository (s3://...) +options: + group_id: + description: + - The Maven groupId coordinate + required: true + artifact_id: + description: + - The maven artifactId coordinate + required: true + version: + description: + - The maven version coordinate + required: false + default: latest + classifier: + description: + - The maven classifier coordinate + required: false + default: null + extension: + description: + - The maven type/extension coordinate + required: false + default: jar + repository_url: + description: + - The URL of the Maven Repository to download from. + - Use s3://... if the repository is hosted on Amazon S3, added in version 2.2. + required: false + default: http://repo1.maven.org/maven2 + username: + description: + - The username to authenticate as to the Maven Repository. Use AWS secret key of the repository is hosted on S3 + required: false + default: null + aliases: [ "aws_secret_key" ] + password: + description: + - The password to authenticate with to the Maven Repository. Use AWS secret access key of the repository is hosted on S3 + required: false + default: null + aliases: [ "aws_secret_access_key" ] + dest: + description: + - The path where the artifact should be written to + required: true + default: false + state: + description: + - The desired state of the artifact + required: true + default: present + choices: [present,absent] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be set to C(no) when no other option exists. + required: false + default: 'yes' + choices: ['yes', 'no'] + version_added: "1.9.3" +''' + +EXAMPLES = ''' +# Download the latest version of the JUnit framework artifact from Maven Central +- maven_artifact: group_id=junit artifact_id=junit dest=/tmp/junit-latest.jar + +# Download JUnit 4.11 from Maven Central +- maven_artifact: group_id=junit artifact_id=junit version=4.11 dest=/tmp/junit-4.11.jar + +# Download an artifact from a private repository requiring authentication +- maven_artifact: group_id=com.company artifact_id=library-name repository_url=https://repo.company.com/maven username=user password=pass dest=/tmp/library-name-latest.jar + +# Download a WAR File to the Tomcat webapps directory to be deployed +- maven_artifact: group_id=com.company artifact_id=web-app extension=war repository_url=https://repo.company.com/maven dest=/var/lib/tomcat7/webapps/web-app.war +''' + +class Artifact(object): + def __init__(self, group_id, artifact_id, version, classifier=None, extension='jar'): + if not group_id: + raise ValueError("group_id must be set") + if not artifact_id: + raise ValueError("artifact_id must be set") + + self.group_id = group_id + self.artifact_id = artifact_id + self.version = version + self.classifier = classifier + + if not extension: + self.extension = "jar" + else: + self.extension = extension + + def is_snapshot(self): + return self.version and self.version.endswith("SNAPSHOT") + + def path(self, with_version=True): + base = posixpath.join(self.group_id.replace(".", "/"), self.artifact_id) + if with_version and self.version: + return posixpath.join(base, self.version) + else: + return base + + def _generate_filename(self): + if not self.classifier: + return self.artifact_id + "." + self.extension + else: + return self.artifact_id + "-" + self.classifier + "." + self.extension + + def get_filename(self, filename=None): + if not filename: + filename = self._generate_filename() + elif os.path.isdir(filename): + filename = os.path.join(filename, self._generate_filename()) + return filename + + def __str__(self): + if self.classifier: + return "%s:%s:%s:%s:%s" % (self.group_id, self.artifact_id, self.extension, self.classifier, self.version) + elif self.extension != "jar": + return "%s:%s:%s:%s" % (self.group_id, self.artifact_id, self.extension, self.version) + else: + return "%s:%s:%s" % (self.group_id, self.artifact_id, self.version) + + @staticmethod + def parse(input): + parts = input.split(":") + if len(parts) >= 3: + g = parts[0] + a = parts[1] + v = parts[len(parts) - 1] + t = None + c = None + if len(parts) == 4: + t = parts[2] + if len(parts) == 5: + t = parts[2] + c = parts[3] + return Artifact(g, a, v, c, t) + else: + return None + + +class MavenDownloader: + def __init__(self, module, base="http://repo1.maven.org/maven2"): + self.module = module + if base.endswith("/"): + base = base.rstrip("/") + self.base = base + self.user_agent = "Maven Artifact Downloader/1.0" + + def _find_latest_version_available(self, artifact): + path = "/%s/maven-metadata.xml" % (artifact.path(False)) + xml = self._request(self.base + path, "Failed to download maven-metadata.xml", lambda r: etree.parse(r)) + v = xml.xpath("/metadata/versioning/versions/version[last()]/text()") + if v: + return v[0] + + def find_uri_for_artifact(self, artifact): + if artifact.version == "latest": + artifact.version = self._find_latest_version_available(artifact) + + if artifact.is_snapshot(): + path = "/%s/maven-metadata.xml" % (artifact.path()) + xml = self._request(self.base + path, "Failed to download maven-metadata.xml", lambda r: etree.parse(r)) + timestamp = xml.xpath("/metadata/versioning/snapshot/timestamp/text()")[0] + buildNumber = xml.xpath("/metadata/versioning/snapshot/buildNumber/text()")[0] + return self._uri_for_artifact(artifact, artifact.version.replace("SNAPSHOT", timestamp + "-" + buildNumber)) + + return self._uri_for_artifact(artifact, artifact.version) + + def _uri_for_artifact(self, artifact, version=None): + if artifact.is_snapshot() and not version: + raise ValueError("Expected uniqueversion for snapshot artifact " + str(artifact)) + elif not artifact.is_snapshot(): + version = artifact.version + if artifact.classifier: + return posixpath.join(self.base, artifact.path(), artifact.artifact_id + "-" + version + "-" + artifact.classifier + "." + artifact.extension) + + return posixpath.join(self.base, artifact.path(), artifact.artifact_id + "-" + version + "." + artifact.extension) + + def _request(self, url, failmsg, f): + url_to_use = url + parsed_url = urlparse(url) + if parsed_url.scheme=='s3': + parsed_url = urlparse(url) + bucket_name = parsed_url.netloc + key_name = parsed_url.path[1:] + client = boto3.client('s3',aws_access_key_id=self.module.params.get('username', ''), aws_secret_access_key=self.module.params.get('password', '')) + url_to_use = client.generate_presigned_url('get_object',Params={'Bucket':bucket_name,'Key':key_name},ExpiresIn=10) + + # Hack to add parameters in the way that fetch_url expects + self.module.params['url_username'] = self.module.params.get('username', '') + self.module.params['url_password'] = self.module.params.get('password', '') + self.module.params['http_agent'] = self.module.params.get('user_agent', None) + + response, info = fetch_url(self.module, url_to_use) + if info['status'] != 200: + raise ValueError(failmsg + " because of " + info['msg'] + "for URL " + url_to_use) + else: + return f(response) + + + def download(self, artifact, filename=None): + filename = artifact.get_filename(filename) + if not artifact.version or artifact.version == "latest": + artifact = Artifact(artifact.group_id, artifact.artifact_id, self._find_latest_version_available(artifact), + artifact.classifier, artifact.extension) + + url = self.find_uri_for_artifact(artifact) + if not self.verify_md5(filename, url + ".md5"): + response = self._request(url, "Failed to download artifact " + str(artifact), lambda r: r) + if response: + f = open(filename, 'w') + # f.write(response.read()) + self._write_chunks(response, f, report_hook=self.chunk_report) + f.close() + return True + else: + return False + else: + return True + + def chunk_report(self, bytes_so_far, chunk_size, total_size): + percent = float(bytes_so_far) / total_size + percent = round(percent * 100, 2) + sys.stdout.write("Downloaded %d of %d bytes (%0.2f%%)\r" % + (bytes_so_far, total_size, percent)) + + if bytes_so_far >= total_size: + sys.stdout.write('\n') + + def _write_chunks(self, response, file, chunk_size=8192, report_hook=None): + total_size = response.info().getheader('Content-Length').strip() + total_size = int(total_size) + bytes_so_far = 0 + + while 1: + chunk = response.read(chunk_size) + bytes_so_far += len(chunk) + + if not chunk: + break + + file.write(chunk) + if report_hook: + report_hook(bytes_so_far, chunk_size, total_size) + + return bytes_so_far + + def verify_md5(self, file, remote_md5): + if not os.path.exists(file): + return False + else: + local_md5 = self._local_md5(file) + remote = self._request(remote_md5, "Failed to download MD5", lambda r: r.read()) + return local_md5 == remote + + def _local_md5(self, file): + md5 = hashlib.md5() + f = open(file, 'rb') + for chunk in iter(lambda: f.read(8192), ''): + md5.update(chunk) + f.close() + return md5.hexdigest() + + +def main(): + module = AnsibleModule( + argument_spec = dict( + group_id = dict(default=None), + artifact_id = dict(default=None), + version = dict(default="latest"), + classifier = dict(default=None), + extension = dict(default='jar'), + repository_url = dict(default=None), + username = dict(default=None,aliases=['aws_secret_key']), + password = dict(default=None, no_log=True,aliases=['aws_secret_access_key']), + state = dict(default="present", choices=["present","absent"]), # TODO - Implement a "latest" state + dest = dict(type="path", default=None), + validate_certs = dict(required=False, default=True, type='bool'), + ) + ) + + try: + parsed_url = urlparse(module.params["repository_url"]) + except AttributeError as e: + module.fail_json(msg='url parsing went wrong %s' % e) + + if parsed_url.scheme=='s3' and not HAS_BOTO: + module.fail_json(msg='boto3 required for this module, when using s3:// repository URLs') + + group_id = module.params["group_id"] + artifact_id = module.params["artifact_id"] + version = module.params["version"] + classifier = module.params["classifier"] + extension = module.params["extension"] + repository_url = module.params["repository_url"] + repository_username = module.params["username"] + repository_password = module.params["password"] + state = module.params["state"] + dest = module.params["dest"] + + if not repository_url: + repository_url = "http://repo1.maven.org/maven2" + + #downloader = MavenDownloader(module, repository_url, repository_username, repository_password) + downloader = MavenDownloader(module, repository_url) + + try: + artifact = Artifact(group_id, artifact_id, version, classifier, extension) + except ValueError as e: + module.fail_json(msg=e.args[0]) + + prev_state = "absent" + if os.path.isdir(dest): + dest = posixpath.join(dest, artifact_id + "-" + version + "." + extension) + if os.path.lexists(dest) and downloader.verify_md5(dest, downloader.find_uri_for_artifact(artifact) + '.md5'): + prev_state = "present" + else: + path = os.path.dirname(dest) + if not os.path.exists(path): + os.makedirs(path) + + if prev_state == "present": + module.exit_json(dest=dest, state=state, changed=False) + + try: + if downloader.download(artifact, dest): + module.exit_json(state=state, dest=dest, group_id=group_id, artifact_id=artifact_id, version=version, classifier=classifier, extension=extension, repository_url=repository_url, changed=True) + else: + module.fail_json(msg="Unable to download the artifact") + except ValueError as e: + module.fail_json(msg=e.args[0]) + + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/extras/packaging/language/npm.py b/lib/ansible/modules/extras/packaging/language/npm.py new file mode 100644 index 0000000000..e15bbea903 --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/npm.py @@ -0,0 +1,271 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2013, Chris Hoffman <christopher.hoffman@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: npm +short_description: Manage node.js packages with npm +description: + - Manage node.js packages with Node Package Manager (npm) +version_added: 1.2 +author: "Chris Hoffman (@chrishoffman)" +options: + name: + description: + - The name of a node.js library to install + required: false + path: + description: + - The base path where to install the node.js libraries + required: false + version: + description: + - The version to be installed + required: false + global: + description: + - Install the node.js library globally + required: false + default: no + choices: [ "yes", "no" ] + executable: + description: + - The executable location for npm. + - This is useful if you are using a version manager, such as nvm + required: false + ignore_scripts: + description: + - Use the --ignore-scripts flag when installing. + required: false + choices: [ "yes", "no" ] + default: no + version_added: "1.8" + production: + description: + - Install dependencies in production mode, excluding devDependencies + required: false + choices: [ "yes", "no" ] + default: no + registry: + description: + - The registry to install modules from. + required: false + version_added: "1.6" + state: + description: + - The state of the node.js library + required: false + default: present + choices: [ "present", "absent", "latest" ] +''' + +EXAMPLES = ''' +description: Install "coffee-script" node.js package. +- npm: name=coffee-script path=/app/location + +description: Install "coffee-script" node.js package on version 1.6.1. +- npm: name=coffee-script version=1.6.1 path=/app/location + +description: Install "coffee-script" node.js package globally. +- npm: name=coffee-script global=yes + +description: Remove the globally package "coffee-script". +- npm: name=coffee-script global=yes state=absent + +description: Install "coffee-script" node.js package from custom registry. +- npm: name=coffee-script registry=http://registry.mysite.com + +description: Install packages based on package.json. +- npm: path=/app/location + +description: Update packages based on package.json to their latest version. +- npm: path=/app/location state=latest + +description: Install packages based on package.json using the npm installed with nvm v0.10.1. +- npm: path=/app/location executable=/opt/nvm/v0.10.1/bin/npm state=present +''' + +import os + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + # Let snippet from module_utils/basic.py return a proper error in this case + pass + + +class Npm(object): + def __init__(self, module, **kwargs): + self.module = module + self.glbl = kwargs['glbl'] + self.name = kwargs['name'] + self.version = kwargs['version'] + self.path = kwargs['path'] + self.registry = kwargs['registry'] + self.production = kwargs['production'] + self.ignore_scripts = kwargs['ignore_scripts'] + + if kwargs['executable']: + self.executable = kwargs['executable'].split(' ') + else: + self.executable = [module.get_bin_path('npm', True)] + + if kwargs['version']: + self.name_version = self.name + '@' + str(self.version) + else: + self.name_version = self.name + + def _exec(self, args, run_in_check_mode=False, check_rc=True): + if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): + cmd = self.executable + args + + if self.glbl: + cmd.append('--global') + if self.production: + cmd.append('--production') + if self.ignore_scripts: + cmd.append('--ignore-scripts') + if self.name: + cmd.append(self.name_version) + if self.registry: + cmd.append('--registry') + cmd.append(self.registry) + + #If path is specified, cd into that path and run the command. + cwd = None + if self.path: + if not os.path.exists(self.path): + os.makedirs(self.path) + if not os.path.isdir(self.path): + self.module.fail_json(msg="path %s is not a directory" % self.path) + cwd = self.path + + rc, out, err = self.module.run_command(cmd, check_rc=check_rc, cwd=cwd) + return out + return '' + + def list(self): + cmd = ['list', '--json'] + + installed = list() + missing = list() + data = json.loads(self._exec(cmd, True, False)) + if 'dependencies' in data: + for dep in data['dependencies']: + if 'missing' in data['dependencies'][dep] and data['dependencies'][dep]['missing']: + missing.append(dep) + elif 'invalid' in data['dependencies'][dep] and data['dependencies'][dep]['invalid']: + missing.append(dep) + else: + installed.append(dep) + if self.name and self.name not in installed: + missing.append(self.name) + #Named dependency not installed + else: + missing.append(self.name) + + return installed, missing + + def install(self): + return self._exec(['install']) + + def update(self): + return self._exec(['update']) + + def uninstall(self): + return self._exec(['uninstall']) + + def list_outdated(self): + outdated = list() + data = self._exec(['outdated'], True, False) + for dep in data.splitlines(): + if dep: + # node.js v0.10.22 changed the `npm outdated` module separator + # from "@" to " ". Split on both for backwards compatibility. + pkg, other = re.split('\s|@', dep, 1) + outdated.append(pkg) + + return outdated + + +def main(): + arg_spec = dict( + name=dict(default=None), + path=dict(default=None, type='path'), + version=dict(default=None), + production=dict(default='no', type='bool'), + executable=dict(default=None, type='path'), + registry=dict(default=None), + state=dict(default='present', choices=['present', 'absent', 'latest']), + ignore_scripts=dict(default=False, type='bool'), + ) + arg_spec['global'] = dict(default='no', type='bool') + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + name = module.params['name'] + path = module.params['path'] + version = module.params['version'] + glbl = module.params['global'] + production = module.params['production'] + executable = module.params['executable'] + registry = module.params['registry'] + state = module.params['state'] + ignore_scripts = module.params['ignore_scripts'] + + if not path and not glbl: + module.fail_json(msg='path must be specified when not using global') + if state == 'absent' and not name: + module.fail_json(msg='uninstalling a package is only available for named packages') + + npm = Npm(module, name=name, path=path, version=version, glbl=glbl, production=production, \ + executable=executable, registry=registry, ignore_scripts=ignore_scripts) + + changed = False + if state == 'present': + installed, missing = npm.list() + if len(missing): + changed = True + npm.install() + elif state == 'latest': + installed, missing = npm.list() + outdated = npm.list_outdated() + if len(missing): + changed = True + npm.install() + if len(outdated): + changed = True + npm.update() + else: #absent + installed, missing = npm.list() + if name in installed: + changed = True + npm.uninstall() + + module.exit_json(changed=changed) + +# import module snippets +from ansible.module_utils.basic import * +main() diff --git a/lib/ansible/modules/extras/packaging/language/pear.py b/lib/ansible/modules/extras/packaging/language/pear.py new file mode 100644 index 0000000000..5762f9c815 --- /dev/null +++ b/lib/ansible/modules/extras/packaging/language/pear.py @@ -0,0 +1,227 @@ +#!/usr/bin/python -tt +# -*- coding: utf-8 -*- + +# (c) 2012, Afterburn <http://github.com/afterburn> +# (c) 2013, Aaron Bull Schaefer <aaron@elasticdog.com> +# (c) 2015, Jonathan Lestrelin <jonathan.lestrelin@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: pear +short_description: Manage pear/pecl packages +description: + - Manage PHP packages with the pear package manager. +version_added: 2.0 +author: + - "'jonathan.lestrelin' <jonathan.lestrelin@gmail.com>" +options: + name: + description: + - Name of the package to install, upgrade, or remove. + required: true + + state: + description: + - Desired state of the package. + required: false + default: "present" + choices: ["present", "absent", "latest"] +''' + +EXAMPLES = ''' +# Install pear package +- pear: name=Net_URL2 state=present + +# Install pecl package +- pear: name=pecl/json_post state=present + +# Upgrade package +- pear: name=Net_URL2 state=latest + +# Remove packages +- pear: name=Net_URL2,pecl/json_post state=absent +''' + +import os + +def get_local_version(pear_output): + """Take pear remoteinfo output and get the installed version""" + lines = pear_output.split('\n') + for line in lines: + if 'Installed ' in line: + installed = line.rsplit(None, 1)[-1].strip() + if installed == '-': continue + return installed + return None + +def get_repository_version(pear_output): + """Take pear remote-info output and get the latest version""" + lines = pear_output.split('\n') + for line in lines: + if 'Latest ' in line: + return line.rsplit(None, 1)[-1].strip() + return None + +def query_package(module, name, state="present"): + """Query the package status in both the local system and the repository. + Returns a boolean to indicate if the package is installed, + and a second boolean to indicate if the package is up-to-date.""" + if state == "present": + lcmd = "pear info %s" % (name) + lrc, lstdout, lstderr = module.run_command(lcmd, check_rc=False) + if lrc != 0: + # package is not installed locally + return False, False + + rcmd = "pear remote-info %s" % (name) + rrc, rstdout, rstderr = module.run_command(rcmd, check_rc=False) + + # get the version installed locally (if any) + lversion = get_local_version(rstdout) + + # get the version in the repository + rversion = get_repository_version(rstdout) + + if rrc == 0: + # Return True to indicate that the package is installed locally, + # and the result of the version number comparison + # to determine if the package is up-to-date. + return True, (lversion == rversion) + + return False, False + + +def remove_packages(module, packages): + remove_c = 0 + # Using a for loop incase of error, we can report the package that failed + for package in packages: + # Query the package first, to see if we even need to remove + installed, updated = query_package(module, package) + if not installed: + continue + + cmd = "pear uninstall %s" % (package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to remove %s" % (package)) + + remove_c += 1 + + if remove_c > 0: + + module.exit_json(changed=True, msg="removed %s package(s)" % remove_c) + + module.exit_json(changed=False, msg="package(s) already absent") + + +def install_packages(module, state, packages): + install_c = 0 + + for i, package in enumerate(packages): + # if the package is installed and state == present + # or state == latest and is up-to-date then skip + installed, updated = query_package(module, package) + if installed and (state == 'present' or (state == 'latest' and updated)): + continue + + if state == 'present': + command = 'install' + + if state == 'latest': + command = 'upgrade' + + cmd = "pear %s %s" % (command, package) + rc, stdout, stderr = module.run_command(cmd, check_rc=False) + + if rc != 0: + module.fail_json(msg="failed to install %s" % (package)) + + install_c += 1 + + if install_c > 0: + module.exit_json(changed=True, msg="installed %s package(s)" % (install_c)) + + module.exit_json(changed=False, msg="package(s) already installed") + + +def check_packages(module, packages, state): + would_be_changed = [] + for package in packages: + installed, updated = query_package(module, package) + if ((state in ["present", "latest"] and not installed) or + (state == "absent" and installed) or + (state == "latest" and not updated)): + would_be_changed.append(package) + if would_be_changed: + if state == "absent": + state = "removed" + module.exit_json(changed=True, msg="%s package(s) would be %s" % ( + len(would_be_changed), state)) + else: + module.exit_json(change=False, msg="package(s) already %s" % state) + + +def exe_exists(program): + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK): + return True + + return False + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(aliases=['pkg']), + state = dict(default='present', choices=['present', 'installed', "latest", 'absent', 'removed'])), + required_one_of = [['name']], + supports_check_mode = True) + + if not exe_exists("pear"): + module.fail_json(msg="cannot find pear executable in PATH") + + p = module.params + + # normalize the state parameter + if p['state'] in ['present', 'installed']: + p['state'] = 'present' + elif p['state'] in ['absent', 'removed']: + p['state'] = 'absent' + + if p['name']: + pkgs = p['name'].split(',') + + pkg_files = [] + for i, pkg in enumerate(pkgs): + pkg_files.append(None) + + if module.check_mode: + check_packages(module, pkgs, p['state']) + + if p['state'] in ['present', 'latest']: + install_packages(module, p['state'], pkgs) + elif p['state'] == 'absent': + remove_packages(module, pkgs) + +# import module snippets +from ansible.module_utils.basic import * + +main() |