1#!/usr/local/bin/python3.8 2# 3# Copyright (c) 2021, Felix Fontein <felix@fontein.de> 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9 10DOCUMENTATION = ''' 11--- 12module: docker_container_exec 13 14short_description: Execute command in a docker container 15 16version_added: 1.5.0 17 18description: 19 - Executes a command in a Docker container. 20 21options: 22 container: 23 type: str 24 required: true 25 description: 26 - The name of the container to execute the command in. 27 argv: 28 type: list 29 elements: str 30 description: 31 - The command to execute. 32 - Since this is a list of arguments, no quoting is needed. 33 - Exactly one of I(argv) and I(command) must be specified. 34 command: 35 type: str 36 description: 37 - The command to execute. 38 - Exactly one of I(argv) and I(command) must be specified. 39 chdir: 40 type: str 41 description: 42 - The directory to run the command in. 43 user: 44 type: str 45 description: 46 - If specified, the user to execute this command with. 47 stdin: 48 type: str 49 description: 50 - Set the stdin of the command directly to the specified value. 51 stdin_add_newline: 52 type: bool 53 default: true 54 description: 55 - If set to C(true), appends a newline to I(stdin). 56 strip_empty_ends: 57 type: bool 58 default: true 59 description: 60 - Strip empty lines from the end of stdout/stderr in result. 61 tty: 62 type: bool 63 default: false 64 description: 65 - Whether to allocate a TTY. 66 67extends_documentation_fragment: 68 - community.docker.docker 69 - community.docker.docker.docker_py_1_documentation 70notes: 71 - Does not support C(check_mode). 72author: 73 - "Felix Fontein (@felixfontein)" 74 75requirements: 76 - "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.8.0 (use L(docker-py,https://pypi.org/project/docker-py/) for Python 2.6)" 77 - "Docker API >= 1.20" 78''' 79 80EXAMPLES = ''' 81- name: Run a simple command (command) 82 community.docker.docker_container_exec: 83 container: foo 84 command: /bin/bash -c "ls -lah" 85 chdir: /root 86 register: result 87 88- name: Print stdout 89 debug: 90 var: result.stdout 91 92- name: Run a simple command (argv) 93 community.docker.docker_container_exec: 94 container: foo 95 argv: 96 - /bin/bash 97 - "-c" 98 - "ls -lah > /dev/stderr" 99 chdir: /root 100 register: result 101 102- name: Print stderr lines 103 debug: 104 var: result.stderr_lines 105''' 106 107RETURN = ''' 108stdout: 109 type: str 110 returned: success 111 description: 112 - The standard output of the container command. 113stderr: 114 type: str 115 returned: success 116 description: 117 - The standard error output of the container command. 118rc: 119 type: int 120 returned: success 121 sample: 0 122 description: 123 - The exit code of the command. 124''' 125 126import shlex 127import traceback 128 129from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native 130 131from ansible_collections.community.docker.plugins.module_utils.common import ( 132 AnsibleDockerClient, 133 RequestException, 134) 135 136from ansible_collections.community.docker.plugins.module_utils.socket_helper import ( 137 shutdown_writing, 138 write_to_socket, 139) 140 141from ansible_collections.community.docker.plugins.module_utils.socket_handler import ( 142 find_selectors, 143 DockerSocketHandlerModule, 144) 145 146try: 147 from docker.errors import DockerException, APIError, NotFound 148except Exception: 149 # missing Docker SDK for Python handled in ansible.module_utils.docker.common 150 pass 151 152 153def main(): 154 argument_spec = dict( 155 container=dict(type='str', required=True), 156 argv=dict(type='list', elements='str'), 157 command=dict(type='str'), 158 chdir=dict(type='str'), 159 user=dict(type='str'), 160 stdin=dict(type='str'), 161 stdin_add_newline=dict(type='bool', default=True), 162 strip_empty_ends=dict(type='bool', default=True), 163 tty=dict(type='bool', default=False), 164 ) 165 166 client = AnsibleDockerClient( 167 argument_spec=argument_spec, 168 min_docker_api_version='1.20', 169 mutually_exclusive=[('argv', 'command')], 170 required_one_of=[('argv', 'command')], 171 ) 172 173 container = client.module.params['container'] 174 argv = client.module.params['argv'] 175 command = client.module.params['command'] 176 chdir = client.module.params['chdir'] 177 user = client.module.params['user'] 178 stdin = client.module.params['stdin'] 179 strip_empty_ends = client.module.params['strip_empty_ends'] 180 tty = client.module.params['tty'] 181 182 if command is not None: 183 argv = shlex.split(command) 184 185 if stdin is not None and client.module.params['stdin_add_newline']: 186 stdin += '\n' 187 188 selectors = None 189 if stdin: 190 selectors = find_selectors(client.module) 191 192 try: 193 exec_data = client.exec_create( 194 container, 195 argv, 196 stdout=True, 197 stderr=True, 198 stdin=bool(stdin), 199 user=user or '', 200 workdir=chdir, 201 ) 202 exec_id = exec_data['Id'] 203 204 if selectors: 205 exec_socket = client.exec_start( 206 exec_id, 207 tty=tty, 208 detach=False, 209 socket=True, 210 ) 211 try: 212 with DockerSocketHandlerModule(exec_socket, client.module, selectors) as exec_socket_handler: 213 if stdin: 214 exec_socket_handler.write(to_bytes(stdin)) 215 216 stdout, stderr = exec_socket_handler.consume() 217 finally: 218 exec_socket.close() 219 else: 220 stdout, stderr = client.exec_start( 221 exec_id, 222 tty=tty, 223 detach=False, 224 stream=False, 225 socket=False, 226 demux=True, 227 ) 228 229 result = client.exec_inspect(exec_id) 230 231 stdout = to_text(stdout or b'') 232 stderr = to_text(stderr or b'') 233 if strip_empty_ends: 234 stdout = stdout.rstrip('\r\n') 235 stderr = stderr.rstrip('\r\n') 236 237 client.module.exit_json( 238 changed=True, 239 stdout=stdout, 240 stderr=stderr, 241 rc=result.get('ExitCode') or 0, 242 ) 243 except NotFound: 244 client.fail('Could not find container "{0}"'.format(container)) 245 except APIError as e: 246 if e.response and e.response.status_code == 409: 247 client.fail('The container "{0}" has been paused ({1})'.format(container, to_native(e))) 248 client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) 249 except DockerException as e: 250 client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) 251 except RequestException as e: 252 client.fail( 253 'An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(to_native(e)), 254 exception=traceback.format_exc()) 255 256 257if __name__ == '__main__': 258 main() 259