blob: eb4f3eb8fc67eae08284752e6bb45d01f8ba1500 [file] [log] [blame]
Scott Baker761e1062016-06-20 17:18:17 -07001# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
17#
18
19import os
20import re
21import subprocess
22import shlex
23import pipes
24import random
25import select
26import fcntl
27import hmac
28import pwd
29import gettext
30import pty
31from hashlib import sha1
32import ansible.constants as C
33from ansible.callbacks import vvv
34from ansible import errors
35from ansible import utils
36
37class Connection(object):
38 ''' ssh based connections '''
39
40 def __init__(self, runner, host, port, user, password, private_key_file, *args, **kwargs):
41 self.runner = runner
42 self.host = host
43 self.ipv6 = ':' in self.host
44 self.port = port
45 self.user = str(user)
46 self.password = password
47 self.private_key_file = private_key_file
48 self.HASHED_KEY_MAGIC = "|1|"
49 self.has_pipelining = True
50 #self.instance_id = "instance-00000045" # C.get_config(C.p, "xos", "instance_id", "INSTANCE_ID", None)
51 #self.instance_name = "onlab_hpc-355" # C.get_config(C.p, "xos", "instance_name", "SLIVER_NAME", None)
52
53 inject={}
54 inject= utils.combine_vars(inject, self.runner.inventory.get_variables(self.host))
55
56 self.instance_id = inject["instance_id"]
57 self.instance_name = inject["instance_name"]
58
59 fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
60 self.cp_dir = utils.prepare_writeable_dir('$HOME/.ansible/cp',mode=0700)
61 fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
62
63 def connect(self):
64 ''' connect to the remote host '''
65
66 vvv("ESTABLISH CONNECTION FOR USER: %s" % self.user, host=self.host)
67
68 self.common_args = []
69 extra_args = C.ANSIBLE_SSH_ARGS
70 if extra_args is not None:
71 # make sure there is no empty string added as this can produce weird errors
72 self.common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()]
73 else:
74 self.common_args += ["-o", "ControlMaster=auto",
75 "-o", "ControlPersist=60s",
76 "-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
77
78 self.common_args += ["-o", "ProxyCommand ssh -q -i %s %s@%s" % (self.private_key_file, self.instance_id, self.host)]
79
80 cp_in_use = False
81 cp_path_set = False
82 for arg in self.common_args:
83 if "ControlPersist" in arg:
84 cp_in_use = True
85 if "ControlPath" in arg:
86 cp_path_set = True
87
88 if cp_in_use and not cp_path_set:
89 self.common_args += ["-o", "ControlPath=%s" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self.cp_dir))]
90
91 if not C.HOST_KEY_CHECKING:
92 self.common_args += ["-o", "StrictHostKeyChecking=no"]
93
94 if self.port is not None:
95 self.common_args += ["-o", "Port=%d" % (self.port)]
96 if self.private_key_file is not None:
97 self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.private_key_file)]
98 elif self.runner.private_key_file is not None:
99 self.common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.runner.private_key_file)]
100 if self.password:
101 self.common_args += ["-o", "GSSAPIAuthentication=no",
102 "-o", "PubkeyAuthentication=no"]
103 else:
104 self.common_args += ["-o", "KbdInteractiveAuthentication=no",
105 "-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
106 "-o", "PasswordAuthentication=no"]
107 if self.user != pwd.getpwuid(os.geteuid())[0]:
108 self.common_args += ["-o", "User="+self.user]
109 self.common_args += ["-o", "ConnectTimeout=%d" % self.runner.timeout]
110
111 return self
112
113 def _run(self, cmd, indata):
114 if indata:
115 # do not use pseudo-pty
116 p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
117 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
118 stdin = p.stdin
119 else:
120 # try to use upseudo-pty
121 try:
122 # Make sure stdin is a proper (pseudo) pty to avoid: tcgetattr errors
123 master, slave = pty.openpty()
124 p = subprocess.Popen(cmd, stdin=slave,
125 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
126 stdin = os.fdopen(master, 'w', 0)
127 os.close(slave)
128 except:
129 p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
130 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
131 stdin = p.stdin
132
133 return (p, stdin)
134
135 def _password_cmd(self):
136 if self.password:
137 try:
138 p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE,
139 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
140 p.communicate()
141 except OSError:
142 raise errors.AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
143 (self.rfd, self.wfd) = os.pipe()
144 return ["sshpass", "-d%d" % self.rfd]
145 return []
146
147 def _send_password(self):
148 if self.password:
149 os.close(self.rfd)
150 os.write(self.wfd, "%s\n" % self.password)
151 os.close(self.wfd)
152
153 def _communicate(self, p, stdin, indata, su=False, sudoable=False, prompt=None):
154 fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
155 fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
156 # We can't use p.communicate here because the ControlMaster may have stdout open as well
157 stdout = ''
158 stderr = ''
159 rpipes = [p.stdout, p.stderr]
160 if indata:
161 try:
162 stdin.write(indata)
163 stdin.close()
164 except:
165 raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
166 # Read stdout/stderr from process
167 while True:
168 rfd, wfd, efd = select.select(rpipes, [], rpipes, 1)
169
170 # fail early if the sudo/su password is wrong
171 if self.runner.sudo and sudoable:
172 if self.runner.sudo_pass:
173 incorrect_password = gettext.dgettext(
174 "sudo", "Sorry, try again.")
175 if stdout.endswith("%s\r\n%s" % (incorrect_password,
176 prompt)):
177 raise errors.AnsibleError('Incorrect sudo password')
178
179 if stdout.endswith(prompt):
180 raise errors.AnsibleError('Missing sudo password')
181
182 if self.runner.su and su and self.runner.su_pass:
183 incorrect_password = gettext.dgettext(
184 "su", "Sorry")
185 if stdout.endswith("%s\r\n%s" % (incorrect_password, prompt)):
186 raise errors.AnsibleError('Incorrect su password')
187
188 if p.stdout in rfd:
189 dat = os.read(p.stdout.fileno(), 9000)
190 stdout += dat
191 if dat == '':
192 rpipes.remove(p.stdout)
193 if p.stderr in rfd:
194 dat = os.read(p.stderr.fileno(), 9000)
195 stderr += dat
196 if dat == '':
197 rpipes.remove(p.stderr)
198 # only break out if no pipes are left to read or
199 # the pipes are completely read and
200 # the process is terminated
201 if (not rpipes or not rfd) and p.poll() is not None:
202 break
203 # No pipes are left to read but process is not yet terminated
204 # Only then it is safe to wait for the process to be finished
205 # NOTE: Actually p.poll() is always None here if rpipes is empty
206 elif not rpipes and p.poll() == None:
207 p.wait()
208 # The process is terminated. Since no pipes to read from are
209 # left, there is no need to call select() again.
210 break
211 # close stdin after process is terminated and stdout/stderr are read
212 # completely (see also issue #848)
213 stdin.close()
214 return (p.returncode, stdout, stderr)
215
216 def not_in_host_file(self, host):
217 if 'USER' in os.environ:
218 user_host_file = os.path.expandvars("~${USER}/.ssh/known_hosts")
219 else:
220 user_host_file = "~/.ssh/known_hosts"
221 user_host_file = os.path.expanduser(user_host_file)
222
223 host_file_list = []
224 host_file_list.append(user_host_file)
225 host_file_list.append("/etc/ssh/ssh_known_hosts")
226 host_file_list.append("/etc/ssh/ssh_known_hosts2")
227
228 hfiles_not_found = 0
229 for hf in host_file_list:
230 if not os.path.exists(hf):
231 hfiles_not_found += 1
232 continue
233 try:
234 host_fh = open(hf)
235 except IOError, e:
236 hfiles_not_found += 1
237 continue
238 else:
239 data = host_fh.read()
240 host_fh.close()
241
242 for line in data.split("\n"):
243 if line is None or " " not in line:
244 continue
245 tokens = line.split()
246 if tokens[0].find(self.HASHED_KEY_MAGIC) == 0:
247 # this is a hashed known host entry
248 try:
249 (kn_salt,kn_host) = tokens[0][len(self.HASHED_KEY_MAGIC):].split("|",2)
250 hash = hmac.new(kn_salt.decode('base64'), digestmod=sha1)
251 hash.update(host)
252 if hash.digest() == kn_host.decode('base64'):
253 return False
254 except:
255 # invalid hashed host key, skip it
256 continue
257 else:
258 # standard host file entry
259 if host in tokens[0]:
260 return False
261
262 if (hfiles_not_found == len(host_file_list)):
263 vvv("EXEC previous known host file not found for %s" % host)
264 return True
265
266 def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, executable='/bin/sh', in_data=None, su_user=None, su=False):
267 ''' run a command on the remote host '''
268
269 ssh_cmd = self._password_cmd()
270 ssh_cmd += ["ssh", "-C"]
271 if not in_data:
272 # we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
273 # inside a tty automatically invokes the python interactive-mode but the modules are not
274 # compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
275 ssh_cmd += ["-tt"]
276 if utils.VERBOSITY > 3:
277 ssh_cmd += ["-vvv"]
278 else:
279 ssh_cmd += ["-q"]
280 ssh_cmd += self.common_args
281
282 if self.ipv6:
283 ssh_cmd += ['-6']
284 #ssh_cmd += [self.host]
285 ssh_cmd += [self.instance_name]
286
287 if su and su_user:
288 sudocmd, prompt, success_key = utils.make_su_cmd(su_user, executable, cmd)
289 prompt_re = re.compile(prompt)
290 ssh_cmd.append(sudocmd)
291 elif not self.runner.sudo or not sudoable:
292 prompt = None
293 if executable:
294 ssh_cmd.append(executable + ' -c ' + pipes.quote(cmd))
295 else:
296 ssh_cmd.append(cmd)
297 else:
298 sudocmd, prompt, success_key = utils.make_sudo_cmd(sudo_user, executable, cmd)
299 ssh_cmd.append(sudocmd)
300
301 vvv("EXEC %s" % ssh_cmd, host=self.host)
302
303 not_in_host_file = self.not_in_host_file(self.host)
304
305 if C.HOST_KEY_CHECKING and not_in_host_file:
306 # lock around the initial SSH connectivity so the user prompt about whether to add
307 # the host to known hosts is not intermingled with multiprocess output.
308 fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_EX)
309 fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_EX)
310
311 # create process
312 (p, stdin) = self._run(ssh_cmd, in_data)
313
314 self._send_password()
315
316 if (self.runner.sudo and sudoable and self.runner.sudo_pass) or \
317 (self.runner.su and su and self.runner.su_pass):
318 # several cases are handled for sudo privileges with password
319 # * NOPASSWD (tty & no-tty): detect success_key on stdout
320 # * without NOPASSWD:
321 # * detect prompt on stdout (tty)
322 # * detect prompt on stderr (no-tty)
323 fcntl.fcntl(p.stdout, fcntl.F_SETFL,
324 fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
325 fcntl.fcntl(p.stderr, fcntl.F_SETFL,
326 fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
327 sudo_output = ''
328 sudo_errput = ''
329
330 while True:
331 if success_key in sudo_output or \
332 (self.runner.sudo_pass and sudo_output.endswith(prompt)) or \
333 (self.runner.su_pass and prompt_re.match(sudo_output)):
334 break
335
336 rfd, wfd, efd = select.select([p.stdout, p.stderr], [],
337 [p.stdout], self.runner.timeout)
338 if p.stderr in rfd:
339 chunk = p.stderr.read()
340 if not chunk:
341 raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
342 sudo_errput += chunk
343 incorrect_password = gettext.dgettext(
344 "sudo", "Sorry, try again.")
345 if sudo_errput.strip().endswith("%s%s" % (prompt, incorrect_password)):
346 raise errors.AnsibleError('Incorrect sudo password')
347 elif sudo_errput.endswith(prompt):
348 stdin.write(self.runner.sudo_pass + '\n')
349
350 if p.stdout in rfd:
351 chunk = p.stdout.read()
352 if not chunk:
353 raise errors.AnsibleError('ssh connection closed waiting for sudo or su password prompt')
354 sudo_output += chunk
355
356 if not rfd:
357 # timeout. wrap up process communication
358 stdout = p.communicate()
359 raise errors.AnsibleError('ssh connection error waiting for sudo or su password prompt')
360
361 if success_key not in sudo_output:
362 if sudoable:
363 stdin.write(self.runner.sudo_pass + '\n')
364 elif su:
365 stdin.write(self.runner.su_pass + '\n')
366
367 (returncode, stdout, stderr) = self._communicate(p, stdin, in_data, su=su, sudoable=sudoable, prompt=prompt)
368
369 if C.HOST_KEY_CHECKING and not_in_host_file:
370 # lock around the initial SSH connectivity so the user prompt about whether to add
371 # the host to known hosts is not intermingled with multiprocess output.
372 fcntl.lockf(self.runner.output_lockfile, fcntl.LOCK_UN)
373 fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
374 controlpersisterror = 'Bad configuration option: ControlPersist' in stderr or \
375 'unknown configuration option: ControlPersist' in stderr
376
377 if C.HOST_KEY_CHECKING:
378 if ssh_cmd[0] == "sshpass" and p.returncode == 6:
379 raise errors.AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host\'s fingerprint to your known_hosts file to manage this host.')
380
381 if p.returncode != 0 and controlpersisterror:
382 raise errors.AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" (or ssh_args in [ssh_connection] section of the config file) before running again')
383 if p.returncode == 255 and (in_data or self.runner.module_name == 'raw'):
384 raise errors.AnsibleError('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh')
385
386 return (p.returncode, '', stdout, stderr)
387
388 def put_file(self, in_path, out_path):
389 ''' transfer a file from local to remote '''
390 vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
391 if not os.path.exists(in_path):
392 raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
393 cmd = self._password_cmd()
394
395 host = self.host
396 if self.ipv6:
397 host = '[%s]' % host
398
399 if C.DEFAULT_SCP_IF_SSH:
400 cmd += ["scp"] + self.common_args
401 cmd += [in_path,host + ":" + pipes.quote(out_path)]
402 indata = None
403 else:
404 cmd += ["sftp"] + self.common_args + [host]
405 indata = "put %s %s\n" % (pipes.quote(in_path), pipes.quote(out_path))
406
407 (p, stdin) = self._run(cmd, indata)
408
409 self._send_password()
410
411 (returncode, stdout, stderr) = self._communicate(p, stdin, indata)
412
413 if returncode != 0:
414 raise errors.AnsibleError("failed to transfer file to %s:\n%s\n%s" % (out_path, stdout, stderr))
415
416 def fetch_file(self, in_path, out_path):
417 ''' fetch a file from remote to local '''
418 vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
419 cmd = self._password_cmd()
420
421 host = self.host
422 if self.ipv6:
423 host = '[%s]' % host
424
425 if C.DEFAULT_SCP_IF_SSH:
426 cmd += ["scp"] + self.common_args
427 cmd += [host + ":" + in_path, out_path]
428 indata = None
429 else:
430 cmd += ["sftp"] + self.common_args + [host]
431 indata = "get %s %s\n" % (in_path, out_path)
432
433 p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
434 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
435 self._send_password()
436 stdout, stderr = p.communicate(indata)
437
438 if p.returncode != 0:
439 raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
440
441 def close(self):
442 ''' not applicable since we're executing openssh binaries '''
443 pass
444