diff --git a/LICENSE b/LICENSE index 3953212c..a6de6a9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2016 Pahaz Blinov +Copyright (c) 2014-2019 Pahaz Blinov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 419cbae8..95112458 100644 --- a/README.rst +++ b/README.rst @@ -102,7 +102,7 @@ Code corresponding to **Fig1** above follows, given remote server's address is ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind port. -.. code-block:: py +.. code-block:: python from sshtunnel import SSHTunnelForwarder @@ -127,12 +127,12 @@ Example of a port forwarding to a private server not directly reachable, assuming password protected pkey authentication, remote server's SSH service is listening on port 443 and that port is open in the firewall (**Fig2**): -.. code-block:: py +.. code-block:: python import paramiko - from sshtunnel import SSHTunnelForwarder + import sshtunnel - with SSHTunnelForwarder( + with sshtunnel.open_tunnel( (REMOTE_SERVER_IP, 443), ssh_username="", ssh_pkey="/var/ssh/rsa_key", @@ -154,12 +154,12 @@ Example 3 Example of a port forwarding for the Vagrant MySQL local port: -.. code-block:: py +.. code-block:: python - from sshtunnel import SSHTunnelForwarder + from sshtunnel import open_tunnel from time import sleep - with SSHTunnelForwarder( + with open_tunnel( ('localhost', 2222), ssh_username="vagrant", ssh_password="vagrant", @@ -179,6 +179,40 @@ Or simply using the CLI: (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost +Example 4 +--------- + +Opening an SSH session jumping over two tunnels: + +.. code-block:: python + + import sshtunnel + from paramiko import SSHClient + + + with sshtunnel.open_tunnel( + ssh_address_or_host=('GW1_ip', 20022), + remote_bind_address=('GW2_ip', 22), + block_on_close=False + ) as tunnel1: + print('Connection to tunnel1 (GW1_ip:GW1_port) OK...') + with sshtunnel.open_tunnel( + ssh_address_or_host=('localhost', tunnel1.local_bind_port), + remote_bind_address=('target_ip', 22), + ssh_username='GW2_user', + ssh_password='GW2_pwd', + block_on_close=False + ) as tunnel2: + print('Connection to tunnel2 (GW2_ip:GW2_port) OK...') + with SSHClient() as ssh: + ssh.connect('localhost', + port=tunnel2.local_bind_port, + username='target_user', + password='target_pwd', + ) + ssh.exec_command(...) + + CLI usage ========= @@ -192,7 +226,7 @@ CLI usage ssh_address Pure python ssh tunnel utils - Version 0.1.4 + Version 0.1.5 positional arguments: ssh_address SSH server IP address (GW for SSH tunnels) diff --git a/changelog.rst b/changelog.rst index 6d877bc9..758bec3b 100644 --- a/changelog.rst +++ b/changelog.rst @@ -16,6 +16,9 @@ CONTRIBUTORS CHANGELOG ========= +- v.0.1.5 (`JM Fernández`_) + + Introduce `block_on_close` attribute + - v.0.1.4 (`Niels Zeilemaker`_) + Allow loading pkeys from `~/.ssh` diff --git a/setup.py b/setup.py index 14404054..8b0afadb 100644 --- a/setup.py +++ b/setup.py @@ -119,6 +119,9 @@ def run_tests(self): # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test] + tests_require=[ + 'tox>=1.8.1', + ], extras_require={ 'dev': ['check-manifest'], 'test': [ diff --git a/sshtunnel.py b/sshtunnel.py index bc712819..e97e1707 100644 --- a/sshtunnel.py +++ b/sshtunnel.py @@ -36,13 +36,13 @@ input_ = input -__version__ = '0.1.4' +__version__ = '0.1.5' __author__ = 'pahaz' DEFAULT_LOGLEVEL = logging.ERROR #: default level if no logger passed (ERROR) TUNNEL_TIMEOUT = 1.0 #: Timeout (seconds) for tunnel connection -DAEMON = False +_DAEMON = False #: Use daemon threads in connections TRACE_LEVEL = 1 _CONNECTION_COUNTER = 1 _LOCK = threading.Lock() @@ -415,7 +415,7 @@ class _ThreadingForwardServer(socketserver.ThreadingMixIn, _ForwardServer): Allow concurrent connections to each tunnel """ # If True, cleanly stop threads created by ThreadingMixIn when quitting - daemon_threads = DAEMON + daemon_threads = _DAEMON class _UnixStreamForwardServer(UnixStreamServer): @@ -459,7 +459,7 @@ class _ThreadingUnixStreamForwardServer(socketserver.ThreadingMixIn, Allow concurrent connections to each tunnel """ # If True, cleanly stop threads created by ThreadingMixIn when quitting - daemon_threads = DAEMON + daemon_threads = _DAEMON class SSHTunnelForwarder(object): @@ -614,7 +614,6 @@ class SSHTunnelForwarder(object): host_pkey_directories (list): Look for pkeys in folders on this list, for example ['~/.ssh']. - An empty list disables this feature Default: ``None`` @@ -720,8 +719,8 @@ class SSHTunnelForwarder(object): """ skip_tunnel_checkup = True - daemon_forward_servers = DAEMON #: flag tunnel threads in daemon mode - daemon_transport = DAEMON #: flag SSH transport thread in daemon mode + daemon_forward_servers = _DAEMON #: flag tunnel threads in daemon mode + daemon_transport = _DAEMON #: flag SSH transport thread in daemon mode def local_is_up(self, target): """ @@ -1580,6 +1579,12 @@ def open_tunnel(*args, **kwargs): .. versionadded:: 0.1.0 + block_on_close (boolean): + Wait until all connections are done during close by changing the + value of :attr:`~SSHTunnelForwarder.block_on_close` + + Default: True + .. note:: A value of ``debug_level`` set to 1 == ``TRACE`` enables tracing mode .. note:: @@ -1617,6 +1622,7 @@ def do_something(port): ssh_port = kwargs.pop('ssh_port', None) skip_tunnel_checkup = kwargs.pop('skip_tunnel_checkup', True) + block_on_close = kwargs.pop('block_on_close', DAEMON) if not args: if isinstance(ssh_address_or_host, tuple): args = (ssh_address_or_host, ) @@ -1624,6 +1630,8 @@ def do_something(port): args = ((ssh_address_or_host, ssh_port), ) forwarder = SSHTunnelForwarder(*args, **kwargs) forwarder.skip_tunnel_checkup = skip_tunnel_checkup + forwarder.daemon_forward_servers = not block_on_close + forwarder.daemon_transport = not block_on_close return forwarder diff --git a/tox.ini b/tox.ini index 0b0519a9..51fae5b6 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ basepython = py34: python3.4 py35: python3.5 py36: python3.6 + py37: python3.7 deps = mock pytest @@ -26,7 +27,7 @@ deps = pytest-xdist commands = python -V - py.test --showlocals --cov sshtunnel --durations=10 -n4 tests + py.test --showlocals --cov sshtunnel --durations=10 -n4 tests -W ignore::DeprecationWarning [testenv:docs] basepython = python @@ -47,10 +48,12 @@ deps = mccabe pygments readme + twine commands = check-manifest --ignore "tox.ini,tests*,*.yml" - python setup.py check -m -r -s - flake8 . + python setup.py sdist + twine check dist/* + flake8 --ignore=W504 . bashtest README.rst [flake8]