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