summaryrefslogtreecommitdiff
path: root/lib/ansible/modules/extras/packaging/language
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/modules/extras/packaging/language')
-rw-r--r--lib/ansible/modules/extras/packaging/language/__init__.py0
-rw-r--r--lib/ansible/modules/extras/packaging/language/bower.py227
-rw-r--r--lib/ansible/modules/extras/packaging/language/bundler.py211
-rw-r--r--lib/ansible/modules/extras/packaging/language/composer.py233
-rw-r--r--lib/ansible/modules/extras/packaging/language/cpanm.py220
-rw-r--r--lib/ansible/modules/extras/packaging/language/maven_artifact.py390
-rw-r--r--lib/ansible/modules/extras/packaging/language/npm.py271
-rw-r--r--lib/ansible/modules/extras/packaging/language/pear.py227
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()