command 8.16 KB
Newer Older
Michael DeHaan committed
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
Michael DeHaan committed
3

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# (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/>.
20

Michael DeHaan committed
21 22
import sys
import datetime
23
import traceback
24
import re
25 26
import shlex
import os
27

28 29 30
DOCUMENTATION = '''
---
module: command
31
version_added: historical
32 33
short_description: Executes a command on a remote node
description:
Jan-Piet Mens committed
34
     - The M(command) module takes the command name followed by a list of space-delimited arguments.
35 36
     - 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
37 38
       like C("<"), C(">"), C("|"), and C("&") will not work (use the M(shell)
       module if you need these features).
39 40 41
options:
  free_form:
    description:
42 43
      - the command module takes a free form command to run.  There is no parameter actually named 'free form'.
        See the examples!
44 45 46 47 48 49 50 51
    required: true
    default: null
    aliases: []
  creates:
    description:
      - a filename, when it already exists, this step will B(not) be run.
    required: no
    default: null
52 53 54 55 56 57
  removes:
    description:
      - a filename, when it does not exist, this step will B(not) be run.
    version_added: "0.8"
    required: no
    default: null
58 59 60
  chdir:
    description:
      - cd into this directory before running the command
61
    version_added: "0.6"
62 63
    required: false
    default: null
64 65 66 67 68 69
  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"
70 71 72 73 74
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.
75
    -  " 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."
76 77 78
author: Michael DeHaan
'''

79 80 81 82 83 84 85 86
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
'''

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
# 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)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')

105 106 107 108 109 110 111
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']
112
    chdir = module.params['chdir']
113
    executable = module.params['executable']
114
    args  = module.params['args']
115 116
    creates  = module.params['creates']
    removes  = module.params['removes']
117

118
    if args.strip() == '':
119
        module.fail_json(rc=256, msg="no command given")
Michael DeHaan committed
120

121
    if chdir:
122
        os.chdir(chdir)
123

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
    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,
                skipped=True,
                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,
                skipped=True,
                changed=False,
                stderr=False,
                rc=0
            )

154 155 156 157
    if not shell:
        args = shlex.split(args)
    startd = datetime.datetime.now()

158
    rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell)
159 160 161 162 163

    endd = datetime.datetime.now()
    delta = endd - startd

    if out is None:
164
        out = ''
165
    if err is None:
166
        err = ''
167 168 169

    module.exit_json(
        cmd     = args,
170 171
        stdout  = out.rstrip("\r\n"),
        stderr  = err.rstrip("\r\n"),
172
        rc      = rc,
173 174 175 176 177 178
        start   = str(startd),
        end     = str(endd),
        delta   = str(delta),
        changed = True
    )

179 180
# import module snippets
from ansible.module_utils.basic import *
181 182 183 184 185 186

# only the command module should ever need to do this
# everything else should be simple key=value

class CommandModule(AnsibleModule):

187
    def _handle_aliases(self):
188
        return {}
189 190 191 192

    def _check_invalid_arguments(self):
        pass

193 194
    def _load_params(self):
        ''' read the input and return a dictionary and the arguments string '''
195
        args = MODULE_ARGS
196
        params = {}
James Cammarata committed
197 198 199 200
        params['chdir']      = None
        params['creates']    = None
        params['removes']    = None
        params['shell']      = False
201
        params['executable'] = None
202
        if "#USE_SHELL" in args:
Michael DeHaan committed
203 204
            args = args.replace("#USE_SHELL", "")
            params['shell'] = True
205

James Cammarata committed
206 207
        # use shlex to split up the args, while being careful to preserve
        # single quotes so they're not removed accidentally
208 209
        lexer = shlex.shlex(args)
        lexer.whitespace = '\t '
James Cammarata committed
210 211 212 213
        lexer.whitespace_split = True
        items = list(lexer)

        for x in items:
214 215
            quoted = x.startswith('"') and x.endswith('"') or x.startswith("'") and x.endswith("'")
            if '=' in x and not quoted:
James Cammarata committed
216 217
                # check to see if this is a special parameter for the command
                k, v = x.split('=', 1)
218 219 220 221 222
                # 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]
James Cammarata committed
223 224 225 226 227 228 229 230 231 232
                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
233 234
        # Remove any of the above k=v params from the args string
        args = PARAM_REGEX.sub('', args)
235
        params['args'] = args.strip()
236
        return (params, params['args'])
237 238

main()