scrapli 2023.7.30__py3-none-any.whl → 2024.7.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. scrapli/__init__.py +2 -1
  2. scrapli/channel/__init__.py +1 -0
  3. scrapli/channel/async_channel.py +35 -12
  4. scrapli/channel/base_channel.py +25 -3
  5. scrapli/channel/sync_channel.py +35 -12
  6. scrapli/decorators.py +1 -0
  7. scrapli/driver/__init__.py +1 -0
  8. scrapli/driver/base/__init__.py +1 -0
  9. scrapli/driver/base/async_driver.py +19 -13
  10. scrapli/driver/base/base_driver.py +121 -37
  11. scrapli/driver/base/sync_driver.py +19 -13
  12. scrapli/driver/core/__init__.py +1 -0
  13. scrapli/driver/core/arista_eos/__init__.py +1 -0
  14. scrapli/driver/core/arista_eos/async_driver.py +3 -0
  15. scrapli/driver/core/arista_eos/base_driver.py +3 -2
  16. scrapli/driver/core/arista_eos/sync_driver.py +3 -0
  17. scrapli/driver/core/cisco_iosxe/__init__.py +1 -0
  18. scrapli/driver/core/cisco_iosxe/async_driver.py +3 -0
  19. scrapli/driver/core/cisco_iosxe/base_driver.py +1 -0
  20. scrapli/driver/core/cisco_iosxe/sync_driver.py +3 -0
  21. scrapli/driver/core/cisco_iosxr/__init__.py +1 -0
  22. scrapli/driver/core/cisco_iosxr/async_driver.py +3 -0
  23. scrapli/driver/core/cisco_iosxr/base_driver.py +1 -0
  24. scrapli/driver/core/cisco_iosxr/sync_driver.py +3 -0
  25. scrapli/driver/core/cisco_nxos/__init__.py +1 -0
  26. scrapli/driver/core/cisco_nxos/async_driver.py +3 -0
  27. scrapli/driver/core/cisco_nxos/base_driver.py +9 -4
  28. scrapli/driver/core/cisco_nxos/sync_driver.py +3 -0
  29. scrapli/driver/core/juniper_junos/__init__.py +1 -0
  30. scrapli/driver/core/juniper_junos/async_driver.py +3 -0
  31. scrapli/driver/core/juniper_junos/base_driver.py +1 -0
  32. scrapli/driver/core/juniper_junos/sync_driver.py +3 -0
  33. scrapli/driver/generic/__init__.py +1 -0
  34. scrapli/driver/generic/async_driver.py +45 -3
  35. scrapli/driver/generic/base_driver.py +2 -1
  36. scrapli/driver/generic/sync_driver.py +45 -3
  37. scrapli/driver/network/__init__.py +1 -0
  38. scrapli/driver/network/async_driver.py +27 -0
  39. scrapli/driver/network/base_driver.py +1 -0
  40. scrapli/driver/network/sync_driver.py +27 -0
  41. scrapli/exceptions.py +1 -0
  42. scrapli/factory.py +22 -3
  43. scrapli/helper.py +76 -4
  44. scrapli/logging.py +1 -0
  45. scrapli/response.py +1 -0
  46. scrapli/ssh_config.py +1 -0
  47. scrapli/transport/base/__init__.py +1 -0
  48. scrapli/transport/base/async_transport.py +1 -0
  49. scrapli/transport/base/base_socket.py +1 -0
  50. scrapli/transport/base/base_transport.py +1 -0
  51. scrapli/transport/base/sync_transport.py +1 -0
  52. scrapli/transport/plugins/asyncssh/transport.py +4 -0
  53. scrapli/transport/plugins/asynctelnet/transport.py +13 -6
  54. scrapli/transport/plugins/paramiko/transport.py +1 -0
  55. scrapli/transport/plugins/ssh2/transport.py +6 -3
  56. scrapli/transport/plugins/system/ptyprocess.py +50 -13
  57. scrapli/transport/plugins/system/transport.py +27 -6
  58. scrapli/transport/plugins/telnet/transport.py +13 -7
  59. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/METADATA +74 -47
  60. scrapli-2024.7.30.dist-info/RECORD +74 -0
  61. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/WHEEL +1 -1
  62. scrapli-2023.7.30.dist-info/RECORD +0 -74
  63. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/LICENSE +0 -0
  64. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/top_level.txt +0 -0
scrapli/__init__.py CHANGED
@@ -1,8 +1,9 @@
1
1
  """scrapli"""
2
+
2
3
  from scrapli.driver.base import AsyncDriver, Driver
3
4
  from scrapli.factory import AsyncScrapli, Scrapli
4
5
 
5
- __version__ = "2023.07.30"
6
+ __version__ = "2024.07.30"
6
7
 
7
8
  __all__ = (
8
9
  "AsyncDriver",
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel"""
2
+
2
3
  from scrapli.channel.async_channel import AsyncChannel
3
4
  from scrapli.channel.sync_channel import Channel
4
5
 
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel.async_channel"""
2
+
2
3
  import asyncio
3
4
  import re
4
5
  import time
@@ -10,6 +11,7 @@ from typing import AsyncIterator, List, Optional, Tuple
10
11
  from scrapli.channel.base_channel import BaseChannel, BaseChannelArgs
11
12
  from scrapli.decorators import timeout_wrapper
12
13
  from scrapli.exceptions import ScrapliAuthenticationFailed, ScrapliTimeout
14
+ from scrapli.helper import output_roughly_contains_input
13
15
  from scrapli.transport.base import AsyncTransport
14
16
 
15
17
 
@@ -69,7 +71,7 @@ class AsyncChannel(BaseChannel):
69
71
  buf = await self.transport.read()
70
72
  buf = buf.replace(b"\r", b"")
71
73
 
72
- self.logger.debug(f"read: {buf!r}")
74
+ self.logger.debug("read: %r", buf)
73
75
 
74
76
  if self.channel_log:
75
77
  self.channel_log.write(buf)
@@ -104,9 +106,16 @@ class AsyncChannel(BaseChannel):
104
106
  while True:
105
107
  buf += await self.read()
106
108
 
107
- # replace any backspace chars (particular problem w/ junos), and remove any added spaces
108
- # this is just for comparison of the inputs to what was read from channel
109
- if processed_channel_input in b"".join(buf.lower().replace(b"\x08", b"").split()):
109
+ if not self._base_channel_args.comms_roughly_match_inputs:
110
+ # replace any backspace chars (particular problem w/ junos), and remove any added
111
+ # spaces this is just for comparison of the inputs to what was read from channel
112
+ # note (2024) this would be worked around by using the roughly contains search,
113
+ # *but* that is slower (probably immaterially for most people but... ya know...)
114
+ processed_buf = b"".join(buf.lower().replace(b"\x08", b"").split())
115
+
116
+ if processed_channel_input in processed_buf:
117
+ return buf
118
+ elif output_roughly_contains_input(input_=processed_channel_input, output=buf):
110
119
  return buf
111
120
 
112
121
  async def _read_until_prompt(self, buf: bytes = b"") -> bytes:
@@ -241,7 +250,7 @@ class AsyncChannel(BaseChannel):
241
250
 
242
251
  if (time.time() - start) > read_duration:
243
252
  break
244
- if any((channel_output in search_buf for channel_output in channel_outputs)):
253
+ if any(channel_output in search_buf for channel_output in channel_outputs):
245
254
  break
246
255
  if re.search(pattern=regex_channel_outputs_pattern, string=search_buf):
247
256
  break
@@ -455,6 +464,7 @@ class AsyncChannel(BaseChannel):
455
464
  *,
456
465
  strip_prompt: bool = True,
457
466
  eager: bool = False,
467
+ eager_input: bool = False,
458
468
  ) -> Tuple[bytes, bytes]:
459
469
  """
460
470
  Primary entry point to send data to devices in shell mode; accept input and returns result
@@ -465,6 +475,8 @@ class AsyncChannel(BaseChannel):
465
475
  eager: eager mode reads and returns the `_read_until_input` value, but does not attempt
466
476
  to read to the prompt pattern -- this should not be used manually! (only used by
467
477
  `send_configs` with the eager flag set)
478
+ eager_input: when true does *not* try to read our input off the channel -- generally
479
+ this should be left alone unless you know what you are doing!
468
480
 
469
481
  Returns:
470
482
  Tuple[bytes, bytes]: tuple of "raw" output and "processed" (cleaned up/stripped) output
@@ -479,12 +491,18 @@ class AsyncChannel(BaseChannel):
479
491
  bytes_channel_input = channel_input.encode()
480
492
 
481
493
  self.logger.info(
482
- f"sending channel input: {channel_input}; strip_prompt: {strip_prompt}; eager: {eager}"
494
+ "sending channel input: %s; strip_prompt: %s; eager: %s",
495
+ channel_input,
496
+ strip_prompt,
497
+ eager,
483
498
  )
484
499
 
485
500
  async with self._channel_lock():
486
501
  self.write(channel_input=channel_input)
487
- _buf_until_input = await self._read_until_input(channel_input=bytes_channel_input)
502
+
503
+ if not eager_input:
504
+ _buf_until_input = await self._read_until_input(channel_input=bytes_channel_input)
505
+
488
506
  self.send_return()
489
507
 
490
508
  if not eager:
@@ -531,8 +549,12 @@ class AsyncChannel(BaseChannel):
531
549
  ]
532
550
 
533
551
  self.logger.info(
534
- f"sending channel input and read: {channel_input}; strip_prompt: {strip_prompt}; "
535
- f"expected_outputs: {expected_outputs}; read_duration: {read_duration}"
552
+ "sending channel input and read: %s; strip_prompt: %s; "
553
+ "expected_outputs: %s; read_duration: %s",
554
+ channel_input,
555
+ strip_prompt,
556
+ expected_outputs,
557
+ read_duration,
536
558
  )
537
559
 
538
560
  async with self._channel_lock():
@@ -642,9 +664,10 @@ class AsyncChannel(BaseChannel):
642
664
 
643
665
  _channel_input = channel_input if not hidden_input else "REDACTED"
644
666
  self.logger.info(
645
- f"sending interactive input: {_channel_input}; "
646
- f"expecting: {channel_response}; "
647
- f"hidden_input: {hidden_input}"
667
+ "sending interactive input: %s; expecting: %s; hidden_input: %s",
668
+ _channel_input,
669
+ channel_response,
670
+ hidden_input,
648
671
  )
649
672
 
650
673
  self.write(channel_input=channel_input, redacted=bool(hidden_input))
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel.base_channel"""
2
+
2
3
  import re
3
4
  from dataclasses import dataclass
4
5
  from datetime import datetime
@@ -10,7 +11,19 @@ from scrapli.exceptions import ScrapliAuthenticationFailed, ScrapliTypeError, Sc
10
11
  from scrapli.logging import get_instance_logger
11
12
  from scrapli.transport.base import AsyncTransport, Transport
12
13
 
13
- ANSI_ESCAPE_PATTERN = re.compile(rb"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\)|E)")
14
+ ANSI_ESCAPE_PATTERN = re.compile(
15
+ pattern=rb"[\x1B\x9B\x9D](\s)?" # Prefix ESC (^) or CSI (^[) or OSC (^)
16
+ rb"("
17
+ rb"([78ME])" # control cursor position
18
+ rb"|"
19
+ rb"((\]\d).*?[\x07])" # BEL (Terminal bell)
20
+ rb"|"
21
+ rb"(\[.*?[@-~])" # control codes starts with `[` e.x. ESC [2;37;41m
22
+ rb"|"
23
+ rb"(\[.*?[0-9;]m)" # Select Graphic Rendition (SGR) control sequence
24
+ rb")",
25
+ flags=re.VERBOSE,
26
+ )
14
27
 
15
28
 
16
29
  @dataclass()
@@ -31,6 +44,12 @@ class BaseChannelArgs:
31
44
  comms_prompt_search_depth: depth of the buffer to search in for searching for the prompt
32
45
  in "read_until_prompt"; smaller number here will generally be faster, though may be less
33
46
  reliable; default value is 1000
47
+ comms_roughly_match_inputs: indicates if the channel should "roughly" match inputs sent
48
+ to the device. If False (default) inputs are strictly checked, as in any input
49
+ *must* be read back exactly on the channel. When set to True all input chars *must*
50
+ be read back in order in the output and all chars must be present, but the *exact*
51
+ input string does not need to be seen. This can be useful if a device echoes back
52
+ extra characters or rewrites the terminal during command input.
34
53
  timeout_ops: timeout_ops to assign to the channel, see above
35
54
  channel_log: log "channel" output -- this would be the output you would normally see on a
36
55
  terminal. If `True` logs to `scrapli_channel.log`, if a string is provided, logs to
@@ -53,6 +72,7 @@ class BaseChannelArgs:
53
72
  comms_prompt_pattern: str = r"^[a-z0-9.\-@()/:]{1,32}[#>$]$"
54
73
  comms_return_char: str = "\n"
55
74
  comms_prompt_search_depth: int = 1000
75
+ comms_roughly_match_inputs: bool = False
56
76
  timeout_ops: float = 30.0
57
77
  channel_log: Union[str, bool, BytesIO] = False
58
78
  channel_log_mode: str = "write"
@@ -348,8 +368,10 @@ class BaseChannel:
348
368
  N/A
349
369
 
350
370
  """
351
- log_output = "REDACTED" if redacted else repr(channel_input)
352
- self.logger.debug(f"write: {log_output}")
371
+ if redacted:
372
+ self.logger.debug("write: REDACTED")
373
+ else:
374
+ self.logger.debug("write: %r", channel_input)
353
375
 
354
376
  self.transport.write(channel_input=channel_input.encode())
355
377
 
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel.sync_channel"""
2
+
2
3
  import re
3
4
  import time
4
5
  from contextlib import contextmanager, suppress
@@ -10,6 +11,7 @@ from typing import Iterator, List, Optional, Tuple
10
11
  from scrapli.channel.base_channel import BaseChannel, BaseChannelArgs
11
12
  from scrapli.decorators import timeout_wrapper
12
13
  from scrapli.exceptions import ScrapliAuthenticationFailed, ScrapliConnectionError, ScrapliTimeout
14
+ from scrapli.helper import output_roughly_contains_input
13
15
  from scrapli.transport.base import Transport
14
16
 
15
17
 
@@ -69,7 +71,7 @@ class Channel(BaseChannel):
69
71
  buf = self.transport.read()
70
72
  buf = buf.replace(b"\r", b"")
71
73
 
72
- self.logger.debug(f"read: {buf!r}")
74
+ self.logger.debug("read: %r", buf)
73
75
 
74
76
  if self.channel_log:
75
77
  self.channel_log.write(buf)
@@ -104,9 +106,16 @@ class Channel(BaseChannel):
104
106
  while True:
105
107
  buf += self.read()
106
108
 
107
- # replace any backspace chars (particular problem w/ junos), and remove any added spaces
108
- # this is just for comparison of the inputs to what was read from channel
109
- if processed_channel_input in b"".join(buf.lower().replace(b"\x08", b"").split()):
109
+ if not self._base_channel_args.comms_roughly_match_inputs:
110
+ # replace any backspace chars (particular problem w/ junos), and remove any added
111
+ # spaces this is just for comparison of the inputs to what was read from channel
112
+ # note (2024) this would be worked around by using the roughly contains search,
113
+ # *but* that is slower (probably immaterially for most people but... ya know...)
114
+ processed_buf = b"".join(buf.lower().replace(b"\x08", b"").split())
115
+
116
+ if processed_channel_input in processed_buf:
117
+ return buf
118
+ elif output_roughly_contains_input(input_=processed_channel_input, output=buf):
110
119
  return buf
111
120
 
112
121
  def _read_until_prompt(self, buf: bytes = b"") -> bytes:
@@ -238,7 +247,7 @@ class Channel(BaseChannel):
238
247
 
239
248
  if (time.time() - start) > read_duration:
240
249
  break
241
- if any((channel_output in search_buf for channel_output in channel_outputs)):
250
+ if any(channel_output in search_buf for channel_output in channel_outputs):
242
251
  break
243
252
  if re.search(pattern=regex_channel_outputs_pattern, string=search_buf):
244
253
  break
@@ -456,6 +465,7 @@ class Channel(BaseChannel):
456
465
  *,
457
466
  strip_prompt: bool = True,
458
467
  eager: bool = False,
468
+ eager_input: bool = False,
459
469
  ) -> Tuple[bytes, bytes]:
460
470
  """
461
471
  Primary entry point to send data to devices in shell mode; accept input and returns result
@@ -466,6 +476,8 @@ class Channel(BaseChannel):
466
476
  eager: eager mode reads and returns the `_read_until_input` value, but does not attempt
467
477
  to read to the prompt pattern -- this should not be used manually! (only used by
468
478
  `send_configs` with the eager flag set)
479
+ eager_input: when true does *not* try to read our input off the channel -- generally
480
+ this should be left alone unless you know what you are doing!
469
481
 
470
482
  Returns:
471
483
  Tuple[bytes, bytes]: tuple of "raw" output and "processed" (cleaned up/stripped) output
@@ -480,12 +492,18 @@ class Channel(BaseChannel):
480
492
  bytes_channel_input = channel_input.encode()
481
493
 
482
494
  self.logger.info(
483
- f"sending channel input: {channel_input}; strip_prompt: {strip_prompt}; eager: {eager}"
495
+ "sending channel input: %s; strip_prompt: %s; eager: %s",
496
+ channel_input,
497
+ strip_prompt,
498
+ eager,
484
499
  )
485
500
 
486
501
  with self._channel_lock():
487
502
  self.write(channel_input=channel_input)
488
- _buf_until_input = self._read_until_input(channel_input=bytes_channel_input)
503
+
504
+ if not eager_input:
505
+ _buf_until_input = self._read_until_input(channel_input=bytes_channel_input)
506
+
489
507
  self.send_return()
490
508
 
491
509
  if not eager:
@@ -532,8 +550,12 @@ class Channel(BaseChannel):
532
550
  ]
533
551
 
534
552
  self.logger.info(
535
- f"sending channel input and read: {channel_input}; strip_prompt: {strip_prompt}; "
536
- f"expected_outputs: {expected_outputs}; read_duration: {read_duration}"
553
+ "sending channel input and read: %s; strip_prompt: %s; "
554
+ "expected_outputs: %s; read_duration: %s",
555
+ channel_input,
556
+ strip_prompt,
557
+ expected_outputs,
558
+ read_duration,
537
559
  )
538
560
 
539
561
  with self._channel_lock():
@@ -643,9 +665,10 @@ class Channel(BaseChannel):
643
665
 
644
666
  _channel_input = channel_input if not hidden_input else "REDACTED"
645
667
  self.logger.info(
646
- f"sending interactive input: {_channel_input}; "
647
- f"expecting: {channel_response}; "
648
- f"hidden_input: {hidden_input}"
668
+ "sending interactive input: %s; expecting: %s; hidden_input: %s",
669
+ _channel_input,
670
+ channel_response,
671
+ hidden_input,
649
672
  )
650
673
 
651
674
  self.write(channel_input=channel_input, redacted=bool(hidden_input))
scrapli/decorators.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """scrapli.decorators"""
2
+
2
3
  import asyncio
3
4
  import signal
4
5
  import sys
@@ -1,4 +1,5 @@
1
1
  """scrapli.driver"""
2
+
2
3
  from scrapli.driver.base import AsyncDriver, Driver
3
4
  from scrapli.driver.generic import AsyncGenericDriver, GenericDriver
4
5
  from scrapli.driver.network import AsyncNetworkDriver, NetworkDriver
@@ -1,4 +1,5 @@
1
1
  """scrapli.driver.base"""
2
+
2
3
  from scrapli.driver.base.async_driver import AsyncDriver
3
4
  from scrapli.driver.base.sync_driver import Driver
4
5
 
@@ -1,11 +1,12 @@
1
1
  """scrapli.driver.base.async_driver"""
2
+
2
3
  from types import TracebackType
3
4
  from typing import Any, Optional, Type, TypeVar
4
5
 
5
6
  from scrapli.channel import AsyncChannel
6
7
  from scrapli.driver.base.base_driver import BaseDriver
7
- from scrapli.exceptions import ScrapliValueError
8
- from scrapli.transport import ASYNCIO_TRANSPORTS
8
+ from scrapli.exceptions import ScrapliConnectionError, ScrapliValueError
9
+ from scrapli.transport import ASYNCIO_TRANSPORTS, CORE_TRANSPORTS
9
10
 
10
11
  _T = TypeVar("_T", bound="AsyncDriver")
11
12
 
@@ -14,7 +15,7 @@ class AsyncDriver(BaseDriver):
14
15
  def __init__(self, **kwargs: Any):
15
16
  super().__init__(**kwargs)
16
17
 
17
- if self.transport_name not in ASYNCIO_TRANSPORTS:
18
+ if self.transport_name in CORE_TRANSPORTS and self.transport_name not in ASYNCIO_TRANSPORTS:
18
19
  raise ScrapliValueError(
19
20
  "provided transport is *not* an asyncio transport, must use an async transport with"
20
21
  " the AsyncDriver(s)"
@@ -39,10 +40,22 @@ class AsyncDriver(BaseDriver):
39
40
  _T: a concrete implementation of the opened AsyncDriver object
40
41
 
41
42
  Raises:
42
- N/A
43
+ ScrapliConnectionError: if an exception occurs during opening
43
44
 
44
45
  """
45
- await self.open()
46
+ try:
47
+ await self.open()
48
+ except Exception as exc:
49
+ self.logger.critical(
50
+ "encountered exception during open in context manager,"
51
+ " attempting to close transport and channel"
52
+ )
53
+
54
+ self.transport.close()
55
+ self.channel.close()
56
+
57
+ raise ScrapliConnectionError(exc) from exc
58
+
46
59
  return self
47
60
 
48
61
  async def __aexit__(
@@ -87,14 +100,7 @@ class AsyncDriver(BaseDriver):
87
100
  await self.transport.open()
88
101
  self.channel.open()
89
102
 
90
- if (
91
- self.transport_name
92
- in (
93
- "telnet",
94
- "asynctelnet",
95
- )
96
- and not self.auth_bypass
97
- ):
103
+ if "telnet" in self.transport_name and not self.auth_bypass:
98
104
  await self.channel.channel_authenticate_telnet(
99
105
  auth_username=self.auth_username, auth_password=self.auth_password
100
106
  )
@@ -1,4 +1,5 @@
1
- """scrapli.driver.base.base_driver"""
1
+ """scrapli.driver.base.base_driver""" # noqa: C0302
2
+
2
3
  import importlib
3
4
  from dataclasses import fields
4
5
  from io import BytesIO
@@ -13,6 +14,7 @@ from scrapli.logging import get_instance_logger
13
14
  from scrapli.ssh_config import ssh_config_factory
14
15
  from scrapli.transport import CORE_TRANSPORTS
15
16
  from scrapli.transport.base import BasePluginTransportArgs, BaseTransportArgs
17
+ from scrapli.transport.plugins.system.transport import SystemTransport
16
18
 
17
19
 
18
20
  class BaseDriver:
@@ -34,6 +36,7 @@ class BaseDriver:
34
36
  timeout_ops: float = 30.0,
35
37
  comms_prompt_pattern: str = r"^[a-z0-9.\-@()/:]{1,48}[#>$]\s*$",
36
38
  comms_return_char: str = "\n",
39
+ comms_roughly_match_inputs: bool = False,
37
40
  ssh_config_file: Union[str, bool] = False,
38
41
  ssh_known_hosts_file: Union[str, bool] = False,
39
42
  on_init: Optional[Callable[..., Any]] = None,
@@ -83,6 +86,12 @@ class BaseDriver:
83
86
  should be mostly sorted for you if using network drivers (i.e. `IOSXEDriver`).
84
87
  Lastly, the case insensitive is just a convenience factor so i can be lazy.
85
88
  comms_return_char: character to use to send returns to host
89
+ comms_roughly_match_inputs: indicates if the channel should "roughly" match inputs sent
90
+ to the device. If False (default) inputs are strictly checked, as in any input
91
+ *must* be read back exactly on the channel. When set to True all input chars *must*
92
+ be read back in order in the output and all chars must be present, but the *exact*
93
+ input string does not need to be seen. This can be useful if a device echoes back
94
+ extra characters or rewrites the terminal during command input.
86
95
  ssh_config_file: string to path for ssh config file, True to use default ssh config file
87
96
  or False to ignore default ssh config file
88
97
  ssh_known_hosts_file: string to path for ssh known hosts file, True to use default known
@@ -149,6 +158,7 @@ class BaseDriver:
149
158
  auth_passphrase_pattern=auth_passphrase_pattern,
150
159
  comms_prompt_pattern=comms_prompt_pattern,
151
160
  comms_return_char=comms_return_char,
161
+ comms_roughly_match_inputs=comms_roughly_match_inputs,
152
162
  timeout_ops=timeout_ops,
153
163
  channel_log=channel_log,
154
164
  channel_log_mode=channel_log_mode,
@@ -249,6 +259,7 @@ class BaseDriver:
249
259
  f"timeout_ops={self._base_channel_args.timeout_ops!r}, "
250
260
  f"comms_prompt_pattern={self._base_channel_args.comms_prompt_pattern!r}, "
251
261
  f"comms_return_char={self._base_channel_args.comms_return_char!r}, "
262
+ f"comms_roughly_match_inputs={self._base_channel_args.comms_roughly_match_inputs!r}, "
252
263
  f"ssh_config_file={self.ssh_config_file!r}, "
253
264
  f"ssh_known_hosts_file={self.ssh_known_hosts_file!r}, "
254
265
  f"on_init={self.on_init!r}, "
@@ -362,7 +373,7 @@ class BaseDriver:
362
373
  cfg = ""
363
374
  else:
364
375
  cfg = ssh_config_file
365
- resolved_ssh_config_file = self._resolve_ssh_config(cfg)
376
+ resolved_ssh_config_file = self._resolve_ssh_config(cfg, transport=transport)
366
377
  else:
367
378
  resolved_ssh_config_file = ""
368
379
 
@@ -371,7 +382,9 @@ class BaseDriver:
371
382
  known_hosts = ""
372
383
  else:
373
384
  known_hosts = ssh_known_hosts_file
374
- resolved_ssh_known_hosts_file = self._resolve_ssh_known_hosts(known_hosts)
385
+ resolved_ssh_known_hosts_file = self._resolve_ssh_known_hosts(
386
+ known_hosts, transport=transport
387
+ )
375
388
  else:
376
389
  resolved_ssh_known_hosts_file = ""
377
390
 
@@ -547,7 +560,9 @@ class BaseDriver:
547
560
 
548
561
  return transport_class, plugin_transport_args
549
562
 
550
- def _load_non_core_transport_plugin(self) -> Tuple[Any, Type[BasePluginTransportArgs]]:
563
+ def _load_non_core_transport_plugin(
564
+ self,
565
+ ) -> Tuple[Any, Type[BasePluginTransportArgs]]:
551
566
  """
552
567
  Find non-core transport plugins and required plugin arguments
553
568
 
@@ -586,7 +601,7 @@ class BaseDriver:
586
601
 
587
602
  return transport_class, plugin_transport_args
588
603
 
589
- def _resolve_ssh_config(self, ssh_config_file: str) -> str:
604
+ def _resolve_ssh_config(self, ssh_config_file: str, transport: str) -> str:
590
605
  """
591
606
  Resolve ssh configuration file from provided string
592
607
 
@@ -595,6 +610,8 @@ class BaseDriver:
595
610
 
596
611
  Args:
597
612
  ssh_config_file: string representation of ssh config file to try to use
613
+ transport: string name of selected transport (so we can apply a bit of special handling
614
+ if system transport in use)
598
615
 
599
616
  Returns:
600
617
  str: string path to ssh config file or an empty string
@@ -603,27 +620,20 @@ class BaseDriver:
603
620
  N/A
604
621
 
605
622
  """
606
- self.logger.debug("attempting to resolve 'ssh_config_file' file")
623
+ self.logger.debug(f"attempting to resolve 'ssh_config_file' file {ssh_config_file}")
607
624
 
608
- resolved_ssh_config_file = ""
625
+ if ssh_config_file == "" and transport == "system":
626
+ return SystemTransport.SSH_SYSTEM_CONFIG_MAGIC_STRING
609
627
 
610
- if Path(ssh_config_file).is_file():
611
- resolved_ssh_config_file = str(Path(ssh_config_file))
612
- elif Path("~/.ssh/config").expanduser().is_file():
613
- resolved_ssh_config_file = str(Path("~/.ssh/config").expanduser())
614
- elif Path("/etc/ssh/ssh_config").is_file():
615
- resolved_ssh_config_file = str(Path("/etc/ssh/ssh_config"))
628
+ for path in (ssh_config_file, "~/.ssh/config", "/etc/ssh/ssh_config"):
629
+ full_path = Path(path).expanduser()
630
+ if full_path.is_file():
631
+ return str(full_path)
616
632
 
617
- if resolved_ssh_config_file:
618
- self.logger.debug(
619
- f"using '{resolved_ssh_config_file}' as resolved 'ssh_config_file' file'"
620
- )
621
- else:
622
- self.logger.debug("unable to resolve 'ssh_config_file' file")
633
+ self.logger.debug(f"unable to resolve 'ssh_config_file' file {ssh_config_file}")
634
+ return ""
623
635
 
624
- return resolved_ssh_config_file
625
-
626
- def _resolve_ssh_known_hosts(self, ssh_known_hosts: str) -> str:
636
+ def _resolve_ssh_known_hosts(self, ssh_known_hosts: str, transport: str) -> str:
627
637
  """
628
638
  Resolve ssh known hosts file from provided string
629
639
 
@@ -632,6 +642,8 @@ class BaseDriver:
632
642
 
633
643
  Args:
634
644
  ssh_known_hosts: string representation of ssh config file to try to use
645
+ transport: string name of selected transport (so we can apply a bit of special handling
646
+ if system transport in use)
635
647
 
636
648
  Returns:
637
649
  str: string path to ssh known hosts file or an empty string
@@ -642,23 +654,17 @@ class BaseDriver:
642
654
  """
643
655
  self.logger.debug("attempting to resolve 'ssh_known_hosts file'")
644
656
 
645
- resolved_ssh_known_hosts = ""
646
-
647
- if Path(ssh_known_hosts).is_file():
648
- resolved_ssh_known_hosts = str(Path(ssh_known_hosts))
649
- elif Path("~/.ssh/known_hosts").expanduser().is_file():
650
- resolved_ssh_known_hosts = str(Path("~/.ssh/known_hosts").expanduser())
651
- elif Path("/etc/ssh/ssh_known_hosts").is_file():
652
- resolved_ssh_known_hosts = str(Path("/etc/ssh/ssh_known_hosts"))
657
+ if ssh_known_hosts == "" and transport == "system":
658
+ self.logger.debug("Using system known hosts file as 'ssh_known_hosts' file")
659
+ return SystemTransport.SSH_SYSTEM_KNOWN_HOSTS_FILE_MAGIC_STRING
653
660
 
654
- if resolved_ssh_known_hosts:
655
- self.logger.debug(
656
- f"using '{resolved_ssh_known_hosts}' as resolved 'ssh_known_hosts' file'"
657
- )
658
- else:
659
- self.logger.debug("unable to resolve 'ssh_known_hosts' file")
661
+ for path in (ssh_known_hosts, "~/.ssh/known_hosts", "/etc/ssh/ssh_known_hosts"):
662
+ full_path = Path(path).expanduser()
663
+ if full_path.is_file():
664
+ return str(full_path)
660
665
 
661
- return resolved_ssh_known_hosts
666
+ self.logger.info(f"unable to resolve 'ssh_known_hosts' file {ssh_known_hosts}")
667
+ return ""
662
668
 
663
669
  @property
664
670
  def comms_prompt_pattern(self) -> str:
@@ -738,6 +744,84 @@ class BaseDriver:
738
744
 
739
745
  self._base_channel_args.comms_return_char = value
740
746
 
747
+ @property
748
+ def comms_prompt_search_depth(self) -> int:
749
+ """
750
+ Getter for `comms_prompt_search_depth` attribute
751
+
752
+ Args:
753
+ N/A
754
+
755
+ Returns:
756
+ int: comms_prompt_search_depth int
757
+
758
+ Raises:
759
+ N/A
760
+
761
+ """
762
+ return self._base_channel_args.comms_prompt_search_depth
763
+
764
+ @comms_prompt_search_depth.setter
765
+ def comms_prompt_search_depth(self, value: int) -> None:
766
+ """
767
+ Setter for `comms_prompt_search_depth` attribute
768
+
769
+ Args:
770
+ value: int value for comms_prompt_search_depth
771
+
772
+ Returns:
773
+ None
774
+
775
+ Raises:
776
+ ScrapliTypeError: if value is not of type int
777
+
778
+ """
779
+ self.logger.debug(f"setting 'comms_prompt_search_depth' value to {value!r}")
780
+
781
+ if not isinstance(value, int):
782
+ raise ScrapliTypeError
783
+
784
+ self._base_channel_args.comms_prompt_search_depth = value
785
+
786
+ @property
787
+ def comms_roughly_match_inputs(self) -> bool:
788
+ """
789
+ Getter for `comms_roughly_match_inputs` attribute
790
+
791
+ Args:
792
+ N/A
793
+
794
+ Returns:
795
+ bool: comms_roughly_match_inputs bool
796
+
797
+ Raises:
798
+ N/A
799
+
800
+ """
801
+ return self._base_channel_args.comms_roughly_match_inputs
802
+
803
+ @comms_roughly_match_inputs.setter
804
+ def comms_roughly_match_inputs(self, value: bool) -> None:
805
+ """
806
+ Setter for `comms_roughly_match_inputs` attribute
807
+
808
+ Args:
809
+ value: int value for comms_roughly_match_inputs
810
+
811
+ Returns:
812
+ None
813
+
814
+ Raises:
815
+ ScrapliTypeError: if value is not of type bool
816
+
817
+ """
818
+ self.logger.debug(f"setting 'comms_roughly_match_inputs' value to {value!r}")
819
+
820
+ if not isinstance(value, bool):
821
+ raise ScrapliTypeError
822
+
823
+ self._base_channel_args.comms_roughly_match_inputs = value
824
+
741
825
  @property
742
826
  def timeout_socket(self) -> float:
743
827
  """