Пример #1
0
def cd(path):
    """
    Context manager that keeps directory state when calling `run`/`sudo`.

    Any calls to `run` or `sudo` within the wrapped block will implicitly have
    a string similar to ``"cd <path> && "`` prefixed in order to give the sense
    that there is actually statefulness involved.

    Since all other operations and contrib functions make use of `run` and/or
    `sudo`, they will also naturally be affected by use of `cd`.

    Like the actual 'cd' shell builtin, `cd` may be called with relative paths
    (keep in mind that your default starting directory is your remote user's
    ``$HOME``) and may be nested as well.

    Below is a "normal" attempt at using the shell 'cd', which doesn't work due
    to how shell-less SSH connections are implemented -- state is **not** kept
    between invocations of `run` or `sudo`::

        run('cd /var/www')
        run('ls')

    The above snippet will list the contents of the remote user's ``$HOME``
    instead of ``/var/www``. With `cd`, however, it will work as expected::

        with cd('/var/www'):
            run('ls') # Turns into "cd /var/www && ls"

    Finally, a demonstration (see inline comments) of nesting::

        with cd('/var/www'):
            run('ls') # cd /var/www && ls
            with cd('website1'):
                run('ls') # cd /var/www/website1 && ls

    ..note::

        This context manager is currently implemented by appending to (and, as
        always, restoring afterwards) the current value of an environment
        variable, ``env.cwd``. By default, this variable is empty, and thus no
        prefixing is performed.

        However, this implementation may change in the future, so we do not
        recommend manually altering ``env.cwd`` -- only the *behavior* of `cd`
        will have any guarantee of backwards compatibility.
    """
    if env.get('cwd'):
        # TODO: use platform-specific path join
        new_cwd = env.cwd + '/' + path
    else:
        new_cwd = path
    return _setenv(cwd=new_cwd)
Пример #2
0
def normalize(host_string, omit_port=False):
    """
    Normalizes a given host string, returning explicit host, user, port.

    If ``omit_port`` is given and is True, only the host and user are returned.
    """
    from state import env
    # Get user, host and port separately
    r = host_regex.match(host_string).groupdict()
    # Add any necessary defaults in
    user = r['user'] or env.get('user')
    host = r['host']
    port = r['port'] or '22'
    if omit_port:
        return user, host
    return user, host, port
Пример #3
0
 def host_prompting_wrapper(*args, **kwargs):
     while not env.get('host_string', False):
         env.host_string = raw_input("No hosts found. Please specify (single) host string for connection: ")
     return func(*args, **kwargs)
Пример #4
0
def sudo(command, shell=True, user=None, pty=False):
    """
    Run a shell command on a remote host, with superuser privileges.
    
    As with ``run()``, ``sudo()`` executes within a shell command defaulting to
    the value of ``env.shell``, although it goes one step further and wraps the
    command with ``sudo`` as well. Also similar to ``run()``, the shell

    You may specify a ``user`` keyword argument, which is passed to ``sudo``
    and allows you to run as some user other than root (which is the default).
    On most systems, the ``sudo`` program can take a string username or an
    integer userid (uid); ``user`` may likewise be a string or an int.

    Some remote systems may be configured to disallow sudo access unless a
    terminal or pseudoterminal is being used (e.g. when ``Defaults
    requiretty`` exists in ``/etc/sudoers``.) If updating the remote system's
    ``sudoers`` configuration is not possible or desired, you may pass
    ``pty=True`` to `sudo` to force allocation of a pseudo tty on the remote
    end.
       
    `sudo` will return the result of the remote program's stdout as a
    single (likely multiline) string. This string will exhibit a ``failed``
    boolean attribute specifying whether the command failed or succeeded, and
    will also include the return code as the ``return_code`` attribute.

    Examples::
    
        sudo("~/install_script.py")
        sudo("mkdir /var/www/new_docroot", user="******")
        sudo("ls /home/jdoe", user=1001)
        result = sudo("ls /tmp/")
    
    """
    # Construct sudo command, with user if necessary
    if user is not None:
        if str(user).isdigit():
            user = "******" % user
        sudo_prefix = "sudo -S -p '%%s' -u \"%s\" " % user
    else:
        sudo_prefix = "sudo -S -p '%s' "
    # Put in explicit sudo prompt string (so we know what to look for when
    # detecting prompts)
    sudo_prefix = sudo_prefix % env.sudo_prompt
    # Without using a shell, we just do 'sudo -u blah my_command'
    if (not env.use_shell) or (not shell):
        real_command = "%s %s" % (sudo_prefix, _shell_escape(command))
    # With a shell, we do 'sudo -u blah /bin/bash -l -c "my_command"'
    else:
        # With a shell, we can also honor cwd
        cwd = env.get('cwd', '')
        if cwd:
            # TODO: see if there is any nice way to quote this, given that it
            # ends up inside double quotes down below...
            cwd = 'cd %s && ' % _shell_escape(cwd)
        real_command = '%s %s "%s"' % (sudo_prefix, env.shell,
            _shell_escape(cwd + command))
    # TODO: handle confirm_proceed behavior, as in run()
    if output.debug:
        print("[%s] sudo: %s" % (env.host_string, real_command))
    elif output.running:
        print("[%s] sudo: %s" % (env.host_string, command))
    channel = connections[env.host_string]._transport.open_session()
    # Create pty if necessary (using Paramiko default options, which as of
    # 1.7.4 is vt100 $TERM @ 80x24 characters)
    if pty:
        channel.get_pty()
    # Execute
    channel.exec_command(real_command)
    capture = []

    out_thread = output_thread("[%s] out" % env.host_string, channel, capture=capture)
    err_thread = output_thread("[%s] err" % env.host_string, channel, stderr=True)

    # Close channel when done
    status = channel.recv_exit_status()

    # Wait for threads to exit before returning (otherwise we will occasionally
    # end up returning before the threads have fully wrapped up)
    out_thread.join()
    err_thread.join()

    # Close channel
    channel.close()

    # Assemble stdout string
    out = _AttributeString("".join(capture).strip())

    # Error handling
    out.failed = False
    if status != 0:
        out.failed = True
        msg = "sudo() encountered an error (return code %s) while executing '%s'" % (status, command)
        _handle_failure(message=msg)

    # Attach return code for convenience
    out.return_code = status
    return out
Пример #5
0
def run(command, shell=True, pty=False):
    """
    Run a shell command on a remote host.

    If ``shell`` is True (the default), ``run()`` will execute the given
    command string via a shell interpreter, the value of which may be
    controlled by setting ``env.shell`` (defaulting to something similar to
    ``/bin/bash -l -c "<command>"``.) Any double-quote (``"``) characters in
    ``command`` will be automatically escaped when ``shell`` is True.

    `run` will return the result of the remote program's stdout as a
    single (likely multiline) string. This string will exhibit a ``failed``
    boolean attribute specifying whether the command failed or succeeded, and
    will also include the return code as the ``return_code`` attribute.

    You may pass ``pty=True`` to force allocation of a pseudo tty on
    the remote end. This is not normally required, but some programs may
    complain (or, even more rarely, refuse to run) if a tty is not present.

    Examples::
    
        run("ls /var/www/")
        run("ls /home/myuser", shell=False)
        output = run('ls /var/www/site1')
    
    """
    # Set up new var so original argument can be displayed verbatim later.
    real_command = command
    if shell:
        # Handle cwd munging via 'cd' context manager
        cwd = env.get('cwd', '')
        if cwd:
            # TODO: see if there is any nice way to quote this, given that it
            # ends up inside double quotes down below...
            cwd = 'cd %s && ' % _shell_escape(cwd)
        # Construct final real, full command
        real_command = '%s "%s"' % (env.shell,
            _shell_escape(cwd + real_command))
    # TODO: possibly put back in previously undocumented 'confirm_proceed'
    # functionality, i.e. users may set an option to be prompted before each
    # execution. Pretty sure this should be a global option applying to ALL
    # remote operations! And, of course -- documented.
    if output.debug:
        print("[%s] run: %s" % (env.host_string, real_command))
    elif output.running:
        print("[%s] run: %s" % (env.host_string, command))
    channel = connections[env.host_string]._transport.open_session()
    # Create pty if necessary (using Paramiko default options, which as of
    # 1.7.4 is vt100 $TERM @ 80x24 characters)
    if pty:
        channel.get_pty()
    channel.exec_command(real_command)
    capture = []

    out_thread = output_thread("[%s] out" % env.host_string, channel,
        capture=capture)
    err_thread = output_thread("[%s] err" % env.host_string, channel,
        stderr=True)
    
    # Close when done
    status = channel.recv_exit_status()
    
    # Wait for threads to exit so we aren't left with stale threads
    out_thread.join()
    err_thread.join()

    # Close channel
    channel.close()

    # Assemble output string
    out = _AttributeString("".join(capture).strip())

    # Error handling
    out.failed = False
    if status != 0:
        out.failed = True
        msg = "run() encountered an error (return code %s) while executing '%s'" % (status, command)
        _handle_failure(message=msg)

    # Attach return code to output string so users who have set things to warn
    # only, can inspect the error code.
    out.return_code = status
    return out
Пример #6
0
def prompt(text, key=None, default='', validate=None):
    """
    Prompt user with ``text`` and return the input (like ``raw_input``).

    A single space character will be appended for convenience, but nothing
    else. Thus, you may want to end your prompt text with a question mark or a
    colon, e.g. ``prompt("What hostname?")``.

    If ``key`` is given, the user's input will be stored as ``env.<key>`` in
    addition to being returned by `prompt`. If the key already existed in
    ``env``, its value will be overwritten and a warning printed to the user.

    If ``default`` is given, it is displayed in square brackets and used if the
    user enters nothing (i.e. presses Enter without entering any text).
    ``default`` defaults to the empty string. If non-empty, a space will be
    appended, so that a call such as ``prompt("What hostname?",
    default="foo")`` would result in a prompt of ``What hostname? [foo]`` (with
    a trailing space after the ``[foo]``.)

    The optional keyword argument ``validate`` may be a callable or a string:
    
    * If a callable, it is called with the user's input, and should return the
      value to be stored on success. On failure, it should raise an exception
      with an exception message, which will be printed to the user.
    * If a string, the value passed to ``validate`` is used as a regular
      expression. It is thus recommended to use raw strings in this case. Note
      that the regular expression, if it is not fully matching (bounded by
      ``^`` and ``$``) it will be made so. In other words, the input must fully
      match the regex.

    Either way, `prompt` will re-prompt until validation passes (or the user
    hits ``Ctrl-C``).

    Examples::
    
        # Simplest form:
        environment = prompt('Please specify target environment: ')
        
        # With default, and storing as env.dish:
        prompt('Specify favorite dish: ', 'dish', default='spam & eggs')
        
        # With validation, i.e. requiring integer input:
        prompt('Please specify process nice level: ', key='nice', validate=int)
        
        # With validation against a regular expression:
        release = prompt('Please supply a release name',
                validate=r'^\w+-\d+(\.\d+)?$')
    
    """
    # Store previous env value for later display, if necessary
    if key:
        previous_value = env.get(key)
    # Set up default display
    default_str = ""
    if default != '':
        default_str = " [%s] " % str(default).strip()
    else:
        default_str = " "
    # Construct full prompt string
    prompt_str = text.strip() + default_str
    # Loop until we pass validation
    value = None
    while value is None:
        # Get input
        value = raw_input(prompt_str) or default
        # Handle validation
        if validate:
            # Callable
            if callable(validate):
                # Callable validate() must raise an exception if validation
                # fails.
                try:
                    value = validate(value)
                except Exception, e:
                    # Reset value so we stay in the loop
                    value = None
                    print("Validation failed for the following reason:")
                    print(indent(e.message) + "\n")
            # String / regex must match and will be empty if validation fails.
            else:
                # Need to transform regex into full-matching one if it's not.
                if not validate.startswith('^'):
                    validate = r'^' + validate
                if not validate.endswith('$'):
                    validate += r'$'
                result = re.findall(validate, value)
                if not result:
                    print("Regular expression validation failed: '%s' does not match '%s'\n" % (value, validate))
                    # Reset value so we stay in the loop
                    value = None