bin/ansible-connection
26ec2ecf
 #!/usr/bin/env python
 
62159228
 # (c) 2017, Ansible, Inc. <support@ansible.com>
26ec2ecf
 #
 # 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/>.
 
 ########################################################
 from __future__ import (absolute_import, division, print_function)
 
3f949358
 __metaclass__ = type
26ec2ecf
 __requires__ = ['ansible']
3f949358
 
26ec2ecf
 try:
     import pkg_resources
 except Exception:
     pass
 
 import fcntl
 import os
 import shlex
 import signal
 import socket
 import sys
 import time
 import traceback
6fe9a5e4
 import datetime
62159228
 import errno
26ec2ecf
 
 from ansible import constants as C
28c6b226
 from ansible.module_utils._text import to_bytes, to_native, to_text
d834412e
 from ansible.module_utils.six import PY3
 from ansible.module_utils.six.moves import cPickle
62159228
 from ansible.module_utils.connection import send_data, recv_data
26ec2ecf
 from ansible.playbook.play_context import PlayContext
f9213694
 from ansible.plugins.loader import connection_loader
26ec2ecf
 from ansible.utils.path import unfrackpath, makedirs_safe
66736730
 from ansible.errors import AnsibleConnectionFailure
e20ed8bc
 from ansible.utils.display import Display
26ec2ecf
 
6fe9a5e4
 
26ec2ecf
 def do_fork():
     '''
     Does the required double fork for a daemon process. Based on
     http://code.activestate.com/recipes/66012-fork-a-daemon-process-on-unix/
     '''
     try:
         pid = os.fork()
         if pid > 0:
             return pid
9730d965
         # This is done as a 'good practice' for daemons, but we need to keep the cwd
         # leaving it here as a note that we KNOW its good practice but are not doing it on purpose.
8e0b5800
         # os.chdir("/")
26ec2ecf
         os.setsid()
         os.umask(0)
 
         try:
             pid = os.fork()
             if pid > 0:
                 sys.exit(0)
 
3ff2c471
             if C.DEFAULT_LOG_PATH != '':
d834412e
                 out_file = open(C.DEFAULT_LOG_PATH, 'ab+')
                 err_file = open(C.DEFAULT_LOG_PATH, 'ab+', 0)
3ff2c471
             else:
d834412e
                 out_file = open('/dev/null', 'ab+')
                 err_file = open('/dev/null', 'ab+', 0)
3ff2c471
 
             os.dup2(out_file.fileno(), sys.stdout.fileno())
             os.dup2(err_file.fileno(), sys.stderr.fileno())
26ec2ecf
             os.close(sys.stdin.fileno())
 
             return pid
         except OSError as e:
             sys.exit(1)
     except OSError as e:
         sys.exit(1)
 
ed7cace4
 
26ec2ecf
 class Server():
6fe9a5e4
 
62159228
     def __init__(self, socket_path, play_context):
         self.socket_path = socket_path
26ec2ecf
         self.play_context = play_context
ed7cace4
 
53c52cf6
         display.display(
             'creating new control socket for host %s:%s as user %s' %
             (play_context.remote_addr, play_context.port, play_context.remote_user),
             log_only=True
         )
 
62159228
         display.display('control socket path is %s' % socket_path, log_only=True)
77ce83fe
         display.display('current working directory is %s' % os.getcwd(), log_only=True)
26ec2ecf
 
6fe9a5e4
         self._start_time = datetime.datetime.now()
 
e20ed8bc
         display.display("using connection plugin %s" % self.play_context.connection, log_only=True)
13805154
 
62159228
         self.connection = connection_loader.get(play_context.connection, play_context, sys.stdin)
         self.connection._connect()
 
         if not self.connection.connected:
53c52cf6
             raise AnsibleConnectionFailure('unable to connect to remote host %s' % self._play_context.remote_addr)
6fe9a5e4
 
eed24079
         connection_time = datetime.datetime.now() - self._start_time
53c52cf6
         display.display('connection established to %s in %s' % (play_context.remote_addr, connection_time), log_only=True)
eed24079
 
66736730
         self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
62159228
         self.socket.bind(self.socket_path)
66736730
         self.socket.listen(1)
62159228
         display.display('local socket is set to listening', log_only=True)
26ec2ecf
 
     def run(self):
         try:
             while True:
62159228
                 signal.signal(signal.SIGALRM, self.connect_timeout)
                 signal.signal(signal.SIGTERM, self.handler)
26ec2ecf
                 signal.alarm(C.PERSISTENT_CONNECT_TIMEOUT)
62159228
 
                 (s, addr) = self.socket.accept()
                 display.display('incoming request accepted on persistent socket', log_only=True)
                 signal.alarm(0)
26ec2ecf
 
                 while True:
                     data = recv_data(s)
                     if not data:
                         break
 
62159228
                     signal.signal(signal.SIGALRM, self.command_timeout)
5ec7f401
                     signal.alarm(self.play_context.timeout)
6fe9a5e4
 
28c6b226
                     op = to_text(data.split(b':')[0])
62159228
                     display.display('socket operation is %s' % op, log_only=True)
 
                     method = getattr(self, 'do_%s' % op, None)
 
26ec2ecf
                     rc = 255
62159228
                     stdout = stderr = ''
 
                     if not method:
                         stderr = 'Invalid action specified'
                     else:
                         rc, stdout, stderr = method(data)
26ec2ecf
 
6fe9a5e4
                     signal.alarm(0)
 
62159228
                     display.display('socket operation completed with rc %s' % rc, log_only=True)
6e9244a9
 
d834412e
                     send_data(s, to_bytes(rc))
26ec2ecf
                     send_data(s, to_bytes(stdout))
                     send_data(s, to_bytes(stderr))
62159228
 
26ec2ecf
                 s.close()
62159228
 
26ec2ecf
         except Exception as e:
62159228
             # socket.accept() will raise EINTR if the socket.close() is called
             if e.errno != errno.EINTR:
                 display.display(traceback.format_exc(), log_only=True)
 
26ec2ecf
         finally:
             # when done, close the connection properly and cleanup
             # the socket file so it can be recreated
62159228
             self.shutdown()
6fe9a5e4
             end_time = datetime.datetime.now()
             delta = end_time - self._start_time
62159228
             display.display('shutdown local socket, connection was active for %s secs' % delta, log_only=True)
 
     def connect_timeout(self, signum, frame):
70ce3948
         display.display('persistent connection idle timeout triggered, timeout value is %s secs' % C.PERSISTENT_CONNECT_TIMEOUT, log_only=True)
62159228
         self.shutdown()
 
     def command_timeout(self, signum, frame):
70ce3948
         display.display('command timeout triggered, timeout value is %s secs' % self.play_context.timeout, log_only=True)
62159228
         self.shutdown()
 
     def handler(self, signum, frame):
         display.display('signal handler called with signal %s' % signum, log_only=True)
         self.shutdown()
 
     def shutdown(self):
         display.display('shutdown persistent connection requested', log_only=True)
 
         if not os.path.exists(self.socket_path):
             display.display('persistent connection is not active', log_only=True)
             return
 
         try:
             if self.socket:
                 display.display('closing local listener', log_only=True)
6fe9a5e4
                 self.socket.close()
62159228
             if self.connection:
                 display.display('closing the connection', log_only=True)
76cc19d0
                 self.connection.close()
62159228
         except:
             pass
         finally:
             if os.path.exists(self.socket_path):
                 display.display('removing the local control socket', log_only=True)
                 os.remove(self.socket_path)
 
         display.display('shutdown complete', log_only=True)
 
     def do_EXEC(self, data):
         cmd = data.split(b'EXEC: ')[1]
         return self.connection.exec_command(cmd)
 
     def do_PUT(self, data):
         (op, src, dst) = shlex.split(to_native(data))
         return self.connection.fetch_file(src, dst)
 
     def do_FETCH(self, data):
         (op, src, dst) = shlex.split(to_native(data))
         return self.connection.put_file(src, dst)
 
     def do_CONTEXT(self, data):
         pc_data = data.split(b'CONTEXT: ', 1)[1]
 
         if PY3:
             pc_data = cPickle.loads(pc_data, encoding='bytes')
         else:
             pc_data = cPickle.loads(pc_data)
 
         pc = PlayContext()
         pc.deserialize(pc_data)
 
         try:
             self.connection.update_play_context(pc)
         except AttributeError:
             pass
 
         return (0, 'ok', '')
 
     def do_RUN(self, data):
         timeout = self.play_context.timeout
         while bool(timeout):
             if os.path.exists(self.socket_path):
                 break
             time.sleep(1)
             timeout -= 1
a3404418
         socket_bytes = to_bytes(self.socket_path, errors='surrogate_or_strict')
         return 0, b'\n#SOCKET_PATH#: %s\n' % socket_bytes, ''
62159228
 
 
 def communicate(sock, data):
     send_data(sock, data)
     rc = int(recv_data(sock), 10)
     stdout = recv_data(sock)
     stderr = recv_data(sock)
     return (rc, stdout, stderr)
26ec2ecf
 
8e0b5800
 
26ec2ecf
 def main():
d834412e
     # Need stdin as a byte stream
     if PY3:
         stdin = sys.stdin.buffer
     else:
         stdin = sys.stdin
3f949358
 
26ec2ecf
     try:
         # read the play context data via stdin, which means depickling it
         # FIXME: as noted above, we will probably need to deserialize the
         #        connection loader here as well at some point, otherwise this
         #        won't find role- or playbook-based connection plugins
d834412e
         cur_line = stdin.readline()
         init_data = b''
         while cur_line.strip() != b'#END_INIT#':
84a59e47
             if cur_line == b'':
d834412e
                 raise Exception("EOF found before init data was complete")
26ec2ecf
             init_data += cur_line
d834412e
             cur_line = stdin.readline()
84a59e47
         if PY3:
             pc_data = cPickle.loads(init_data, encoding='bytes')
         else:
             pc_data = cPickle.loads(init_data)
26ec2ecf
 
         pc = PlayContext()
         pc.deserialize(pc_data)
62159228
 
26ec2ecf
     except Exception as e:
         # FIXME: better error message/handling/logging
942ed146
         sys.stderr.write(traceback.format_exc())
         sys.exit("FAIL: %s" % e)
26ec2ecf
 
498aea8a
     ssh = connection_loader.get('ssh', class_only=True)
cd8c1c11
     cp = ssh._create_control_path(pc.remote_addr, pc.port, pc.remote_user, pc.connection)
26ec2ecf
 
     # create the persistent connection dir if need be and create the paths
     # which we will be using later
62159228
     tmp_path = unfrackpath(C.PERSISTENT_CONTROL_PATH_DIR)
26ec2ecf
     makedirs_safe(tmp_path)
62159228
     lock_path = unfrackpath("%s/.ansible_pc_lock" % tmp_path)
     socket_path = unfrackpath(cp % dict(directory=tmp_path))
26ec2ecf
 
     # if the socket file doesn't exist, spin up the daemon process
8e0b5800
     lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600)
26ec2ecf
     fcntl.lockf(lock_fd, fcntl.LOCK_EX)
62159228
 
     if not os.path.exists(socket_path):
26ec2ecf
         pid = do_fork()
         if pid == 0:
e05b2b56
             rc = 0
66736730
             try:
62159228
                 server = Server(socket_path, pc)
e05b2b56
             except AnsibleConnectionFailure as exc:
53c52cf6
                 display.display('connecting to host %s returned an error' % pc.remote_addr, log_only=True)
                 display.display(str(exc), log_only=True)
e05b2b56
                 rc = 1
66736730
             except Exception as exc:
53c52cf6
                 display.display('failed to create control socket for host %s' % pc.remote_addr, log_only=True)
                 display.display(traceback.format_exc(), log_only=True)
e05b2b56
                 rc = 1
6fe9a5e4
             fcntl.lockf(lock_fd, fcntl.LOCK_UN)
             os.close(lock_fd)
e05b2b56
             if rc == 0:
                 server.run()
             sys.exit(rc)
ed7cace4
     else:
53c52cf6
         display.display('re-using existing socket for %s@%s:%s' % (pc.remote_user, pc.remote_addr, pc.port), log_only=True)
62159228
 
26ec2ecf
     fcntl.lockf(lock_fd, fcntl.LOCK_UN)
     os.close(lock_fd)
 
62159228
     timeout = pc.timeout
     while bool(timeout):
         if os.path.exists(socket_path):
             display.vvvv('connected to local socket in %s' % (pc.timeout - timeout), pc.remote_addr)
             break
         time.sleep(1)
         timeout -= 1
     else:
         raise AnsibleConnectionFailure('timeout waiting for local socket', pc.remote_addr)
 
26ec2ecf
     # now connect to the daemon process
     # FIXME: if the socket file existed but the daemonized process was killed,
     #        the connection will timeout here. Need to make this more resilient.
62159228
     while True:
d834412e
         data = stdin.readline()
         if data == b'':
26ec2ecf
             break
d834412e
         if data.strip() == b'':
26ec2ecf
             continue
62159228
 
         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 
70ce3948
         connect_retry_timeout = C.PERSISTENT_CONNECT_RETRY_TIMEOUT
         while bool(connect_retry_timeout):
26ec2ecf
             try:
62159228
                 sock.connect(socket_path)
26ec2ecf
                 break
             except socket.error:
70ce3948
                 time.sleep(1)
                 connect_retry_timeout -= 1
62159228
         else:
70ce3948
             display.display('connect retry timeout expired, unable to connect to control socket', pc.remote_addr, pc.remote_user, log_only=True)
             display.display('persistent_connect_retry_timeout is %s secs' % (C.PERSISTENT_CONNECT_RETRY_TIMEOUT), pc.remote_addr, pc.remote_user, log_only=True)
62159228
             sys.stderr.write('failed to connect to control socket')
             sys.exit(255)
26ec2ecf
 
6fe9a5e4
         # send the play_context back into the connection so the connection
3f949358
         # can handle any privilege escalation activities
d834412e
         pc_data = b'CONTEXT: %s' % init_data
62159228
         communicate(sock, pc_data)
6fe9a5e4
 
62159228
         rc, stdout, stderr = communicate(sock, data.strip())
6fe9a5e4
 
26ec2ecf
         sys.stdout.write(to_native(stdout))
         sys.stderr.write(to_native(stderr))
 
62159228
         sock.close()
26ec2ecf
         break
6fe9a5e4
 
26ec2ecf
     sys.exit(rc)
 
 if __name__ == '__main__':
e20ed8bc
     display = Display()
26ec2ecf
     main()