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.
- scrapli/__init__.py +2 -1
- scrapli/channel/__init__.py +1 -0
- scrapli/channel/async_channel.py +35 -12
- scrapli/channel/base_channel.py +25 -3
- scrapli/channel/sync_channel.py +35 -12
- scrapli/decorators.py +1 -0
- scrapli/driver/__init__.py +1 -0
- scrapli/driver/base/__init__.py +1 -0
- scrapli/driver/base/async_driver.py +19 -13
- scrapli/driver/base/base_driver.py +121 -37
- scrapli/driver/base/sync_driver.py +19 -13
- scrapli/driver/core/__init__.py +1 -0
- scrapli/driver/core/arista_eos/__init__.py +1 -0
- scrapli/driver/core/arista_eos/async_driver.py +3 -0
- scrapli/driver/core/arista_eos/base_driver.py +3 -2
- scrapli/driver/core/arista_eos/sync_driver.py +3 -0
- scrapli/driver/core/cisco_iosxe/__init__.py +1 -0
- scrapli/driver/core/cisco_iosxe/async_driver.py +3 -0
- scrapli/driver/core/cisco_iosxe/base_driver.py +1 -0
- scrapli/driver/core/cisco_iosxe/sync_driver.py +3 -0
- scrapli/driver/core/cisco_iosxr/__init__.py +1 -0
- scrapli/driver/core/cisco_iosxr/async_driver.py +3 -0
- scrapli/driver/core/cisco_iosxr/base_driver.py +1 -0
- scrapli/driver/core/cisco_iosxr/sync_driver.py +3 -0
- scrapli/driver/core/cisco_nxos/__init__.py +1 -0
- scrapli/driver/core/cisco_nxos/async_driver.py +3 -0
- scrapli/driver/core/cisco_nxos/base_driver.py +9 -4
- scrapli/driver/core/cisco_nxos/sync_driver.py +3 -0
- scrapli/driver/core/juniper_junos/__init__.py +1 -0
- scrapli/driver/core/juniper_junos/async_driver.py +3 -0
- scrapli/driver/core/juniper_junos/base_driver.py +1 -0
- scrapli/driver/core/juniper_junos/sync_driver.py +3 -0
- scrapli/driver/generic/__init__.py +1 -0
- scrapli/driver/generic/async_driver.py +45 -3
- scrapli/driver/generic/base_driver.py +2 -1
- scrapli/driver/generic/sync_driver.py +45 -3
- scrapli/driver/network/__init__.py +1 -0
- scrapli/driver/network/async_driver.py +27 -0
- scrapli/driver/network/base_driver.py +1 -0
- scrapli/driver/network/sync_driver.py +27 -0
- scrapli/exceptions.py +1 -0
- scrapli/factory.py +22 -3
- scrapli/helper.py +76 -4
- scrapli/logging.py +1 -0
- scrapli/response.py +1 -0
- scrapli/ssh_config.py +1 -0
- scrapli/transport/base/__init__.py +1 -0
- scrapli/transport/base/async_transport.py +1 -0
- scrapli/transport/base/base_socket.py +1 -0
- scrapli/transport/base/base_transport.py +1 -0
- scrapli/transport/base/sync_transport.py +1 -0
- scrapli/transport/plugins/asyncssh/transport.py +4 -0
- scrapli/transport/plugins/asynctelnet/transport.py +13 -6
- scrapli/transport/plugins/paramiko/transport.py +1 -0
- scrapli/transport/plugins/ssh2/transport.py +6 -3
- scrapli/transport/plugins/system/ptyprocess.py +50 -13
- scrapli/transport/plugins/system/transport.py +27 -6
- scrapli/transport/plugins/telnet/transport.py +13 -7
- {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/METADATA +74 -47
- scrapli-2024.7.30.dist-info/RECORD +74 -0
- {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/WHEEL +1 -1
- scrapli-2023.7.30.dist-info/RECORD +0 -74
- {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/LICENSE +0 -0
- {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/top_level.txt +0 -0
scrapli/__init__.py
CHANGED
scrapli/channel/__init__.py
CHANGED
scrapli/channel/async_channel.py
CHANGED
@@ -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(
|
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
|
-
|
108
|
-
|
109
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
535
|
-
|
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
|
-
|
646
|
-
|
647
|
-
|
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))
|
scrapli/channel/base_channel.py
CHANGED
@@ -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(
|
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
|
-
|
352
|
-
|
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
|
|
scrapli/channel/sync_channel.py
CHANGED
@@ -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(
|
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
|
-
|
108
|
-
|
109
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
536
|
-
|
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
|
-
|
647
|
-
|
648
|
-
|
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
scrapli/driver/__init__.py
CHANGED
scrapli/driver/base/__init__.py
CHANGED
@@ -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
|
-
|
43
|
+
ScrapliConnectionError: if an exception occurs during opening
|
43
44
|
|
44
45
|
"""
|
45
|
-
|
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(
|
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(
|
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
|
-
|
625
|
+
if ssh_config_file == "" and transport == "system":
|
626
|
+
return SystemTransport.SSH_SYSTEM_CONFIG_MAGIC_STRING
|
609
627
|
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
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
|
-
|
618
|
-
|
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
|
-
|
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
|
-
|
646
|
-
|
647
|
-
|
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
|
-
|
655
|
-
|
656
|
-
|
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
|
-
|
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
|
"""
|