functional updates to ansible-connection (#18574)

* sends the serialized play_context into an already established connection
* hooks the alarm_handler() method in the connection plugin if it exists
* added configuration options for connect interval and retries
* adds syslog logging to Server() instance

This update will send the updated play_context back into an already
established connection in case privilege escalation / descalation activities
need to be performed.  This change will also hook the alarm_handler() method
in the connection instance (if available) and call it in case of a
sigalarm raised.

This update adds two new configuration options

* PERSISTENT_CONNECT_INTERVAL - time to wait in between connection attempts
* PERSISTENT_CONNECT_RETRIES - max number of retries
This commit is contained in:
Peter Sprygada 2016-11-30 16:26:49 -05:00 committed by GitHub
parent 900b3ffcba
commit 6fe9a5e40c
2 changed files with 80 additions and 23 deletions

View file

@ -37,6 +37,8 @@ import struct
import sys import sys
import time import time
import traceback import traceback
import syslog
import datetime
from io import BytesIO from io import BytesIO
@ -47,6 +49,7 @@ from ansible.playbook.play_context import PlayContext
from ansible.plugins import connection_loader from ansible.plugins import connection_loader
from ansible.utils.path import unfrackpath, makedirs_safe from ansible.utils.path import unfrackpath, makedirs_safe
def do_fork(): def do_fork():
''' '''
Does the required double fork for a daemon process. Based on Does the required double fork for a daemon process. Based on
@ -97,16 +100,22 @@ def recv_data(s):
data += d data += d
return data return data
class Server(): class Server():
def __init__(self, path, play_context): def __init__(self, path, play_context):
self.path = path self.path = path
self.play_context = play_context self.play_context = play_context
self._start_time = datetime.datetime.now()
try:
# FIXME: the connection loader here is created brand new, # FIXME: the connection loader here is created brand new,
# so it will not see any custom paths loaded (ie. via # so it will not see any custom paths loaded (ie. via
# roles), so we will need to serialize the connection # roles), so we will need to serialize the connection
# loader and send it over as we do the PlayContext # loader and send it over as we do the PlayContext
# in the main() method below. # in the main() method below.
self.log('loading connection plugin %s' % str(play_context.connection))
self.conn = connection_loader.get(play_context.connection, play_context, sys.stdin) self.conn = connection_loader.get(play_context.connection, play_context, sys.stdin)
self.conn._connect() self.conn._connect()
@ -114,8 +123,23 @@ class Server():
self.socket.bind(path) self.socket.bind(path)
self.socket.listen(1) self.socket.listen(1)
except Exception as exc:
self.log(exc)
return
signal.signal(signal.SIGALRM, self.alarm_handler) signal.signal(signal.SIGALRM, self.alarm_handler)
def log(self, msg):
syslog_msg = '[%s] %s' % (self.play_context.remote_addr, msg)
facility = getattr(syslog, C.DEFAULT_SYSLOG_FACILITY, syslog.LOG_USER)
syslog.openlog('ansible-connection', 0, facility)
syslog.syslog(syslog.LOG_INFO, syslog_msg)
def dispatch(self, obj, name, *args, **kwargs):
meth = getattr(obj, name, None)
if meth:
return meth(*args, **kwargs)
def alarm_handler(self, signum, frame): def alarm_handler(self, signum, frame):
''' '''
Alarm handler Alarm handler
@ -124,6 +148,9 @@ class Server():
# areas of code to check, so they can terminate # areas of code to check, so they can terminate
# earlier than the socket going back to the accept # earlier than the socket going back to the accept
# call and failing there. # call and failing there.
#
# hooks the connection plugin to handle any cleanup
self.dispatch(self.conn, 'alarm_handler', signum, frame)
self.socket.close() self.socket.close()
def run(self): def run(self):
@ -150,6 +177,8 @@ class Server():
if not data: if not data:
break break
signal.alarm(C.DEFAULT_TIMEOUT)
rc = 255 rc = 255
try: try:
if data.startswith(b'EXEC: '): if data.startswith(b'EXEC: '):
@ -166,6 +195,18 @@ class Server():
rc = 0 rc = 0
except: except:
pass pass
elif data.startswith(b'CONTEXT: '):
pc_data = data.split(b'CONTEXT: ')[1]
src = StringIO(pc_data)
pc_data = cPickle.load(src)
src.close()
pc = PlayContext()
pc.deserialize(pc_data)
self.dispatch(self.conn, 'update_play_context', pc)
continue
else: else:
stdout = '' stdout = ''
stderr = 'Invalid action specified' stderr = 'Invalid action specified'
@ -173,19 +214,25 @@ class Server():
stdout = '' stdout = ''
stderr = traceback.format_exc() stderr = traceback.format_exc()
signal.alarm(0)
send_data(s, to_bytes(str(rc))) send_data(s, to_bytes(str(rc)))
send_data(s, to_bytes(stdout)) send_data(s, to_bytes(stdout))
send_data(s, to_bytes(stderr)) send_data(s, to_bytes(stderr))
s.close() s.close()
except Exception as e: except Exception as e:
# FIXME: proper logging and error handling here # FIXME: proper logging and error handling here
print("run exception: %s" % e) self.log('runtime exception: %s' % e)
print(traceback.format_exc()) print(traceback.format_exc())
finally: finally:
# when done, close the connection properly and cleanup # when done, close the connection properly and cleanup
# the socket file so it can be recreated # the socket file so it can be recreated
end_time = datetime.datetime.now()
delta = end_time - self._start_time
self.log('shutting down connection, connection was active for %s secs' % delta)
try: try:
self.conn.close() self.conn.close()
self.socket.close()
except Exception as e: except Exception as e:
pass pass
os.remove(self.path) os.remove(self.path)
@ -205,7 +252,7 @@ def main():
cur_line = sys.stdin.readline() cur_line = sys.stdin.readline()
src = BytesIO(to_bytes(init_data)) src = BytesIO(to_bytes(init_data))
pc_data = cPickle.load(src) pc_data = cPickle.load(src)
src.close() #src.close()
pc = PlayContext() pc = PlayContext()
pc.deserialize(pc_data) pc.deserialize(pc_data)
@ -262,24 +309,32 @@ def main():
break break
except socket.error: except socket.error:
# FIXME: better error handling/logging/message here # FIXME: better error handling/logging/message here
# FIXME: make # of retries configurable? time.sleep(C.PERSISTENT_CONNECT_INTERVAL)
time.sleep(0.1)
attempts += 1 attempts += 1
if attempts > 10: if attempts > C.PERSISTENT_CONNECT_RETRIES:
sys.stderr.write("failed to connect to the host, connection timeout\n") sys.stderr.write("failed to connect to the host, connection timeout")
sys.exit(255) sys.exit(255)
#
# send the play_context back into the connection so the connection
# can handle any privilege escalation or deescalation activities
#
pc_data = 'CONTEXT: %s' % src.getvalue()
send_data(sf, to_bytes(pc_data))
src.close()
send_data(sf, to_bytes(data.strip())) send_data(sf, to_bytes(data.strip()))
rc = int(recv_data(sf), 10) rc = int(recv_data(sf), 10)
stdout = recv_data(sf) stdout = recv_data(sf)
stderr = recv_data(sf) stderr = recv_data(sf)
sys.stdout.write(to_native(stdout)) sys.stdout.write(to_native(stdout))
sys.stderr.write(to_native(stderr)) sys.stderr.write(to_native(stderr))
#sys.stdout.flush()
#sys.stderr.flush()
sf.close() sf.close()
break break
sys.exit(rc) sys.exit(rc)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -308,6 +308,8 @@ PARAMIKO_HOST_KEY_AUTO_ADD = get_config(p, 'paramiko_connection', 'host_key_
PARAMIKO_PROXY_COMMAND = get_config(p, 'paramiko_connection', 'proxy_command', 'ANSIBLE_PARAMIKO_PROXY_COMMAND', None) PARAMIKO_PROXY_COMMAND = get_config(p, 'paramiko_connection', 'proxy_command', 'ANSIBLE_PARAMIKO_PROXY_COMMAND', None)
PARAMIKO_LOOK_FOR_KEYS = get_config(p, 'paramiko_connection', 'look_for_keys', 'ANSIBLE_PARAMIKO_LOOK_FOR_KEYS', True, value_type='boolean') PARAMIKO_LOOK_FOR_KEYS = get_config(p, 'paramiko_connection', 'look_for_keys', 'ANSIBLE_PARAMIKO_LOOK_FOR_KEYS', True, value_type='boolean')
PERSISTENT_CONNECT_TIMEOUT = get_config(p, 'persistent_connection', 'connect_timeout', 'ANSIBLE_PERSISTENT_CONNECT_TIMEOUT', 30, value_type='integer') PERSISTENT_CONNECT_TIMEOUT = get_config(p, 'persistent_connection', 'connect_timeout', 'ANSIBLE_PERSISTENT_CONNECT_TIMEOUT', 30, value_type='integer')
PERSISTENT_CONNECT_RETRIES = get_config(p, 'persistent_connection', 'connect_retries', 'ANSIBLE_PERSISTENT_CONNECT_RETRIES', 10, value_type='integer')
PERSISTENT_CONNECT_INTERVAL = get_config(p, 'persistent_connection', 'connect_interval', 'ANSIBLE_PERSISTENT_CONNECT_INTERVAL', 1, value_type='integer')
# obsolete -- will be formally removed # obsolete -- will be formally removed
ZEROMQ_PORT = get_config(p, 'fireball_connection', 'zeromq_port', 'ANSIBLE_ZEROMQ_PORT', 5099, value_type='integer') ZEROMQ_PORT = get_config(p, 'fireball_connection', 'zeromq_port', 'ANSIBLE_ZEROMQ_PORT', 5099, value_type='integer')