trigger 2.2.1__tar.gz → 2.2.3__tar.gz
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.
- {trigger-2.2.1/trigger.egg-info → trigger-2.2.3}/PKG-INFO +12 -25
- {trigger-2.2.1 → trigger-2.2.3}/README.md +8 -24
- {trigger-2.2.1 → trigger-2.2.3}/pyproject.toml +5 -1
- trigger-2.2.3/tests/test_twister.py +255 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/netdevices/__init__.py +6 -2
- {trigger-2.2.1 → trigger-2.2.3}/trigger/twister.py +54 -8
- {trigger-2.2.1 → trigger-2.2.3}/trigger/twister2.py +2 -1
- {trigger-2.2.1 → trigger-2.2.3/trigger.egg-info}/PKG-INFO +12 -25
- {trigger-2.2.1 → trigger-2.2.3}/trigger.egg-info/requires.txt +4 -0
- trigger-2.2.1/tests/test_twister.py +0 -34
- {trigger-2.2.1 → trigger-2.2.3}/AUTHORS.md +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/LICENSE.md +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/setup.cfg +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_acl.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_acl_db.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_acl_queue.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_changemgmt.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_except.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_netdevices.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_scripts.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_tacacsrc.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_templates.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_twister2.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/tests/test_utils.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/autoacl.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/db.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/dicts.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/grammar.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/ios.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/junos.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/models.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/parser.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/queue.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/support.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/acl/tools.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/acl.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/acl_script.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/aclconv.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/check_access.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/check_syntax.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/fe.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/find_access.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/gnng.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/gong.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/load_acl.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/load_config.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/netdev.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/optimizer.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/bin/run_cmds.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/changemgmt/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/changemgmt/bounce.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/cmds.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/conf/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/conf/global_settings.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/contrib/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/exceptions.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/gorc.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/netdevices/loader.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/netscreen.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/packages/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/packages/peewee.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/rancid.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/tacacsrc.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/__init__.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/cli.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/importlib.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/network.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/rcs.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/templates.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/url.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger/utils/xmltodict.py +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger.egg-info/SOURCES.txt +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger.egg-info/dependency_links.txt +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger.egg-info/entry_points.txt +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/trigger.egg-info/top_level.txt +0 -0
- {trigger-2.2.1 → trigger-2.2.3}/twisted/plugins/trigger_xmlrpc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trigger
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.3
|
|
4
4
|
Summary: Network automation toolkit for managing network devices
|
|
5
5
|
Author-email: Jathan McCollum <jathan@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -33,12 +33,14 @@ Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
|
33
33
|
Requires-Dist: pytest-twisted>=1.14.0; extra == "dev"
|
|
34
34
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
35
|
Requires-Dist: python-semantic-release>=9.0.0; extra == "dev"
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx_rtd_theme>=2.0; extra == "docs"
|
|
36
39
|
Dynamic: license-file
|
|
37
40
|
|
|
38
41
|
# What is Trigger?
|
|
39
42
|
|
|
40
|
-
[](https://gitter.im/trigger/trigger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
43
|
+
[](https://github.com/trigger/trigger/actions/workflows/ci.yml)
|
|
42
44
|
|
|
43
45
|
Trigger is a robust network automation toolkit written in Python that was
|
|
44
46
|
designed for interfacing with network devices and managing network
|
|
@@ -79,16 +81,9 @@ network devices with ease. Here are some of things that make Trigger tick:
|
|
|
79
81
|
+ Flexible access-list & firewall policy parser that can test access if access
|
|
80
82
|
is permitted, or easily convert ACLs from one format to another.
|
|
81
83
|
+ Detailed support for timezones and maintenance windows.
|
|
84
|
+
+ Import your metadata from an existing [RANCID](http://shrubbery.net/rancid/) installation or a CSV file to get up-and-running quickly.
|
|
82
85
|
+ A suite of tools for simplifying many common tasks.
|
|
83
86
|
|
|
84
|
-
New in version 1.2:
|
|
85
|
-
|
|
86
|
-
+ Import your metadata from an existing [RANCID](http://shrubbery.net/rancid/) installation to get up-and-running quickly!
|
|
87
|
-
|
|
88
|
-
New in version 1.3:
|
|
89
|
-
|
|
90
|
-
+ Import your metadata from a CSV file and get up-and-running even quicker!
|
|
91
|
-
|
|
92
87
|
## Getting Started
|
|
93
88
|
|
|
94
89
|
The best way to get started is to read the documentation hosted by [Read the
|
|
@@ -142,21 +137,13 @@ See the [Migration Guide](https://trigger.readthedocs.io/en/latest/migration.htm
|
|
|
142
137
|
|
|
143
138
|
### Before you begin
|
|
144
139
|
|
|
145
|
-
+ The [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
+ The [master](https://github.com/trigger/trigger/tree/master) branch is
|
|
150
|
-
the stable branch, and will reflect the latest production-ready changes. It
|
|
151
|
-
is recommended that this is the branch you use if you are installing Trigger
|
|
152
|
-
for the first time.
|
|
153
|
-
+ Each point release of Trigger is maintained as a [tag branch](https://github.com/trigger/trigger/tags). If you require a
|
|
140
|
+
+ The [main](https://github.com/trigger/trigger/tree/main) branch is the
|
|
141
|
+
primary branch for all development and releases. All pull requests target
|
|
142
|
+
`main`.
|
|
143
|
+
+ Each point release of Trigger is maintained as a [tag](https://github.com/trigger/trigger/tags). If you require a
|
|
154
144
|
specific Trigger version, please refer to these.
|
|
155
145
|
|
|
156
146
|
### Get in touch!
|
|
157
147
|
|
|
158
|
-
If you run into any snags, have questions, feedback, or just want to talk shop
|
|
159
|
-
[
|
|
160
|
-
|
|
161
|
-
**Pro tip**: Find us on IRC at `#trigger` on Freenode
|
|
162
|
-
([irc://irc.freenode.net/trigger](irc://irc.freenode.net/trigger)).
|
|
148
|
+
If you run into any snags, have questions, feedback, or just want to talk shop,
|
|
149
|
+
please open an issue on [GitHub Issues](https://github.com/trigger/trigger/issues).
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# What is Trigger?
|
|
2
2
|
|
|
3
|
-
[](https://gitter.im/trigger/trigger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
3
|
+
[](https://github.com/trigger/trigger/actions/workflows/ci.yml)
|
|
5
4
|
|
|
6
5
|
Trigger is a robust network automation toolkit written in Python that was
|
|
7
6
|
designed for interfacing with network devices and managing network
|
|
@@ -42,16 +41,9 @@ network devices with ease. Here are some of things that make Trigger tick:
|
|
|
42
41
|
+ Flexible access-list & firewall policy parser that can test access if access
|
|
43
42
|
is permitted, or easily convert ACLs from one format to another.
|
|
44
43
|
+ Detailed support for timezones and maintenance windows.
|
|
44
|
+
+ Import your metadata from an existing [RANCID](http://shrubbery.net/rancid/) installation or a CSV file to get up-and-running quickly.
|
|
45
45
|
+ A suite of tools for simplifying many common tasks.
|
|
46
46
|
|
|
47
|
-
New in version 1.2:
|
|
48
|
-
|
|
49
|
-
+ Import your metadata from an existing [RANCID](http://shrubbery.net/rancid/) installation to get up-and-running quickly!
|
|
50
|
-
|
|
51
|
-
New in version 1.3:
|
|
52
|
-
|
|
53
|
-
+ Import your metadata from a CSV file and get up-and-running even quicker!
|
|
54
|
-
|
|
55
47
|
## Getting Started
|
|
56
48
|
|
|
57
49
|
The best way to get started is to read the documentation hosted by [Read the
|
|
@@ -105,21 +97,13 @@ See the [Migration Guide](https://trigger.readthedocs.io/en/latest/migration.htm
|
|
|
105
97
|
|
|
106
98
|
### Before you begin
|
|
107
99
|
|
|
108
|
-
+ The [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
+ The [master](https://github.com/trigger/trigger/tree/master) branch is
|
|
113
|
-
the stable branch, and will reflect the latest production-ready changes. It
|
|
114
|
-
is recommended that this is the branch you use if you are installing Trigger
|
|
115
|
-
for the first time.
|
|
116
|
-
+ Each point release of Trigger is maintained as a [tag branch](https://github.com/trigger/trigger/tags). If you require a
|
|
100
|
+
+ The [main](https://github.com/trigger/trigger/tree/main) branch is the
|
|
101
|
+
primary branch for all development and releases. All pull requests target
|
|
102
|
+
`main`.
|
|
103
|
+
+ Each point release of Trigger is maintained as a [tag](https://github.com/trigger/trigger/tags). If you require a
|
|
117
104
|
specific Trigger version, please refer to these.
|
|
118
105
|
|
|
119
106
|
### Get in touch!
|
|
120
107
|
|
|
121
|
-
If you run into any snags, have questions, feedback, or just want to talk shop
|
|
122
|
-
[
|
|
123
|
-
|
|
124
|
-
**Pro tip**: Find us on IRC at `#trigger` on Freenode
|
|
125
|
-
([irc://irc.freenode.net/trigger](irc://irc.freenode.net/trigger)).
|
|
108
|
+
If you run into any snags, have questions, feedback, or just want to talk shop,
|
|
109
|
+
please open an issue on [GitHub Issues](https://github.com/trigger/trigger/issues).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trigger"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.3"
|
|
8
8
|
description = "Network automation toolkit for managing network devices"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -42,6 +42,10 @@ dev = [
|
|
|
42
42
|
"ruff>=0.1.0",
|
|
43
43
|
"python-semantic-release>=9.0.0",
|
|
44
44
|
]
|
|
45
|
+
docs = [
|
|
46
|
+
"sphinx>=7.0",
|
|
47
|
+
"sphinx_rtd_theme>=2.0",
|
|
48
|
+
]
|
|
45
49
|
|
|
46
50
|
[project.scripts]
|
|
47
51
|
acl = "trigger.bin.acl:main"
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from trigger.conf import settings
|
|
6
|
+
from trigger.twister import compile_prompt_pattern, prompt_match_start
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_ioslike_prompt_pattern_enabled():
|
|
10
|
+
"""Test enabled that IOS-like prompt patterns match correctly."""
|
|
11
|
+
pat = settings.IOSLIKE_PROMPT_PAT
|
|
12
|
+
|
|
13
|
+
prompt_tests = [
|
|
14
|
+
"foo-bar1#",
|
|
15
|
+
"foo-bar1# ",
|
|
16
|
+
"foo-bar1(config)# ",
|
|
17
|
+
"\rfoo-bar01(config)# \x08 ", # "Bonus" backspace in there
|
|
18
|
+
"foo-bar01(config) \r#", # "Bonus" '\s\r' in there
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for prompt in prompt_tests:
|
|
22
|
+
assert re.search(pat, prompt) is not None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_ioslike_prompt_pattern_nonenabled():
|
|
26
|
+
"""Test non-enabled that IOS-like prompt patterns match correctly."""
|
|
27
|
+
pat = settings.IOSLIKE_ENABLE_PAT
|
|
28
|
+
|
|
29
|
+
prompt_tests = [
|
|
30
|
+
"foo-bar1>",
|
|
31
|
+
"foo-bar1> ",
|
|
32
|
+
"\rfoo-bar01)> \x08 ", # "Bonus" backspace in there
|
|
33
|
+
"foo-bar01) \r>", # "Bonus" '\s\r' in there
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for prompt in prompt_tests:
|
|
37
|
+
assert re.search(pat, prompt) is not None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =============================================================================
|
|
41
|
+
# False-positive prevention tests (Issue #317)
|
|
42
|
+
# =============================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestFalsePositivePrevention:
|
|
46
|
+
"""Verify that command output containing '>' or '#' is NOT matched as a prompt."""
|
|
47
|
+
|
|
48
|
+
def test_juniper_rsync_flag_not_matched(self):
|
|
49
|
+
"""Juniper 'Flags: <Sync RSync>' must not match as a prompt (issue #317)."""
|
|
50
|
+
pat = compile_prompt_pattern(settings.PROMPT_PATTERNS["juniper"])
|
|
51
|
+
buffer = "Flags: <Sync RSync>\r\n"
|
|
52
|
+
assert pat.search(buffer) is None
|
|
53
|
+
|
|
54
|
+
def test_ioslike_comment_not_matched(self):
|
|
55
|
+
"""A '# comment' line must not match IOS-like prompt pattern."""
|
|
56
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
57
|
+
buffer = "some output\r\n# This is a comment in output\r\n"
|
|
58
|
+
assert pat.search(buffer) is None
|
|
59
|
+
|
|
60
|
+
def test_enable_angle_bracket_not_matched(self):
|
|
61
|
+
"""A '>' inside command output must not match the enable pattern."""
|
|
62
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_ENABLE_PAT)
|
|
63
|
+
buffer = "Description: Traffic >1Gbps\r\nMore output here\r\n"
|
|
64
|
+
assert pat.search(buffer) is None
|
|
65
|
+
|
|
66
|
+
def test_ioslike_prompt_mid_line_not_matched(self):
|
|
67
|
+
"""IOS-like prompt pattern should not match '#' in the middle of a line."""
|
|
68
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
69
|
+
buffer = "some leading text device-name# "
|
|
70
|
+
assert pat.search(buffer) is None
|
|
71
|
+
|
|
72
|
+
def test_juniper_xml_angle_bracket_not_matched(self):
|
|
73
|
+
"""Juniper XML output with '>' should not match as prompt."""
|
|
74
|
+
pat = compile_prompt_pattern(settings.PROMPT_PATTERNS["juniper"])
|
|
75
|
+
buffer = '<rpc-reply xmlns:junos="http://xml.juniper.net">\r\n'
|
|
76
|
+
assert pat.search(buffer) is None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# Valid prompt regression tests
|
|
81
|
+
# =============================================================================
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestValidPromptMatching:
|
|
85
|
+
"""Verify that real prompts still match correctly after the anchoring fix."""
|
|
86
|
+
|
|
87
|
+
def test_first_prompt_no_preceding_newline(self):
|
|
88
|
+
"""A prompt at the very start of the buffer (no preceding newline) must match."""
|
|
89
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
90
|
+
buffer = "router1# "
|
|
91
|
+
m = pat.search(buffer)
|
|
92
|
+
assert m is not None
|
|
93
|
+
|
|
94
|
+
def test_prompt_after_newline(self):
|
|
95
|
+
"""A prompt after \\n must match."""
|
|
96
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
97
|
+
buffer = "some output\nrouter1# "
|
|
98
|
+
m = pat.search(buffer)
|
|
99
|
+
assert m is not None
|
|
100
|
+
assert prompt_match_start(m) == len("some output\n")
|
|
101
|
+
|
|
102
|
+
def test_prompt_after_crlf(self):
|
|
103
|
+
"""A prompt after \\r\\n must match."""
|
|
104
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
105
|
+
buffer = "some output\r\nrouter1# "
|
|
106
|
+
m = pat.search(buffer)
|
|
107
|
+
assert m is not None
|
|
108
|
+
assert prompt_match_start(m) == len("some output\r\n")
|
|
109
|
+
|
|
110
|
+
def test_juniper_prompt_after_master_banner(self):
|
|
111
|
+
"""Juniper prompt after {master} banner must match."""
|
|
112
|
+
pat = compile_prompt_pattern(settings.PROMPT_PATTERNS["juniper"])
|
|
113
|
+
buffer = "{master}\nuser@router> "
|
|
114
|
+
m = pat.search(buffer)
|
|
115
|
+
assert m is not None
|
|
116
|
+
|
|
117
|
+
def test_ioslike_config_mode_prompt(self):
|
|
118
|
+
"""IOS config mode prompt must match."""
|
|
119
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
120
|
+
buffer = "output line\nswitch1(config)# "
|
|
121
|
+
m = pat.search(buffer)
|
|
122
|
+
assert m is not None
|
|
123
|
+
|
|
124
|
+
def test_enable_prompt_at_start(self):
|
|
125
|
+
"""Enable prompt at start of buffer must match."""
|
|
126
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_ENABLE_PAT)
|
|
127
|
+
buffer = "router1> "
|
|
128
|
+
m = pat.search(buffer)
|
|
129
|
+
assert m is not None
|
|
130
|
+
assert prompt_match_start(m) == 0
|
|
131
|
+
|
|
132
|
+
def test_paloalto_prompt_with_crlf(self):
|
|
133
|
+
"""Palo Alto prompt with \\r\\n prefix must match."""
|
|
134
|
+
pat = compile_prompt_pattern(settings.PROMPT_PATTERNS["paloalto"])
|
|
135
|
+
buffer = "output\r\nadmin@fw1> "
|
|
136
|
+
m = pat.search(buffer)
|
|
137
|
+
assert m is not None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# prompt_match_start correctness tests
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestPromptMatchStart:
|
|
146
|
+
"""Verify prompt_match_start returns the correct index."""
|
|
147
|
+
|
|
148
|
+
def test_match_at_buffer_start(self):
|
|
149
|
+
"""Match at buffer start returns 0."""
|
|
150
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
151
|
+
buffer = "router1# "
|
|
152
|
+
m = pat.search(buffer)
|
|
153
|
+
assert m is not None
|
|
154
|
+
assert prompt_match_start(m) == 0
|
|
155
|
+
|
|
156
|
+
def test_match_after_lf(self):
|
|
157
|
+
"""Match after \\n skips the newline character."""
|
|
158
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
159
|
+
buffer = "output\nrouter1# "
|
|
160
|
+
m = pat.search(buffer)
|
|
161
|
+
assert m is not None
|
|
162
|
+
# The \n is at index 6, prompt starts at index 7
|
|
163
|
+
assert prompt_match_start(m) == 7
|
|
164
|
+
|
|
165
|
+
def test_match_after_crlf(self):
|
|
166
|
+
"""Match after \\r\\n skips both characters."""
|
|
167
|
+
pat = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
168
|
+
buffer = "output\r\nrouter1# "
|
|
169
|
+
m = pat.search(buffer)
|
|
170
|
+
assert m is not None
|
|
171
|
+
# \r at 6, \n at 7, prompt starts at 8
|
|
172
|
+
assert prompt_match_start(m) == 8
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# =============================================================================
|
|
176
|
+
# compile_prompt_pattern behavior tests
|
|
177
|
+
# =============================================================================
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestCompilePromptPattern:
|
|
181
|
+
"""Verify compile_prompt_pattern handles various inputs correctly."""
|
|
182
|
+
|
|
183
|
+
def test_already_compiled_pattern_returned_unchanged(self):
|
|
184
|
+
"""An already-compiled re.Pattern should be returned as-is."""
|
|
185
|
+
compiled = re.compile(r"foo#\s?$")
|
|
186
|
+
result = compile_prompt_pattern(compiled)
|
|
187
|
+
assert result is compiled
|
|
188
|
+
|
|
189
|
+
def test_pattern_starting_with_caret_not_double_anchored(self):
|
|
190
|
+
"""A pattern starting with ^ should not get a redundant prefix."""
|
|
191
|
+
pat = compile_prompt_pattern(r"^\S+#\s?$")
|
|
192
|
+
assert pat.search("router1# ") is not None
|
|
193
|
+
# Verify no double-anchor by checking pattern string
|
|
194
|
+
assert not pat.pattern.startswith(r"(?:^|\r?\n)^")
|
|
195
|
+
|
|
196
|
+
def test_pattern_starting_with_backslash_r_not_prefixed(self):
|
|
197
|
+
"""A pattern starting with \\r should not get a prefix (e.g. paloalto)."""
|
|
198
|
+
pat = compile_prompt_pattern(r"\r\n\S+(?:\>|#)\s?$")
|
|
199
|
+
# Pattern should still work
|
|
200
|
+
assert pat.search("\r\nadmin@fw1> ") is not None
|
|
201
|
+
# Verify no prefix added
|
|
202
|
+
assert not pat.pattern.startswith(r"(?:^|\r?\n)\r")
|
|
203
|
+
|
|
204
|
+
def test_pattern_starting_with_backslash_n_not_prefixed(self):
|
|
205
|
+
"""A pattern starting with \\n should not get a prefix."""
|
|
206
|
+
pat = compile_prompt_pattern(r"\n\S+#\s?$")
|
|
207
|
+
assert not pat.pattern.startswith(r"(?:^|\r?\n)\n")
|
|
208
|
+
|
|
209
|
+
def test_multiline_flag_is_set(self):
|
|
210
|
+
"""The compiled pattern should have re.MULTILINE enabled."""
|
|
211
|
+
pat = compile_prompt_pattern(r"\S+#\s?$")
|
|
212
|
+
assert pat.flags & re.MULTILINE
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# =============================================================================
|
|
216
|
+
# Parametrized vendor pattern tests
|
|
217
|
+
# =============================================================================
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Map each vendor pattern to a sample prompt that should match on its own line
|
|
221
|
+
VENDOR_PROMPT_SAMPLES = {
|
|
222
|
+
"aruba": "(Aruba7010) #",
|
|
223
|
+
"avocent": "admin-0->",
|
|
224
|
+
"citrix": " Done\n",
|
|
225
|
+
"cumulus": "cumulus@switch# ",
|
|
226
|
+
"f5": "admin@(bigip1)(cfg-sync Standalone)(Active)(/Common)(tmos)# ",
|
|
227
|
+
"juniper": "user@router> ",
|
|
228
|
+
"mrv": "\r\nMRV OptiSwitch 1 >>",
|
|
229
|
+
"netscreen": "fw1-> ",
|
|
230
|
+
"paloalto": "\r\nadmin@fw1> ",
|
|
231
|
+
"pica8": "admin@switch> ",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.mark.parametrize(
|
|
236
|
+
("vendor", "sample_prompt"),
|
|
237
|
+
list(VENDOR_PROMPT_SAMPLES.items()),
|
|
238
|
+
ids=list(VENDOR_PROMPT_SAMPLES.keys()),
|
|
239
|
+
)
|
|
240
|
+
def test_vendor_pattern_matches_sample_prompt(vendor, sample_prompt):
|
|
241
|
+
"""Each vendor pattern must match its expected sample prompt."""
|
|
242
|
+
pat = compile_prompt_pattern(settings.PROMPT_PATTERNS[vendor])
|
|
243
|
+
assert pat.search(sample_prompt) is not None, (
|
|
244
|
+
f"Vendor {vendor!r} pattern failed to match sample prompt {sample_prompt!r}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@pytest.mark.parametrize(
|
|
249
|
+
"vendor",
|
|
250
|
+
list(settings.PROMPT_PATTERNS.keys()),
|
|
251
|
+
)
|
|
252
|
+
def test_vendor_pattern_compiles_without_error(vendor):
|
|
253
|
+
"""Every vendor pattern in PROMPT_PATTERNS must compile successfully."""
|
|
254
|
+
pat = compile_prompt_pattern(settings.PROMPT_PATTERNS[vendor])
|
|
255
|
+
assert isinstance(pat, re.Pattern)
|
|
@@ -500,7 +500,9 @@ class NetDevice:
|
|
|
500
500
|
self.factories["base"] = factory
|
|
501
501
|
|
|
502
502
|
# FIXME(jathan): prompt_pattern could move back to protocol?
|
|
503
|
-
|
|
503
|
+
from trigger.twister import compile_prompt_pattern
|
|
504
|
+
|
|
505
|
+
prompt = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
504
506
|
proto = endpoint.connect(factory, prompt_pattern=prompt)
|
|
505
507
|
self._proto = proto # Track this for later, too.
|
|
506
508
|
|
|
@@ -594,7 +596,9 @@ class NetDevice:
|
|
|
594
596
|
|
|
595
597
|
# Here's where we're using self._connect injected on .open()
|
|
596
598
|
ep = TriggerSSHShellClientEndpointBase.existingConnection(self._conn)
|
|
597
|
-
|
|
599
|
+
from trigger.twister import compile_prompt_pattern
|
|
600
|
+
|
|
601
|
+
prompt = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
598
602
|
proto = ep.connect(factory, prompt_pattern=prompt)
|
|
599
603
|
|
|
600
604
|
d = defer.Deferred()
|
|
@@ -99,6 +99,52 @@ def is_awaiting_confirmation(prompt):
|
|
|
99
99
|
return any(prompt.endswith(match.lower()) for match in matchlist)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def compile_prompt_pattern(pattern):
|
|
103
|
+
r"""Compile a prompt pattern with line-start anchoring to prevent false matches.
|
|
104
|
+
|
|
105
|
+
Prepends ``(?:^|\r?\n)`` to the pattern so that prompts only match at the
|
|
106
|
+
start of a line (beginning of buffer or after a newline). Already-compiled
|
|
107
|
+
``re.Pattern`` objects and patterns that already start with a line anchor
|
|
108
|
+
(``^``, ``\r``, ``\n``) are returned unchanged.
|
|
109
|
+
|
|
110
|
+
:param pattern:
|
|
111
|
+
A prompt regex string or compiled ``re.Pattern``.
|
|
112
|
+
|
|
113
|
+
:returns:
|
|
114
|
+
A compiled ``re.Pattern`` with ``re.MULTILINE`` enabled.
|
|
115
|
+
"""
|
|
116
|
+
if isinstance(pattern, re.Pattern):
|
|
117
|
+
return pattern
|
|
118
|
+
if pattern.startswith(("^", r"\r", r"\n")):
|
|
119
|
+
return re.compile(pattern, re.MULTILINE)
|
|
120
|
+
return re.compile(r"(?:^|\r?\n)" + pattern, re.MULTILINE)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def prompt_match_start(match):
|
|
124
|
+
r"""Return the start position of the actual prompt within a match.
|
|
125
|
+
|
|
126
|
+
When ``compile_prompt_pattern`` prepends a ``(?:^|\r?\n)`` prefix, the
|
|
127
|
+
match may begin with ``\r`` or ``\n`` characters that are not part of the
|
|
128
|
+
prompt itself. This helper skips those leading characters and returns the
|
|
129
|
+
index where the real prompt text begins.
|
|
130
|
+
|
|
131
|
+
:param match:
|
|
132
|
+
A ``re.Match`` object from a prompt search.
|
|
133
|
+
|
|
134
|
+
:returns:
|
|
135
|
+
An integer index suitable for slicing the buffer at the prompt boundary.
|
|
136
|
+
"""
|
|
137
|
+
s = match.start()
|
|
138
|
+
text = match.group()
|
|
139
|
+
# Skip any leading \r or \n consumed by the anchor prefix
|
|
140
|
+
for ch in text:
|
|
141
|
+
if ch in "\r\n":
|
|
142
|
+
s += 1
|
|
143
|
+
else:
|
|
144
|
+
break
|
|
145
|
+
return s
|
|
146
|
+
|
|
147
|
+
|
|
102
148
|
def requires_enable(proto_obj, data):
|
|
103
149
|
"""Check if a device requires enable.
|
|
104
150
|
|
|
@@ -951,7 +997,7 @@ class TriggerSSHChannelFactory(TriggerClientFactory):
|
|
|
951
997
|
self.timeout = timeout
|
|
952
998
|
self.channel_class = channel_class
|
|
953
999
|
self.command_interval = command_interval
|
|
954
|
-
self.prompt =
|
|
1000
|
+
self.prompt = compile_prompt_pattern(prompt_pattern)
|
|
955
1001
|
self.device = device
|
|
956
1002
|
self.connection_class = connection_class
|
|
957
1003
|
TriggerClientFactory.__init__(self, deferred, creds)
|
|
@@ -1292,7 +1338,7 @@ class Interactor(protocol.Protocol):
|
|
|
1292
1338
|
|
|
1293
1339
|
def __init__(self, log_to=None):
|
|
1294
1340
|
self._log_to = log_to
|
|
1295
|
-
self.enable_prompt =
|
|
1341
|
+
self.enable_prompt = compile_prompt_pattern(settings.IOSLIKE_ENABLE_PAT)
|
|
1296
1342
|
self.enabled = False
|
|
1297
1343
|
self.initialized = False
|
|
1298
1344
|
|
|
@@ -1306,7 +1352,7 @@ class Interactor(protocol.Protocol):
|
|
|
1306
1352
|
c.dataReceived = self.write
|
|
1307
1353
|
self.stdio = stdio.StandardIO(c)
|
|
1308
1354
|
self.device = self.factory.device # Attach the device object
|
|
1309
|
-
self.prompt =
|
|
1355
|
+
self.prompt = compile_prompt_pattern(self.device.vendor.prompt_pattern)
|
|
1310
1356
|
|
|
1311
1357
|
def loseConnection(self):
|
|
1312
1358
|
"""Terminate the connection. Link this to the transport method of the same
|
|
@@ -1415,7 +1461,7 @@ class TriggerSSHChannelBase(channel.SSHChannel, TimeoutMixin):
|
|
|
1415
1461
|
log.msg(f"[{self.device}] My startup commands: {self.startup_commands!r}")
|
|
1416
1462
|
|
|
1417
1463
|
# For IOS-like devices that require 'enable'
|
|
1418
|
-
self.enable_prompt =
|
|
1464
|
+
self.enable_prompt = compile_prompt_pattern(settings.IOSLIKE_ENABLE_PAT)
|
|
1419
1465
|
self.enabled = False
|
|
1420
1466
|
|
|
1421
1467
|
def channelOpen(self, data):
|
|
@@ -1467,7 +1513,7 @@ class TriggerSSHChannelBase(channel.SSHChannel, TimeoutMixin):
|
|
|
1467
1513
|
# Or just use the matched regex object...
|
|
1468
1514
|
log.msg(f"[{self.device}] STATE: buffer {self.data!r}")
|
|
1469
1515
|
log.msg(f"[{self.device}] STATE: prompt {m.group()!r}")
|
|
1470
|
-
prompt_idx = m
|
|
1516
|
+
prompt_idx = prompt_match_start(m)
|
|
1471
1517
|
|
|
1472
1518
|
# Strip the prompt from the match result
|
|
1473
1519
|
result = self.data[:prompt_idx] # Cut the prompt out
|
|
@@ -1759,7 +1805,7 @@ class TriggerSSHNetscalerChannel(TriggerSSHChannelBase):
|
|
|
1759
1805
|
return
|
|
1760
1806
|
log.msg(f"[{self.device}] STATE: prompt {m.group()!r}")
|
|
1761
1807
|
|
|
1762
|
-
result = self.data[: m
|
|
1808
|
+
result = self.data[: prompt_match_start(m)] # Strip ' Done\n' from results.
|
|
1763
1809
|
|
|
1764
1810
|
if self.initialized:
|
|
1765
1811
|
self.results.append(result)
|
|
@@ -2026,7 +2072,7 @@ class IoslikeSendExpect(protocol.Protocol, TimeoutMixin):
|
|
|
2026
2072
|
self.with_errors = with_errors
|
|
2027
2073
|
self.timeout = timeout
|
|
2028
2074
|
self.command_interval = command_interval
|
|
2029
|
-
self.prompt =
|
|
2075
|
+
self.prompt = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
|
|
2030
2076
|
self.startup_commands = copy.copy(self.device.startup_commands)
|
|
2031
2077
|
log.msg(f"[{self.device}] My initialize commands: {self.startup_commands!r}")
|
|
2032
2078
|
self.initialized = False
|
|
@@ -2059,7 +2105,7 @@ class IoslikeSendExpect(protocol.Protocol, TimeoutMixin):
|
|
|
2059
2105
|
return
|
|
2060
2106
|
else:
|
|
2061
2107
|
# Or just use the matched regex object...
|
|
2062
|
-
prompt_idx = m
|
|
2108
|
+
prompt_idx = prompt_match_start(m)
|
|
2063
2109
|
|
|
2064
2110
|
result = self.data[:prompt_idx]
|
|
2065
2111
|
# Trim off the echoed-back command. This should *not* be necessary
|
|
@@ -37,6 +37,7 @@ from trigger.conf import settings
|
|
|
37
37
|
from trigger.twister import (
|
|
38
38
|
has_ioslike_error,
|
|
39
39
|
is_awaiting_confirmation,
|
|
40
|
+
prompt_match_start,
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
|
|
@@ -655,7 +656,7 @@ class IoslikeSendExpect(protocol.Protocol, TimeoutMixin):
|
|
|
655
656
|
return
|
|
656
657
|
else:
|
|
657
658
|
# Or just use the matched regex object...
|
|
658
|
-
prompt_idx = m
|
|
659
|
+
prompt_idx = prompt_match_start(m)
|
|
659
660
|
|
|
660
661
|
result = self.data[:prompt_idx]
|
|
661
662
|
# Trim off the echoed-back command. This should *not* be necessary
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trigger
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.3
|
|
4
4
|
Summary: Network automation toolkit for managing network devices
|
|
5
5
|
Author-email: Jathan McCollum <jathan@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -33,12 +33,14 @@ Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
|
33
33
|
Requires-Dist: pytest-twisted>=1.14.0; extra == "dev"
|
|
34
34
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
35
35
|
Requires-Dist: python-semantic-release>=9.0.0; extra == "dev"
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx_rtd_theme>=2.0; extra == "docs"
|
|
36
39
|
Dynamic: license-file
|
|
37
40
|
|
|
38
41
|
# What is Trigger?
|
|
39
42
|
|
|
40
|
-
[](https://gitter.im/trigger/trigger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
43
|
+
[](https://github.com/trigger/trigger/actions/workflows/ci.yml)
|
|
42
44
|
|
|
43
45
|
Trigger is a robust network automation toolkit written in Python that was
|
|
44
46
|
designed for interfacing with network devices and managing network
|
|
@@ -79,16 +81,9 @@ network devices with ease. Here are some of things that make Trigger tick:
|
|
|
79
81
|
+ Flexible access-list & firewall policy parser that can test access if access
|
|
80
82
|
is permitted, or easily convert ACLs from one format to another.
|
|
81
83
|
+ Detailed support for timezones and maintenance windows.
|
|
84
|
+
+ Import your metadata from an existing [RANCID](http://shrubbery.net/rancid/) installation or a CSV file to get up-and-running quickly.
|
|
82
85
|
+ A suite of tools for simplifying many common tasks.
|
|
83
86
|
|
|
84
|
-
New in version 1.2:
|
|
85
|
-
|
|
86
|
-
+ Import your metadata from an existing [RANCID](http://shrubbery.net/rancid/) installation to get up-and-running quickly!
|
|
87
|
-
|
|
88
|
-
New in version 1.3:
|
|
89
|
-
|
|
90
|
-
+ Import your metadata from a CSV file and get up-and-running even quicker!
|
|
91
|
-
|
|
92
87
|
## Getting Started
|
|
93
88
|
|
|
94
89
|
The best way to get started is to read the documentation hosted by [Read the
|
|
@@ -142,21 +137,13 @@ See the [Migration Guide](https://trigger.readthedocs.io/en/latest/migration.htm
|
|
|
142
137
|
|
|
143
138
|
### Before you begin
|
|
144
139
|
|
|
145
|
-
+ The [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
+ The [master](https://github.com/trigger/trigger/tree/master) branch is
|
|
150
|
-
the stable branch, and will reflect the latest production-ready changes. It
|
|
151
|
-
is recommended that this is the branch you use if you are installing Trigger
|
|
152
|
-
for the first time.
|
|
153
|
-
+ Each point release of Trigger is maintained as a [tag branch](https://github.com/trigger/trigger/tags). If you require a
|
|
140
|
+
+ The [main](https://github.com/trigger/trigger/tree/main) branch is the
|
|
141
|
+
primary branch for all development and releases. All pull requests target
|
|
142
|
+
`main`.
|
|
143
|
+
+ Each point release of Trigger is maintained as a [tag](https://github.com/trigger/trigger/tags). If you require a
|
|
154
144
|
specific Trigger version, please refer to these.
|
|
155
145
|
|
|
156
146
|
### Get in touch!
|
|
157
147
|
|
|
158
|
-
If you run into any snags, have questions, feedback, or just want to talk shop
|
|
159
|
-
[
|
|
160
|
-
|
|
161
|
-
**Pro tip**: Find us on IRC at `#trigger` on Freenode
|
|
162
|
-
([irc://irc.freenode.net/trigger](irc://irc.freenode.net/trigger)).
|
|
148
|
+
If you run into any snags, have questions, feedback, or just want to talk shop,
|
|
149
|
+
please open an issue on [GitHub Issues](https://github.com/trigger/trigger/issues).
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
from trigger.conf import settings
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_ioslike_prompt_pattern_enabled():
|
|
7
|
-
"""Test enabled that IOS-like prompt patterns match correctly."""
|
|
8
|
-
pat = settings.IOSLIKE_PROMPT_PAT
|
|
9
|
-
|
|
10
|
-
prompt_tests = [
|
|
11
|
-
"foo-bar1#",
|
|
12
|
-
"foo-bar1# ",
|
|
13
|
-
"foo-bar1(config)# ",
|
|
14
|
-
"\rfoo-bar01(config)# \x08 ", # "Bonus" backspace in there
|
|
15
|
-
"foo-bar01(config) \r#", # "Bonus" '\s\r' in there
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
for prompt in prompt_tests:
|
|
19
|
-
assert re.search(pat, prompt) is not None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_ioslike_prompt_pattern_nonenabled():
|
|
23
|
-
"""Test non-enabled that IOS-like prompt patterns match correctly."""
|
|
24
|
-
pat = settings.IOSLIKE_ENABLE_PAT
|
|
25
|
-
|
|
26
|
-
prompt_tests = [
|
|
27
|
-
"foo-bar1>",
|
|
28
|
-
"foo-bar1> ",
|
|
29
|
-
"\rfoo-bar01)> \x08 ", # "Bonus" backspace in there
|
|
30
|
-
"foo-bar01) \r>", # "Bonus" '\s\r' in there
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
for prompt in prompt_tests:
|
|
34
|
-
assert re.search(pat, prompt) is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|