trigger 2.2.2__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.
Files changed (79) hide show
  1. {trigger-2.2.2/trigger.egg-info → trigger-2.2.3}/PKG-INFO +12 -25
  2. {trigger-2.2.2 → trigger-2.2.3}/README.md +8 -24
  3. {trigger-2.2.2 → trigger-2.2.3}/pyproject.toml +5 -1
  4. trigger-2.2.3/tests/test_twister.py +255 -0
  5. {trigger-2.2.2 → trigger-2.2.3}/trigger/netdevices/__init__.py +6 -2
  6. {trigger-2.2.2 → trigger-2.2.3}/trigger/twister.py +54 -8
  7. {trigger-2.2.2 → trigger-2.2.3}/trigger/twister2.py +2 -1
  8. {trigger-2.2.2 → trigger-2.2.3/trigger.egg-info}/PKG-INFO +12 -25
  9. {trigger-2.2.2 → trigger-2.2.3}/trigger.egg-info/requires.txt +4 -0
  10. trigger-2.2.2/tests/test_twister.py +0 -34
  11. {trigger-2.2.2 → trigger-2.2.3}/AUTHORS.md +0 -0
  12. {trigger-2.2.2 → trigger-2.2.3}/LICENSE.md +0 -0
  13. {trigger-2.2.2 → trigger-2.2.3}/setup.cfg +0 -0
  14. {trigger-2.2.2 → trigger-2.2.3}/tests/test_acl.py +0 -0
  15. {trigger-2.2.2 → trigger-2.2.3}/tests/test_acl_db.py +0 -0
  16. {trigger-2.2.2 → trigger-2.2.3}/tests/test_acl_queue.py +0 -0
  17. {trigger-2.2.2 → trigger-2.2.3}/tests/test_changemgmt.py +0 -0
  18. {trigger-2.2.2 → trigger-2.2.3}/tests/test_except.py +0 -0
  19. {trigger-2.2.2 → trigger-2.2.3}/tests/test_netdevices.py +0 -0
  20. {trigger-2.2.2 → trigger-2.2.3}/tests/test_scripts.py +0 -0
  21. {trigger-2.2.2 → trigger-2.2.3}/tests/test_tacacsrc.py +0 -0
  22. {trigger-2.2.2 → trigger-2.2.3}/tests/test_templates.py +0 -0
  23. {trigger-2.2.2 → trigger-2.2.3}/tests/test_twister2.py +0 -0
  24. {trigger-2.2.2 → trigger-2.2.3}/tests/test_utils.py +0 -0
  25. {trigger-2.2.2 → trigger-2.2.3}/trigger/__init__.py +0 -0
  26. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/__init__.py +0 -0
  27. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/autoacl.py +0 -0
  28. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/db.py +0 -0
  29. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/dicts.py +0 -0
  30. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/grammar.py +0 -0
  31. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/ios.py +0 -0
  32. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/junos.py +0 -0
  33. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/models.py +0 -0
  34. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/parser.py +0 -0
  35. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/queue.py +0 -0
  36. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/support.py +0 -0
  37. {trigger-2.2.2 → trigger-2.2.3}/trigger/acl/tools.py +0 -0
  38. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/__init__.py +0 -0
  39. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/acl.py +0 -0
  40. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/acl_script.py +0 -0
  41. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/aclconv.py +0 -0
  42. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/check_access.py +0 -0
  43. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/check_syntax.py +0 -0
  44. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/fe.py +0 -0
  45. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/find_access.py +0 -0
  46. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/gnng.py +0 -0
  47. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/gong.py +0 -0
  48. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/load_acl.py +0 -0
  49. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/load_config.py +0 -0
  50. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/netdev.py +0 -0
  51. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/optimizer.py +0 -0
  52. {trigger-2.2.2 → trigger-2.2.3}/trigger/bin/run_cmds.py +0 -0
  53. {trigger-2.2.2 → trigger-2.2.3}/trigger/changemgmt/__init__.py +0 -0
  54. {trigger-2.2.2 → trigger-2.2.3}/trigger/changemgmt/bounce.py +0 -0
  55. {trigger-2.2.2 → trigger-2.2.3}/trigger/cmds.py +0 -0
  56. {trigger-2.2.2 → trigger-2.2.3}/trigger/conf/__init__.py +0 -0
  57. {trigger-2.2.2 → trigger-2.2.3}/trigger/conf/global_settings.py +0 -0
  58. {trigger-2.2.2 → trigger-2.2.3}/trigger/contrib/__init__.py +0 -0
  59. {trigger-2.2.2 → trigger-2.2.3}/trigger/exceptions.py +0 -0
  60. {trigger-2.2.2 → trigger-2.2.3}/trigger/gorc.py +0 -0
  61. {trigger-2.2.2 → trigger-2.2.3}/trigger/netdevices/loader.py +0 -0
  62. {trigger-2.2.2 → trigger-2.2.3}/trigger/netscreen.py +0 -0
  63. {trigger-2.2.2 → trigger-2.2.3}/trigger/packages/__init__.py +0 -0
  64. {trigger-2.2.2 → trigger-2.2.3}/trigger/packages/peewee.py +0 -0
  65. {trigger-2.2.2 → trigger-2.2.3}/trigger/rancid.py +0 -0
  66. {trigger-2.2.2 → trigger-2.2.3}/trigger/tacacsrc.py +0 -0
  67. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/__init__.py +0 -0
  68. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/cli.py +0 -0
  69. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/importlib.py +0 -0
  70. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/network.py +0 -0
  71. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/rcs.py +0 -0
  72. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/templates.py +0 -0
  73. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/url.py +0 -0
  74. {trigger-2.2.2 → trigger-2.2.3}/trigger/utils/xmltodict.py +0 -0
  75. {trigger-2.2.2 → trigger-2.2.3}/trigger.egg-info/SOURCES.txt +0 -0
  76. {trigger-2.2.2 → trigger-2.2.3}/trigger.egg-info/dependency_links.txt +0 -0
  77. {trigger-2.2.2 → trigger-2.2.3}/trigger.egg-info/entry_points.txt +0 -0
  78. {trigger-2.2.2 → trigger-2.2.3}/trigger.egg-info/top_level.txt +0 -0
  79. {trigger-2.2.2 → 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.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
- [![Tests](https://github.com/trigger/trigger/workflows/Tests/badge.svg)](https://github.com/trigger/trigger/actions)
41
- [![Join the chat at https://gitter.im/trigger/trigger](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/trigger/trigger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
43
+ [![CI](https://github.com/trigger/trigger/workflows/CI/badge.svg)](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 [develop](https://github.com/trigger/trigger/tree/develop) branch is
146
- the default branch that will be active when you clone this repository. While
147
- it is generally stable this branch is not considered production-ready. Use at
148
- your own risk!
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
- [contact us](https://trigger.readthedocs.io/en/latest/#getting-help)!
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
- [![Tests](https://github.com/trigger/trigger/workflows/Tests/badge.svg)](https://github.com/trigger/trigger/actions)
4
- [![Join the chat at https://gitter.im/trigger/trigger](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/trigger/trigger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
3
+ [![CI](https://github.com/trigger/trigger/workflows/CI/badge.svg)](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 [develop](https://github.com/trigger/trigger/tree/develop) branch is
109
- the default branch that will be active when you clone this repository. While
110
- it is generally stable this branch is not considered production-ready. Use at
111
- your own risk!
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
- [contact us](https://trigger.readthedocs.io/en/latest/#getting-help)!
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.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
- prompt = re.compile(settings.IOSLIKE_PROMPT_PAT)
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
- prompt = re.compile(settings.IOSLIKE_PROMPT_PAT)
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 = re.compile(prompt_pattern)
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 = re.compile(settings.IOSLIKE_ENABLE_PAT)
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 = re.compile(self.device.vendor.prompt_pattern)
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 = re.compile(settings.IOSLIKE_ENABLE_PAT)
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.start()
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.start()] # Strip ' Done\n' from results.
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 = re.compile(settings.IOSLIKE_PROMPT_PAT)
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.start()
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.start()
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.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
- [![Tests](https://github.com/trigger/trigger/workflows/Tests/badge.svg)](https://github.com/trigger/trigger/actions)
41
- [![Join the chat at https://gitter.im/trigger/trigger](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/trigger/trigger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
43
+ [![CI](https://github.com/trigger/trigger/workflows/CI/badge.svg)](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 [develop](https://github.com/trigger/trigger/tree/develop) branch is
146
- the default branch that will be active when you clone this repository. While
147
- it is generally stable this branch is not considered production-ready. Use at
148
- your own risk!
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
- [contact us](https://trigger.readthedocs.io/en/latest/#getting-help)!
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).
@@ -19,3 +19,7 @@ pytest>=7.4.0
19
19
  pytest-twisted>=1.14.0
20
20
  ruff>=0.1.0
21
21
  python-semantic-release>=9.0.0
22
+
23
+ [docs]
24
+ sphinx>=7.0
25
+ sphinx_rtd_theme>=2.0
@@ -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