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)
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
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)
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
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
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