scrapli 2.0.0a2__py3-none-musllinux_1_1_aarch64.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.
@@ -0,0 +1,148 @@
1
+ """scrapli.cli_decorators"""
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from ctypes import c_uint64
5
+ from functools import update_wrapper
6
+ from typing import TYPE_CHECKING, Concatenate, ParamSpec
7
+
8
+ from scrapli.cli_result import Result
9
+ from scrapli.exceptions import OptionsException
10
+
11
+ if TYPE_CHECKING:
12
+ from scrapli.cli import Cli
13
+
14
+ P = ParamSpec("P")
15
+
16
+
17
+ def handle_operation_timeout(
18
+ wrapped: Callable[Concatenate["Cli", P], Result],
19
+ ) -> Callable[Concatenate["Cli", P], Result]:
20
+ """
21
+ Wraps a Cli operation and sets the timeout value for the duration of the operation.
22
+
23
+ Args:
24
+ wrapped: the operation function
25
+
26
+ Returns:
27
+ callable: the wrapper function
28
+
29
+ Raises:
30
+ N/A
31
+
32
+ """
33
+
34
+ def wrapper(inst: "Cli", /, *args: P.args, **kwargs: P.kwargs) -> Result:
35
+ """
36
+ The operation timeout wrapper.
37
+
38
+ Args:
39
+ inst: the Cli instance
40
+ args: the arguments to pass to the wrapped function
41
+ kwargs: the keyword arguments to pass to the wrapped function
42
+
43
+ Returns:
44
+ Result: the result of the wrapped function
45
+
46
+ Raises:
47
+ OptionsException: if the operation timeout failed to set
48
+
49
+ """
50
+ operation_timeout_ns = kwargs.get("operation_timeout_ns")
51
+ if operation_timeout_ns is None:
52
+ return wrapped(inst, *args, **kwargs)
53
+
54
+ if operation_timeout_ns == inst.session_options.operation_timeout_ns:
55
+ return wrapped(inst, *args, **kwargs)
56
+
57
+ if not isinstance(operation_timeout_ns, int):
58
+ # ignore an invalid type for the timeout
59
+ return wrapped(inst, *args, **kwargs)
60
+
61
+ status = inst.ffi_mapping.options_mapping.session.set_operation_timeout_ns(
62
+ inst._ptr_or_exception(),
63
+ c_uint64(operation_timeout_ns),
64
+ )
65
+ if status != 0:
66
+ raise OptionsException("failed to set session operation timeout")
67
+
68
+ res = wrapped(inst, *args, **kwargs)
69
+
70
+ status = inst.ffi_mapping.options_mapping.session.set_operation_timeout_ns(
71
+ inst._ptr_or_exception(),
72
+ c_uint64(operation_timeout_ns),
73
+ )
74
+ if status != 0:
75
+ raise OptionsException("failed to set session operation timeout")
76
+
77
+ return res
78
+
79
+ update_wrapper(wrapper=wrapper, wrapped=wrapped)
80
+
81
+ return wrapper
82
+
83
+
84
+ def handle_operation_timeout_async(
85
+ wrapped: Callable[Concatenate["Cli", P], Awaitable[Result]],
86
+ ) -> Callable[Concatenate["Cli", P], Awaitable[Result]]:
87
+ """
88
+ Wraps a Cli operation and sets the timeout value for the duration of the operation.
89
+
90
+ Args:
91
+ wrapped: the operation function
92
+
93
+ Returns:
94
+ callable: the wrapper function
95
+
96
+ Raises:
97
+ N/A
98
+
99
+ """
100
+
101
+ async def wrapper(inst: "Cli", /, *args: P.args, **kwargs: P.kwargs) -> Result:
102
+ """
103
+ The operation timeout wrapper.
104
+
105
+ Args:
106
+ inst: the Cli instance
107
+ args: the arguments to pass to the wrapped function
108
+ kwargs: the keyword arguments to pass to the wrapped function
109
+
110
+ Returns:
111
+ Result: the result of the wrapped function
112
+
113
+ Raises:
114
+ OptionsException: if the operation timeout failed to set
115
+
116
+ """
117
+ operation_timeout_ns = kwargs.get("operation_timeout_ns")
118
+ if operation_timeout_ns is None:
119
+ return await wrapped(inst, *args, **kwargs)
120
+
121
+ if operation_timeout_ns == inst.session_options.operation_timeout_ns:
122
+ return await wrapped(inst, *args, **kwargs)
123
+
124
+ if not isinstance(operation_timeout_ns, int):
125
+ # ignore an invalid type for the timeout
126
+ return await wrapped(inst, *args, **kwargs)
127
+
128
+ status = inst.ffi_mapping.options_mapping.session.set_operation_timeout_ns(
129
+ inst._ptr_or_exception(),
130
+ c_uint64(operation_timeout_ns),
131
+ )
132
+ if status != 0:
133
+ raise OptionsException("failed to set session operation timeout")
134
+
135
+ res = await wrapped(inst, *args, **kwargs)
136
+
137
+ status = inst.ffi_mapping.options_mapping.session.set_operation_timeout_ns(
138
+ inst._ptr_or_exception(),
139
+ c_uint64(operation_timeout_ns),
140
+ )
141
+ if status != 0:
142
+ raise OptionsException("failed to set session operation timeout")
143
+
144
+ return res
145
+
146
+ update_wrapper(wrapper=wrapper, wrapped=wrapped)
147
+
148
+ return wrapper
scrapli/cli_parse.py ADDED
@@ -0,0 +1,161 @@
1
+ """cli_parse"""
2
+
3
+ import urllib.request
4
+ from importlib import import_module, resources
5
+ from io import BytesIO, TextIOWrapper
6
+ from logging import getLogger
7
+ from typing import Any, TextIO
8
+
9
+ from scrapli.exceptions import ParsingException
10
+
11
+ logger = getLogger(__name__)
12
+
13
+
14
+ def textfsm_get_template(platform: str, command: str) -> TextIO | None:
15
+ """
16
+ Find correct TextFSM template based on platform and command executed
17
+
18
+ Args:
19
+ platform: ntc-templates device type; i.e. cisco_ios, arista_eos, etc.
20
+ command: string of command that was executed (to find appropriate template)
21
+
22
+ Returns:
23
+ None or TextIO of opened template
24
+
25
+ Raises:
26
+ N/A
27
+
28
+ """
29
+ try:
30
+ import_module(name=".templates", package="ntc_templates")
31
+ cli_table_obj = getattr(import_module(name=".clitable", package="textfsm"), "CliTable")
32
+ except ModuleNotFoundError as exc:
33
+ raise ParsingException("optional extra 'textfsm' not found") from exc
34
+
35
+ template_dir = f"{resources.files('ntc_templates')}/templates"
36
+
37
+ cli_table = cli_table_obj("index", template_dir)
38
+ template_index = cli_table.index.GetRowMatch({"Platform": platform, "Command": command})
39
+
40
+ if not template_index:
41
+ logger.warning(
42
+ f"No match in ntc_templates index for platform `{platform}` and command `{command}`"
43
+ )
44
+ return None
45
+
46
+ template_name = cli_table.index.index[template_index]["Template"]
47
+
48
+ return open(f"{template_dir}/{template_name}", encoding="utf-8")
49
+
50
+
51
+ def _textfsm_to_dict(
52
+ structured_output: list[Any] | dict[str, Any], header: list[str]
53
+ ) -> list[Any] | dict[str, Any]:
54
+ """
55
+ Create list of dicts from textfsm output and header
56
+
57
+ Args:
58
+ structured_output: parsed textfsm output
59
+ header: list of strings representing column headers for textfsm output
60
+
61
+ Returns:
62
+ output: structured data
63
+
64
+ Raises:
65
+ N/A
66
+
67
+ """
68
+ logger.debug("converting textfsm output to dictionary representation")
69
+
70
+ header_lower = [h.lower() for h in header]
71
+ structured_output = [dict(zip(header_lower, row)) for row in structured_output]
72
+
73
+ return structured_output
74
+
75
+
76
+ def textfsm_parse(
77
+ template: str | TextIO, output: str, to_dict: bool = True
78
+ ) -> list[Any] | dict[str, Any]:
79
+ """
80
+ Parse output with TextFSM and ntc-templates, try to return structured output
81
+
82
+ Args:
83
+ template: TextIO or string of URL or filesystem path to template to use to parse data
84
+ output: unstructured output from device to parse
85
+ to_dict: convert textfsm output from list of lists to list of dicts -- basically create dict
86
+ from header and row data so it is easier to read/parse the output
87
+
88
+ Returns:
89
+ output: structured data
90
+
91
+ Raises:
92
+ ScrapliException: If raise_err is set and a textfsm parsing error occurs, raises from the
93
+ originating textfsm.parser.TextFSMError exception.
94
+
95
+ """
96
+ import textfsm
97
+
98
+ if isinstance(template, str):
99
+ if template.startswith("http://") or template.startswith("https://"):
100
+ with urllib.request.urlopen(template) as response:
101
+ re_table = textfsm.TextFSM(
102
+ TextIOWrapper(
103
+ BytesIO(response.read()),
104
+ encoding=response.headers.get_content_charset(),
105
+ )
106
+ )
107
+ else:
108
+ re_table = textfsm.TextFSM(open(template, mode="rb"))
109
+ else:
110
+ re_table = textfsm.TextFSM(template)
111
+
112
+ try:
113
+ structured_output: list[Any] | dict[str, Any] = re_table.ParseText(output)
114
+
115
+ if to_dict:
116
+ structured_output = _textfsm_to_dict(
117
+ structured_output=structured_output, header=re_table.header
118
+ )
119
+
120
+ return structured_output
121
+ except textfsm.parser.TextFSMError as exc:
122
+ raise ParsingException("failed parsing output with 'textfsm'") from exc
123
+
124
+ return []
125
+
126
+
127
+ def genie_parse(platform: str, command: str, output: str) -> list[Any] | dict[str, Any]:
128
+ """
129
+ Parse output with Cisco genie parsers, try to return structured output
130
+
131
+ Args:
132
+ platform: genie device type; i.e. iosxe, iosxr, etc.
133
+ command: string of command that was executed (to find appropriate parser)
134
+ output: unstructured output from device to parse
135
+
136
+ Returns:
137
+ output: structured data
138
+
139
+ Raises:
140
+ N/A
141
+
142
+ """
143
+ try:
144
+ device = getattr(import_module(name=".conf.base", package="genie"), "Device")
145
+ get_parser = getattr(
146
+ import_module(name=".libs.parser.utils", package="genie"), "get_parser"
147
+ )
148
+ except ModuleNotFoundError as exc:
149
+ raise ParsingException("optional extra 'genie' not found") from exc
150
+
151
+ genie_device = device("scrapli_device", custom={"abstraction": {"order": ["os"]}}, os=platform)
152
+
153
+ try:
154
+ get_parser(command, genie_device)
155
+ genie_parsed_result = genie_device.parse(command, output=output)
156
+ if isinstance(genie_parsed_result, list | dict):
157
+ return genie_parsed_result
158
+ except Exception as exc:
159
+ raise ParsingException("failed parsing output with 'genie'") from exc
160
+
161
+ return []
scrapli/cli_result.py ADDED
@@ -0,0 +1,197 @@
1
+ """scrapli.result"""
2
+
3
+ from typing import Any, TextIO
4
+
5
+ from scrapli.cli_parse import genie_parse, textfsm_get_template, textfsm_parse
6
+ from scrapli.exceptions import ParsingException
7
+
8
+ OPERATION_DELIMITER = "__libscrapli__"
9
+
10
+
11
+ class Result:
12
+ """
13
+ Result represents a set of results from some Cli operation(s).
14
+
15
+ Args:
16
+ N/A
17
+
18
+ Returns:
19
+ None
20
+
21
+ Raises:
22
+ N/A
23
+
24
+ """
25
+
26
+ def __init__( # noqa: PLR0913
27
+ self,
28
+ *,
29
+ host: str,
30
+ port: int,
31
+ inputs: str,
32
+ start_time: int,
33
+ splits: list[int],
34
+ results_raw: bytes,
35
+ results: str,
36
+ results_failed_indicator: str,
37
+ textfsm_platform: str,
38
+ genie_platform: str,
39
+ ) -> None:
40
+ self.host = host
41
+ self.port = port
42
+ self.inputs = inputs.split(OPERATION_DELIMITER)
43
+ self.start_time = start_time
44
+ self.splits = splits
45
+ self.results_raw = results_raw.split(OPERATION_DELIMITER.encode())
46
+ self.results = results.split(OPERATION_DELIMITER)
47
+ self.results_failed_indicator = results_failed_indicator
48
+ self.textfsm_platform = textfsm_platform
49
+ self.genie_platform = genie_platform
50
+
51
+ def extend(self, result: "Result") -> None:
52
+ """
53
+ Extends this Result object with another Result object.
54
+
55
+ Args:
56
+ result: the result object with which to extend this result object
57
+
58
+ Returns:
59
+ N/A
60
+
61
+ Raises:
62
+ N/A
63
+
64
+ """
65
+ self.inputs.extend(result.inputs)
66
+ self.results_raw.extend(result.results_raw)
67
+ self.results.extend(result.results)
68
+ self.splits.extend(result.splits)
69
+
70
+ @property
71
+ def failed(self) -> bool:
72
+ """
73
+ Returns True if any failed indicators were seen, otherwise False.
74
+
75
+ Args:
76
+ N/A
77
+
78
+ Returns:
79
+ bool: True for failed, otherwise False
80
+
81
+ Raises:
82
+ N/A
83
+
84
+ """
85
+ return bool(self.results_failed_indicator)
86
+
87
+ @property
88
+ def end_time(self) -> int:
89
+ """
90
+ Returns the end time of the operations in unix nano.
91
+
92
+ Args:
93
+ N/A
94
+
95
+ Returns:
96
+ int: end time in unix nano
97
+
98
+ Raises:
99
+ N/A
100
+
101
+ """
102
+ if not self.splits:
103
+ # if we had no splits it was a "noop" type op (like enter mode when
104
+ # you are already in the requested mode), so we'll lie and say it
105
+ # was a 1ns op time
106
+ return self.start_time + 1
107
+
108
+ return self.splits[-1]
109
+
110
+ @property
111
+ def elapsed_time_seconds(self) -> float:
112
+ """
113
+ Returns the number of seconds the operation took.
114
+
115
+ Args:
116
+ N/A
117
+
118
+ Returns:
119
+ float: duration in seconds
120
+
121
+ Raises:
122
+ N/A
123
+
124
+ """
125
+ return (self.end_time - self.start_time) / 1_000_000_000
126
+
127
+ @property
128
+ def result(self) -> str:
129
+ """
130
+ Returns the results joined on newline chars. Note this does *not* include inputs sent.
131
+
132
+ Args:
133
+ N/A
134
+
135
+ Returns:
136
+ str: joined results
137
+
138
+ Raises:
139
+ N/A
140
+
141
+ """
142
+ return "\n".join(self.results)
143
+
144
+ def textfsm_parse(
145
+ self,
146
+ index: int = 0,
147
+ template: str | TextIO | None = None,
148
+ to_dict: bool = True,
149
+ ) -> list[Any] | dict[str, Any]:
150
+ """
151
+ Parse results with textfsm, always return structured data
152
+
153
+ Returns an empty list if parsing fails!
154
+
155
+ Args:
156
+ index: the index of the result to parse, assumes first/zeroith if not provided
157
+ template: string path to textfsm template or opened textfsm template file
158
+ to_dict: convert textfsm output from list of lists to list of dicts -- basically create
159
+ dict from header and row data so it is easier to read/parse the output
160
+
161
+ Returns:
162
+ structured_result: empty list or parsed data from textfsm
163
+
164
+ Raises:
165
+ N/A
166
+
167
+ """
168
+ if template is None:
169
+ template = textfsm_get_template(
170
+ platform=self.textfsm_platform, command=self.inputs[index]
171
+ )
172
+
173
+ if template is None:
174
+ raise ParsingException("no template provided or available for input")
175
+
176
+ return textfsm_parse(template=template, output=self.result, to_dict=to_dict)
177
+
178
+ def genie_parse(
179
+ self,
180
+ index: int = 0,
181
+ ) -> dict[str, Any] | list[Any]:
182
+ """
183
+ Parse results with genie, always return structured data
184
+
185
+ Returns an empty list if parsing fails!
186
+
187
+ Args:
188
+ index: the index of the result to parse, assumes first/zeroith if not provided
189
+
190
+ Returns:
191
+ structured_result: empty list or parsed data from genie
192
+
193
+ Raises:
194
+ N/A
195
+
196
+ """
197
+ return genie_parse(self.genie_platform, self.inputs[index], self.results[index])
@@ -0,0 +1 @@
1
+ """scrapli.definitions"""
@@ -0,0 +1,64 @@
1
+ ---
2
+ prompt_pattern: '^[^<\r\n]{1,}[>#$]\s?$'
3
+ default_mode: 'privileged_exec'
4
+ modes:
5
+ - name: 'exec'
6
+ prompt_pattern: '^[\w.\-@()/: ]{1,63}>\s?$'
7
+ accessible_modes:
8
+ - name: 'privileged_exec'
9
+ instructions:
10
+ - send_prompted_input:
11
+ input: 'enable'
12
+ prompt_exact: 'Password:'
13
+ response: '__lookup::enable'
14
+ - name: 'privileged_exec'
15
+ prompt_pattern: '^[\w.\-@()/: ]{1,63}#\s?$'
16
+ prompt_excludes:
17
+ - '(config'
18
+ accessible_modes:
19
+ - name: 'exec'
20
+ instructions:
21
+ - send_input:
22
+ input: 'disable'
23
+ - name: 'configuration'
24
+ instructions:
25
+ - send_input:
26
+ input: 'configure terminal'
27
+ - name: 'bash'
28
+ instructions:
29
+ - send_input:
30
+ input: 'bash'
31
+ - name: 'configuration'
32
+ prompt_pattern: '^[\w.\-@()/: ]{1,63}\(config[\w.\-@/:+]{0,63}\)#\s?$'
33
+ accessible_modes:
34
+ - name: 'privileged_exec'
35
+ instructions:
36
+ - send_input:
37
+ input: 'end'
38
+ - name: 'bash'
39
+ prompt_pattern: '^\[[\w.\-@()\/: ]{1,63}~\]\$'
40
+ accessible_modes:
41
+ - name: 'privileged_exec'
42
+ instructions:
43
+ - send_input:
44
+ input: 'exit'
45
+ failure_indicators:
46
+ - '% Ambiguous command'
47
+ - '% Error'
48
+ - '% Incomplete command'
49
+ - '% Invalid input'
50
+ - '% Cannot commit'
51
+ - '% Unavailable command'
52
+ - '% Duplicate sequence number'
53
+ on_open_instructions:
54
+ - enter_mode:
55
+ requested_mode: 'privileged_exec'
56
+ - send_input:
57
+ input: 'term width 32767'
58
+ - send_input:
59
+ input: 'term len 0'
60
+ on_close_instructions:
61
+ - enter_mode:
62
+ requested_mode: 'privileged_exec'
63
+ - write:
64
+ input: 'exit'
@@ -0,0 +1,63 @@
1
+ ---
2
+ prompt_pattern: '^.*[>#$]\s?+$'
3
+ default_mode: 'privileged_exec'
4
+ modes:
5
+ - name: 'exec'
6
+ prompt_pattern: '^[\w.\-@/:]{1,63}>\s?+$'
7
+ accessible_modes:
8
+ - name: 'privileged_exec'
9
+ instructions:
10
+ - send_prompted_input:
11
+ input: 'enable'
12
+ prompt_exact: 'Password:'
13
+ response: '__lookup::enable'
14
+ - name: 'privileged_exec'
15
+ prompt_pattern: '^[\w.\-@/:]{1,63}#\s?+$'
16
+ accessible_modes:
17
+ - name: 'exec'
18
+ instructions:
19
+ - send_input:
20
+ input: 'disable'
21
+ - name: 'configuration'
22
+ instructions:
23
+ - send_input:
24
+ input: 'configure terminal'
25
+ - name: 'tclsh'
26
+ instructions:
27
+ - send_input:
28
+ input: 'tclsh'
29
+ - name: 'configuration'
30
+ prompt_pattern: '^[\w.\-@/:]{1,63}\([\w.\-@/:+]{0,32}\)#\s?+$'
31
+ prompt_excludes:
32
+ - 'tcl)'
33
+ accessible_modes:
34
+ - name: 'privileged_exec'
35
+ instructions:
36
+ - send_input:
37
+ input: 'end'
38
+ - name: 'tclsh'
39
+ prompt_pattern: '^([\w.\-@/+>:]+\(tcl\)[>#]|\+>)\s?+$'
40
+ accessible_modes:
41
+ - name: 'privileged_exec'
42
+ instructions:
43
+ - send_input:
44
+ input: 'tclquit'
45
+ failure_indicators:
46
+ - '% Ambiguous command'
47
+ - '% Incomplete command'
48
+ - '% Invalid input detected'
49
+ - '% Unknown command'
50
+ on_open_instructions:
51
+ - enter_mode:
52
+ requested_mode: 'privileged_exec'
53
+ - send_input:
54
+ input: 'term width 512'
55
+ - send_input:
56
+ input: 'term len 0'
57
+ on_close_instructions:
58
+ - enter_mode:
59
+ requested_mode: 'privileged_exec'
60
+ - write:
61
+ input: 'exit'
62
+ ntc_templates_platform: 'cisco_iosxe'
63
+ genie_platform: 'iosxe'
@@ -0,0 +1,47 @@
1
+ ---
2
+ prompt_pattern: '^.*[>#$]\s?+$'
3
+ default_mode: 'privileged_exec'
4
+ modes:
5
+ - name: 'privileged_exec'
6
+ prompt_pattern: '^[\w.\-@/:]{1,63}#\s?$'
7
+ accessible_modes:
8
+ - name: 'configuration'
9
+ instructions:
10
+ - send_input:
11
+ input: 'configure terminal'
12
+ - name: 'configuration_exclusive'
13
+ instructions:
14
+ - send_input:
15
+ input: 'configure exclusive'
16
+ - name: 'configuration'
17
+ prompt_pattern: '^[\w.\-@/:]{1,63}\(config[\w.\-@/:]{0,32}\)#\s?$'
18
+ accessible_modes:
19
+ - name: 'privileged_exec'
20
+ instructions:
21
+ - send_input:
22
+ input: 'end'
23
+ - name: 'configuration_exclusive'
24
+ prompt_pattern: '^[\w.\-@/:]{1,63}\(config[\w.\-@/:]{0,32}\)#\s?$'
25
+ accessible_modes:
26
+ - name: 'privileged_exec'
27
+ instructions:
28
+ - send_input:
29
+ input: 'end'
30
+ failure_indicators:
31
+ - '% Ambiguous command'
32
+ - '% Incomplete command'
33
+ - '% Invalid input detected'
34
+ on_open_instructions:
35
+ - enter_mode:
36
+ requested_mode: 'privileged_exec'
37
+ - send_input:
38
+ input: 'term width 512'
39
+ - send_input:
40
+ input: 'term len 0'
41
+ on_close_instructions:
42
+ - enter_mode:
43
+ requested_mode: 'privileged_exec'
44
+ - write:
45
+ input: 'exit'
46
+ ntc_templates_platform: 'cisco_iosxe'
47
+ genie_platform: 'iosxe'