command 6.87 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 31 32
DOCUMENTATION = '''
---
module: command
short_description: Executes a command on a remote node
description:
Jan-Piet Mens committed
33
     - The M(command) module takes the command name followed by a list of space-delimited arguments.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
     - 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. As such, all
       paths to commands must be fully qualified
options:
  free_form:
    description:
      - the command module takes a free form command to run
    required: true
    default: null
    aliases: []
  creates:
    description:
      - a filename, when it already exists, this step will B(not) be run.
    required: no
    default: null
50 51 52 53 54 55
  removes:
    description:
      - a filename, when it does not exist, this step will B(not) be run.
    version_added: "0.8"
    required: no
    default: null
56 57 58 59 60 61
  chdir:
    description:
      - cd into this directory before running the command
    version_added: "0.6"
    required: false
    default: null
62 63 64 65 66 67
  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"
68
examples:
69
   - code: "command: /sbin/shutdown -t now"
70
     description: "Example from Ansible Playbooks"
71
   - code: "command: /usr/bin/make_database.sh arg1 arg2 creates=/path/to/database"
Jan-Piet Mens committed
72
     description: "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."
73 74 75 76 77 78 79 80
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.
author: Michael DeHaan
'''

81 82 83 84 85 86 87
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']
88
    chdir = module.params['chdir']
89
    executable = module.params['executable']
90
    args  = module.params['args']
91 92
    creates  = module.params['creates']
    removes  = module.params['removes']
93

94
    if args.strip() == '':
95
        module.fail_json(rc=256, msg="no command given")
Michael DeHaan committed
96

97
    if chdir:
98
        os.chdir(os.path.expanduser(chdir))
99

100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
    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
            )

130 131 132 133
    if not shell:
        args = shlex.split(args)
    startd = datetime.datetime.now()

134
    rc, out, err = module.run_command(args, executable=executable)
135 136 137 138 139

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

    if out is None:
140
        out = ''
141
    if err is None:
142
        err = ''
143 144 145

    module.exit_json(
        cmd     = args,
146 147
        stdout  = out.rstrip("\r\n"),
        stderr  = err.rstrip("\r\n"),
148
        rc      = rc,
149 150 151 152 153 154 155 156 157 158 159 160 161 162
        start   = str(startd),
        end     = str(endd),
        delta   = str(delta),
        changed = True
    )

# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

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

class CommandModule(AnsibleModule):

163
    def _handle_aliases(self):
164
        return {}
165 166 167 168

    def _check_invalid_arguments(self):
        pass

169 170
    def _load_params(self):
        ''' read the input and return a dictionary and the arguments string '''
171
        args = MODULE_ARGS
172
        params = {}
173
        params['chdir'] = None
174 175
        params['creates'] = None
        params['removes'] = None
176
        params['shell'] = False
177
        params['executable'] = None
178
        if args.find("#USE_SHELL") != -1:
Michael DeHaan committed
179 180
            args = args.replace("#USE_SHELL", "")
            params['shell'] = True
181

182
        r = re.compile(r'(^|\s)(creates|removes|chdir|executable)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
183 184 185
        for m in r.finditer(args):
            v = m.group(4).replace("\\", "")
            if m.group(2) == "creates":
186
                params['creates'] = v
187
            elif m.group(2) == "removes":
188
                params['removes'] = v
189
            elif m.group(2) == "chdir":
190
                v = os.path.expanduser(v)
191
                if not (os.path.exists(v) and os.path.isdir(v)):
192
                    self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v)
193
                elif v[0] != os.sep:
194
                    self.fail_json(rc=259, msg="the path for 'chdir' argument must be fully qualified")
195
                params['chdir'] = v
196 197 198 199 200 201 202
            elif m.group(2) == "executable":
                v = 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)
                elif v[0] != '/':
                    self.fail_json(rc=259, msg="the path for 'executable' argument must be fully qualified")
                params['executable'] = v
203 204
        args = r.sub("", args)
        params['args'] = args
205
        return (params, params['args'])
206 207

main()