コード例 #1
0
ファイル: lsi.py プロジェクト: NarrativeScience/lsi
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'))
コード例 #2
0
ファイル: hosts.py プロジェクト: NarrativeScience/lsi
    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
コード例 #3
0
ファイル: hosts.py プロジェクト: adnelson/lsi
    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
コード例 #4
0
ファイル: lsi.py プロジェクト: NarrativeScience/lsi
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()
コード例 #5
0
ファイル: lsi.py プロジェクト: NarrativeScience/lsi
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'))
コード例 #6
0
ファイル: hosts.py プロジェクト: adnelson/lsi
    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)
コード例 #7
0
ファイル: hosts.py プロジェクト: NarrativeScience/lsi
    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)
コード例 #8
0
ファイル: lsi.py プロジェクト: NarrativeScience/lsi
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'))
コード例 #9
0
ファイル: lsi.py プロジェクト: NarrativeScience/lsi
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)
コード例 #10
0
ファイル: lsi.py プロジェクト: NarrativeScience/lsi
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'))