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:
parent
900b3ffcba
commit
6fe9a5e40c
2 changed files with 80 additions and 23 deletions
|
@ -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,25 +100,46 @@ 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
|
||||||
|
|
||||||
# FIXME: the connection loader here is created brand new,
|
self._start_time = datetime.datetime.now()
|
||||||
# so it will not see any custom paths loaded (ie. via
|
|
||||||
# roles), so we will need to serialize the connection
|
|
||||||
# loader and send it over as we do the PlayContext
|
|
||||||
# in the main() method below.
|
|
||||||
self.conn = connection_loader.get(play_context.connection, play_context, sys.stdin)
|
|
||||||
self.conn._connect()
|
|
||||||
|
|
||||||
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
try:
|
||||||
self.socket.bind(path)
|
# FIXME: the connection loader here is created brand new,
|
||||||
self.socket.listen(1)
|
# so it will not see any custom paths loaded (ie. via
|
||||||
|
# roles), so we will need to serialize the connection
|
||||||
|
# loader and send it over as we do the PlayContext
|
||||||
|
# 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._connect()
|
||||||
|
|
||||||
|
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.socket.bind(path)
|
||||||
|
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)
|
||||||
|
@ -236,11 +283,11 @@ def main():
|
||||||
if not os.path.exists(sf_path):
|
if not os.path.exists(sf_path):
|
||||||
pid = do_fork()
|
pid = do_fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
server = Server(sf_path, pc)
|
server = Server(sf_path, pc)
|
||||||
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
|
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
|
||||||
os.close(lock_fd)
|
os.close(lock_fd)
|
||||||
server.run()
|
server.run()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
|
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
|
||||||
os.close(lock_fd)
|
os.close(lock_fd)
|
||||||
|
|
||||||
|
@ -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__':
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue