summaryrefslogtreecommitdiff
path: root/commands/command.py
diff options
context:
space:
mode:
authorMichael DeHaan <michael.dehaan@gmail.com>2014-09-26 10:37:56 -0400
committerMichael DeHaan <michael.dehaan@gmail.com>2014-09-26 10:37:56 -0400
commitc8e1a2077ed356fb893cd9886f758ac3d284e6b9 (patch)
tree2c8238e5cabfd8f0f088707a4ef70f5dade57bea /commands/command.py
parent417309a626e39396196a4abc6208c9f6db158f9d (diff)
downloadansible-modules-core-c8e1a2077ed356fb893cd9886f758ac3d284e6b9.tar.gz
file extensions!
Diffstat (limited to 'commands/command.py')
-rw-r--r--commands/command.py275
1 files changed, 275 insertions, 0 deletions
diff --git a/commands/command.py b/commands/command.py
new file mode 100644
index 00000000..c1fabd4f
--- /dev/null
+++ b/commands/command.py
@@ -0,0 +1,275 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
+#
+# 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/>.
+
+import sys
+import datetime
+import traceback
+import re
+import shlex
+import os
+
+DOCUMENTATION = '''
+---
+module: command
+version_added: historical
+short_description: Executes a command on a remote node
+description:
+ - The M(command) module takes the command name followed by a list of space-delimited arguments.
+ - The given command will be executed on all selected nodes. It will not be
+ processed through the shell, so variables like C($HOME) and operations
+ like C("<"), C(">"), C("|"), and C("&") will not work (use the M(shell)
+ module if you need these features).
+options:
+ free_form:
+ description:
+ - the command module takes a free form command to run. There is no parameter actually named 'free form'.
+ See the examples!
+ required: true
+ default: null
+ aliases: []
+ creates:
+ description:
+ - a filename, when it already exists, this step will B(not) be run.
+ required: no
+ default: null
+ removes:
+ description:
+ - a filename, when it does not exist, this step will B(not) be run.
+ version_added: "0.8"
+ required: no
+ default: null
+ chdir:
+ description:
+ - cd into this directory before running the command
+ version_added: "0.6"
+ required: false
+ default: null
+ executable:
+ description:
+ - change the shell used to execute the command. Should be an absolute path to the executable.
+ required: false
+ default: null
+ version_added: "0.9"
+ warn:
+ version_added: "1.8"
+ default: yes
+ description:
+ - if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false.
+ required: false
+ default: True
+notes:
+ - If you want to run a command through the shell (say you are using C(<),
+ C(>), C(|), etc), you actually want the M(shell) module instead. The
+ M(command) module is much more secure as it's not affected by the user's
+ environment.
+ - " C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not exist, use this."
+author: Michael DeHaan
+'''
+
+EXAMPLES = '''
+# Example from Ansible Playbooks.
+- command: /sbin/shutdown -t now
+
+# Run the command if the specified file does not exist.
+- command: /usr/bin/make_database.sh arg1 arg2 creates=/path/to/database
+
+# You can also use the 'args' form to provide the options. This command
+# will change the working directory to somedir/ and will only run when
+# /path/to/database doesn't exist.
+- command: /usr/bin/make_database.sh arg1 arg2
+ args:
+ chdir: somedir/
+ creates: /path/to/database
+'''
+
+# This is a pretty complex regex, which functions as follows:
+#
+# 1. (^|\s)
+# ^ look for a space or the beginning of the line
+# 2. (creates|removes|chdir|executable|NO_LOG)=
+# ^ look for a valid param, followed by an '='
+# 3. (?P<quote>[\'"])?
+# ^ look for an optional quote character, which can either be
+# a single or double quote character, and store it for later
+# 4. (.*?)
+# ^ match everything in a non-greedy manner until...
+# 5. (?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)
+# ^ a non-escaped space or a non-escaped quote of the same kind
+# that was matched in the first 'quote' is found, or the end of
+# the line is reached
+
+PARAM_REGEX = re.compile(r'(^|\s)(creates|removes|chdir|executable|NO_LOG|warn)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
+
+
+def check_command(commandline):
+ arguments = { 'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group',
+ 'ln': 'state=link', 'mkdir': 'state=directory',
+ 'rmdir': 'state=absent', 'rm': 'state=absent', 'touch': 'state=touch' }
+ commands = { 'git': 'git', 'hg': 'hg', 'curl': 'get_url', 'wget': 'get_url',
+ 'svn': 'subversion', 'service': 'service',
+ 'mount': 'mount', 'rpm': 'yum', 'yum': 'yum', 'apt-get': 'apt-get',
+ 'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'template or lineinfile',
+ 'rsync': 'synchronize' }
+ warnings = list()
+ command = os.path.basename(commandline.split()[0])
+ if command in arguments:
+ warnings.append("Consider using file module with %s rather than running %s" % (arguments[command], command))
+ if command in commands:
+ warnings.append("Consider using %s module rather than running %s" % (commands[command], command))
+ return warnings
+
+
+def main():
+
+ # the command module is the one ansible module that does not take key=value args
+ # hence don't copy this one if you are looking to build others!
+ module = CommandModule(argument_spec=dict())
+
+ shell = module.params['shell']
+ chdir = module.params['chdir']
+ executable = module.params['executable']
+ args = module.params['args']
+ creates = module.params['creates']
+ removes = module.params['removes']
+ warn = module.params.get('warn', True)
+
+ if args.strip() == '':
+ module.fail_json(rc=256, msg="no command given")
+
+ if chdir:
+ os.chdir(chdir)
+
+ if creates:
+ # do not run the command if the line contains creates=filename
+ # and the filename already exists. This allows idempotence
+ # of command executions.
+ v = os.path.expanduser(creates)
+ if os.path.exists(v):
+ module.exit_json(
+ cmd=args,
+ stdout="skipped, since %s exists" % v,
+ changed=False,
+ stderr=False,
+ rc=0
+ )
+
+ if removes:
+ # do not run the command if the line contains removes=filename
+ # and the filename does not exist. This allows idempotence
+ # of command executions.
+ v = os.path.expanduser(removes)
+ if not os.path.exists(v):
+ module.exit_json(
+ cmd=args,
+ stdout="skipped, since %s does not exist" % v,
+ changed=False,
+ stderr=False,
+ rc=0
+ )
+
+ warnings = list()
+ if warn:
+ warnings = check_command(args)
+
+ if not shell:
+ args = shlex.split(args)
+ startd = datetime.datetime.now()
+
+ rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell)
+
+ endd = datetime.datetime.now()
+ delta = endd - startd
+
+ if out is None:
+ out = ''
+ if err is None:
+ err = ''
+
+ module.exit_json(
+ cmd = args,
+ stdout = out.rstrip("\r\n"),
+ stderr = err.rstrip("\r\n"),
+ rc = rc,
+ start = str(startd),
+ end = str(endd),
+ delta = str(delta),
+ changed = True,
+ warnings = warnings
+ )
+
+# import module snippets
+from ansible.module_utils.basic import *
+from ansible.module_utils.splitter import *
+
+# only the command module should ever need to do this
+# everything else should be simple key=value
+
+class CommandModule(AnsibleModule):
+
+ def _handle_aliases(self):
+ return {}
+
+ def _check_invalid_arguments(self):
+ pass
+
+ def _load_params(self):
+ ''' read the input and return a dictionary and the arguments string '''
+ args = MODULE_ARGS
+ params = {}
+ params['chdir'] = None
+ params['creates'] = None
+ params['removes'] = None
+ params['shell'] = False
+ params['executable'] = None
+ params['warn'] = True
+ if "#USE_SHELL" in args:
+ args = args.replace("#USE_SHELL", "")
+ params['shell'] = True
+
+ items = split_args(args)
+
+ for x in items:
+ quoted = x.startswith('"') and x.endswith('"') or x.startswith("'") and x.endswith("'")
+ if '=' in x and not quoted:
+ # check to see if this is a special parameter for the command
+ k, v = x.split('=', 1)
+ v = unquote(v)
+ # because we're not breaking out quotes in the shlex split
+ # above, the value of the k=v pair may still be quoted. If
+ # so, remove them.
+ if len(v) > 1 and (v.startswith('"') and v.endswith('"') or v.startswith("'") and v.endswith("'")):
+ v = v[1:-1]
+ if k in ('creates', 'removes', 'chdir', 'executable', 'NO_LOG'):
+ if k == "chdir":
+ v = os.path.abspath(os.path.expanduser(v))
+ if not (os.path.exists(v) and os.path.isdir(v)):
+ self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v)
+ elif k == "executable":
+ v = os.path.abspath(os.path.expanduser(v))
+ if not (os.path.exists(v)):
+ self.fail_json(rc=258, msg="cannot use executable '%s': file does not exist" % v)
+ params[k] = v
+ # Remove any of the above k=v params from the args string
+ args = PARAM_REGEX.sub('', args)
+ params['args'] = args.strip()
+
+ return (params, params['args'])
+
+main()