From 4258807bfcedb190155d690ab5e059cd9670e90d Mon Sep 17 00:00:00 2001 From: Joe Buck Date: Tue, 19 Mar 2013 21:26:16 -0700 Subject: [PATCH] teuthology: remove previous test ssh keys Updated the ssh-keys task to cleanup any left-over keys from previous tasks (indicated by the user being 'ssh-keys-user'). Also, some of the functions in the ssh_keys task seem like they could be useful in general. This patch refactors them into misc.py. Signed-off-by: Joe Buck Reviewd-by: Sam Lang --- teuthology/misc.py | 146 ++++++++++++++++++++++++++++ teuthology/task/ssh_keys.py | 185 +++++++++++++++++------------------- 2 files changed, 231 insertions(+), 100 deletions(-) diff --git a/teuthology/misc.py b/teuthology/misc.py index 1fdeadf4aa5aa..1838dcc3a3769 100644 --- a/teuthology/misc.py +++ b/teuthology/misc.py @@ -270,6 +270,152 @@ def sudo_write_file(remote, path, data, perms=None): stdin=data, ) +def move_file(remote, from_path, to_path, sudo=False): + + # need to stat the file first, to make sure we + # maintain the same permissions + args = [] + if sudo: + args.append('sudo') + args.extend([ + 'stat', + '-c', + '\"%a\"', + to_path + ]) + proc = remote.run( + args=args, + stdout=StringIO(), + ) + perms = proc.stdout.getvalue().rstrip().strip('\"') + + args = [] + if sudo: + args.append('sudo') + args.extend([ + 'mv', + '--', + from_path, + to_path, + ]) + proc = remote.run( + args=args, + stdout=StringIO(), + ) + + # reset the file back to the original permissions + args = [] + if sudo: + args.append('sudo') + args.extend([ + 'chmod', + perms, + to_path, + ]) + proc = remote.run( + args=args, + stdout=StringIO(), + ) + +def delete_file(remote, path, sudo=False): + args = [] + if sudo: + args.append('sudo') + args.extend([ + 'rm', + '--', + path, + ]) + proc = remote.run( + args=args, + stdout=StringIO(), + ) + +def remove_lines_from_file(remote, path, line_is_valid_test, string_to_test_for): + # read in the specified file + in_data = get_file(remote, path, False) + out_data = "" + + first_line = True + # use the 'line_is_valid_test' function to remove unwanted lines + for line in in_data.split('\n'): + if line_is_valid_test(line, string_to_test_for): + if not first_line: + out_data += '\n' + else: + first_line = False + + out_data += '{line}'.format(line=line) + + else: + log.info('removing line: {bad_line}'.format(bad_line=line)) + + # get a temp file path on the remote host to write to, + # we don't want to blow away the remote file and then have the + # network drop out + temp_file_path = get_remote_tempnam(remote) + + # write out the data to a temp file + write_file(remote, temp_file_path, out_data) + + # then do a 'mv' to the actual file location + move_file(remote, temp_file_path, path) + +def append_lines_to_file(remote, path, lines, sudo=False): + temp_file_path = get_remote_tempnam(remote) + + data = get_file(remote, path, sudo) + + # add the additional data and write it back out, using a temp file + # in case of connectivity of loss, and then mv it to the + # actual desired location + data += lines + temp_file_path + write_file(remote, temp_file_path, data) + + # then do a 'mv' to the actual file location + move_file(remote, temp_file_path, path) + +def get_remote_tempnam(remote, sudo=False): + args = [] + if sudo: + args.append('sudo') + args.extend([ + 'python', + '-c', + 'import os; print os.tempnam()' + ]) + proc = remote.run( + args=args, + stdout=StringIO(), + ) + data = proc.stdout.getvalue() + return data + +def create_file(remote, path, data="", permissions=str(644), sudo=False): + """ + Create a file on the remote host. + """ + args = [] + if sudo: + args.append('sudo') + args.extend([ + 'touch', + path, + run.Raw('&&'), + 'chmod', + permissions, + '--', + path + ]) + proc = remote.run( + args=args, + stdout=StringIO(), + ) + # now write out the data if any was passed in + if "" != data: + append_lines_to_file(remote, path, data, sudo) + def get_file(remote, path, sudo=False): """ Read a file from remote host into memory. diff --git a/teuthology/task/ssh_keys.py b/teuthology/task/ssh_keys.py index c2637d01eab44..60fcacfd6c289 100644 --- a/teuthology/task/ssh_keys.py +++ b/teuthology/task/ssh_keys.py @@ -6,10 +6,12 @@ import re from cStringIO import StringIO from teuthology import contextutil +import teuthology.misc as misc from ..orchestra import run from ..orchestra.connection import create_key log = logging.getLogger(__name__) +ssh_keys_user = 'ssh-keys-user' # generatees a public and private key def generate_keys(): @@ -18,66 +20,53 @@ def generate_keys(): key.write_private_key(privateString) return key.get_base64(), privateString.getvalue() +def particular_ssh_key_test(line_to_test, ssh_key): + match = re.match('[\w-]+ {key} \S+@\S+'.format(key=ssh_key), line_to_test) + + if match: + log.info('found matching ssh_key line: {line}'.format(line=line_to_test)) + return False + else: + return True + +def ssh_keys_user_line_test(line_to_test, username ): + match = re.match('[\w-]+ \S+ {username}@\S+'.format(username=username), line_to_test) + + if match: + log.info('found a ssh-keys-user line: {line}'.format(line=line_to_test)) + return False + else: + return True + +# deletes keys that were previously generated +def pre_run_cleanup_keys(ctx): + log.info('cleaning up any left-over keys from previous tests') + + for remote in ctx.cluster.remotes: + username, hostname = str(remote).split('@') + if "" == username or "" == hostname: + continue + else: + path = '/home/{user}/.ssh/authorized_keys'.format(user=username) + + misc.remove_lines_from_file(remote, path, ssh_keys_user_line_test, ssh_keys_user) + # deletes the keys and removes ~/.ssh/authorized_keys entries we added -def cleanup_keys(ctx, public_key): - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - for host in ctx.cluster.remotes.iterkeys(): - username, hostname = str(host).split('@') - log.info('cleaning up keys on {host}'.format(host=hostname, user=username)) - - # try to extract a public key for the host from the ctx.config entries - host_key_found = False - for t, host_key in ctx.config['targets'].iteritems(): - - if str(t) == str(host): - keytype, key = host_key.split(' ',1) - client.get_host_keys().add( - hostname=hostname, - keytype=keytype, - key=create_key(keytype,key) - ) - host_key_found = True - log.info('ssh key found in ctx') - - # if we did not find a key, load the system keys - if False == host_key_found: - client.load_system_host_keys() - log.info('no key found in ctx, using system host keys') - - client.connect(hostname, username=username) - client.exec_command('rm ~/.ssh/id_rsa') - client.exec_command('rm ~/.ssh/id_rsa.pub') - - # get the absolute path for authorized_keys - stdin, stdout, stderr = client.exec_command('ls ~/.ssh/authorized_keys') - auth_keys_file = stdout.readlines()[0].rstrip() - - mySftp = client.open_sftp() - - # write to a different authorized_keys file in case something - # fails 1/2 way through (don't want to break ssh on the vm) - old_auth_keys_file = mySftp.open(auth_keys_file) - new_auth_keys_file = mySftp.open(auth_keys_file + '.new', 'w') - - for line in old_auth_keys_file.readlines(): - match = re.search(re.escape(public_key), line) - - if match: - pass - else: - new_auth_keys_file.write(line) - - # close the files - old_auth_keys_file.close() - new_auth_keys_file.close() - - # now try to do an atomic-ish rename. If we botch this, it's bad news - stdin, stdout, stderr = client.exec_command('mv ~/.ssh/authorized_keys.new ~/.ssh/authorized_keys') - - mySftp.close() - client.close() +def cleanup_added_key(ctx, public_key): + log.info('cleaning up keys added for testing') + + for remote in ctx.cluster.remotes: + username, hostname = str(remote).split('@') + if "" == username or "" == hostname: + continue + else: + log.info(' cleaning up keys for user {user} on {host}'.format(host=hostname, user=username)) + + misc.delete_file(remote, '/home/{user}/.ssh/id_rsa'.format(user=username)) + misc.delete_file(remote, '/home/{user}/.ssh/id_rsa.pub'.format(user=username)) + path = '/home/{user}/.ssh/authorized_keys'.format(user=username) + + misc.remove_lines_from_file(remote, path, particular_ssh_key_test, public_key) @contextlib.contextmanager def tweak_ssh_config(ctx, config): @@ -107,46 +96,39 @@ def tweak_ssh_config(ctx, config): @contextlib.contextmanager def push_keys_to_host(ctx, config, public_key, private_key): - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - for host in ctx.cluster.remotes.iterkeys(): - log.info('host: {host}'.format(host=host)) - username, hostname = str(host).split('@') - - # try to extract a public key for the host from the ctx.config entries - host_key_found = False - for t, host_key in ctx.config['targets'].iteritems(): - - if str(t) == str(host): - keytype, key = host_key.split(' ',1) - client.get_host_keys().add( - hostname=hostname, - keytype=keytype, - key=create_key(keytype,key) - ) - host_key_found = True - log.info('ssh key found in ctx') - - # if we did not find a key, load the system keys - if False == host_key_found: - client.load_system_host_keys() - log.info('no key found in ctx, using system host keys') - - log.info('pushing keys to {host} for {user}'.format(host=hostname, user=username)) - - client.connect(hostname, username=username) - client.exec_command('echo "{priv_key}" > ~/.ssh/id_rsa'.format(priv_key=private_key)) - # the default file permissions cause ssh to balk - client.exec_command('chmod 500 ~/.ssh/id_rsa') - client.exec_command('echo "ssh-rsa {pub_key} {user_host}" > ~/.ssh/id_rsa.pub'.format(pub_key=public_key,user_host=host)) - - # for this host, add all hosts to the ~/.ssh/authorized_keys file - for inner_host in ctx.cluster.remotes.iterkeys(): - client.exec_command('echo "ssh-rsa {pub_key} {user_host}" >> ~/.ssh/authorized_keys'.format(pub_key=public_key,user_host=str(inner_host))) - - - client.close() + # add an entry for all hosts in ctx to auth_keys_data + auth_keys_data = '' + + for inner_host in ctx.cluster.remotes.iterkeys(): + inner_username, inner_hostname = str(inner_host).split('@') + # create a 'user@hostname' string using our fake hostname + fake_hostname = '{user}@{host}'.format(user=ssh_keys_user,host=str(inner_hostname)) + auth_keys_data += '\nssh-rsa {pub_key} {user_host}'.format(pub_key=public_key,user_host=fake_hostname) + + # for each host in ctx, add keys for all other hosts + for remote in ctx.cluster.remotes: + username, hostname = str(remote).split('@') + if "" == username or "" == hostname: + continue + else: + log.info('pushing keys to {host} for {user}'.format(host=hostname, user=username)) + + # adding a private key + priv_key_file = '/home/{user}/.ssh/id_rsa'.format(user=username) + priv_key_data = '{priv_key}'.format(priv_key=private_key) + # Hadoop requires that .ssh/id_rsa have permissions of '500' + misc.create_file(remote, priv_key_file, priv_key_data, str(500)) + + # then a private key + pub_key_file = '/home/{user}/.ssh/id_rsa.pub'.format(user=username) + pub_key_data = 'ssh-rsa {pub_key} {user_host}'.format(pub_key=public_key,user_host=str(remote)) + misc.create_file(remote, pub_key_file, pub_key_data) + + # adding appropriate entries to the authorized_keys file for this host + auth_keys_file = '/home/{user}/.ssh/authorized_keys'.format(user=username) + + # now add the list of keys for hosts in ctx to ~/.ssh/authorized_keys + misc.append_lines_to_file(remote, auth_keys_file, auth_keys_data) try: yield @@ -154,7 +136,7 @@ def push_keys_to_host(ctx, config, public_key, private_key): finally: # cleanup the keys log.info("Cleaning up SSH keys") - cleanup_keys(ctx, public_key) + cleanup_added_key(ctx, public_key) @contextlib.contextmanager @@ -178,6 +160,9 @@ def task(ctx, config): # ctx, so I'm keeping it outside of the nested calls public_key_string, private_key_string = generate_keys() + # cleanup old keys if they were somehow left behind + pre_run_cleanup_keys(ctx) + with contextutil.nested( lambda: push_keys_to_host(ctx, config, public_key_string, private_key_string), lambda: tweak_ssh_config(ctx, config), -- 2.39.5