def _run_ssh_command(entries, username, idfile, command, tunnel, parallel=False): """ Runs the given command over SSH in parallel on all hosts in `entries`. :param entries: The host entries the hostnames from. :type entries: ``list`` of :py:class:`HostEntry` :param username: To use a specific username. :type username: ``str`` or ``NoneType`` :param idfile: The SSH identity file to use, or none. :type idfile: ``str`` or ``NoneType`` :param command: The command to run. :type command: ``str`` :param parallel: If true, commands will be run in parallel. :type parallel: ``bool`` """ if len(entries) == 0: print('(No hosts to run command on)') return 1 if command.strip() == '' or command is None: raise ValueError('No command given') print('Running command {0} on {1} matching hosts' .format(green(repr(command)), len(entries))) shell_cmds = [] for entry in entries: hname = entry.hostname or entry.public_ip cmd = _build_ssh_command(hname, username, idfile, command, tunnel) shell_cmds.append({ 'command': cmd, 'description': entry.display() }) stream_commands(shell_cmds, parallel=parallel) print(green('All commands finished'))
def _get_attrib(self, attr, convert_to_str=False): """ Given an attribute name, looks it up on the entry. Names that start with ``tags.`` are looked up in the ``tags`` dictionary. :param attr: Name of attribute to look up. :type attr: ``str`` :param convert_to_str: Convert result to a string. :type convert_to_str: ``bool`` :rtype: ``object`` """ if attr.startswith('tags.'): tag = attr[len('tags.'):] if tag in self.tags and self.tags[tag] != '': return self.tags[tag] elif convert_to_str is True: return '<not set>' else: return self.tags.get(tag) elif not hasattr(self, attr): raise AttributeError('Invalid attribute: {0}. Perhaps you meant ' '{1}?'.format(red(attr), green('tags.' + attr))) else: result = getattr(self, attr) if convert_to_str is True and not result: return '<none>' elif convert_to_str is True and isinstance(result, list): return ', '.join(result) elif convert_to_str is True: return str(result) else: return result
def _connect_ssh(entry, username, idfile, tunnel=None): """ SSH into to a host. :param entry: The host entry to pull the hostname from. :type entry: :py:class:`HostEntry` :param username: To use a specific username. :type username: ``str`` or ``NoneType`` :param idfile: The SSH identity file to use, if supplying a username. :type idfile: ``str`` or ``NoneType`` :param tunnel: Host to tunnel SSH command through. :type tunnel: ``str`` or ``NoneType`` :return: An exit status code. :rtype: ``int`` """ if entry.hostname != "" and entry.hostname is not None: _host = entry.hostname elif entry.public_ip != "" and entry.public_ip is not None: _host = entry.public_ip elif entry.private_ip != "" and entry.private_ip is not None: if tunnel is None: raise ValueError("Entry does not have a hostname or public IP. " "You can connect via private IP if you use a " "tunnel.") _host = entry.private_ip else: raise ValueError("No hostname, public IP or private IP information " "found on host entry. I don't know how to connect.") command = _build_ssh_command(_host, username, idfile, None, tunnel) print('Connecting to %s...' % cyan(entry.display())) print('SSH command: %s' % green(command)) proc = subprocess.Popen(command, shell=True) return proc.wait()
def _copy_to(entries, remote_path, local_path, profile): """ Performs an SCP command where the remote_path is the target and the local_path is the source. :param entries: A list of entries. :type entries: ``list`` of :py:class:`HostEntry` :param remote_path: The target path on the remote machine(s). :type remote_path: ``str`` :param local_path: The source path on the local machine. :type local_path: ``str`` :param profile: The profile, holding username/idfile info, etc. :type profile: :py:class:`Profile` """ commands = [] for entry in entries: hname = entry.hostname or entry.public_ip cmd = _build_scp_command(hname, profile.username, profile.identity_file, is_get=False, local_path=local_path, remote_path=remote_path) print('Command:', cmd) commands.append({ 'command': cmd, 'description': entry.display() }) stream_commands(commands) print(green('Finished copying'))
def render_entries(cls, entries, additional_columns=None, only_show=None, numbers=False): """ Pretty-prints a list of entries. If the window is wide enough to support printing as a table, runs the `print_table.render_table` function on the table. Otherwise, constructs a line-by-line representation.. :param entries: A list of entries. :type entries: [:py:class:`HostEntry`] :param additional_columns: Columns to show in addition to defaults. :type additional_columns: ``list`` of ``str`` :param only_show: A specific list of columns to show. :type only_show: ``NoneType`` or ``list`` of ``str`` :param numbers: Whether to include a number column. :type numbers: ``bool`` :return: A pretty-printed string. :rtype: ``str`` """ additional_columns = additional_columns or [] if only_show is not None: columns = _uniquify(only_show) else: columns = _uniquify(cls.DEFAULT_COLUMNS + additional_columns) top_row = [cls.prettyname(col) for col in columns] table = [top_row] if numbers is False else [[''] + top_row] for i, entry in enumerate(entries): row = [entry._get_attrib(c, convert_to_str=True) for c in columns] table.append(row if numbers is False else [i] + row) cur_width = get_current_terminal_width() colors = [ get_color_hash(c, MIN_COLOR_BRIGHT, MAX_COLOR_BRIGHT) for c in columns ] if cur_width >= get_table_width(table): return render_table( table, column_colors=colors if numbers is False else [green] + colors) else: result = [] first_index = 1 if numbers is True else 0 for row in table[1:]: rep = [green('%s:' % row[0] if numbers is True else '-----')] for i, val in enumerate(row[first_index:]): color = colors[i - 1 if numbers is True else i] name = columns[i] rep.append(' %s: %s' % (name, color(val))) result.append('\n'.join(rep)) return '\n'.join(result)
def render_entries(cls, entries, additional_columns=None, only_show=None, numbers=False): """ Pretty-prints a list of entries. If the window is wide enough to support printing as a table, runs the `print_table.render_table` function on the table. Otherwise, constructs a line-by-line representation.. :param entries: A list of entries. :type entries: [:py:class:`HostEntry`] :param additional_columns: Columns to show in addition to defaults. :type additional_columns: ``list`` of ``str`` :param only_show: A specific list of columns to show. :type only_show: ``NoneType`` or ``list`` of ``str`` :param numbers: Whether to include a number column. :type numbers: ``bool`` :return: A pretty-printed string. :rtype: ``str`` """ additional_columns = additional_columns or [] if only_show is not None: columns = _uniquify(only_show) else: columns = _uniquify(cls.DEFAULT_COLUMNS + additional_columns) top_row = [cls.prettyname(col) for col in columns] table = [top_row] if numbers is False else [[''] + top_row] for i, entry in enumerate(entries): row = [entry._get_attrib(c, convert_to_str=True) for c in columns] table.append(row if numbers is False else [i] + row) cur_width = get_current_terminal_width() colors = [get_color_hash(c, MIN_COLOR_BRIGHT, MAX_COLOR_BRIGHT) for c in columns] if cur_width >= get_table_width(table): return render_table(table, column_colors=colors if numbers is False else [green] + colors) else: result = [] first_index = 1 if numbers is True else 0 for row in table[1:]: rep = [green('%s:' % row[0] if numbers is True else '-----')] for i, val in enumerate(row[first_index:]): color = colors[i-1 if numbers is True else i] name = columns[i] rep.append(' %s: %s' % (name, color(val))) result.append('\n'.join(rep)) return '\n'.join(result)
def _copy_from(entries, remote_path, local_path, profile): """ Performs an SCP command where the remote_path is the source and the local_path is a format string, formatted individually for each host being copied from so as to create one or more distinct paths on the local system. :param entries: A list of entries. :type entries: ``list`` of :py:class:`HostEntry` :param remote_path: The source path on the remote machine(s). :type remote_path: ``str`` :param local_path: A format string for the path on the local machine. :type local_path: ``str`` :param profile: The profile, holding username/idfile info, etc. :type profile: :py:class:`Profile` """ commands = [] paths = set() for entry in entries: hname = entry.hostname or entry.public_ip _local_path = entry.format_string(local_path) if _local_path in paths: raise ValueError('Duplicate local paths: one or more paths ' 'had value {} after formatting.' .format(local_path)) paths.add(_local_path) # If the path references a folder, create the folder if it doesn't # exist. _folder = os.path.split(_local_path)[0] if len(_folder) > 0: if not os.path.exists(_folder): print('Creating directory ' + _folder) os.makedirs(_folder) cmd = _build_scp_command(hname, profile.username, profile.identity_file, is_get=True, local_path=_local_path, remote_path=remote_path) print('Command:', cmd) commands.append({ 'command': cmd, 'description': entry.display() }) stream_commands(commands) print(green('Finished copying'))
def _run_ssh(entries, username, idfile, no_prompt=False, command=None, show=None, only=None, sort_by=None, limit=None, tunnel=None): """ Lets the user choose which instance to SSH into. :param entries: The list of host entries. :type entries: [:py:class:`HostEntry`] :param username: The SSH username to use. Defaults to current user. :type username: ``str`` or ``NoneType`` :param idfile: The identity file to use. Optional. :type idfile: ``str`` or ``NoneType`` :param no_prompt: Whether to disable confirmation for SSH command. :type no_prompt: ``bool`` :param command: SSH command to run on matching instances. :type command: ``str`` or ``NoneType`` :param show: Instance attributes to show in addition to defaults. :type show: ``NoneType`` or ``list`` of ``str`` :param only: If not ``None``, will *only* show these attributes. :type only: ``NoneType`` or ``list`` of ``str`` :param sort_by: What to sort columns by. By default, sort by 'name'. :type sort_by: ``str`` :param limit: At most how many results to show. :type limit: ``int`` or ``NoneType`` """ _print_entries = True _print_help = False if len(entries) == 0: exit('No entries matched the filters.') if no_prompt is True and command is not None: return _run_ssh_command(entries, username, idfile, command, tunnel) elif len(entries) == 1: if command is None: return _connect_ssh(entries[0], username, idfile, tunnel) else: return _run_ssh_command(entries, username, idfile, command, tunnel) elif command is not None: print(HostEntry.render_entries(entries, additional_columns=show, only_show=only, numbers=True)) if no_prompt is False: get_input("Press enter to run command {} on the {} " "above machines (Ctrl-C to cancel)" .format(cyan(command), len(entries))) return _run_ssh_command(entries, username, idfile, command, tunnel) else: while True: if sort_by is not None: entries = HostEntry.sort_by(entries, sort_by) if limit is not None: entries = entries[:limit] if _print_entries is True: print(HostEntry.render_entries(entries, additional_columns=show, only_show=only, numbers=True)) print('%s matching entries.' % len(entries)) _print_entries = False if _print_help is True: cmd_str = green(command) if command is not None else 'none set' msg = COMMANDS_STRING.format(username=username or 'none set', idfile=idfile or 'none set', cur_cmd=cmd_str) print(msg) _print_help = False elif command is not None: print('Set to run ssh command: %s' % cyan(command)) msg = 'Enter command (%s for help, %s to quit): ' % (cyan('h'), cyan('q')) choice = get_input(msg) if isinstance(choice, int): if 0 <= choice <= len(entries): break else: msg = 'Invalid number: must be between 0 and %s' print(msg % (len(entries) - 1)) elif choice == 'x': if command is None: print('No command has been set. Set command with `c`') else: return _run_ssh_command(entries, username, idfile, command, tunnel) elif choice == 'h': _print_help = True elif choice in ['q', 'quit', 'exit']: print('bye!') return else: # All of these commands take one or more arguments, so the # split length must be at least 2. commands = choice.split() if len(commands) < 2: print(yellow('Unknown command "%s".' % choice)) else: cmd = commands[0] if cmd in ['u', 'i', 'p']: if cmd == 'u': username = commands[1] elif cmd == 'i': _idfile = commands[1] if not os.path.exists(_idfile): print(yellow('No such file: %s' % _idfile)) continue idfile = _idfile elif cmd == 'p': p = commands[1] try: profile = LsiProfile.load(p) _username = profile.username _idfile = expanduser(profile.identity_file) except LsiProfile.LoadError: print(yellow('No such profile: %s' % repr(p))) continue username = _username idfile = _idfile print('username: %s' % green(repr(username))) print('identity file: %s' % green(repr(idfile))) elif cmd == 'f': entries = filter_entries(entries, commands[1:], []) _print_entries = True elif cmd == 'e': entries = filter_entries(entries, [], commands[1:]) _print_entries = True elif cmd == 'c': command = ' '.join(commands[1:]) elif cmd == 'limit': try: limit = int(commands[1]) _print_entries = True except ValueError: print(yellow('Invalid limit (must be an integer)')) elif cmd == 'sort': sort_by = commands[1] if sort_by not in show: show.append(sort_by) _print_entries = True elif cmd == 'show': if show is None: show = commands[1:] else: show.extend(commands[1:]) _print_entries = True else: print(yellow('Unknown command "%s".' % cmd)) return _connect_ssh(entries[choice], username, idfile, tunnel)
COMMANDS_STRING = \ '''{commands}: h: Show this message <number {n}>: Connect to the {n}th instance in the list p {profile}: Use profile {profile} u {username}: Change SSH username to {username} (currently {_username}) i {idfile}: Change identity file to {idfile} (currently {_idfile}) f <one or more {pattern}s>: Restrict results to those with {pattern}s e <one or more {pattern}s>: Restrict results to those without {pattern}s limit {n}: Limit output to first {n} lines sort <{attribute}>: Sort the list by {attribute} show <one or more {attribute}s>: Additionally show those {attribute}s c <{command}>: Set ssh command to run on matching hosts (currently {cur_cmd}) x: Execute the above command on the above host(s) {q}: Quit '''.format(commands=green('Commands'), n=cyan('n'), profile=cyan('profile'), attribute=cyan('attribute'), username=cyan('username'), _username='******', idfile=cyan('idfile'), _idfile='{idfile}', pattern=cyan('pattern'), command=cyan('command'), cur_cmd='{cur_cmd}', q=cyan('q')) # Path to where the SSH known hosts will be stored. _KNOWN_HOSTS_FILE = os.path.expanduser(os.environ.get('LSI_KNOWN_HOSTS', '~/.lsi-known-hosts'))