29
29
import subprocess
30
30
import time
31
31
32
+ from functools import wraps
32
33
from ansible import constants as C
33
34
from ansible .compat import selectors
34
35
from ansible .compat .six import PY3 , text_type , binary_type
51
52
SSHPASS_AVAILABLE = None
52
53
53
54
55
+ def _ssh_retry (func ):
56
+ """
57
+ Decorator to retry ssh/scp/sftp in the case of a connection failure
58
+
59
+ Will retry if:
60
+ * an exception is caught
61
+ * ssh returns 255
62
+ Will not retry if
63
+ * remaining_tries is <2
64
+ * retries limit reached
65
+ """
66
+ @wraps (func )
67
+ def wrapped (self , * args , ** kwargs ):
68
+ remaining_tries = int (C .ANSIBLE_SSH_RETRIES ) + 1
69
+ cmd_summary = "%s..." % args [0 ]
70
+ for attempt in range (remaining_tries ):
71
+ try :
72
+ return_tuple = func (self , * args , ** kwargs )
73
+ display .vvv (return_tuple , host = self .host )
74
+ # 0 = success
75
+ # 1-254 = remote command return code
76
+ # 255 = failure from the ssh command itself
77
+ if return_tuple [0 ] != 255 :
78
+ break
79
+ else :
80
+ raise AnsibleConnectionFailure ("Failed to connect to the host via ssh: %s" % to_native (return_tuple [2 ]))
81
+ except (AnsibleConnectionFailure , Exception ) as e :
82
+ if attempt == remaining_tries - 1 :
83
+ raise
84
+ else :
85
+ pause = 2 ** attempt - 1
86
+ if pause > 30 :
87
+ pause = 30
88
+
89
+ if isinstance (e , AnsibleConnectionFailure ):
90
+ msg = "ssh_retry: attempt: %d, ssh return code is 255. cmd (%s), pausing for %d seconds" % (attempt , cmd_summary , pause )
91
+ else :
92
+ msg = "ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds" % (attempt , e , cmd_summary , pause )
93
+
94
+ display .vv (msg , host = self .host )
95
+
96
+ time .sleep (pause )
97
+ continue
98
+
99
+ return return_tuple
100
+ return wrapped
101
+
102
+
54
103
class Connection (ConnectionBase ):
55
104
''' ssh based connections '''
56
105
@@ -352,6 +401,7 @@ def _examine_output(self, source, state, b_chunk, sudoable):
352
401
353
402
return b'' .join (output ), remainder
354
403
404
+ @_ssh_retry
355
405
def _run (self , cmd , in_data , sudoable = True , checkrc = True ):
356
406
'''
357
407
Starts the command and communicates with it until it ends.
@@ -618,28 +668,6 @@ def _run(self, cmd, in_data, sudoable=True, checkrc=True):
618
668
619
669
return (p .returncode , b_stdout , b_stderr )
620
670
621
- def _exec_command (self , cmd , in_data = None , sudoable = True ):
622
- ''' run a command on the remote host '''
623
-
624
- super (Connection , self ).exec_command (cmd , in_data = in_data , sudoable = sudoable )
625
-
626
- display .vvv (u"ESTABLISH SSH CONNECTION FOR USER: {0}" .format (self ._play_context .remote_user ), host = self ._play_context .remote_addr )
627
-
628
-
629
- # we can only use tty when we are not pipelining the modules. piping
630
- # data into /usr/bin/python inside a tty automatically invokes the
631
- # python interactive-mode but the modules are not compatible with the
632
- # interactive-mode ("unexpected indent" mainly because of empty lines)
633
- if not in_data and sudoable :
634
- args = ('ssh' , '-tt' , self .host , cmd )
635
- else :
636
- args = ('ssh' , self .host , cmd )
637
-
638
- cmd = self ._build_command (* args )
639
- (returncode , stdout , stderr ) = self ._run (cmd , in_data , sudoable = sudoable )
640
-
641
- return (returncode , stdout , stderr )
642
-
643
671
def _file_transport_command (self , in_path , out_path , sftp_action ):
644
672
# scp and sftp require square brackets for IPv6 addresses, but
645
673
# accept them for hostnames and IPv4 addresses too.
@@ -674,7 +702,6 @@ def _file_transport_command(self, in_path, out_path, sftp_action):
674
702
methods = ['sftp' ]
675
703
676
704
success = False
677
- res = None
678
705
for method in methods :
679
706
returncode = stdout = stderr = None
680
707
if method == 'sftp' :
@@ -693,77 +720,58 @@ def _file_transport_command(self, in_path, out_path, sftp_action):
693
720
if sftp_action == 'get' :
694
721
# we pass sudoable=False to disable pty allocation, which
695
722
# would end up mixing stdout/stderr and screwing with newlines
696
- (returncode , stdout , stderr ) = self ._exec_command ('dd if=%s bs=%s' % (in_path , BUFSIZE ), sudoable = False )
723
+ (returncode , stdout , stderr ) = self .exec_command ('dd if=%s bs=%s' % (in_path , BUFSIZE ), sudoable = False )
697
724
out_file = open (to_bytes (out_path , errors = 'surrogate_or_strict' ), 'wb+' )
698
725
out_file .write (stdout )
699
726
out_file .close ()
700
727
else :
701
728
in_data = open (to_bytes (in_path , errors = 'surrogate_or_strict' ), 'rb' ).read ()
702
729
in_data = to_bytes (in_data , nonstring = 'passthru' )
703
- (returncode , stdout , stderr ) = self ._exec_command ('dd of=%s bs=%s' % (out_path , BUFSIZE ), in_data = in_data )
730
+ (returncode , stdout , stderr ) = self .exec_command ('dd of=%s bs=%s' % (out_path , BUFSIZE ), in_data = in_data )
704
731
705
732
# Check the return code and rollover to next method if failed
706
733
if returncode == 0 :
707
- success = True
708
- break
734
+ return (returncode , stdout , stderr )
709
735
else :
710
736
# If not in smart mode, the data will be printed by the raise below
711
737
if len (methods ) > 1 :
712
738
display .warning (msg = '%s transfer mechanism failed on %s. Use ANSIBLE_DEBUG=1 to see detailed information' % (method , host ))
713
739
display .debug (msg = '%s' % to_native (stdout ))
714
740
display .debug (msg = '%s' % to_native (stderr ))
715
- res = (returncode , stdout , stderr )
716
741
717
- if not success :
718
- raise AnsibleError ("failed to transfer file {0} to {1}:\n {2}\n {3}" \
719
- .format (to_native (in_path ), to_native (out_path ), to_native (res [1 ]), to_native (res [2 ])))
742
+ if returncode == 255 :
743
+ raise AnsibleConnectionFailure ("Failed to connect to the host via %s: %s" % (method , to_native (stderr )))
744
+ else :
745
+ raise AnsibleError ("failed to transfer file to {0} {1}:\n {2}\n {3}" \
746
+ .format (to_native (in_path ), to_native (out_path ), to_native (stdout ), to_native (stderr )))
720
747
721
748
#
722
749
# Main public methods
723
750
#
724
- def exec_command (self , * args , ** kwargs ):
725
- """
726
- Wrapper around _exec_command to retry in the case of an ssh failure
727
-
728
- Will retry if:
729
- * an exception is caught
730
- * ssh returns 255
731
- Will not retry if
732
- * remaining_tries is <2
733
- * retries limit reached
734
- """
751
+ def exec_command (self , cmd , in_data = None , sudoable = True ):
752
+ ''' run a command on the remote host '''
735
753
736
- remaining_tries = int (C .ANSIBLE_SSH_RETRIES ) + 1
737
- cmd_summary = "%s..." % args [0 ]
738
- for attempt in range (remaining_tries ):
739
- try :
740
- return_tuple = self ._exec_command (* args , ** kwargs )
741
- # 0 = success
742
- # 1-254 = remote command return code
743
- # 255 = failure from the ssh command itself
744
- if return_tuple [0 ] != 255 :
745
- break
746
- else :
747
- raise AnsibleConnectionFailure ("Failed to connect to the host via ssh: %s" % to_native (return_tuple [2 ]))
748
- except (AnsibleConnectionFailure , Exception ) as e :
749
- if attempt == remaining_tries - 1 :
750
- raise
751
- else :
752
- pause = 2 ** attempt - 1
753
- if pause > 30 :
754
- pause = 30
754
+ super (Connection , self ).exec_command (cmd , in_data = in_data , sudoable = sudoable )
755
755
756
- if isinstance (e , AnsibleConnectionFailure ):
757
- msg = "ssh_retry: attempt: %d, ssh return code is 255. cmd (%s), pausing for %d seconds" % (attempt , cmd_summary , pause )
758
- else :
759
- msg = "ssh_retry: attempt: %d, caught exception(%s) from cmd (%s), pausing for %d seconds" % (attempt , e , cmd_summary , pause )
756
+ display .vvv (u"ESTABLISH SSH CONNECTION FOR USER: {0}" .format (self ._play_context .remote_user ), host = self ._play_context .remote_addr )
760
757
761
- display .vv (msg , host = self .host )
762
758
763
- time .sleep (pause )
764
- continue
759
+ # we can only use tty when we are not pipelining the modules. piping
760
+ # data into /usr/bin/python inside a tty automatically invokes the
761
+ # python interactive-mode but the modules are not compatible with the
762
+ # interactive-mode ("unexpected indent" mainly because of empty lines)
765
763
766
- return return_tuple
764
+ ssh_executable = self ._play_context .ssh_executable
765
+
766
+ if not in_data and sudoable :
767
+ args = (ssh_executable , '-tt' , self .host , cmd )
768
+ else :
769
+ args = (ssh_executable , self .host , cmd )
770
+
771
+ cmd = self ._build_command (* args )
772
+ (returncode , stdout , stderr ) = self ._run (cmd , in_data , sudoable = sudoable )
773
+
774
+ return (returncode , stdout , stderr )
767
775
768
776
def put_file (self , in_path , out_path ):
769
777
''' transfer a file from local to remote '''
@@ -774,15 +782,15 @@ def put_file(self, in_path, out_path):
774
782
if not os .path .exists (to_bytes (in_path , errors = 'surrogate_or_strict' )):
775
783
raise AnsibleFileNotFound ("file or module does not exist: {0}" .format (to_native (in_path )))
776
784
777
- self ._file_transport_command (in_path , out_path , 'put' )
785
+ return self ._file_transport_command (in_path , out_path , 'put' )
778
786
779
787
def fetch_file (self , in_path , out_path ):
780
788
''' fetch a file from remote to local '''
781
789
782
790
super (Connection , self ).fetch_file (in_path , out_path )
783
791
784
792
display .vvv (u"FETCH {0} TO {1}" .format (in_path , out_path ), host = self .host )
785
- self ._file_transport_command (in_path , out_path , 'get' )
793
+ return self ._file_transport_command (in_path , out_path , 'get' )
786
794
787
795
def reset (self ):
788
796
# If we have a persistent ssh connection (ControlPersist), we can ask it to stop listening.
0 commit comments