pax_global_header00006660000000000000000000000064127107367730014527gustar00rootroot0000000000000052 comment=a5f54cf211c0f5b094ad3b2c6f7b07b8f4a119c6 spur.py-0.3.17/000077500000000000000000000000001271073677300132375ustar00rootroot00000000000000spur.py-0.3.17/.gitignore000066400000000000000000000000571271073677300152310ustar00rootroot00000000000000*.pyc /_virtualenv /*.egg-info /MANIFEST /.tox spur.py-0.3.17/CHANGES000066400000000000000000000052451271073677300142400ustar00rootroot00000000000000# CHANGES ## 0.3.17 * When encoding argument is set, decode output before writing to stdout and stderr arguments. This changes the behaviour to match the documented behaviour: previously, only the output on the result object was decoded. ## 0.3.16 * Remove non-ASCII character from README.rst. ## 0.3.15 * Add encoding argument to spawn and run. * SshShell: add load_system_host_keys argument. * LocalShell: don't raise NoSuchCommandError when cwd does not exist. ## 0.3.14 * Raise spur.CommandInitializationError when SshShell fails to read integers from stdout before the command is actually run. ## 0.3.13 * Add look_for_private_keys argument to SshShell to allow searching for private keys to be disabled. ## 0.3.12 * Add shell_type argument to SshShell to allow better support for minimal shells, such as those found on embedded systems. * Open files in text mode by default. When opening files over SSH, this means that files are decoded using the encoding returned by locale.getpreferredencoding(). ## 0.3.11 * Add support for platforms that don't support the pty module, such as Windows. ## 0.3.10 * SshShell: Use "which" if "command -v" is not available. Fixes GitHub issue #15: https://github.com/mwilliamson/spur.py/issues/15 ## 0.3.9 * Treat output as bytes rather than attempting to decode to string when generating RunProcessError.message. Fixes GitHub issue #13: https://github.com/mwilliamson/spur.py/pull/13 * Support unicode commands over SSH. ## 0.3.8 * Add full support for Python 3. ## 0.3.7 * Handle buffering more consistently across different Python versions. ## 0.3.6 * LocalShell: Add support for Python 3. Since paramiko is currently unsupported on Python 3, use the package "spur.local" rather than "spur". ## 0.3.5 * SshShell: Use "command -v" instead of "which" for better POSIX compliance. * SshShell: Skip blank lines when expecting echoed return code. ## 0.3.4 * LocalShell: Use Popen.wait instead of Popen.poll to get return code of local process to ensure process has exited. ## 0.3.3 * Make username argument to SshShell optional. Closes GitHub issue #4. ## 0.3.2 * Include original error and original traceback on spur.ssh.ConnectionError * Add experimental use_pty argument for run and spawn. Use at your own risk! ## 0.3.1 * spur.NoSuchCommandError is now raised if the command passed to run or spawn doesn't exist ## 0.3.0 * Change default behaviour to raise an error when a host key is missing. * Allow selection of behaviour when a host key is missing by adding host_key_missing argument to SshShell constructor. ## 0.2.4 * Catch EOFError and wrap it in spur.ssh.ConnectionError when opening SSH session spur.py-0.3.17/CONTRIBUTING.rst000066400000000000000000000007041271073677300157010ustar00rootroot00000000000000Contributing ============ Tests ----- You can run the tests using tox. The SSH tests require a running SSH server. The following environment variables are used in the SSH tests: * ``TEST_SSH_HOSTNAME`` * ``TEST_SSH_PORT`` (optional) * ``TEST_SSH_USERNAME`` * ``TEST_SSH_PASSWORD`` Tests should pass on CPython 2.6/3.2 and later. There's currently no support for PyPy since paramiko doesn't appear to install on PyPy (in turn, because of PyCrypto). spur.py-0.3.17/LICENSE000066400000000000000000000024301271073677300142430ustar00rootroot00000000000000Copyright (c) 2014, Michael Williamson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. spur.py-0.3.17/MANIFEST.in000066400000000000000000000000231271073677300147700ustar00rootroot00000000000000include README.rst spur.py-0.3.17/README.rst000066400000000000000000000263771271073677300147450ustar00rootroot00000000000000spur.py: Run commands and manipulate files locally or over SSH using the same interface ======================================================================================= To run echo locally: .. code-block:: python import spur shell = spur.LocalShell() result = shell.run(["echo", "-n", "hello"]) print(result.output) # prints hello Executing the same command over SSH uses the same interface -- the only difference is how the shell is created: .. code-block:: python import spur shell = spur.SshShell(hostname="localhost", username="bob", password="password1") with shell: result = shell.run(["echo", "-n", "hello"]) print(result.output) # prints hello Installation ------------ ``$ pip install spur`` Shell constructors ------------------ LocalShell ~~~~~~~~~~ Takes no arguments: .. code-block:: sh spur.LocalShell() SshShell ~~~~~~~~ Requires a hostname. Also requires some combination of a username, password and private key, as necessary to authenticate: .. code-block:: python # Use a password spur.SshShell( hostname="localhost", username="bob", password="password1" ) # Use a private key spur.SshShell( hostname="localhost", username="bob", private_key_file="path/to/private.key" ) # Use a port other than 22 spur.SshShell( hostname="localhost", port=50022, username="bob", password="password1" ) Optional arguments: * ``connect_timeout`` -- a timeout in seconds for establishing an SSH connection. Defaults to 60 (one minute). * ``missing_host_key`` -- by default, an error is raised when a host key is missing. One of the following values can be used to change the behaviour when a host key is missing: - ``spur.ssh.MissingHostKey.raise_error`` -- raise an error - ``spur.ssh.MissingHostKey.warn`` -- accept the host key and log a warning - ``spur.ssh.MissingHostKey.accept`` -- accept the host key * ``shell_type`` -- the type of shell used by the host. Defaults to ``spur.ssh.ShellTypes.sh``, which should be appropriate for most Linux distributions. If the host uses a different shell, such as simpler shells often found on embedded systems, try changing ``shell_type`` to a more appropriate value, such as ``spur.ssh.ShellTypes.minimal``. The following shell types are currently supported: - ``spur.ssh.ShellTypes.sh`` -- the Bourne shell. Supports all features. - ``spur.ssh.ShellTypes.minimal`` -- a minimal shell. Several features are unsupported: - Non-existent commands will not raise ``spur.NoSuchCommandError``. - The following arguments to ``spawn`` and ``run`` are unsupported unless set to their default values: ``cwd``, ``update_env``, and ``store_pid``. * ``look_for_private_keys`` -- by default, Spur will search for discoverable private key files in ``~/.ssh/``. Set to ``False`` to disable this behaviour. * ``load_system_host_keys`` -- by default, Spur will attempt to read host keys from the user's known hosts file, as used by OpenSSH, and no exception will be raised if the file can't be read. Set to ``False`` to disable this behaviour. Shell interface --------------- run(command, cwd, update\_env, store\_pid, allow\_error, stdout, stderr, encoding) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Run a command and wait for it to complete. The command is expected to be a list of strings. Returns an instance of ``ExecutionResult``. .. code-block:: python result = shell.run(["echo", "-n", "hello"]) print(result.output) # prints hello Note that arguments are passed without any shell expansion. For instance, ``shell.run(["echo", "$PATH"])`` will print the literal string ``$PATH`` rather than the value of the environment variable ``$PATH``. Raises ``spur.NoSuchCommandError`` if trying to execute a non-existent command. Optional arguments: * ``cwd`` -- change the current directory to this value before executing the command. * ``update_env`` -- a ``dict`` containing environment variables to be set before running the command. If there's an existing environment variable with the same name, it will be overwritten. Otherwise, it is unchanged. * ``store_pid`` -- if set to ``True`` when calling ``spawn``, store the process id of the spawned process as the attribute ``pid`` on the returned process object. Has no effect when calling ``run``. * ``allow_error`` -- ``False`` by default. If ``False``, an exception is raised if the return code of the command is anything but 0. If ``True``, a result is returned irrespective of return code. * ``stdout`` -- if not ``None``, anything the command prints to standard output during its execution will also be written to ``stdout`` using ``stdout.write``. * ``stderr`` -- if not ``None``, anything the command prints to standard error during its execution will also be written to ``stderr`` using ``stderr.write``. * ``encoding`` -- if set, this is used to decode any output. By default, any output is treated as raw bytes. If set, the raw bytes are decoded before writing to the passed ``stdout`` and ``stderr`` arguments (if set) and before setting the output attributes on the result. ``shell.run(*args, **kwargs)`` should behave similarly to ``shell.spawn(*args, **kwargs).wait_for_result()`` spawn(command, cwd, update\_env, store\_pid, allow\_error, stdout, stderr, encoding) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Behaves the same as ``run`` except that ``spawn`` immediately returns an object representing the running process. Raises ``spur.NoSuchCommandError`` if trying to execute a non-existent command. open(path, mode="r") ~~~~~~~~~~~~~~~~~~~~ Open the file at ``path``. Returns a file-like object. By default, files are opened in text mode. Appending `"b"` to the mode will open the file in binary mode. For instance, to copy a binary file over SSH, assuming you already have an instance of ``SshShell``: .. code-block:: python with ssh_shell.open("/path/to/remote", "rb") as remote_file: with open("/path/to/local", "wb") as local_file: shutil.copyfileobj(remote_file, local_file) Process interface ----------------- Returned by calls to ``shell.spawn``. Has the following attributes: * ``pid`` -- the process ID of the process. Only available if ``store_pid`` was set to ``True`` when calling ``spawn``. Has the following methods: * ``is_running()`` -- return ``True`` if the process is still running, ``False`` otherwise. * ``stdin_write(value)`` -- write ``value`` to the standard input of the process. * ``wait_for_result()`` -- wait for the process to exit, and then return an instance of ``ExecutionResult``. Will raise ``RunProcessError`` if the return code is not zero and ``shell.spawn`` was not called with ``allow_error=True``. * ``send_signal(signal)`` -- sends the process the signal ``signal``. Only available if ``store_pid`` was set to ``True`` when calling ``spawn``. Classes ------- ExecutionResult ~~~~~~~~~~~~~~~ ``ExecutionResult`` has the following properties: * ``return_code`` -- the return code of the command * ``output`` -- a string containing the result of capturing stdout * ``stderr_output`` -- a string containing the result of capturing stdout It also has the following methods: * ``to_error()`` -- return the corresponding RunProcessError. This is useful if you want to conditionally raise RunProcessError, for instance: .. code-block:: python result = shell.run(["some-command"], allow_error=True) if result.return_code > 4: raise result.to_error() RunProcessError ~~~~~~~~~~~~~~~ A subclass of ``RuntimeError`` with the same properties as ``ExecutionResult``: * ``return_code`` -- the return code of the command * ``output`` -- a string containing the result of capturing stdout * ``stderr_output`` -- a string containing the result of capturing stdout NoSuchCommandError ~~~~~~~~~~~~~~~~~~ ``NoSuchCommandError`` has the following properties: * ``command`` -- the command that could not be found API stability ------------- Using the the terminology from `Semantic Versioning `_, if the version of spur is X.Y.Z, then X is the major version, Y is the minor version, and Z is the patch version. While the major version is 0, incrementing the patch version indicates a backwards compatible change. For instance, if you're using 0.3.1, then it should be safe to upgrade to 0.3.2. Incrementing the minor version indicates a change in the API. This means that any code using previous minor versions of spur may need updating before it can use the current minor version. Undocumented features ~~~~~~~~~~~~~~~~~~~~~ Some features are undocumented, and should be considered experimental. Use them at your own risk. They may not behave correctly, and their behaviour and interface may change at any time. Troubleshooting --------------- I get the error "Connection refused" when trying to connect to a virtual machine using a forwarded port on ``localhost`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Try using ``"127.0.0.1"`` instead of ``"localhost"`` as the hostname. I get the error "Connection refused" when trying to execute commands over SSH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Try connecting to the machine using SSH on the command line with the same settings. For instance, if you're using the code: .. code-block:: python shell = spur.SshShell( hostname="remote", port=2222, username="bob", private_key_file="/home/bob/.ssh/id_rsa" ) with shell: result = shell.run(["echo", "hello"]) Try running: .. code-block:: sh ssh bob@remote -p 2222 -i /home/bob/.ssh/id_rsa If the ``ssh`` command succeeds, make sure that the arguments to ``ssh.SshShell`` and the ``ssh`` command are the same. If any of the arguments to ``ssh.SshShell`` are dynamically generated, try hard-coding them to make sure they're set to the values you expect. I can't spawn or run commands over SSH ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're having trouble spawning or running commands over SSH, try passing ``shell_type=spur.ssh.ShellTypes.minimal`` as an argument to ``spur.SshShell``. For instance: .. code-block:: python import spur import spur.ssh spur.SshShell( hostname="localhost", username="bob", password="password1", shell_type=spur.ssh.ShellTypes.minimal, ) This makes minimal assumptions about the features that the host shell supports, and is especially well-suited to minimal shells found on embedded systems. If the host shell is more fully-featured but only works with ``spur.ssh.ShellTypes.minimal``, feel free to submit an issue. Why don't shell features such as variables and redirection work? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Commands are run directly rather than through a shell. If you want to use any shell features such as variables and redirection, then you'll need to run those commands within an appropriate shell. For instance: .. code-block:: python shell.run(["sh", "-c", "echo $PATH"]) shell.run(["sh", "-c", "ls | grep bananas"]) spur.py-0.3.17/makefile000066400000000000000000000007351271073677300147440ustar00rootroot00000000000000.PHONY: test upload clean bootstrap test: sh -c '. _virtualenv/bin/activate; nosetests -m'\''^$$'\'' `find tests -name '\''*.py'\''`' upload: python setup.py sdist upload make clean register: python setup.py register clean: rm -f MANIFEST rm -rf dist bootstrap: _virtualenv _virtualenv/bin/pip install -e . ifneq ($(wildcard test-requirements.txt),) _virtualenv/bin/pip install -r test-requirements.txt endif make clean _virtualenv: virtualenv _virtualenv spur.py-0.3.17/setup.py000066400000000000000000000022471271073677300147560ustar00rootroot00000000000000#!/usr/bin/env python import os from setuptools import setup def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name='spur', version='0.3.17', description='Run commands and manipulate files locally or over SSH using the same interface', long_description=read("README.rst"), author='Michael Williamson', author_email='mike@zwobble.org', url='http://github.com/mwilliamson/spur.py', keywords="ssh shell subprocess process", packages=['spur'], install_requires=["paramiko>=1.13.1,<2"], classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Internet', ], ) spur.py-0.3.17/spur/000077500000000000000000000000001271073677300142305ustar00rootroot00000000000000spur.py-0.3.17/spur/__init__.py000066400000000000000000000004211271073677300163360ustar00rootroot00000000000000from .local import LocalShell from .ssh import SshShell from .results import RunProcessError from .errors import NoSuchCommandError, CommandInitializationError __all__ = ["LocalShell", "SshShell", "RunProcessError", "NoSuchCommandError", "CommandInitializationError"] spur.py-0.3.17/spur/errors.py000066400000000000000000000012571271073677300161230ustar00rootroot00000000000000class NoSuchCommandError(OSError): def __init__(self, command): if "/" in command: message = "No such command: {0}".format(command) else: message = "Command not found: {0}. Check that {0} is installed and on $PATH".format(command) super(type(self), self).__init__(message) self.command = command class CommandInitializationError(Exception): def __init__(self, line): super(type(self), self).__init__( """Error while initializing command. The most likely cause is an unsupported shell. Try using a minimal shell type when calling 'spawn' or 'run'. (Failed to parse line '{0}' as integer)""".format(line) ) spur.py-0.3.17/spur/files.py000066400000000000000000000013371271073677300157100ustar00rootroot00000000000000import os class FileOperations(object): def __init__(self, shell): self._shell = shell def copy_file(self, source, destination=None, dir=None): if destination is None and dir is None: raise TypeError("Destination required for copy") if destination is not None: self._shell.run(["cp", "-T", source, destination]) elif dir is not None: self._shell.run(["cp", source, "-t", dir]) def write_file(self, path, contents): self._shell.run(["mkdir", "-p", os.path.dirname(path)]) file = self._shell.open(path, "w") try: file.write(contents) finally: file.close() spur.py-0.3.17/spur/io.py000066400000000000000000000042761271073677300152220ustar00rootroot00000000000000from __future__ import unicode_literals import threading import os import codecs class IoHandler(object): def __init__(self, channels, encoding): self._handlers = [ _output_handler(channel, encoding) for channel in channels ] def wait(self): return [handler.wait() for handler in self._handlers] class Channel(object): def __init__(self, file_in, file_out, is_pty=False): self.file_in = file_in self.file_out = file_out self.is_pty = is_pty def _output_handler(channel, encoding): if encoding is None: file_in = channel.file_in empty = b"" else: file_in = codecs.getreader(encoding)(channel.file_in) empty = "" if channel.file_out is None and not channel.is_pty: return _ReadOutputAtEnd(file_in) else: return _ContinuousReader( file_in=file_in, file_out=channel.file_out, is_pty=channel.is_pty, empty=empty, ) class _ReadOutputAtEnd(object): def __init__(self, file_in): self._file_in = file_in def wait(self): return self._file_in.read() class _ContinuousReader(object): def __init__(self, file_in, file_out, is_pty, empty): self._file_in = file_in self._file_out = file_out self._is_pty = is_pty self._empty = empty self._output = empty self._thread = threading.Thread(target=self._capture_output) self._thread.daemon = True self._thread.start() def wait(self): self._thread.join() return self._output def _capture_output(self): output_buffer = [] while True: try: output = self._file_in.read(1) except IOError: if self._is_pty: output = self._empty else: raise if output: if self._file_out is not None: self._file_out.write(output) output_buffer.append(output) else: self._output = self._empty.join(output_buffer) return spur.py-0.3.17/spur/local.py000066400000000000000000000130441271073677300156760ustar00rootroot00000000000000from __future__ import absolute_import import os import sys import subprocess import shutil import io import threading import errno try: import pty except ImportError: pty = None from .tempdir import create_temporary_dir from .files import FileOperations from . import results from .io import IoHandler, Channel from .errors import NoSuchCommandError class LocalShell(object): def __enter__(self): return self def __exit__(self, *args): pass def upload_dir(self, source, dest, ignore=None): shutil.copytree(source, dest, ignore=shutil.ignore_patterns(*ignore)) def upload_file(self, source, dest): shutil.copyfile(source, dest) def open(self, name, mode="r"): return open(name, mode) def write_file(self, remote_path, contents): subprocess.check_call(["mkdir", "-p", os.path.dirname(remote_path)]) open(remote_path, "w").write(contents) def spawn(self, command, *args, **kwargs): stdout = kwargs.pop("stdout", None) stderr = kwargs.pop("stderr", None) allow_error = kwargs.pop("allow_error", False) store_pid = kwargs.pop("store_pid", False) use_pty = kwargs.pop("use_pty", False) encoding = kwargs.pop("encoding", None) if use_pty: if pty is None: raise ValueError("use_pty is not supported when the pty module cannot be imported") master, slave = pty.openpty() stdin_arg = slave stdout_arg = slave stderr_arg = subprocess.STDOUT else: stdin_arg = subprocess.PIPE stdout_arg = subprocess.PIPE stderr_arg = subprocess.PIPE try: process = subprocess.Popen( stdin=stdin_arg, stdout=stdout_arg, stderr=stderr_arg, bufsize=0, **self._subprocess_args(command, *args, **kwargs) ) except OSError as error: if self._is_no_such_command_oserror(error, command[0]): raise NoSuchCommandError(command[0]) else: raise if use_pty: # TODO: Should close master ourselves rather than relying on # garbage collection process_stdin = os.fdopen(os.dup(master), "wb", 0) process_stdout = os.fdopen(master, "rb", 0) process_stderr = io.BytesIO() def close_slave_on_exit(): process.wait() # TODO: ensure the IO handler has finished before closing os.close(slave) thread = threading.Thread(target=close_slave_on_exit) thread.daemon = True thread.start() else: process_stdin = process.stdin process_stdout = process.stdout process_stderr = process.stderr spur_process = LocalProcess( process, allow_error=allow_error, process_stdin=process_stdin, io_handler=IoHandler([ Channel(process_stdout, stdout, is_pty=use_pty), Channel(process_stderr, stderr, is_pty=use_pty), ], encoding=encoding) ) if store_pid: spur_process.pid = process.pid return spur_process def run(self, *args, **kwargs): return self.spawn(*args, **kwargs).wait_for_result() def temporary_dir(self): return create_temporary_dir() @property def files(self): return FileOperations(self) def _subprocess_args(self, command, cwd=None, update_env=None, new_process_group=False): kwargs = { "args": command, "cwd": cwd, } if update_env is not None: new_env = os.environ.copy() new_env.update(update_env) kwargs["env"] = new_env if new_process_group: kwargs["preexec_fn"] = os.setpgrp return kwargs def _is_no_such_command_oserror(self, error, command): if error.errno != errno.ENOENT: return False if sys.version_info[0] < 3: return error.filename is None else: # In Python 3, filename and filename2 are None both when # the command and cwd don't exist, but in both cases, # the repr of the non-existent path is appended to the # error message return error.args[1] == os.strerror(error.errno) + ": " + repr(command) class LocalProcess(object): def __init__(self, subprocess, allow_error, process_stdin, io_handler): self._subprocess = subprocess self._allow_error = allow_error self._process_stdin = process_stdin self._result = None self._io = io_handler def is_running(self): return self._subprocess.poll() is None def stdin_write(self, value): self._process_stdin.write(value) def send_signal(self, signal): self._subprocess.send_signal(signal) def wait_for_result(self): if self._result is None: self._result = self._generate_result() return self._result def _generate_result(self): output, stderr_output = self._io.wait() return_code = self._subprocess.wait() return results.result( return_code, self._allow_error, output, stderr_output ) spur.py-0.3.17/spur/results.py000066400000000000000000000025541271073677300163110ustar00rootroot00000000000000from __future__ import unicode_literals import sys def result(return_code, allow_error, output, stderr_output): result = ExecutionResult(return_code, output, stderr_output) if return_code == 0 or allow_error: return result else: raise result.to_error() class ExecutionResult(object): def __init__(self, return_code, output, stderr_output): self.return_code = return_code self.output = output self.stderr_output = stderr_output def to_error(self): return RunProcessError( self.return_code, self.output, self.stderr_output ) class RunProcessError(RuntimeError): def __init__(self, return_code, output, stderr_output): message = "return code: {0}\noutput:{1}\nstderr output:{2}".format( return_code, _render_output(output), _render_output(stderr_output)) super(type(self), self).__init__(message) self.return_code = return_code self.output = output self.stderr_output = stderr_output def _render_output(output): if isinstance(output, unicode): return "\n" + output else: result = repr(output) if result.startswith("b"): return " " + result else: return " b" + result if sys.version_info[0] >= 3: unicode = str spur.py-0.3.17/spur/ssh.py000066400000000000000000000265021271073677300154040ustar00rootroot00000000000000from __future__ import unicode_literals from __future__ import absolute_import import subprocess import os import os.path import shutil import contextlib import uuid import socket import traceback import sys import io import paramiko from .tempdir import create_temporary_dir from .files import FileOperations from . import results from .io import IoHandler, Channel from .errors import NoSuchCommandError, CommandInitializationError _ONE_MINUTE = 60 class ConnectionError(Exception): pass class UnsupportedArgumentError(Exception): pass class AcceptParamikoPolicy(paramiko.MissingHostKeyPolicy): def missing_host_key(self, client, hostname, key): return class MissingHostKey(object): raise_error = paramiko.RejectPolicy() warn = paramiko.WarningPolicy() auto_add = paramiko.AutoAddPolicy() accept = AcceptParamikoPolicy() class MinimalShellType(object): supports_which = False def generate_run_command(self, command_args, store_pid, cwd=None, update_env={}, new_process_group=False): if store_pid: raise self._unsupported_argument_error("store_pid") if cwd is not None: raise self._unsupported_argument_error("cwd") if update_env: raise self._unsupported_argument_error("update_env") if new_process_group: raise self._unsupported_argument_error("new_process_group") return " ".join(map(escape_sh, command_args)) def _unsupported_argument_error(self, name): return UnsupportedArgumentError("'{0}' is not supported when using a minimal shell".format(name)) class ShShellType(object): supports_which = True def generate_run_command(self, command_args, store_pid, cwd=None, update_env={}, new_process_group=False): commands = [] if store_pid: commands.append("echo $$") if cwd is not None: commands.append("cd {0}".format(escape_sh(cwd))) update_env_commands = [ "export {0}={1}".format(key, escape_sh(value)) for key, value in _iteritems(update_env) ] commands += update_env_commands commands.append(" || ".join(self._generate_which_commands(command_args[0]))) commands.append("echo $?") command = " ".join(map(escape_sh, command_args)) command = "exec {0}".format(command) if new_process_group: command = "setsid {0}".format(command) commands.append(command) return "; ".join(commands) def _generate_which_commands(self, command): which_commands = ["command -v {0}", "which {0}"] return ( self._generate_which_command(which, command) for which in which_commands ) def _generate_which_command(self, which, command): return which.format(escape_sh(command)) + " > /dev/null 2>&1" class ShellTypes(object): minimal = MinimalShellType() sh = ShShellType() class SshShell(object): def __init__(self, hostname, username=None, password=None, port=22, private_key_file=None, connect_timeout=None, missing_host_key=None, shell_type=None, look_for_private_keys=True, load_system_host_keys=True): if shell_type is None: shell_type = ShellTypes.sh self._hostname = hostname self._port = port self._username = username self._password = password self._private_key_file = private_key_file self._client = None self._connect_timeout = connect_timeout if not None else _ONE_MINUTE self._look_for_private_keys = look_for_private_keys self._load_system_host_keys = load_system_host_keys self._closed = False if missing_host_key is None: self._missing_host_key = MissingHostKey.raise_error else: self._missing_host_key = missing_host_key self._shell_type = shell_type def __enter__(self): return self def __exit__(self, *args): self._closed = True if self._client is not None: self._client.close() def run(self, *args, **kwargs): return self.spawn(*args, **kwargs).wait_for_result() def spawn(self, command, *args, **kwargs): stdout = kwargs.pop("stdout", None) stderr = kwargs.pop("stderr", None) allow_error = kwargs.pop("allow_error", False) store_pid = kwargs.pop("store_pid", False) use_pty = kwargs.pop("use_pty", False) encoding = kwargs.pop("encoding", None) command_in_cwd = self._shell_type.generate_run_command(command, *args, store_pid=store_pid, **kwargs) try: channel = self._get_ssh_transport().open_session() except EOFError as error: raise self._connection_error(error) if use_pty: channel.get_pty() channel.exec_command(command_in_cwd) process_stdout = channel.makefile('rb') if store_pid: pid = _read_int_initialization_line(process_stdout) if self._shell_type.supports_which: which_return_code = _read_int_initialization_line(process_stdout) if which_return_code != 0: raise NoSuchCommandError(command[0]) process = SshProcess( channel, allow_error=allow_error, process_stdout=process_stdout, stdout=stdout, stderr=stderr, encoding=encoding, shell=self, ) if store_pid: process.pid = pid return process @contextlib.contextmanager def temporary_dir(self): result = self.run(["mktemp", "--directory"]) temp_dir = result.output.strip() try: yield temp_dir finally: self.run(["rm", "-rf", temp_dir]) def upload_dir(self, local_dir, remote_dir, ignore): with create_temporary_dir() as temp_dir: content_tarball_path = os.path.join(temp_dir, "content.tar.gz") content_path = os.path.join(temp_dir, "content") shutil.copytree(local_dir, content_path, ignore=shutil.ignore_patterns(*ignore)) subprocess.check_call( ["tar", "czf", content_tarball_path, "content"], cwd=temp_dir ) with self._connect_sftp() as sftp: remote_tarball_path = "/tmp/{0}.tar.gz".format(uuid.uuid4()) sftp.put(content_tarball_path, remote_tarball_path) self.run(["mkdir", "-p", remote_dir]) self.run([ "tar", "xzf", remote_tarball_path, "--strip-components", "1", "--directory", remote_dir ]) sftp.remove(remote_tarball_path) def open(self, name, mode="r"): sftp = self._open_sftp_client() sftp_file = SftpFile(sftp, sftp.open(name, mode), mode) if "b" not in mode: sftp_file = io.TextIOWrapper(sftp_file) return sftp_file @property def files(self): return FileOperations(self) def _get_ssh_transport(self): try: return self._connect_ssh().get_transport() except (socket.error, paramiko.SSHException, EOFError) as error: raise self._connection_error(error) def _connect_ssh(self): if self._client is None: if self._closed: raise RuntimeError("Shell is closed") client = paramiko.SSHClient() if self._load_system_host_keys: client.load_system_host_keys() client.set_missing_host_key_policy(self._missing_host_key) client.connect( hostname=self._hostname, port=self._port, username=self._username, password=self._password, key_filename=self._private_key_file, look_for_keys=self._look_for_private_keys, timeout=self._connect_timeout ) self._client = client return self._client @contextlib.contextmanager def _connect_sftp(self): sftp = self._open_sftp_client() try: yield sftp finally: sftp.close() def _open_sftp_client(self): return self._get_ssh_transport().open_sftp_client() def _connection_error(self, error): connection_error = ConnectionError( "Error creating SSH connection\n" + "Original error: {0}".format(error) ) connection_error.original_error = error connection_error.original_traceback = traceback.format_exc() return connection_error def _read_int_initialization_line(output_file): while True: line = output_file.readline().strip() if line: try: return int(line) except ValueError: raise CommandInitializationError(line) class SftpFile(object): def __init__(self, sftp, file, mode): self._sftp = sftp self._file = file self._mode = mode def __getattr__(self, key): return getattr(self._file, key) def close(self): try: self._file.close() finally: self._sftp.close() def readable(self): return "r" in self._mode or "+" in self._mode def writable(self): return "w" in self._mode or "+" in self._mode or "a" in self._mode def seekable(self): return True def __enter__(self): return self def __exit__(self, *args): self.close() def escape_sh(value): return "'" + value.replace("'", "'\\''") + "'" class SshProcess(object): def __init__(self, channel, allow_error, process_stdout, stdout, stderr, encoding, shell): self._channel = channel self._allow_error = allow_error self._stdin = channel.makefile('wb') self._stdout = process_stdout self._stderr = channel.makefile_stderr('rb') self._shell = shell self._result = None self._io = IoHandler([ Channel(self._stdout, stdout), Channel(self._stderr, stderr), ], encoding=encoding) def is_running(self): return not self._channel.exit_status_ready() def stdin_write(self, value): self._channel.sendall(value) def send_signal(self, signal): self._shell.run(["kill", "-{0}".format(signal), str(self.pid)]) def wait_for_result(self): if self._result is None: self._result = self._generate_result() return self._result def _generate_result(self): output, stderr_output = self._io.wait() return_code = self._channel.recv_exit_status() return results.result( return_code, self._allow_error, output, stderr_output ) if sys.version_info[0] < 3: _iteritems = lambda d: d.iteritems() else: _iteritems = lambda d: d.items() spur.py-0.3.17/spur/tempdir.py000066400000000000000000000003101271073677300162400ustar00rootroot00000000000000import contextlib import tempfile import shutil @contextlib.contextmanager def create_temporary_dir(): dir = tempfile.mkdtemp() try: yield dir finally: shutil.rmtree(dir) spur.py-0.3.17/test-requirements.txt000066400000000000000000000000171271073677300174760ustar00rootroot00000000000000nose>=1.2.1,<2 spur.py-0.3.17/tests/000077500000000000000000000000001271073677300144015ustar00rootroot00000000000000spur.py-0.3.17/tests/__init__.py000066400000000000000000000000011271073677300165010ustar00rootroot00000000000000 spur.py-0.3.17/tests/local_tests.py000066400000000000000000000005631271073677300172730ustar00rootroot00000000000000import spur from nose.tools import istest from .process_test_set import ProcessTestSet from .open_test_set import OpenTestSet class LocalTestMixin(object): def create_shell(self): return spur.LocalShell() @istest class LocalOpenTests(OpenTestSet, LocalTestMixin): pass @istest class LocalProcessTests(ProcessTestSet, LocalTestMixin): pass spur.py-0.3.17/tests/open_test_set.py000066400000000000000000000031441271073677300176300ustar00rootroot00000000000000from __future__ import unicode_literals import uuid import functools from nose.tools import assert_equal, istest, nottest __all__ = ["OpenTestSet"] @nottest def test(test_func): @functools.wraps(test_func) @istest def run_test(self, *args, **kwargs): with self.create_shell() as shell: test_func(shell) return run_test class OpenTestSet(object): @test def can_write_to_files_opened_by_open(shell): path = "/tmp/{0}".format(uuid.uuid4()) f = shell.open(path, "w") try: f.write("hello") f.flush() assert_equal(b"hello", shell.run(["cat", path]).output) finally: f.close() shell.run(["rm", path]) @test def can_read_files_opened_by_open(shell): path = "/tmp/{0}".format(uuid.uuid4()) shell.run(["sh", "-c", "echo hello > '{0}'".format(path)]) f = shell.open(path) try: assert_equal("hello\n", f.read()) finally: f.close() shell.run(["rm", path]) @test def open_can_be_used_as_context_manager(shell): path = "/tmp/{0}".format(uuid.uuid4()) shell.run(["sh", "-c", "echo hello > '{0}'".format(path)]) with shell.open(path) as f: assert_equal("hello\n", f.read()) @test def files_can_be_opened_in_binary_mode(shell): path = "/tmp/{0}".format(uuid.uuid4()) shell.run(["sh", "-c", "echo hello > '{0}'".format(path)]) with shell.open(path, "rb") as f: assert_equal(b"hello\n", f.read()) spur.py-0.3.17/tests/process_test_set.py000066400000000000000000000242151271073677300203470ustar00rootroot00000000000000# coding=utf8 import io import time import signal import functools from nose.tools import istest, nottest, assert_equal, assert_not_equal, assert_raises, assert_true import spur __all__ = ["ProcessTestSet"] @nottest def test(test_func): @functools.wraps(test_func) @istest def run_test(self, *args, **kwargs): with self.create_shell() as shell: test_func(shell) return run_test class ProcessTestSet(object): @test def output_of_run_is_stored(shell): result = shell.run(["echo", "hello"]) assert_equal(b"hello\n", result.output) @test def output_is_not_truncated_when_not_ending_in_a_newline(shell): result = shell.run(["echo", "-n", "hello"]) assert_equal(b"hello", result.output) @test def trailing_newlines_are_not_stripped_from_run_output(shell): result = shell.run(["echo", "\n\n"]) assert_equal(b"\n\n\n", result.output) @test def stderr_output_of_run_is_stored(shell): result = shell.run(["sh", "-c", "echo hello 1>&2"]) assert_equal(b"hello\n", result.stderr_output) @test def output_bytes_are_decoded_if_encoding_is_set(shell): result = shell.run(["bash", "-c", r'echo -e "\u2603"'], encoding="utf8") assert_equal(_u("☃\n"), result.output) @test def cwd_of_run_can_be_set(shell): result = shell.run(["pwd"], cwd="/") assert_equal(b"/\n", result.output) @test def environment_variables_can_be_added_for_run(shell): result = shell.run(["sh", "-c", "echo $NAME"], update_env={"NAME": "Bob"}) assert_equal(b"Bob\n", result.output) @test def exception_is_raised_if_return_code_is_not_zero(shell): assert_raises(spur.RunProcessError, lambda: shell.run(["false"])) @test def exception_has_output_from_command(shell): try: shell.run(["sh", "-c", "echo Hello world!; false"]) assert_true(False) except spur.RunProcessError as error: assert_equal(b"Hello world!\n", error.output) @test def exception_has_stderr_output_from_command(shell): try: shell.run(["sh", "-c", "echo Hello world! 1>&2; false"]) assert_true(False) except spur.RunProcessError as error: assert_equal(b"Hello world!\n", error.stderr_output) @test def exception_message_contains_return_code_and_all_output(shell): try: shell.run(["sh", "-c", "echo starting; echo failed! 1>&2; exit 1"]) assert_true(False) except spur.RunProcessError as error: assert_equal( """return code: 1\noutput: b'starting\\n'\nstderr output: b'failed!\\n'""", error.args[0] ) @test def exception_message_contains_output_as_string_if_encoding_is_set(shell): try: shell.run(["sh", "-c", "echo starting; echo failed! 1>&2; exit 1"], encoding="ascii") assert_true(False) except spur.RunProcessError as error: assert_equal( """return code: 1\noutput:\nstarting\n\nstderr output:\nfailed!\n""", error.args[0] ) @test def exception_message_shows_unicode_bytes(shell): try: shell.run(["sh", "-c", _u("echo ‽; exit 1")]) assert_true(False) except spur.RunProcessError as error: assert_equal( """return code: 1\noutput: b'\\xe2\\x80\\xbd\\n'\nstderr output: b''""", error.args[0] ) @test def return_code_stored_if_errors_allowed(shell): result = shell.run(["sh", "-c", "exit 14"], allow_error=True) assert_equal(14, result.return_code) @test def can_get_result_of_spawned_process(shell): process = shell.spawn(["echo", "hello"]) result = process.wait_for_result() assert_equal(b"hello\n", result.output) @test def calling_wait_for_result_is_idempotent(shell): process = shell.spawn(["echo", "hello"]) process.wait_for_result() result = process.wait_for_result() assert_equal(b"hello\n", result.output) @test def wait_for_result_raises_error_if_return_code_is_not_zero(shell): process = shell.spawn(["false"]) assert_raises(spur.RunProcessError, process.wait_for_result) @test def can_write_to_stdin_of_spawned_processes(shell): process = shell.spawn(["sh", "-c", "read value; echo $value"]) process.stdin_write(b"hello\n") result = process.wait_for_result() assert_equal(b"hello\n", result.output) @test def can_tell_if_spawned_process_is_running(shell): process = shell.spawn(["sh", "-c", "echo after; read dont_care; echo after"]) assert_equal(True, process.is_running()) process.stdin_write(b"\n") _wait_for_assertion(lambda: assert_equal(False, process.is_running())) @test def can_write_stdout_to_file_object_while_process_is_executing(shell): output_file = io.BytesIO() process = shell.spawn( ["sh", "-c", "echo hello; read dont_care;"], stdout=output_file ) _wait_for_assertion(lambda: assert_equal(b"hello\n", output_file.getvalue())) assert process.is_running() process.stdin_write(b"\n") assert_equal(b"hello\n", process.wait_for_result().output) @test def can_write_stderr_to_file_object_while_process_is_executing(shell): output_file = io.BytesIO() process = shell.spawn( ["sh", "-c", "echo hello 1>&2; read dont_care;"], stderr=output_file ) _wait_for_assertion(lambda: assert_equal(b"hello\n", output_file.getvalue())) assert process.is_running() process.stdin_write(b"\n") assert_equal(b"hello\n", process.wait_for_result().stderr_output) @test def when_encoding_is_set_then_stdout_is_decoded_before_writing_to_stdout_argument(shell): output_file = io.StringIO() process = shell.spawn( ["bash", "-c", r'echo -e "\u2603"hello; read dont_care'], stdout=output_file, encoding="utf-8", ) _wait_for_assertion(lambda: assert_equal(_u("☃hello\n"), output_file.getvalue())) assert process.is_running() process.stdin_write(b"\n") assert_equal(_u("☃hello\n"), process.wait_for_result().output) @test def can_get_process_id_of_process_if_store_pid_is_true(shell): process = shell.spawn(["sh", "-c", "echo $$"], store_pid=True) result = process.wait_for_result() assert_equal(int(result.output.strip()), process.pid) @test def process_id_is_not_available_if_store_pid_is_not_set(shell): process = shell.spawn(["sh", "-c", "echo $$"]) assert not hasattr(process, "pid") @test def can_send_signal_to_process_if_store_pid_is_set(shell): process = shell.spawn(["cat"], store_pid=True) assert process.is_running() process.send_signal(signal.SIGTERM) _wait_for_assertion(lambda: assert_equal(False, process.is_running())) @test def spawning_non_existent_command_raises_specific_no_such_command_exception(shell): try: shell.spawn(["bin/i-am-not-a-command"]) # Expected exception assert False except spur.NoSuchCommandError as error: assert_equal("No such command: bin/i-am-not-a-command", error.args[0]) assert_equal("bin/i-am-not-a-command", error.command) @test def spawning_command_that_uses_path_env_variable_asks_if_command_is_installed(shell): try: shell.spawn(["i-am-not-a-command"]) # Expected exception assert False except spur.NoSuchCommandError as error: expected_message = ( "Command not found: i-am-not-a-command." + " Check that i-am-not-a-command is installed and on $PATH" ) assert_equal(expected_message, error.args[0]) assert_equal("i-am-not-a-command", error.command) @test def using_non_existent_cwd_does_not_raise_no_such_command_error(shell): cwd = "/some/path/that/hopefully/doesnt/exists/ljaslkfjaslkfjas" try: shell.spawn(["echo", "1"], cwd=cwd) # Expected exception assert False except Exception as error: assert not isinstance(error, spur.NoSuchCommandError) @test def commands_are_run_without_pseudo_terminal_by_default(shell): result = shell.run(["bash", "-c", "[ -t 0 ]"], allow_error=True) assert_not_equal(0, result.return_code) @test def command_can_be_explicitly_run_with_pseudo_terminal(shell): result = shell.run(["bash", "-c", "[ -t 0 ]"], allow_error=True, use_pty=True) assert_equal(0, result.return_code) @test def output_is_captured_when_using_pty(shell): result = shell.run(["echo", "-n", "hello"], use_pty=True) assert_equal(b"hello", result.output) @test def stderr_is_redirected_stdout_when_using_pty(shell): result = shell.run(["sh", "-c", "echo -n hello 1>&2"], use_pty=True) assert_equal(b"hello", result.output) assert_equal(b"", result.stderr_output) @test def can_write_to_stdin_of_spawned_process_when_using_pty(shell): process = shell.spawn(["sh", "-c", "read value; echo $value"], use_pty=True) process.stdin_write(b"hello\n") result = process.wait_for_result() # Get the output twice since the pty echoes input assert_equal(b"hello\r\nhello\r\n", result.output) # TODO: timeouts in wait_for_result def _wait_for_assertion(assertion): timeout = 1 period = 0.01 start = time.time() while True: try: assertion() return except AssertionError: if time.time() - start > timeout: raise time.sleep(period) def _u(b): if isinstance(b, bytes): return b.decode("utf8") else: return b spur.py-0.3.17/tests/ssh_tests.py000066400000000000000000000117041271073677300167750ustar00rootroot00000000000000from __future__ import unicode_literals import io from nose.tools import istest, assert_raises, assert_equal import spur import spur.ssh from .testing import create_ssh_shell from .process_test_set import ProcessTestSet from .open_test_set import OpenTestSet class SshTestMixin(object): def create_shell(self): return create_ssh_shell() @istest class SshOpenTests(OpenTestSet, SshTestMixin): pass @istest class SshProcessTests(ProcessTestSet, SshTestMixin): pass @istest def attempting_to_connect_to_wrong_port_raises_connection_error(): def try_connection(): shell = _create_shell_with_wrong_port() shell.run(["echo", "hello"]) assert_raises(spur.ssh.ConnectionError, try_connection) @istest def connection_error_contains_original_error(): try: shell = _create_shell_with_wrong_port() shell.run(["true"]) # Expected error assert False except spur.ssh.ConnectionError as error: assert isinstance(error.original_error, IOError) @istest def connection_error_contains_traceback_for_original_error(): try: shell = _create_shell_with_wrong_port() shell.run(["true"]) # Expected error assert False except spur.ssh.ConnectionError as error: assert "Traceback (most recent call last):" in error.original_traceback @istest def missing_host_key_set_to_accept_allows_connection_with_missing_host_key(): with create_ssh_shell(missing_host_key=spur.ssh.MissingHostKey.accept) as shell: shell.run(["true"]) @istest def missing_host_key_set_to_warn_allows_connection_with_missing_host_key(): with create_ssh_shell(missing_host_key=spur.ssh.MissingHostKey.warn) as shell: shell.run(["true"]) @istest def missing_host_key_set_to_raise_error_raises_error_when_missing_host_key(): with create_ssh_shell(missing_host_key=spur.ssh.MissingHostKey.raise_error) as shell: assert_raises(spur.ssh.ConnectionError, lambda: shell.run(["true"])) @istest def trying_to_use_ssh_shell_after_exit_results_in_error(): with create_ssh_shell() as shell: pass assert_raises(Exception, lambda: shell.run(["true"])) def _create_shell_with_wrong_port(): return spur.SshShell( username="bob", password="password1", hostname="localhost", port=54321, missing_host_key=spur.ssh.MissingHostKey.accept, ) class MinimalSshTestMixin(object): def create_shell(self): return create_ssh_shell(shell_type=spur.ssh.ShellTypes.minimal) @istest class MinimalSshOpenTests(OpenTestSet, MinimalSshTestMixin): pass @istest class MinimalSshProcessTests(ProcessTestSet, MinimalSshTestMixin): spawning_command_that_uses_path_env_variable_asks_if_command_is_installed = None spawning_non_existent_command_raises_specific_no_such_command_exception = None can_get_process_id_of_process_if_store_pid_is_true = None can_send_signal_to_process_if_store_pid_is_set = None @istest def cannot_store_pid(self): self._assert_unsupported_feature(store_pid=True) cwd_of_run_can_be_set = None @istest def cannot_set_cwd(self): self._assert_unsupported_feature(cwd="/") environment_variables_can_be_added_for_run = None @istest def update_env_can_be_empty(self): self._assert_supported_feature(update_env={}) @istest def cannot_update_env(self): self._assert_unsupported_feature(update_env={"x": "one"}) @istest def cannot_set_new_process_group(self): self._assert_unsupported_feature(new_process_group=True) def _assert_supported_feature(self, **kwargs): with self.create_shell() as shell: result = shell.run(["echo", "hello"], **kwargs) assert_equal(b"hello\n", result.output) def _assert_unsupported_feature(self, **kwargs): name, = kwargs.keys() try: with self.create_shell() as shell: shell.run(["echo", "hello"], **kwargs) assert False, "Expected error" except spur.ssh.UnsupportedArgumentError as error: assert_equal("'{0}' is not supported when using a minimal shell".format(name), str(error)) @istest class ReadInitializationLineTests(object): @istest def reading_initialization_line_returns_int_from_line_of_file(self): assert_equal(42, spur.ssh._read_int_initialization_line(io.StringIO("42\n"))) @istest def blank_lines_are_skipped(self): assert_equal(42, spur.ssh._read_int_initialization_line(io.StringIO("\n \n\t\t\n42\n"))) @istest def error_if_non_blank_line_is_not_integer(self): try: spur.ssh._read_int_initialization_line(io.StringIO("x\n")) assert False, "Expected error" except spur.CommandInitializationError as error: assert "Failed to parse line 'x' as integer" in str(error) spur.py-0.3.17/tests/testing.py000066400000000000000000000010311271073677300164230ustar00rootroot00000000000000import os import spur import spur.ssh def create_ssh_shell(missing_host_key=None, shell_type=None): port_var = os.environ.get("TEST_SSH_PORT") port = int(port_var) if port_var is not None else None return spur.SshShell( hostname=os.environ.get("TEST_SSH_HOSTNAME", "127.0.0.1"), username=os.environ["TEST_SSH_USERNAME"], password=os.environ["TEST_SSH_PASSWORD"], port=port, missing_host_key=(missing_host_key or spur.ssh.MissingHostKey.accept), shell_type=shell_type, ) spur.py-0.3.17/tox.ini000066400000000000000000000002661271073677300145560ustar00rootroot00000000000000[tox] envlist = py26,py27,py32,py33,py34,py35 [testenv] changedir = {envtmpdir} deps=-r{toxinidir}/test-requirements.txt commands= nosetests {toxinidir}/tests passenv=TEST_SSH_*