command 7.11 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 23
import subprocess
import sys
import datetime
24
import traceback
25
import re
26 27
import shlex
import os
28

29 30 31 32 33
DOCUMENTATION = '''
---
module: command
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50
     - 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
51 52 53 54 55 56
  removes:
    description:
      - a filename, when it does not exist, this step will B(not) be run.
    version_added: "0.8"
    required: no
    default: null
57 58 59 60 61 62
  chdir:
    description:
      - cd into this directory before running the command
    version_added: "0.6"
    required: false
    default: null
63 64 65 66 67 68
  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"
69
examples:
70
   - code: "command: /sbin/shutdown -t now"
71
     description: "Example from Ansible Playbooks"
72
   - code: "command: /usr/bin/make_database.sh arg1 arg2 creates=/path/to/database"
Jan-Piet Mens committed
73
     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."
74 75 76 77 78 79 80 81
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
'''

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

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

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

99 100 101 102 103
    if not shell:
        args = shlex.split(args)
    startd = datetime.datetime.now()

    try:
104
        cmd = subprocess.Popen(args, executable=executable, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
105 106
        out, err = cmd.communicate()
    except (OSError, IOError), e:
107
        module.fail_json(rc=e.errno, msg=str(e), cmd=args)
108
    except:
109
        module.fail_json(rc=257, msg=traceback.format_exc(), cmd=args)
110 111 112 113 114

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

    if out is None:
115
        out = ''
116
    if err is None:
117
        err = ''
118 119 120

    module.exit_json(
        cmd     = args,
121 122
        stdout  = out.rstrip("\r\n"),
        stderr  = err.rstrip("\r\n"),
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
        rc      = cmd.returncode,
        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):

138 139 140 141 142 143
    def _handle_aliases(self):
        pass

    def _check_invalid_arguments(self):
        pass

144 145
    def _load_params(self):
        ''' read the input and return a dictionary and the arguments string '''
146
        args = MODULE_ARGS
147
        params = {}
148
        params['chdir'] = None
149
        params['shell'] = False
150
        params['executable'] = None
151
        if args.find("#USE_SHELL") != -1:
Michael DeHaan committed
152 153
            args = args.replace("#USE_SHELL", "")
            params['shell'] = True
154

155
        r = re.compile(r'(^|\s)(creates|removes|chdir|executable)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
156 157 158
        for m in r.finditer(args):
            v = m.group(4).replace("\\", "")
            if m.group(2) == "creates":
159 160 161
                # do not run the command if the line contains creates=filename
                # and the filename already exists.  This allows idempotence
                # of command executions.
162
                v = os.path.expanduser(v)
163 164 165 166 167 168 169 170 171
                if os.path.exists(v):
                    self.exit_json(
                        cmd=args,
                        stdout="skipped, since %s exists" % v,
                        skipped=True,
                        changed=False,
                        stderr=False,
                        rc=0
                    )
172
            elif m.group(2) == "removes":
173 174 175
                # do not run the command if the line contains removes=filename
                # and the filename do not exists.  This allows idempotence
                # of command executions.
176
                v = os.path.expanduser(v)
177 178 179
                if not os.path.exists(v):
                    self.exit_json(
                        cmd=args,
180
                        stdout="skipped, since %s does not exist" % v,
181 182 183 184 185
                        skipped=True,
                        changed=False,
                        stderr=False,
                        rc=0
                    )
186
            elif m.group(2) == "chdir":
187
                v = os.path.expanduser(v)
188
                if not (os.path.exists(v) and os.path.isdir(v)):
189
                    self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v)
190
                elif v[0] != '/':
191
                    self.fail_json(rc=259, msg="the path for 'chdir' argument must be fully qualified")
192
                params['chdir'] = v
193 194 195 196 197 198 199
            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
200 201
        args = r.sub("", args)
        params['args'] = args
202
        return (params, params['args'])
203 204

main()