diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index fd845fd..c94a178 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -1,6 +1,6 @@ name: Commit -on: [push] +on: [push, pull_request] jobs: darglint: @@ -9,7 +9,7 @@ jobs: max-parallel: 1 matrix: os: [ubuntu-latest] - version: [3.8] + version: [3.9] steps: - uses: actions/checkout@v2 - name: set up python ${{ matrix.version }} @@ -27,16 +27,26 @@ jobs: build_posix: runs-on: ${{ matrix.os }} strategy: - max-parallel: 6 + max-parallel: 9 matrix: os: [ubuntu-latest, macos-latest] - version: [3.6, 3.7, 3.8] + version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: set up python ${{ matrix.version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.version }} + - name: get friendly (for nox) python version + # not super friendly looking, but easy way to get major.minor version so we can easily exec only the specific + # version we are targeting with nox, while still having versions like 3.9.0a4 + run: | + echo "FRIENDLY_PYTHON_VERSION=$(python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")" >> $GITHUB_ENV + - name: install libxml2 and libxslt seems to only be needed for 3.9 image for some reason + if: matrix.os == 'ubuntu-latest' && matrix.version == '3.9' + run: | + sudo apt install libxml2-dev + sudo apt install libxslt-dev - name: setup test env run: | python -m pip install --upgrade pip @@ -46,4 +56,4 @@ jobs: env: # needed to make the terminal a tty (i think? without this system ssh is super broken) TERM: xterm - run: python -m nox -p ${{ matrix.version }} -k "not darglint" + run: python -m nox -p $FRIENDLY_PYTHON_VERSION -k "not darglint" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 7ae00b8..e083476 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: set up python 3.8 + - name: set up python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: setup publish env run: | python -m pip install --upgrade pip diff --git a/.github/workflows/weekly.yaml b/.github/workflows/weekly.yaml index e312427..23412b9 100644 --- a/.github/workflows/weekly.yaml +++ b/.github/workflows/weekly.yaml @@ -14,7 +14,7 @@ jobs: max-parallel: 1 matrix: os: [ubuntu-latest] - version: [3.8] + version: [3.9] steps: - uses: actions/checkout@v2 - name: set up python ${{ matrix.version }} @@ -29,26 +29,5 @@ jobs: - name: run nox darglint run: python -m nox -s darglint - build_posix: - runs-on: ${{ matrix.os }} - strategy: - max-parallel: 6 - matrix: os: [ubuntu-latest, macos-latest] - version: [3.6, 3.7, 3.8] - steps: - - uses: actions/checkout@v2 - - name: set up python ${{ matrix.version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.version }} - - name: setup test env - run: | - python -m pip install --upgrade pip - python -m pip install setuptools - python -m pip install nox - - name: run nox - env: - # needed to make the terminal a tty (i think? without this system ssh is super broken) - TERM: xterm - run: python -m nox -p ${{ matrix.version }} -k "not darglint" + diff --git a/CHANGELOG.md b/CHANGELOG.md index c2de991..a2c9869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,20 @@ CHANGELOG ======= -# 2020.XX.XX +# 2020.10.10 - Minor internal updates to appease updated pylint/isort +- Add 3.9 to actions, update pins, try to behave like an adult :D +- Set preferred auth options at each authentication method to only try that explicit auth method +- Remove keepalive stuff for now (in line w/ scrapli core) +- Remove transport session locks + # 2020.07.04 - Disable ssh agent for now -- this will probably get supported/added in the future, but can cause hard to troubleshoot delays for now! - Made transport timeout actually be used :) + # 2020.06.06 - First "real" release of scrapli_asyncssh -- still very early, but this has been working great in testing! Give it a shot! diff --git a/docs/scrapli_asyncssh/index.html b/docs/scrapli_asyncssh/index.html index 2e230ee..3a7e480 100644 --- a/docs/scrapli_asyncssh/index.html +++ b/docs/scrapli_asyncssh/index.html @@ -3,15 +3,17 @@ - + scrapli_asyncssh API documentation - - - - + + + + + +
@@ -27,7 +29,7 @@

Package scrapli_asyncssh

"""scrapli asyncssh transport plugin"""
 
-__version__ = "2020.06.06.post2"
+__version__ = "2020.10.10"
@@ -61,9 +63,7 @@

Index

- - \ No newline at end of file diff --git a/docs/scrapli_asyncssh/transport/asyncssh_.html b/docs/scrapli_asyncssh/transport/asyncssh_.html index ee25692..21f4865 100644 --- a/docs/scrapli_asyncssh/transport/asyncssh_.html +++ b/docs/scrapli_asyncssh/transport/asyncssh_.html @@ -3,15 +3,17 @@ - + scrapli_asyncssh.transport.asyncssh_ API documentation - - - - + + + + + +
@@ -27,7 +29,6 @@

Module scrapli_asyncssh.transport.asyncssh_

"""scrapli_asyncssh.transport.asyncssh_"""
 import asyncio
-from threading import Lock
 from typing import Any, Dict, Optional, Tuple
 
 from asyncssh import connect
@@ -63,18 +64,13 @@ 

Module scrapli_asyncssh.transport.asyncssh_

timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, - keepalive: bool = False, - keepalive_interval: int = 30, - keepalive_type: str = "", - keepalive_pattern: str = "\005", ssh_config_file: str = "", ssh_known_hosts_file: str = "", ) -> None: """ AsyncSSHTransport Object - Inherit from Transport ABC - AsyncSSHTransport <- Transport (ABC) + AsyncSSHTransport <- AsyncTransport <- Transport (ABC) Args: host: host ip/name to connect to @@ -85,19 +81,7 @@

Module scrapli_asyncssh.transport.asyncssh_

auth_strict_key: True/False to enforce strict key checking (default is True) timeout_socket: timeout for establishing socket in seconds timeout_transport: timeout for ssh transport in seconds - timeout_exit: True/False close transport if timeout encountered. If False and keepalives - are in use, keepalives will prevent program from exiting so you should be sure to - catch Timeout exceptions and handle them appropriately - keepalive: whether or not to try to keep session alive - keepalive_interval: interval to use for session keepalives - keepalive_type: network|standard -- 'network' sends actual characters over the - transport channel. This is useful for network-y type devices that may not support - 'standard' keepalive mechanisms. 'standard' is not currently implemented w/ asyncssh - keepalive_pattern: pattern to send to keep network channel alive. Default is - u'\005' which is equivalent to 'ctrl+e'. This pattern moves cursor to end of the - line which should be an innocuous pattern. This will only be entered *if* a lock - can be acquired. This is only applicable if using keepalives and if the keepalive - type is 'network' + timeout_exit: True/False close transport if timeout encountered ssh_config_file: string to path for ssh config file ssh_known_hosts_file: string to path for ssh known hosts file @@ -119,19 +103,16 @@

Module scrapli_asyncssh.transport.asyncssh_

timeout_socket, timeout_transport, timeout_exit, - keepalive, - keepalive_interval, - keepalive_type, - keepalive_pattern, ) + self.timeout_transport: int + self.auth_username: str = auth_username or cfg_user self.auth_private_key: str = auth_private_key or cfg_private_key self.auth_password: str = auth_password self.auth_strict_key: bool = auth_strict_key self.ssh_known_hosts_file: str = ssh_known_hosts_file self.port = port - self.session_lock: Lock = Lock() self.session: SSHClientConnection self.stdout: SSHReader @@ -225,7 +206,6 @@

Module scrapli_asyncssh.transport.asyncssh_

self.logger.debug(f"Attempting to validate {self.host} public key is in known hosts") self._verify_key() - self.session_lock.acquire() await self._authenticate() if self.auth_strict_key: @@ -234,7 +214,6 @@

Module scrapli_asyncssh.transport.asyncssh_

) self._verify_key_value() - self.session_lock.release() # it seems we must pass a terminal type to force a pty(?) which i think we want in like... # every case?? https://invisible-island.net/ncurses/ncurses.faq.html#xterm_color # set encoding to None so we get bytes for consistency w/ other scrapli transports @@ -280,7 +259,6 @@

Module scrapli_asyncssh.transport.asyncssh_

if not await self._authenticate_password(common_args=common_args): msg = f"Authentication to host {self.host} failed" self.logger.critical(msg) - self.session_lock.release() raise ScrapliAuthenticationFailed(msg) self.logger.debug(f"Authenticated to host {self.host} with password") @@ -302,17 +280,19 @@

Module scrapli_asyncssh.transport.asyncssh_

""" try: self.session = await asyncio.wait_for( - connect(client_keys=self.auth_private_key, **common_args), + connect( + client_keys=self.auth_private_key, preferred_auth=("publickey",), **common_args + ), timeout=self.timeout_socket, ) return True - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = ( f"Private key authentication with host {self.host} failed. " "Authentication Timed Out." ) self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc except PermissionDenied: self.logger.critical( f"Private key authentication with host {self.host} failed. Authentication Error." @@ -341,13 +321,21 @@

Module scrapli_asyncssh.transport.asyncssh_

""" try: self.session = await asyncio.wait_for( - connect(password=self.auth_password, **common_args), timeout=self.timeout_socket + connect( + password=self.auth_password, + preferred_auth=( + "keyboard-interactive", + "password", + ), + **common_args, + ), + timeout=self.timeout_socket, ) return True - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = f"Password authentication with host {self.host} failed. Authentication Timed Out." self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc except PermissionDenied: self.logger.critical( f"Password authentication with host {self.host} failed. Authentication Error." @@ -390,11 +378,9 @@

Module scrapli_asyncssh.transport.asyncssh_

N/A """ - self.session_lock.acquire() self.session.close() self.session._auth_complete = False # pylint: disable=W0212 self.logger.debug(f"Channel to host {self.host} closed") - self.session_lock.release() def isalive(self) -> bool: """ @@ -443,10 +429,10 @@

Module scrapli_asyncssh.transport.asyncssh_

self.stdout.read(65535), timeout=self.timeout_transport ) return output - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = f"Timed out reading from transport, transport timeout: {self.timeout_transport}" self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc @requires_open_session() def write(self, channel_input: str) -> None: @@ -465,7 +451,7 @@

Module scrapli_asyncssh.transport.asyncssh_

""" self.stdin.write(channel_input.encode()) - def set_timeout(self, timeout: Optional[int] = None) -> None: + def set_timeout(self, timeout: int) -> None: """ Set session timeout @@ -479,27 +465,7 @@

Module scrapli_asyncssh.transport.asyncssh_

N/A """ - if isinstance(timeout, int): - set_timeout = timeout - else: - set_timeout = self.timeout_transport - self.timeout_transport = set_timeout - - def _keepalive_standard(self) -> None: - """ - Send 'out of band' (protocol level) keepalives to devices. - - Args: - N/A - - Returns: - N/A # noqa: DAR202 - - Raises: - NotImplementedError: not yet implemented for asyncssh - - """ - raise NotImplementedError("No 'standard' keepalive mechanism for asyncssh.")
+ self.timeout_transport = timeout
@@ -513,14 +479,13 @@

Classes

class AsyncSSHTransport -(host: str, port: int = -1, auth_username: str = '', auth_private_key: str = '', auth_password: str = '', auth_strict_key: bool = True, timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, keepalive: bool = False, keepalive_interval: int = 30, keepalive_type: str = '', keepalive_pattern: str = '\x05', ssh_config_file: str = '', ssh_known_hosts_file: str = '') +(host: str, port: int = -1, auth_username: str = '', auth_private_key: str = '', auth_password: str = '', auth_strict_key: bool = True, timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, ssh_config_file: str = '', ssh_known_hosts_file: str = '')

Helper class that provides a standard way to create an ABC using inheritance.

AsyncSSHTransport Object

-

Inherit from Transport ABC -AsyncSSHTransport <- Transport (ABC)

+

AsyncSSHTransport <- AsyncTransport <- Transport (ABC)

Args

host
@@ -540,39 +505,17 @@

Args

timeout_transport
timeout for ssh transport in seconds
timeout_exit
-
True/False close transport if timeout encountered. If False and keepalives -are in use, keepalives will prevent program from exiting so you should be sure to -catch Timeout exceptions and handle them appropriately
-
keepalive
-
whether or not to try to keep session alive
-
keepalive_interval
-
interval to use for session keepalives
-
keepalive_type
-
network|standard – 'network' sends actual characters over the -transport channel. This is useful for network-y type devices that may not support -'standard' keepalive mechanisms. 'standard' is not currently implemented w/ asyncssh
-
keepalive_pattern
-
pattern to send to keep network channel alive. Default is -u'' which is equivalent to 'ctrl+e'. This pattern moves cursor to end of the -line which should be an innocuous pattern. This will only be entered if a lock -can be acquired. This is only applicable if using keepalives and if the keepalive -type is 'network'
+
True/False close transport if timeout encountered
ssh_config_file
string to path for ssh config file
ssh_known_hosts_file
string to path for ssh known hosts file

Returns

-
-
N/A -# noqa: DAR202
-
 
-
+

N/A +# noqa: DAR202

Raises

-
-
N/A
-
 
-
+

N/A

Expand source code @@ -589,18 +532,13 @@

Raises

timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, - keepalive: bool = False, - keepalive_interval: int = 30, - keepalive_type: str = "", - keepalive_pattern: str = "\005", ssh_config_file: str = "", ssh_known_hosts_file: str = "", ) -> None: """ AsyncSSHTransport Object - Inherit from Transport ABC - AsyncSSHTransport <- Transport (ABC) + AsyncSSHTransport <- AsyncTransport <- Transport (ABC) Args: host: host ip/name to connect to @@ -611,19 +549,7 @@

Raises

auth_strict_key: True/False to enforce strict key checking (default is True) timeout_socket: timeout for establishing socket in seconds timeout_transport: timeout for ssh transport in seconds - timeout_exit: True/False close transport if timeout encountered. If False and keepalives - are in use, keepalives will prevent program from exiting so you should be sure to - catch Timeout exceptions and handle them appropriately - keepalive: whether or not to try to keep session alive - keepalive_interval: interval to use for session keepalives - keepalive_type: network|standard -- 'network' sends actual characters over the - transport channel. This is useful for network-y type devices that may not support - 'standard' keepalive mechanisms. 'standard' is not currently implemented w/ asyncssh - keepalive_pattern: pattern to send to keep network channel alive. Default is - u'\005' which is equivalent to 'ctrl+e'. This pattern moves cursor to end of the - line which should be an innocuous pattern. This will only be entered *if* a lock - can be acquired. This is only applicable if using keepalives and if the keepalive - type is 'network' + timeout_exit: True/False close transport if timeout encountered ssh_config_file: string to path for ssh config file ssh_known_hosts_file: string to path for ssh known hosts file @@ -645,19 +571,16 @@

Raises

timeout_socket, timeout_transport, timeout_exit, - keepalive, - keepalive_interval, - keepalive_type, - keepalive_pattern, ) + self.timeout_transport: int + self.auth_username: str = auth_username or cfg_user self.auth_private_key: str = auth_private_key or cfg_private_key self.auth_password: str = auth_password self.auth_strict_key: bool = auth_strict_key self.ssh_known_hosts_file: str = ssh_known_hosts_file self.port = port - self.session_lock: Lock = Lock() self.session: SSHClientConnection self.stdout: SSHReader @@ -751,7 +674,6 @@

Raises

self.logger.debug(f"Attempting to validate {self.host} public key is in known hosts") self._verify_key() - self.session_lock.acquire() await self._authenticate() if self.auth_strict_key: @@ -760,7 +682,6 @@

Raises

) self._verify_key_value() - self.session_lock.release() # it seems we must pass a terminal type to force a pty(?) which i think we want in like... # every case?? https://invisible-island.net/ncurses/ncurses.faq.html#xterm_color # set encoding to None so we get bytes for consistency w/ other scrapli transports @@ -806,7 +727,6 @@

Raises

if not await self._authenticate_password(common_args=common_args): msg = f"Authentication to host {self.host} failed" self.logger.critical(msg) - self.session_lock.release() raise ScrapliAuthenticationFailed(msg) self.logger.debug(f"Authenticated to host {self.host} with password") @@ -828,17 +748,19 @@

Raises

""" try: self.session = await asyncio.wait_for( - connect(client_keys=self.auth_private_key, **common_args), + connect( + client_keys=self.auth_private_key, preferred_auth=("publickey",), **common_args + ), timeout=self.timeout_socket, ) return True - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = ( f"Private key authentication with host {self.host} failed. " "Authentication Timed Out." ) self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc except PermissionDenied: self.logger.critical( f"Private key authentication with host {self.host} failed. Authentication Error." @@ -867,13 +789,21 @@

Raises

""" try: self.session = await asyncio.wait_for( - connect(password=self.auth_password, **common_args), timeout=self.timeout_socket + connect( + password=self.auth_password, + preferred_auth=( + "keyboard-interactive", + "password", + ), + **common_args, + ), + timeout=self.timeout_socket, ) return True - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = f"Password authentication with host {self.host} failed. Authentication Timed Out." self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc except PermissionDenied: self.logger.critical( f"Password authentication with host {self.host} failed. Authentication Error." @@ -916,11 +846,9 @@

Raises

N/A """ - self.session_lock.acquire() self.session.close() self.session._auth_complete = False # pylint: disable=W0212 self.logger.debug(f"Channel to host {self.host} closed") - self.session_lock.release() def isalive(self) -> bool: """ @@ -969,10 +897,10 @@

Raises

self.stdout.read(65535), timeout=self.timeout_transport ) return output - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = f"Timed out reading from transport, transport timeout: {self.timeout_transport}" self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc @requires_open_session() def write(self, channel_input: str) -> None: @@ -991,7 +919,7 @@

Raises

""" self.stdin.write(channel_input.encode()) - def set_timeout(self, timeout: Optional[int] = None) -> None: + def set_timeout(self, timeout: int) -> None: """ Set session timeout @@ -1005,27 +933,7 @@

Raises

N/A """ - if isinstance(timeout, int): - set_timeout = timeout - else: - set_timeout = self.timeout_transport - self.timeout_transport = set_timeout - - def _keepalive_standard(self) -> None: - """ - Send 'out of band' (protocol level) keepalives to devices. - - Args: - N/A - - Returns: - N/A # noqa: DAR202 - - Raises: - NotImplementedError: not yet implemented for asyncssh - - """ - raise NotImplementedError("No 'standard' keepalive mechanism for asyncssh.") + self.timeout_transport = timeout

Ancestors

    @@ -1036,23 +944,17 @@

    Ancestors

    Methods

    -def close(self) -> NoneType +def close(self) ‑> NoneType

    Close session and socket

    Args

    N/A

    Returns

    -
    -
    N/A -# noqa: DAR202
    -
     
    -
    +

    N/A +# noqa: DAR202

    Raises

    -
    -
    N/A
    -
     
    -
    +

    N/A

    Expand source code @@ -1071,15 +973,13 @@

    Raises

    N/A """ - self.session_lock.acquire() self.session.close() self.session._auth_complete = False # pylint: disable=W0212 - self.logger.debug(f"Channel to host {self.host} closed") - self.session_lock.release() + self.logger.debug(f"Channel to host {self.host} closed")
    -def isalive(self) -> bool +def isalive(self) ‑> bool

    Check if socket is alive and session is authenticated

    @@ -1091,10 +991,7 @@

    Returns

    True if socket is alive and session authenticated, else False

    Raises

    -
    -
    N/A
    -
     
    -
    +

    N/A

    Expand source code @@ -1128,23 +1025,17 @@

    Raises

-async def open(self) -> NoneType +async def open(self) ‑> NoneType

Parent method to open session, authenticate and acquire shell

Args

N/A

Returns

-
-
N/A -# noqa: DAR202
-
 
-
+

N/A +# noqa: DAR202

Raises

-
-
N/A
-
 
-
+

N/A

Expand source code @@ -1167,7 +1058,6 @@

Raises

self.logger.debug(f"Attempting to validate {self.host} public key is in known hosts") self._verify_key() - self.session_lock.acquire() await self._authenticate() if self.auth_strict_key: @@ -1176,7 +1066,6 @@

Raises

) self._verify_key_value() - self.session_lock.release() # it seems we must pass a terminal type to force a pty(?) which i think we want in like... # every case?? https://invisible-island.net/ncurses/ncurses.faq.html#xterm_color # set encoding to None so we get bytes for consistency w/ other scrapli transports @@ -1186,7 +1075,7 @@

Raises

-def read(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) -> Any +def read(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) ‑> Any
@@ -1195,20 +1084,21 @@

Raises

Expand source code
def requires_open_session_wrapper(
-    *args: Union[str, int], **kwargs: Dict[str, Union[str, int]],
+    *args: Union[str, int],
+    **kwargs: Dict[str, Union[str, int]],
 ) -> Any:
     try:
         return wrapped_func(*args, **kwargs)
-    except AttributeError:
+    except AttributeError as exc:
         raise ConnectionNotOpened(
             "Attempting to call method that requires an open connection, but connection is "
             "not open. Call the `.open()` method of your connection object, or use a "
             "context manager to ensue your connection has been opened."
-        )
+ ) from exc
-def set_timeout(self, timeout: Union[int, NoneType] = None) -> NoneType +def set_timeout(self, timeout: int) ‑> NoneType

Set session timeout

@@ -1218,21 +1108,15 @@

Args

timeout in seconds

Returns

-
-
N/A -# noqa: DAR202
-
 
-
+

N/A +# noqa: DAR202

Raises

-
-
N/A
-
 
-
+

N/A

Expand source code -
def set_timeout(self, timeout: Optional[int] = None) -> None:
+
def set_timeout(self, timeout: int) -> None:
     """
     Set session timeout
 
@@ -1246,15 +1130,11 @@ 

Raises

N/A """ - if isinstance(timeout, int): - set_timeout = timeout - else: - set_timeout = self.timeout_transport - self.timeout_transport = set_timeout
+ self.timeout_transport = timeout
-def write(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) -> Any +def write(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) ‑> Any
@@ -1263,16 +1143,17 @@

Raises

Expand source code
def requires_open_session_wrapper(
-    *args: Union[str, int], **kwargs: Dict[str, Union[str, int]],
+    *args: Union[str, int],
+    **kwargs: Dict[str, Union[str, int]],
 ) -> Any:
     try:
         return wrapped_func(*args, **kwargs)
-    except AttributeError:
+    except AttributeError as exc:
         raise ConnectionNotOpened(
             "Attempting to call method that requires an open connection, but connection is "
             "not open. Call the `.open()` method of your connection object, or use a "
             "context manager to ensue your connection has been opened."
-        )
+ ) from exc
@@ -1310,9 +1191,7 @@

- - \ No newline at end of file diff --git a/docs/scrapli_asyncssh/transport/index.html b/docs/scrapli_asyncssh/transport/index.html index 74cacf6..4d29c19 100644 --- a/docs/scrapli_asyncssh/transport/index.html +++ b/docs/scrapli_asyncssh/transport/index.html @@ -3,15 +3,17 @@ - + scrapli_asyncssh.transport API documentation - - - - + + + + + +
@@ -53,14 +55,13 @@

Classes

class Transport -(host: str, port: int = -1, auth_username: str = '', auth_private_key: str = '', auth_password: str = '', auth_strict_key: bool = True, timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, keepalive: bool = False, keepalive_interval: int = 30, keepalive_type: str = '', keepalive_pattern: str = '\x05', ssh_config_file: str = '', ssh_known_hosts_file: str = '') +(host: str, port: int = -1, auth_username: str = '', auth_private_key: str = '', auth_password: str = '', auth_strict_key: bool = True, timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, ssh_config_file: str = '', ssh_known_hosts_file: str = '')

Helper class that provides a standard way to create an ABC using inheritance.

AsyncSSHTransport Object

-

Inherit from Transport ABC -AsyncSSHTransport <- Transport (ABC)

+

AsyncSSHTransport <- AsyncTransport <- Transport (ABC)

Args

host
@@ -80,39 +81,17 @@

Args

timeout_transport
timeout for ssh transport in seconds
timeout_exit
-
True/False close transport if timeout encountered. If False and keepalives -are in use, keepalives will prevent program from exiting so you should be sure to -catch Timeout exceptions and handle them appropriately
-
keepalive
-
whether or not to try to keep session alive
-
keepalive_interval
-
interval to use for session keepalives
-
keepalive_type
-
network|standard – 'network' sends actual characters over the -transport channel. This is useful for network-y type devices that may not support -'standard' keepalive mechanisms. 'standard' is not currently implemented w/ asyncssh
-
keepalive_pattern
-
pattern to send to keep network channel alive. Default is -u'' which is equivalent to 'ctrl+e'. This pattern moves cursor to end of the -line which should be an innocuous pattern. This will only be entered if a lock -can be acquired. This is only applicable if using keepalives and if the keepalive -type is 'network'
+
True/False close transport if timeout encountered
ssh_config_file
string to path for ssh config file
ssh_known_hosts_file
string to path for ssh known hosts file

Returns

-
-
N/A -# noqa: DAR202
-
 
-
+

N/A +# noqa: DAR202

Raises

-
-
N/A
-
 
-
+

N/A

Expand source code @@ -129,18 +108,13 @@

Raises

timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, - keepalive: bool = False, - keepalive_interval: int = 30, - keepalive_type: str = "", - keepalive_pattern: str = "\005", ssh_config_file: str = "", ssh_known_hosts_file: str = "", ) -> None: """ AsyncSSHTransport Object - Inherit from Transport ABC - AsyncSSHTransport <- Transport (ABC) + AsyncSSHTransport <- AsyncTransport <- Transport (ABC) Args: host: host ip/name to connect to @@ -151,19 +125,7 @@

Raises

auth_strict_key: True/False to enforce strict key checking (default is True) timeout_socket: timeout for establishing socket in seconds timeout_transport: timeout for ssh transport in seconds - timeout_exit: True/False close transport if timeout encountered. If False and keepalives - are in use, keepalives will prevent program from exiting so you should be sure to - catch Timeout exceptions and handle them appropriately - keepalive: whether or not to try to keep session alive - keepalive_interval: interval to use for session keepalives - keepalive_type: network|standard -- 'network' sends actual characters over the - transport channel. This is useful for network-y type devices that may not support - 'standard' keepalive mechanisms. 'standard' is not currently implemented w/ asyncssh - keepalive_pattern: pattern to send to keep network channel alive. Default is - u'\005' which is equivalent to 'ctrl+e'. This pattern moves cursor to end of the - line which should be an innocuous pattern. This will only be entered *if* a lock - can be acquired. This is only applicable if using keepalives and if the keepalive - type is 'network' + timeout_exit: True/False close transport if timeout encountered ssh_config_file: string to path for ssh config file ssh_known_hosts_file: string to path for ssh known hosts file @@ -185,19 +147,16 @@

Raises

timeout_socket, timeout_transport, timeout_exit, - keepalive, - keepalive_interval, - keepalive_type, - keepalive_pattern, ) + self.timeout_transport: int + self.auth_username: str = auth_username or cfg_user self.auth_private_key: str = auth_private_key or cfg_private_key self.auth_password: str = auth_password self.auth_strict_key: bool = auth_strict_key self.ssh_known_hosts_file: str = ssh_known_hosts_file self.port = port - self.session_lock: Lock = Lock() self.session: SSHClientConnection self.stdout: SSHReader @@ -291,7 +250,6 @@

Raises

self.logger.debug(f"Attempting to validate {self.host} public key is in known hosts") self._verify_key() - self.session_lock.acquire() await self._authenticate() if self.auth_strict_key: @@ -300,7 +258,6 @@

Raises

) self._verify_key_value() - self.session_lock.release() # it seems we must pass a terminal type to force a pty(?) which i think we want in like... # every case?? https://invisible-island.net/ncurses/ncurses.faq.html#xterm_color # set encoding to None so we get bytes for consistency w/ other scrapli transports @@ -346,7 +303,6 @@

Raises

if not await self._authenticate_password(common_args=common_args): msg = f"Authentication to host {self.host} failed" self.logger.critical(msg) - self.session_lock.release() raise ScrapliAuthenticationFailed(msg) self.logger.debug(f"Authenticated to host {self.host} with password") @@ -368,17 +324,19 @@

Raises

""" try: self.session = await asyncio.wait_for( - connect(client_keys=self.auth_private_key, **common_args), + connect( + client_keys=self.auth_private_key, preferred_auth=("publickey",), **common_args + ), timeout=self.timeout_socket, ) return True - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = ( f"Private key authentication with host {self.host} failed. " "Authentication Timed Out." ) self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc except PermissionDenied: self.logger.critical( f"Private key authentication with host {self.host} failed. Authentication Error." @@ -407,13 +365,21 @@

Raises

""" try: self.session = await asyncio.wait_for( - connect(password=self.auth_password, **common_args), timeout=self.timeout_socket + connect( + password=self.auth_password, + preferred_auth=( + "keyboard-interactive", + "password", + ), + **common_args, + ), + timeout=self.timeout_socket, ) return True - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = f"Password authentication with host {self.host} failed. Authentication Timed Out." self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc except PermissionDenied: self.logger.critical( f"Password authentication with host {self.host} failed. Authentication Error." @@ -456,11 +422,9 @@

Raises

N/A """ - self.session_lock.acquire() self.session.close() self.session._auth_complete = False # pylint: disable=W0212 self.logger.debug(f"Channel to host {self.host} closed") - self.session_lock.release() def isalive(self) -> bool: """ @@ -509,10 +473,10 @@

Raises

self.stdout.read(65535), timeout=self.timeout_transport ) return output - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: msg = f"Timed out reading from transport, transport timeout: {self.timeout_transport}" self.logger.exception(msg) - raise ScrapliTimeout(msg) + raise ScrapliTimeout(msg) from exc @requires_open_session() def write(self, channel_input: str) -> None: @@ -531,7 +495,7 @@

Raises

""" self.stdin.write(channel_input.encode()) - def set_timeout(self, timeout: Optional[int] = None) -> None: + def set_timeout(self, timeout: int) -> None: """ Set session timeout @@ -545,27 +509,7 @@

Raises

N/A """ - if isinstance(timeout, int): - set_timeout = timeout - else: - set_timeout = self.timeout_transport - self.timeout_transport = set_timeout - - def _keepalive_standard(self) -> None: - """ - Send 'out of band' (protocol level) keepalives to devices. - - Args: - N/A - - Returns: - N/A # noqa: DAR202 - - Raises: - NotImplementedError: not yet implemented for asyncssh - - """ - raise NotImplementedError("No 'standard' keepalive mechanism for asyncssh.")
+ self.timeout_transport = timeout

Ancestors

    @@ -576,23 +520,17 @@

    Ancestors

    Methods

    -def close(self) -> NoneType +def close(self) ‑> NoneType

    Close session and socket

    Args

    N/A

    Returns

    -
    -
    N/A -# noqa: DAR202
    -
     
    -
    +

    N/A +# noqa: DAR202

    Raises

    -
    -
    N/A
    -
     
    -
    +

    N/A

    Expand source code @@ -611,15 +549,13 @@

    Raises

    N/A """ - self.session_lock.acquire() self.session.close() self.session._auth_complete = False # pylint: disable=W0212 - self.logger.debug(f"Channel to host {self.host} closed") - self.session_lock.release() + self.logger.debug(f"Channel to host {self.host} closed")
    -def isalive(self) -> bool +def isalive(self) ‑> bool

    Check if socket is alive and session is authenticated

    @@ -631,10 +567,7 @@

    Returns

    True if socket is alive and session authenticated, else False

    Raises

    -
    -
    N/A
    -
     
    -
    +

    N/A

    Expand source code @@ -668,23 +601,17 @@

    Raises

-async def open(self) -> NoneType +async def open(self) ‑> NoneType

Parent method to open session, authenticate and acquire shell

Args

N/A

Returns

-
-
N/A -# noqa: DAR202
-
 
-
+

N/A +# noqa: DAR202

Raises

-
-
N/A
-
 
-
+

N/A

Expand source code @@ -707,7 +634,6 @@

Raises

self.logger.debug(f"Attempting to validate {self.host} public key is in known hosts") self._verify_key() - self.session_lock.acquire() await self._authenticate() if self.auth_strict_key: @@ -716,7 +642,6 @@

Raises

) self._verify_key_value() - self.session_lock.release() # it seems we must pass a terminal type to force a pty(?) which i think we want in like... # every case?? https://invisible-island.net/ncurses/ncurses.faq.html#xterm_color # set encoding to None so we get bytes for consistency w/ other scrapli transports @@ -726,7 +651,7 @@

Raises

-def read(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) -> Any +def read(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) ‑> Any
@@ -735,20 +660,21 @@

Raises

Expand source code
def requires_open_session_wrapper(
-    *args: Union[str, int], **kwargs: Dict[str, Union[str, int]],
+    *args: Union[str, int],
+    **kwargs: Dict[str, Union[str, int]],
 ) -> Any:
     try:
         return wrapped_func(*args, **kwargs)
-    except AttributeError:
+    except AttributeError as exc:
         raise ConnectionNotOpened(
             "Attempting to call method that requires an open connection, but connection is "
             "not open. Call the `.open()` method of your connection object, or use a "
             "context manager to ensue your connection has been opened."
-        )
+ ) from exc
-def set_timeout(self, timeout: Union[int, NoneType] = None) -> NoneType +def set_timeout(self, timeout: int) ‑> NoneType

Set session timeout

@@ -758,21 +684,15 @@

Args

timeout in seconds

Returns

-
-
N/A -# noqa: DAR202
-
 
-
+

N/A +# noqa: DAR202

Raises

-
-
N/A
-
 
-
+

N/A

Expand source code -
def set_timeout(self, timeout: Optional[int] = None) -> None:
+
def set_timeout(self, timeout: int) -> None:
     """
     Set session timeout
 
@@ -786,15 +706,11 @@ 

Raises

N/A """ - if isinstance(timeout, int): - set_timeout = timeout - else: - set_timeout = self.timeout_transport - self.timeout_transport = set_timeout
+ self.timeout_transport = timeout
-def write(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) -> Any +def write(*args: Union[str, int], **kwargs: Dict[str, Union[str, int]]) ‑> Any
@@ -803,16 +719,17 @@

Raises

Expand source code
def requires_open_session_wrapper(
-    *args: Union[str, int], **kwargs: Dict[str, Union[str, int]],
+    *args: Union[str, int],
+    **kwargs: Dict[str, Union[str, int]],
 ) -> Any:
     try:
         return wrapped_func(*args, **kwargs)
-    except AttributeError:
+    except AttributeError as exc:
         raise ConnectionNotOpened(
             "Attempting to call method that requires an open connection, but connection is "
             "not open. Call the `.open()` method of your connection object, or use a "
             "context manager to ensue your connection has been opened."
-        )
+ ) from exc
@@ -855,9 +772,7 @@

-

Generated by pdoc 0.8.1.

+

Generated by pdoc 0.8.4.

- - \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 9560f93..209772e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -35,7 +35,7 @@ DEV_REQUIREMENTS[parsed_requirement.groups()[0]] = requirement -@nox.session(python=["3.6", "3.7", "3.8"]) +@nox.session(python=["3.6", "3.7", "3.8", "3.9"]) def unit_tests(session): """ Nox run unit tests @@ -63,7 +63,7 @@ def unit_tests(session): ) -@nox.session(python=["3.8"]) +@nox.session(python=["3.9"]) def isort(session): """ Nox run isort @@ -82,7 +82,7 @@ def isort(session): session.run("isort", "-c", ".") -@nox.session(python=["3.8"]) +@nox.session(python=["3.9"]) def black(session): """ Nox run black @@ -101,7 +101,7 @@ def black(session): session.run("black", "--check", ".") -@nox.session(python=["3.8"]) +@nox.session(python=["3.9"]) def pylama(session): """ Nox run pylama @@ -121,7 +121,7 @@ def pylama(session): session.run("pylama", ".") -@nox.session(python=["3.8"]) +@nox.session(python=["3.9"]) def pydocstyle(session): """ Nox run pydocstyle @@ -140,7 +140,7 @@ def pydocstyle(session): session.run("pydocstyle", ".") -@nox.session(python=["3.8"]) +@nox.session(python=["3.9"]) def mypy(session): """ Nox run mypy @@ -161,7 +161,7 @@ def mypy(session): session.run("mypy", "--strict", "scrapli_asyncssh/") -@nox.session(python=["3.8"]) +@nox.session(python=["3.9"]) def darglint(session): """ Nox run darglint diff --git a/requirements-dev.txt b/requirements-dev.txt index 45c5889..e0fd4d9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,15 +1,15 @@ nox==2020.8.22 black==20.8b1 -isort==5.4.2 -mypy==0.782 -pytest==6.0.1 +isort==5.6.2 +mypy==0.790 +pytest==6.1.1 pytest-cov==2.10.1 pyfakefs==4.1.0 pylama==7.7.1 pycodestyle>=2.6.0 -pydocstyle==5.1.0 +pydocstyle==5.1.1 pylint==2.6.0 -darglint==1.5.3 -pdoc3==0.8.4 ; sys_platform != "win32" +darglint==1.5.5 +pdoc3==0.9.1 ; sys_platform != "win32" -e git+https://github.com/scrapli/scrapli_stubs@master#egg=scrapli_stubs -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 11912eb..df349b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ asyncssh>=2.2.1,<3.0.0 -scrapli>=2020.06.06 \ No newline at end of file +scrapli>=2020.10.10 \ No newline at end of file diff --git a/scrapli_asyncssh/__init__.py b/scrapli_asyncssh/__init__.py index 9061dda..44ea24f 100644 --- a/scrapli_asyncssh/__init__.py +++ b/scrapli_asyncssh/__init__.py @@ -1,3 +1,3 @@ """scrapli asyncssh transport plugin""" -__version__ = "2020.07.04" +__version__ = "2020.10.10" diff --git a/scrapli_asyncssh/transport/asyncssh_.py b/scrapli_asyncssh/transport/asyncssh_.py index 63fc785..6407255 100644 --- a/scrapli_asyncssh/transport/asyncssh_.py +++ b/scrapli_asyncssh/transport/asyncssh_.py @@ -1,6 +1,5 @@ """scrapli_asyncssh.transport.asyncssh_""" import asyncio -from threading import Lock from typing import Any, Dict, Optional, Tuple from asyncssh import connect @@ -36,18 +35,13 @@ def __init__( timeout_socket: int = 5, timeout_transport: int = 5, timeout_exit: bool = True, - keepalive: bool = False, - keepalive_interval: int = 30, - keepalive_type: str = "", - keepalive_pattern: str = "\005", ssh_config_file: str = "", ssh_known_hosts_file: str = "", ) -> None: """ AsyncSSHTransport Object - Inherit from Transport ABC - AsyncSSHTransport <- Transport (ABC) + AsyncSSHTransport <- AsyncTransport <- Transport (ABC) Args: host: host ip/name to connect to @@ -58,19 +52,7 @@ def __init__( auth_strict_key: True/False to enforce strict key checking (default is True) timeout_socket: timeout for establishing socket in seconds timeout_transport: timeout for ssh transport in seconds - timeout_exit: True/False close transport if timeout encountered. If False and keepalives - are in use, keepalives will prevent program from exiting so you should be sure to - catch Timeout exceptions and handle them appropriately - keepalive: whether or not to try to keep session alive - keepalive_interval: interval to use for session keepalives - keepalive_type: network|standard -- 'network' sends actual characters over the - transport channel. This is useful for network-y type devices that may not support - 'standard' keepalive mechanisms. 'standard' is not currently implemented w/ asyncssh - keepalive_pattern: pattern to send to keep network channel alive. Default is - u'\005' which is equivalent to 'ctrl+e'. This pattern moves cursor to end of the - line which should be an innocuous pattern. This will only be entered *if* a lock - can be acquired. This is only applicable if using keepalives and if the keepalive - type is 'network' + timeout_exit: True/False close transport if timeout encountered ssh_config_file: string to path for ssh config file ssh_known_hosts_file: string to path for ssh known hosts file @@ -92,10 +74,6 @@ def __init__( timeout_socket, timeout_transport, timeout_exit, - keepalive, - keepalive_interval, - keepalive_type, - keepalive_pattern, ) self.timeout_transport: int @@ -106,7 +84,6 @@ def __init__( self.auth_strict_key: bool = auth_strict_key self.ssh_known_hosts_file: str = ssh_known_hosts_file self.port = port - self.session_lock: Lock = Lock() self.session: SSHClientConnection self.stdout: SSHReader @@ -200,7 +177,6 @@ async def open(self) -> None: self.logger.debug(f"Attempting to validate {self.host} public key is in known hosts") self._verify_key() - self.session_lock.acquire() await self._authenticate() if self.auth_strict_key: @@ -209,7 +185,6 @@ async def open(self) -> None: ) self._verify_key_value() - self.session_lock.release() # it seems we must pass a terminal type to force a pty(?) which i think we want in like... # every case?? https://invisible-island.net/ncurses/ncurses.faq.html#xterm_color # set encoding to None so we get bytes for consistency w/ other scrapli transports @@ -255,7 +230,6 @@ async def _authenticate(self) -> None: if not await self._authenticate_password(common_args=common_args): msg = f"Authentication to host {self.host} failed" self.logger.critical(msg) - self.session_lock.release() raise ScrapliAuthenticationFailed(msg) self.logger.debug(f"Authenticated to host {self.host} with password") @@ -277,7 +251,9 @@ async def _authenticate_private_key(self, common_args: Dict[str, Any]) -> bool: """ try: self.session = await asyncio.wait_for( - connect(client_keys=self.auth_private_key, **common_args), + connect( + client_keys=self.auth_private_key, preferred_auth=("publickey",), **common_args + ), timeout=self.timeout_socket, ) return True @@ -316,7 +292,15 @@ async def _authenticate_password(self, common_args: Dict[str, Any]) -> bool: """ try: self.session = await asyncio.wait_for( - connect(password=self.auth_password, **common_args), timeout=self.timeout_socket + connect( + password=self.auth_password, + preferred_auth=( + "keyboard-interactive", + "password", + ), + **common_args, + ), + timeout=self.timeout_socket, ) return True except asyncio.TimeoutError as exc: @@ -365,11 +349,9 @@ def close(self) -> None: N/A """ - self.session_lock.acquire() self.session.close() self.session._auth_complete = False # pylint: disable=W0212 self.logger.debug(f"Channel to host {self.host} closed") - self.session_lock.release() def isalive(self) -> bool: """ @@ -440,7 +422,7 @@ def write(self, channel_input: str) -> None: """ self.stdin.write(channel_input.encode()) - def set_timeout(self, timeout: Optional[int] = None) -> None: + def set_timeout(self, timeout: int) -> None: """ Set session timeout @@ -454,24 +436,4 @@ def set_timeout(self, timeout: Optional[int] = None) -> None: N/A """ - if isinstance(timeout, int): - set_timeout = timeout - else: - set_timeout = self.timeout_transport - self.timeout_transport = set_timeout - - def _keepalive_standard(self) -> None: - """ - Send 'out of band' (protocol level) keepalives to devices. - - Args: - N/A - - Returns: - N/A # noqa: DAR202 - - Raises: - NotImplementedError: not yet implemented for asyncssh - - """ - raise NotImplementedError("No 'standard' keepalive mechanism for asyncssh.") + self.timeout_transport = timeout diff --git a/setup.py b/setup.py index fe6240b..5dedbec 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,9 @@ with open("README.md", "r", encoding="utf-8") as f: README = f.read() +with open(f"requirements.txt", "r") as f: + INSTALL_REQUIRES = f.read().splitlines() + setuptools.setup( name="scrapli_asyncssh", version=__version__, @@ -19,7 +22,7 @@ long_description_content_type="text/markdown", url="https://github.com/scrapli/scrapli_asyncssh", packages=setuptools.find_packages(), - install_requires=["scrapli>=2020.06.06", "asyncssh>=2.2.1"], + install_requires=INSTALL_REQUIRES, extras_require={}, classifiers=[ "License :: OSI Approved :: MIT License", @@ -27,6 +30,7 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", ], diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data/files/__init__.py b/tests/test_data/files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data/files/_ssh_config b/tests/test_data/files/_ssh_config new file mode 100644 index 0000000..48e6c7c --- /dev/null +++ b/tests/test_data/files/_ssh_config @@ -0,0 +1,20 @@ +Host 1.2.3.4 someswitch1 + User carl + IdentityFile ~/.ssh/mysshkey + IdentitiesOnly yes + Hostname someswitch1.bogus.com + Port 1234 + +Host someswitch? + User notcarl + IdentityFile ~/.ssh/mysshkey + IdentitiesOnly yes + Hostname someswitch1.bogus.com + Port 1234 + +Host scrapli + User scrapli + +Host * + User somebodyelse + IdentityFile ~/.ssh/lastresortkey diff --git a/tests/test_data/files/_ssh_known_hosts b/tests/test_data/files/_ssh_known_hosts new file mode 100644 index 0000000..0b1ae06 --- /dev/null +++ b/tests/test_data/files/_ssh_known_hosts @@ -0,0 +1 @@ +172.18.0.11 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+9q0c7+tuKT0+xS5JqMhlSoZ5gMuePUwMj1ELoij2vjoPj1Vk/+MvubDTr/VGn6FwomQS9Ge3jNswk1mJN0SIcJuthg3OBN5LsQ/zEbh4RgrDnxaBjYkypabkTtOL3xTTd1mZBsa7+OvfGEb+/qfv53wNT7Oy6K7fLhxaSm5bd5CioIV5i9SyOpzxy7ss2wPKX6pGaRx8GERfyfF2FnqyM/rLAYdiKHuuyJPwjFDxe2dRbOzpqmH+RDd9lvggKaVzaL0XooXAhpDpz7BdD5efefwq6TysdLGtRvXEH0V/YhqodOCqntcjXTpRPX+Mi3fa8VS9FMS4qY5YKiLvRcil \ No newline at end of file diff --git a/tests/test_data/files/_ssh_private_key b/tests/test_data/files/_ssh_private_key new file mode 100644 index 0000000..46cf133 --- /dev/null +++ b/tests/test_data/files/_ssh_private_key @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EQQQQDAQABAAACAQCfkqooEX/dEkMUQqESL3eDKMu22TQG2XXilLKq64JWWOj44Ty7xhkddymiFyRU27BzpeByxZuP/BM9ZEU9Om387Dli+3eQIazc2Ab6j8HWjq6AQ1wTy1bwzY89F78apkRwIHua2tnmNd89IajjUG5GO8ibQ/GwMcKI8y5zvyP/rDUAcVtaUjSn8PUptL0kKuoNr0KSXZ2FGJ6lTy4/qCLEAV6kgcD0Vj3sVDQmV1YAA/yPndX+iX1F8MiEaQN+IEg2ZpzlsFpDZUY3IWbqxd8OyaGkaFhj7ph+xjvVTmYiX540e/bfr723MOiLw03hZXQxoIrKbzhWHWyPmlLEGPQFmVPiJ6OjDTWO0jNxCfSaeZddakJIe5h1eCF7kOmsT/te5TLrqxlTkGmspzVHiJiF10C7dS3sxRiDxh08ASc9URbxahW3B/0A6/LSQbOOgAsHgvkXrqV2gU4Gv7EZIRrcNraJKy2PM1kzYtiL1cIKLA4xSzTrDf/lA2u4tVOyxGKNkyavIFmeWFRdQQbUnL90ROSAkrg92ui5fAZDxam6N8XY2JU6nXY+w8ciO+MwIBq84Z8XcVUEUr2N2YiHgDe/Itsi3wgLmpwuwFz6FaFot/GDtOZgeolJ4ks3qjZ1X1zfseZ90T9vla3NOYNcOPIq2gX2mw8OKbuMhLuGHAvXEw== carl@scrapli \ No newline at end of file diff --git a/tests/unit/transport/test_asyncssh_.py b/tests/unit/transport/test_asyncssh_.py index 4c3c4c5..8918b63 100644 --- a/tests/unit/transport/test_asyncssh_.py +++ b/tests/unit/transport/test_asyncssh_.py @@ -1,7 +1,165 @@ +from pathlib import Path + +import pytest + +import scrapli_asyncssh +from scrapli.exceptions import KeyVerificationFailed from scrapli_asyncssh.transport import Transport +TEST_DATA_DIR = f"{Path(scrapli_asyncssh.__file__).parents[1]}/tests/test_data" + + +class DummyClass: + pass + def test_creation(): conn = Transport("localhost") assert conn.host == "localhost" assert conn.port == 22 + + +@pytest.mark.parametrize( + "test_host", + [ + ( + "1.2.3.4", + ["carl", "~/.ssh/mysshkey", 1234], + ), + ( + "5.6.7.8", + ["somebodyelse", "~/.ssh/lastresortkey", 22], + ), + ( + "scrapli", + ["scrapli", "~/.ssh/lastresortkey", 22], + ), + ], + ids=["host_1.2.3.4", "catch_all", "specific_user_catch_all_key"], +) +def test__process_ssh_config(test_host): + host = test_host[0] + expected_auth_username = test_host[1][0] + expected_private_key = test_host[1][1] + expected_port = test_host[1][2] + + conn = Transport(host, ssh_config_file=f"{TEST_DATA_DIR}/files/_ssh_config") + assert conn.host == host + assert conn.auth_username == expected_auth_username + assert conn.auth_private_key == str(Path(expected_private_key).expanduser()) + assert conn.port == expected_port + + +def test__verify_key_valid(): + conn = Transport("172.18.0.11") + conn.ssh_known_hosts_file = f"{TEST_DATA_DIR}/files/_ssh_known_hosts" + + conn.session = DummyClass() + + def mock_export_public_key(): + return ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+9q0c7+tuKT0+xS5JqMhlSoZ5gMuePUwMj1ELoij2vjoPj1Vk/+MvubDTr" + b"/VGn6FwomQS9Ge3jNswk1mJN0SIcJuthg3OBN5LsQ/zEbh4RgrDnxaBjYkypabkTtOL3xTTd1mZBsa7+OvfGEb" + b"+/qfv53wNT7Oy6K7fLhxaSm5bd5CioIV5i9SyOpzxy7ss2wPKX6pGaRx8GERfyfF2FnqyM/rLAYdiKHuuyJPwjFDxe2dRbOzpqmH" + b"+RDd9lvggKaVzaL0XooXAhpDpz7BdD5efefwq6TysdLGtRvXEH0V/YhqodOCqntcjXTpRPX+Mi3fa8VS9FMS4qY5YKiLvRcil\n " + ) + + def mock_get_server_host_key(): + remote_server_key = DummyClass() + remote_server_key.export_public_key = mock_export_public_key + return remote_server_key + + conn.session.get_server_host_key = mock_get_server_host_key + + conn._verify_key_value() + + +def test__verify_key_invalid(): + conn = Transport("172.18.0.11") + conn.ssh_known_hosts_file = f"{TEST_DATA_DIR}/files/_ssh_known_hosts" + + conn.session = DummyClass() + + def mock_export_public_key(): + return b"ssh-rsa blah\n " + + def mock_get_server_host_key(): + remote_server_key = DummyClass() + remote_server_key.export_public_key = mock_export_public_key + return remote_server_key + + conn.session.get_server_host_key = mock_get_server_host_key + + with pytest.raises(KeyVerificationFailed) as exc: + conn._verify_key_value() + + assert str(exc.value) == "172.18.0.11 in known_hosts but public key does not match!" + + +def test__verify_key_not_found(): + conn = Transport("1.1.1.1") + conn.ssh_known_hosts_file = f"{TEST_DATA_DIR}/files/_ssh_known_hosts" + + conn.session = DummyClass() + + def mock_export_public_key(): + return b"ssh-rsa blah\n " + + def mock_get_server_host_key(): + remote_server_key = DummyClass() + remote_server_key.export_public_key = mock_export_public_key + return remote_server_key + + conn.session.get_server_host_key = mock_get_server_host_key + + with pytest.raises(KeyVerificationFailed) as exc: + conn._verify_key() + + assert str(exc.value) == "1.1.1.1 not in known_hosts!" + + +@pytest.mark.asyncio +async def test_open_verify_key(): + conn = Transport("172.18.0.11", auth_strict_key=True) + conn.ssh_known_hosts_file = f"{TEST_DATA_DIR}/files/_ssh_known_hosts" + + conn.session = DummyClass() + + def mock_export_public_key(): + return ( + b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+9q0c7+tuKT0+xS5JqMhlSoZ5gMuePUwMj1ELoij2vjoPj1Vk/+MvubDTr" + b"/VGn6FwomQS9Ge3jNswk1mJN0SIcJuthg3OBN5LsQ/zEbh4RgrDnxaBjYkypabkTtOL3xTTd1mZBsa7+OvfGEb" + b"+/qfv53wNT7Oy6K7fLhxaSm5bd5CioIV5i9SyOpzxy7ss2wPKX6pGaRx8GERfyfF2FnqyM/rLAYdiKHuuyJPwjFDxe2dRbOzpqmH" + b"+RDd9lvggKaVzaL0XooXAhpDpz7BdD5efefwq6TysdLGtRvXEH0V/YhqodOCqntcjXTpRPX+Mi3fa8VS9FMS4qY5YKiLvRcil\n " + ) + + def mock_get_server_host_key(): + remote_server_key = DummyClass() + remote_server_key.export_public_key = mock_export_public_key + return remote_server_key + + async def mock_authenticate(): + return True + + async def mock_open_session(**kwargs): + return 1, 2, 3 + + conn._authenticate = mock_authenticate + conn.session.get_server_host_key = mock_get_server_host_key + conn.session.open_session = mock_open_session + + await conn.open() + + +def test_set_timeout(): + conn = Transport("172.18.0.11") + assert conn.timeout_transport == 5 + conn.set_timeout(999) + assert conn.timeout_transport == 999 + + +def test__keepalive_standard(): + conn = Transport("172.18.0.11") + with pytest.raises(NotImplementedError) as exc: + conn._keepalive_standard() + assert str(exc.value) == "No 'standard' keepalive mechanism for asyncssh."