Commit 2ee7ab8e authored by Simon Redman's avatar Simon Redman

Update ssh_helper to be stateful rather than closing SSH connection

parent f1b0098c
......@@ -21,22 +21,21 @@ import argparse
import getpass
import json
ADDUSER_COMMAND = "sudo adduser --disabled-password --gecos quagga"
def add_quagga_user_to_network(hostnames, usernames, passwords=None, ssh_options=ssh_helper.DEFAULT_SSH_OPTIONS):
def add_quagga_user_to_network(sessions):
"""
Add the quagga user to every host in the network
:param hostnames: list of hosts on which commands should be run
:param usernames: list of usernames to use to log in to each host
:param passwords: (optional) list of passwords to log in to each host
:param sessions: logged-in sessions on which to run commands
:param ssh_options: options to pass to ssh, as per pxssh documentation
:return: list of passwords which were used to log in to the servers, where empty string means no password was used
"""
commands = [ADDUSER_COMMAND for i in range(len(hostnames))]
ssh_helper.run_commands_on_network(commands, hostnames, usernames, passwords, ssh_options)
outputs = ssh_helper.run_commands_on_many_hosts(sessions, commands)
return outputs
if __name__ == "__main__":
......@@ -51,5 +50,6 @@ if __name__ == "__main__":
netjson = json.load(netjson_file)
hostnames = [node['properties']['management-ip'] for node in netjson['nodes']]
usernames = [args.username for i in range(len(hostnames))]
passwords = add_quagga_user_to_network(hostnames, usernames)
sessions, passwords = ssh_helper.log_in_many_sessions(hostnames, usernames)
outputs = add_quagga_user_to_network(sessions)
pass
......@@ -24,36 +24,71 @@ DEFAULT_SSH_OPTIONS = {"StrictHostKeyChecking": "no",
"UserKnownHostsFile": "/dev/null"}
def login_password_failover(session, hostname, username, password=""):
def log_in_password_failover(hostname, username, password="", ssh_options=DEFAULT_SSH_OPTIONS):
"""
Attempt to log in a pxssh session using only ssh agent-provided public keys, then failover to requesting a password
:param session: non-logged-in session
:param hostname: remote to connect to
:param username: login username
:param password: login password -- Will be prompted for if needed and not passed
:return: password if key-based login failed, otherwise empty string
:param ssh_options: pxssh options, as would be in an ssh config file
:return: connected pxssh session and the password used to log in to the host
"""
# First, try in-memory authentication tools, including the password if it is defined
try:
session = pxssh.pxssh(options=ssh_options)
session.force_password = False
session.login(hostname, username, password)
return ""
except pxssh.ExceptionPxssh:
session.close() # Unlikely to be necessary, but make sure the pxssh file descriptor, etc. get cleaned up
pass
# That didn't work. Try a password.
# Make a new session because you can only use each once
original_force_password = session.force_password
session = pxssh.pxssh(options=session.options)
session.force_password = True # Probably never necessary -- we already tried key-based login
password = getpass.getpass("Password for {user}@{host}: ".format(user=username, host=hostname))
session.login(hostname, username, password)
session.force_password = original_force_password
# That didn't work. Try a password.
session = pxssh.pxssh(options=ssh_options)
session.force_password = True # Probably never necessary -- we already tried key-based login
password = getpass.getpass("Password for {user}@{host}: ".format(user=username, host=hostname))
session.login(hostname, username, password)
return session, password
def log_in_many_sessions(hostnames, usernames, passwords=None, ssh_options=DEFAULT_SSH_OPTIONS):
"""
Attempt to log in a pxssh session for all requested hosts
:param hostnames: list of hosts on which commands should be run
:param usernames: list of usernames to use to log in to each host
:param passwords: (optional) list of passwords to log in to each host
:param ssh_options: options to pass to ssh, as per pxssh documentation
:return: list of pxssh sessions and list of passwords which were used to log in to the hosts, where empty string means no password was used
"""
sessions = []
num_hosts = len(hostnames)
assert len(usernames) == num_hosts, "Please provide one username for every host"
if passwords is None:
# Generate empty passwords for every host if no passwords were provided
passwords = ["" for i in range(len(usernames))]
assert len(passwords) == num_hosts, "If provided, there must be one password per username"
for host_idx in range(0, num_hosts):
hostname = hostnames[host_idx]
username = usernames[host_idx]
password = passwords[host_idx]
if passwords == "":
# To try to avoid prompting for passwords, try the previously-used password first
password = passwords[host_idx - 1]
try:
session, returned_password = log_in_password_failover(hostname, username, password, ssh_options)
except pxssh.ExceptionPxssh as e:
print("Unable to log in to {user}@{host} using in-memory SSH keys nor provided password".format(user=username, host=hostname), file=sys.stderr)
print(e, file=sys.stderr)
continue
sessions.append(session)
passwords[host_idx] = returned_password
return password
return sessions, passwords
def decode_output(session, encoding=sys.stdout.encoding):
def get_output(session, encoding=sys.stdout.encoding):
"""
Decode the raw bytes written by the SSH session
......@@ -61,71 +96,43 @@ def decode_output(session, encoding=sys.stdout.encoding):
:param encoding: encoding to use to interpret the output
:return: string-form of the read bytes
"""
session.prompt()
return str(session.before.decode(encoding))
def run_command_on_host(command, hostname, username, password="", ssh_options=DEFAULT_SSH_OPTIONS):
def run_command_on_host(session, command):
"""
Run a specified command on a single host
:param session: logged-in pxssh session to run the command on
:param command: command to run
:param hostname: hostname (or IP address) to SSH to
:param username: ssh username
:param password: ssh password - Will prompt if not provided and in-memory SSH keys don't work
:param ssh_options: ssh options as would be in an ssh config file
:return: password used to log in to this host, if used, otherwise empty string
:return: None
"""
session = pxssh.pxssh(options=ssh_options)
password = login_password_failover(session, hostname, username, password)
session.sendline(command)
session.prompt()
session.logout()
return password
def run_commands_on_network(commands, hostnames, usernames, passwords=None, ssh_options=DEFAULT_SSH_OPTIONS):
def run_commands_on_many_hosts(sessions, commands):
"""
Add the specified command on each host in the network
:param sessions: list of logged-in pxssh sessions to run commands on
:param commands: list of commands to run, one per host
:param hostnames: list of hosts on which commands should be run
:param usernames: list of usernames to use to log in to each host
:param passwords: (optional) list of passwords to log in to each host
:param ssh_options: options to pass to ssh, as per pxssh documentation
:return: list of passwords which were used to log in to the servers, where empty string means no password was used
:return: output from running each command, in the same order as the sessions were presented
"""
num_hosts = len(hostnames)
assert len(usernames) == num_hosts, "Please provide one username for every host"
assert len(commands) == num_hosts, "Please provide one command for every host"
if passwords is None:
# Generate empty passwords for every host if no passwords were provided
passwords = ["" for i in range(len(usernames))]
assert len(passwords) == num_hosts, "If provided, there must be one password per username"
outputs = []
num_hosts = len(sessions)
assert len(commands) == num_hosts, "Please provide one command for every session"
for host_idx in range(0, num_hosts):
session = sessions[host_idx]
command = commands[host_idx]
hostname = hostnames[host_idx]
username = usernames[host_idx]
password = passwords[host_idx]
if passwords == "":
# To try to avoid prompting for passwords, try the previously-used password first
password = passwords[host_idx - 1]
try:
returned_password = run_command_on_host(command, hostname, username, password, ssh_options)
except pxssh.ExceptionPxssh as e:
print("Unable to log in to {user}@{host} using in-memory SSH keys nor provided password".format(user=username, host=hostname), file=sys.stderr)
print(e, file=sys.stderr)
continue
run_command_on_host(session, command)
for host_idx in range(0, num_hosts):
session = sessions[host_idx]
# Save a successful password to avoid prompting the user in the future
if returned_password == "":
# Probably key-based login was successful, in which case updating the list of passwords is meaningless
pass
else:
passwords[host_idx] = returned_password
output = get_output(session)
outputs.append(output)
return passwords
return outputs
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment