trigger 2.2.5__tar.gz → 2.3.0__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 (78) hide show
  1. {trigger-2.2.5/trigger.egg-info → trigger-2.3.0}/PKG-INFO +2 -2
  2. {trigger-2.2.5 → trigger-2.3.0}/pyproject.toml +2 -2
  3. {trigger-2.2.5 → trigger-2.3.0}/tests/test_twister.py +187 -1
  4. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/ios.py +2 -2
  5. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/support.py +104 -58
  6. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/tools.py +5 -6
  7. {trigger-2.2.5 → trigger-2.3.0}/trigger/cmds.py +9 -7
  8. {trigger-2.2.5 → trigger-2.3.0}/trigger/conf/global_settings.py +4 -4
  9. {trigger-2.2.5 → trigger-2.3.0}/trigger/netscreen.py +5 -5
  10. {trigger-2.2.5 → trigger-2.3.0}/trigger/twister.py +22 -15
  11. {trigger-2.2.5 → trigger-2.3.0}/trigger/twister2.py +10 -6
  12. {trigger-2.2.5 → trigger-2.3.0/trigger.egg-info}/PKG-INFO +2 -2
  13. {trigger-2.2.5 → trigger-2.3.0}/trigger.egg-info/requires.txt +1 -1
  14. {trigger-2.2.5 → trigger-2.3.0}/AUTHORS.md +0 -0
  15. {trigger-2.2.5 → trigger-2.3.0}/LICENSE.md +0 -0
  16. {trigger-2.2.5 → trigger-2.3.0}/README.md +0 -0
  17. {trigger-2.2.5 → trigger-2.3.0}/setup.cfg +0 -0
  18. {trigger-2.2.5 → trigger-2.3.0}/tests/test_acl.py +0 -0
  19. {trigger-2.2.5 → trigger-2.3.0}/tests/test_acl_db.py +0 -0
  20. {trigger-2.2.5 → trigger-2.3.0}/tests/test_acl_queue.py +0 -0
  21. {trigger-2.2.5 → trigger-2.3.0}/tests/test_changemgmt.py +0 -0
  22. {trigger-2.2.5 → trigger-2.3.0}/tests/test_except.py +0 -0
  23. {trigger-2.2.5 → trigger-2.3.0}/tests/test_netdevices.py +0 -0
  24. {trigger-2.2.5 → trigger-2.3.0}/tests/test_scripts.py +0 -0
  25. {trigger-2.2.5 → trigger-2.3.0}/tests/test_tacacsrc.py +0 -0
  26. {trigger-2.2.5 → trigger-2.3.0}/tests/test_templates.py +0 -0
  27. {trigger-2.2.5 → trigger-2.3.0}/tests/test_twister2.py +0 -0
  28. {trigger-2.2.5 → trigger-2.3.0}/tests/test_utils.py +0 -0
  29. {trigger-2.2.5 → trigger-2.3.0}/trigger/__init__.py +0 -0
  30. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/__init__.py +0 -0
  31. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/autoacl.py +0 -0
  32. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/db.py +0 -0
  33. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/dicts.py +0 -0
  34. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/grammar.py +0 -0
  35. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/junos.py +0 -0
  36. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/models.py +0 -0
  37. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/parser.py +0 -0
  38. {trigger-2.2.5 → trigger-2.3.0}/trigger/acl/queue.py +0 -0
  39. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/__init__.py +0 -0
  40. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/acl.py +0 -0
  41. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/acl_script.py +0 -0
  42. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/aclconv.py +0 -0
  43. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/check_access.py +0 -0
  44. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/check_syntax.py +0 -0
  45. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/fe.py +0 -0
  46. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/find_access.py +0 -0
  47. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/gnng.py +0 -0
  48. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/gong.py +0 -0
  49. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/load_acl.py +0 -0
  50. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/load_config.py +0 -0
  51. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/netdev.py +0 -0
  52. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/optimizer.py +0 -0
  53. {trigger-2.2.5 → trigger-2.3.0}/trigger/bin/run_cmds.py +0 -0
  54. {trigger-2.2.5 → trigger-2.3.0}/trigger/changemgmt/__init__.py +0 -0
  55. {trigger-2.2.5 → trigger-2.3.0}/trigger/changemgmt/bounce.py +0 -0
  56. {trigger-2.2.5 → trigger-2.3.0}/trigger/conf/__init__.py +0 -0
  57. {trigger-2.2.5 → trigger-2.3.0}/trigger/contrib/__init__.py +0 -0
  58. {trigger-2.2.5 → trigger-2.3.0}/trigger/exceptions.py +0 -0
  59. {trigger-2.2.5 → trigger-2.3.0}/trigger/gorc.py +0 -0
  60. {trigger-2.2.5 → trigger-2.3.0}/trigger/netdevices/__init__.py +0 -0
  61. {trigger-2.2.5 → trigger-2.3.0}/trigger/netdevices/loader.py +0 -0
  62. {trigger-2.2.5 → trigger-2.3.0}/trigger/packages/__init__.py +0 -0
  63. {trigger-2.2.5 → trigger-2.3.0}/trigger/packages/peewee.py +0 -0
  64. {trigger-2.2.5 → trigger-2.3.0}/trigger/rancid.py +0 -0
  65. {trigger-2.2.5 → trigger-2.3.0}/trigger/tacacsrc.py +0 -0
  66. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/__init__.py +0 -0
  67. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/cli.py +0 -0
  68. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/importlib.py +0 -0
  69. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/network.py +0 -0
  70. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/rcs.py +0 -0
  71. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/templates.py +0 -0
  72. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/url.py +0 -0
  73. {trigger-2.2.5 → trigger-2.3.0}/trigger/utils/xmltodict.py +0 -0
  74. {trigger-2.2.5 → trigger-2.3.0}/trigger.egg-info/SOURCES.txt +0 -0
  75. {trigger-2.2.5 → trigger-2.3.0}/trigger.egg-info/dependency_links.txt +0 -0
  76. {trigger-2.2.5 → trigger-2.3.0}/trigger.egg-info/entry_points.txt +0 -0
  77. {trigger-2.2.5 → trigger-2.3.0}/trigger.egg-info/top_level.txt +0 -0
  78. {trigger-2.2.5 → trigger-2.3.0}/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.5
3
+ Version: 2.3.0
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
@@ -13,7 +13,7 @@ Requires-Python: <3.12,>=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE.md
15
15
  License-File: AUTHORS.md
16
- Requires-Dist: IPy>=1.01
16
+ Requires-Dist: netaddr<2,>=1.0.0
17
17
  Requires-Dist: cryptography>=41.0.0
18
18
  Requires-Dist: Twisted>=22.10.0
19
19
  Requires-Dist: crochet>=2.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trigger"
7
- version = "2.2.5"
7
+ version = "2.3.0"
8
8
  description = "Network automation toolkit for managing network devices"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Framework :: Twisted",
19
19
  ]
20
20
  dependencies = [
21
- "IPy>=1.01",
21
+ "netaddr>=1.0.0,<2",
22
22
  "cryptography>=41.0.0",
23
23
  "Twisted>=22.10.0",
24
24
  "crochet>=2.0.0",
@@ -1,9 +1,14 @@
1
1
  import re
2
+ from unittest.mock import MagicMock
2
3
 
3
4
  import pytest
4
5
 
5
6
  from trigger.conf import settings
6
- from trigger.twister import compile_prompt_pattern, prompt_match_start
7
+ from trigger.twister import (
8
+ compile_prompt_pattern,
9
+ is_awaiting_confirmation,
10
+ prompt_match_start,
11
+ )
7
12
 
8
13
 
9
14
  def test_ioslike_prompt_pattern_enabled():
@@ -253,3 +258,184 @@ def test_vendor_pattern_compiles_without_error(vendor):
253
258
  """Every vendor pattern in PROMPT_PATTERNS must compile successfully."""
254
259
  pat = compile_prompt_pattern(settings.PROMPT_PATTERNS[vendor])
255
260
  assert isinstance(pat, re.Pattern)
261
+
262
+
263
+ # =============================================================================
264
+ # is_awaiting_confirmation tests
265
+ # =============================================================================
266
+
267
+
268
+ class TestIsAwaitingConfirmation:
269
+ """Verify is_awaiting_confirmation detects all CONTINUE_PROMPTS patterns."""
270
+
271
+ @pytest.mark.parametrize(
272
+ "prompt",
273
+ [
274
+ "Do you want to continue?",
275
+ "Are you sure you want to proceed?",
276
+ "Save changes (y/n):",
277
+ "Confirm action [y/n]:",
278
+ "Press enter to [confirm]",
279
+ "Overwrite existing config [yes/no]: ",
280
+ "overwrite file [startup-config] ?[yes/press any key for no]....",
281
+ "Destination filename [running-config]? ",
282
+ ],
283
+ )
284
+ def test_detects_confirmation_prompts(self, prompt):
285
+ """Each CONTINUE_PROMPTS pattern should be detected."""
286
+ assert is_awaiting_confirmation(prompt) is True
287
+
288
+ @pytest.mark.parametrize(
289
+ "prompt",
290
+ [
291
+ "router1# ",
292
+ "show version",
293
+ "Building configuration...",
294
+ "interface GigabitEthernet0/0",
295
+ "",
296
+ ],
297
+ )
298
+ def test_rejects_non_confirmation_prompts(self, prompt):
299
+ """Normal command output should not be detected as confirmation."""
300
+ assert is_awaiting_confirmation(prompt) is False
301
+
302
+ def test_case_insensitive(self):
303
+ """Detection should be case-insensitive."""
304
+ assert is_awaiting_confirmation("CONTINUE?") is True
305
+ assert is_awaiting_confirmation("Proceed?") is True
306
+ assert is_awaiting_confirmation("(Y/N):") is True
307
+
308
+
309
+ # =============================================================================
310
+ # SSH channel confirmation auto-response tests (issue #91)
311
+ # =============================================================================
312
+
313
+
314
+ class TestSSHChannelConfirmationAutoResponse:
315
+ """Verify TriggerSSHChannelBase auto-responds to confirmation prompts."""
316
+
317
+ def _make_channel(self, delimiter="\n"):
318
+ """Create a minimal mock of TriggerSSHChannelBase for testing dataReceived."""
319
+ ch = MagicMock()
320
+ ch.data = ""
321
+ # Device must be an object with a delimiter attribute, not a string
322
+ ch.device = MagicMock()
323
+ ch.device.nodeName = "test-device1"
324
+ ch.device.delimiter = delimiter
325
+ ch.device.__str__ = lambda self: self.nodeName
326
+ ch.enabled = True
327
+ ch.initialized = True
328
+ ch.results = []
329
+ ch.with_errors = False
330
+ ch.command_interval = 0
331
+ ch.prompt = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
332
+ return ch
333
+
334
+ def test_confirmation_prompt_sends_delimiter(self):
335
+ """When a confirmation prompt is detected, write(device.delimiter) should be called."""
336
+ from trigger.twister import TriggerSSHChannelBase
337
+
338
+ ch = self._make_channel()
339
+
340
+ # Simulate receiving a confirmation prompt
341
+ ch.data = ""
342
+ incoming = "copy running-config startup-config\nDestination filename [running-config]? "
343
+ ch.data += incoming
344
+
345
+ # Call the real dataReceived logic
346
+ TriggerSSHChannelBase.dataReceived(ch, incoming)
347
+
348
+ ch.write.assert_called_once_with("\n")
349
+ ch.resetTimeout.assert_called_once()
350
+ assert ch.data == ""
351
+
352
+ def test_confirmation_prompt_uses_device_delimiter(self):
353
+ """Confirmation response should use the device-specific delimiter (e.g. \\r\\n for Force10)."""
354
+ from trigger.twister import TriggerSSHChannelBase
355
+
356
+ # Force10 devices use \r\n as delimiter
357
+ ch = self._make_channel(delimiter="\r\n")
358
+
359
+ incoming = "copy running-config startup-config\nDestination filename [running-config]? "
360
+ ch.data = incoming
361
+
362
+ TriggerSSHChannelBase.dataReceived(ch, incoming)
363
+
364
+ ch.write.assert_called_once_with("\r\n")
365
+
366
+ def test_confirmation_prompt_does_not_advance_commands(self):
367
+ """When a confirmation prompt is detected, results should not be appended."""
368
+ from trigger.twister import TriggerSSHChannelBase
369
+
370
+ ch = self._make_channel()
371
+
372
+ incoming = "Overwrite file [startup-config] ?[Yes/press any key for no]...."
373
+ ch.data = incoming
374
+
375
+ TriggerSSHChannelBase.dataReceived(ch, incoming)
376
+
377
+ # Should not have appended any results
378
+ assert ch.results == []
379
+
380
+
381
+ class TestTelnetConfirmationAutoResponse:
382
+ """Verify IoslikeSendExpect (telnet) auto-responds to confirmation prompts."""
383
+
384
+ def _make_protocol(self, delimiter="\n"):
385
+ """Create a minimal mock of IoslikeSendExpect for testing dataReceived."""
386
+ proto = MagicMock()
387
+ proto.data = ""
388
+ # Device must be an object with a delimiter attribute, not a string
389
+ proto.device = MagicMock()
390
+ proto.device.nodeName = "test-device1"
391
+ proto.device.delimiter = delimiter
392
+ proto.device.__str__ = lambda self: self.nodeName
393
+ proto.initialized = True
394
+ proto.results = []
395
+ proto.with_errors = False
396
+ proto.command_interval = 0
397
+ proto.prompt = compile_prompt_pattern(settings.IOSLIKE_PROMPT_PAT)
398
+ proto.factory = MagicMock()
399
+ return proto
400
+
401
+ def test_confirmation_prompt_sends_delimiter(self):
402
+ """Telnet channel should send device.delimiter on confirmation prompt."""
403
+ from trigger.twister import IoslikeSendExpect
404
+
405
+ proto = self._make_protocol()
406
+
407
+ incoming = "Save changes [y/n]:"
408
+ proto.data = incoming
409
+
410
+ IoslikeSendExpect.dataReceived(proto, incoming)
411
+
412
+ proto.write.assert_called_once_with("\n")
413
+ proto.resetTimeout.assert_called_once()
414
+ assert proto.data == ""
415
+
416
+ def test_confirmation_prompt_uses_device_delimiter(self):
417
+ """Telnet confirmation response should use device-specific delimiter (e.g. \\r\\n for Force10)."""
418
+ from trigger.twister import IoslikeSendExpect
419
+
420
+ # Force10 devices use \r\n as delimiter
421
+ proto = self._make_protocol(delimiter="\r\n")
422
+
423
+ incoming = "Save changes [y/n]:"
424
+ proto.data = incoming
425
+
426
+ IoslikeSendExpect.dataReceived(proto, incoming)
427
+
428
+ proto.write.assert_called_once_with("\r\n")
429
+
430
+ def test_confirmation_prompt_does_not_advance_commands(self):
431
+ """Telnet channel should not append results on confirmation prompt."""
432
+ from trigger.twister import IoslikeSendExpect
433
+
434
+ proto = self._make_protocol()
435
+
436
+ incoming = "Are you sure you want to proceed?"
437
+ proto.data = incoming
438
+
439
+ IoslikeSendExpect.dataReceived(proto, incoming)
440
+
441
+ assert proto.results == []
@@ -27,7 +27,7 @@ class Remark(Comment): # noqa: F405
27
27
  # Build a table to unwind Cisco's weird inverse netmask.
28
28
  # TODO (jathan): These don't actually get sorted properly, but it doesn't seem
29
29
  # to have mattered up until now. Worth looking into it at some point, though.
30
- inverse_mask_table = dict([(make_inverse_mask(x), x) for x in range(33)]) # noqa: F405
30
+ inverse_mask_table = {str(make_inverse_mask(x)): x for x in range(33)} # noqa: F405
31
31
 
32
32
 
33
33
  def handle_ios_match(a): # noqa: D103, PLR0912
@@ -129,7 +129,7 @@ rules.update( # noqa: F405
129
129
  ),
130
130
  "ipv4_inverse_mask": (
131
131
  literals(inverse_mask_table), # noqa: F405
132
- lambda x: inverse_mask_table[TIP(x)], # noqa: F405
132
+ lambda x: inverse_mask_table[x],
133
133
  ),
134
134
  "kw_ip": ('"ip"', None),
135
135
  S("ios_match"): ( # noqa: F405
@@ -29,15 +29,12 @@ support the various modules for parsing. This file is not meant to by used by it
29
29
  import contextlib
30
30
  from typing import ClassVar
31
31
 
32
- import IPy
32
+ import netaddr
33
33
 
34
34
  from trigger import exceptions
35
35
 
36
36
  from .dicts import * # noqa: F403
37
37
 
38
- # Python 2/3 compatibility
39
- unicode = str
40
-
41
38
  # Temporary resting place for comments, so the rest of the parser can
42
39
  # ignore them. Yes, this makes the library not thread-safe.
43
40
  Comments = []
@@ -130,13 +127,13 @@ def do_dscp_lookup(arg): # noqa: D103
130
127
 
131
128
 
132
129
  def make_inverse_mask(prefixlen):
133
- """Return an IP object of the inverse mask of the CIDR prefix.
130
+ """Return an IP address object of the inverse mask of the CIDR prefix.
134
131
 
135
132
  :param prefixlen:
136
133
  CIDR prefix
137
134
  """
138
135
  inverse_bits = 2 ** (32 - prefixlen) - 1
139
- return TIP(inverse_bits)
136
+ return netaddr.IPAddress(inverse_bits)
140
137
 
141
138
 
142
139
  def strip_comments(tags): # noqa: D103
@@ -431,12 +428,13 @@ class RangeList:
431
428
  return self.data.__iter__()
432
429
 
433
430
 
434
- class TIP(IPy.IP):
435
- """Class based on IPy.IP, but with extensions for Trigger.
431
+ class TIP(netaddr.IPNetwork):
432
+ """Class based on netaddr.IPNetwork, but with extensions for Trigger.
436
433
 
437
434
  Currently, only the only extension is the ability to negate a network
438
435
  block. Only used internally within the parser, as it's not complete
439
- (doesn't interact well with IPy.IP objects). Does not handle IPv6 yet.
436
+ (doesn't interact well with netaddr.IPNetwork objects). Does not handle
437
+ IPv6 yet.
440
438
  """
441
439
 
442
440
  def __init__(self, data, **kwargs): # noqa: D107
@@ -448,7 +446,7 @@ class TIP(IPy.IP):
448
446
  inactive = getattr(data, "inactive", False)
449
447
 
450
448
  # Is data a string?
451
- if isinstance(data, (str, unicode)):
449
+ if isinstance(data, str):
452
450
  d = data.split()
453
451
  # This means we got something like "1.2.3.4 except" or "inactive:
454
452
  # 1.2.3.4'
@@ -471,87 +469,101 @@ class TIP(IPy.IP):
471
469
 
472
470
  self.negated = negated # Set 'negated' variable
473
471
  self.inactive = inactive # Set 'inactive' variable
474
- IPy.IP.__init__(self, data, **kwargs)
475
472
 
476
- # Make it print prefixes for /32, /128 if we're negated or inactive (and
477
- # therefore assuming we're being used in a Juniper ACL.)
478
- if self.negated or self.inactive:
479
- self.NoPrefixForSingleIp = False
473
+ # Expand partial IPv4 addresses like "10/8" → "10.0.0.0/8"
474
+ if isinstance(data, str) and "/" in data:
475
+ parts = data.split("/")
476
+ addr_part = parts[0]
477
+ # Only apply to IPv4 (no colons in address part)
478
+ if ":" not in addr_part:
479
+ octets = addr_part.split(".")
480
+ while len(octets) < 4: # noqa: PLR2004
481
+ octets.append("0")
482
+ data = ".".join(octets) + "/" + parts[1]
483
+
484
+ super().__init__(data, **kwargs)
480
485
 
481
486
  def _compare_to(self, other):
482
487
  """Helper method for comparison. Returns -1, 0, or 1.""" # noqa: D401
483
- # Regular IPy sorts by prefix length before network base, but Juniper
484
- # (our baseline) does not. We also need comparisons to be different for
485
- # negation. Following Juniper's sorting, use IP compare, and then break
488
+ # Following Juniper's sorting, use IP compare, and then break
486
489
  # ties where negated < not negated.
487
- # Python 3: Implement cmp() logic inline
488
- if self.ip < other.ip:
490
+ self_first = self.first
491
+ if hasattr(other, "first"):
492
+ other_first = other.first
493
+ else:
494
+ return NotImplemented
495
+ if self_first < other_first:
489
496
  diff = -1
490
- elif self.ip > other.ip:
497
+ elif self_first > other_first:
491
498
  diff = 1
492
499
  else:
493
500
  diff = 0
494
501
 
495
502
  if diff == 0:
496
503
  # If the same IP, compare by prefixlen
497
- if self.prefixlen() < other.prefixlen():
504
+ if self.prefixlen < other.prefixlen:
498
505
  diff = -1
499
- elif self.prefixlen() > other.prefixlen():
506
+ elif self.prefixlen > other.prefixlen:
500
507
  diff = 1
501
508
  else:
502
509
  diff = 0
503
510
 
504
511
  # If both negated, they're the same
505
- if self.negated == other.negated:
512
+ other_negated = getattr(other, "negated", False)
513
+ if self.negated == other_negated:
506
514
  return diff
507
515
  # Sort to make negated < not negated
508
516
  return -1 if self.negated else 1
509
- # Return the base comparison
510
517
 
511
518
  def __lt__(self, other): # noqa: D105
512
- return self._compare_to(other) < 0
519
+ result = self._compare_to(other)
520
+ if result is NotImplemented:
521
+ return NotImplemented
522
+ return result < 0
513
523
 
514
524
  def __le__(self, other): # noqa: D105
515
- return self._compare_to(other) <= 0
525
+ result = self._compare_to(other)
526
+ if result is NotImplemented:
527
+ return NotImplemented
528
+ return result <= 0
516
529
 
517
530
  def __gt__(self, other): # noqa: D105
518
- return self._compare_to(other) > 0
531
+ result = self._compare_to(other)
532
+ if result is NotImplemented:
533
+ return NotImplemented
534
+ return result > 0
519
535
 
520
536
  def __ge__(self, other): # noqa: D105
521
- return self._compare_to(other) >= 0
537
+ result = self._compare_to(other)
538
+ if result is NotImplemented:
539
+ return NotImplemented
540
+ return result >= 0
522
541
 
523
542
  def __eq__(self, other): # noqa: D105
524
- return self._compare_to(other) == 0
543
+ result = self._compare_to(other)
544
+ if result is NotImplemented:
545
+ return NotImplemented
546
+ return result == 0
525
547
 
526
548
  def __ne__(self, other): # noqa: D105
527
- return self._compare_to(other) != 0
549
+ result = self._compare_to(other)
550
+ if result is NotImplemented:
551
+ return NotImplemented
552
+ return result != 0
528
553
 
529
554
  def __hash__(self): # noqa: D105
530
- # Make TIP hashable for use in sets and as dict keys
531
- # Base hash on IP address, prefix length, and negation status
532
- return hash((str(self.ip), self.prefixlen(), self.negated, self.inactive))
555
+ return hash((self.first, self.prefixlen, self.negated, self.inactive))
533
556
 
534
557
  def __repr__(self): # noqa: D105
535
- # Just stick an 'except' at the end if except is set since we don't
536
- # code to accept this in the constructor really just provided, for now,
537
- # as a debugging aid.
538
- rs = IPy.IP.__repr__(self)
539
- if self.negated:
540
- # Insert ' except' into the repr. (Yes, it's a hack!)
541
- rs = rs.split("'")
542
- rs[1] += " except"
543
- rs = "'".join(rs) # Restore original repr
544
- if self.inactive:
545
- # Insert 'inactive: ' into the repr. (Yes, it's also a hack!)
546
- rs = rs.split("'")
547
- rs[1] = "inactive: " + rs[1]
548
- rs = "'".join(rs) # Restore original repr
549
- return rs
558
+ return f"TIP('{self!s}')"
550
559
 
551
560
  def __str__(self): # noqa: D105
552
- # IPy is not a new-style class, so the following doesn't work:
553
- # return super(TIP, self).__str__() # noqa: ERA001
554
- rs = IPy.IP.__str__(self)
561
+ # Show prefix for all networks, but omit for single hosts (/32, /128)
562
+ # unless negated or inactive (Juniper ACL style needs prefix)
563
+ if self.prefixlen in (32, 128) and not self.negated and not self.inactive:
564
+ rs = str(self.ip)
565
+ else:
566
+ rs = f"{self.network}/{self.prefixlen}"
555
567
  if self.negated:
556
568
  rs += " except"
557
569
  if self.inactive:
@@ -566,9 +578,39 @@ class TIP(IPy.IP):
566
578
  # If one item is negated, it's never contained.
567
579
  if xor:
568
580
  return False
569
- matched = IPy.IP.__contains__(self, item)
581
+ matched = super().__contains__(item)
570
582
  return matched ^ self.negated
571
583
 
584
+ # Prevent netaddr's iteration/subscript protocol from interfering with
585
+ # RangeList, which would otherwise treat TIP as a sequence of integers.
586
+ def __iter__(self): # noqa: D105
587
+ msg = f"'{type(self).__name__}' object is not iterable"
588
+ raise TypeError(msg)
589
+
590
+ def __getitem__(self, index): # noqa: D105
591
+ msg = f"'{type(self).__name__}' object is not subscriptable"
592
+ raise TypeError(msg)
593
+
594
+ def __len__(self): # noqa: D105
595
+ msg = f"'{type(self).__name__}' object has no len()"
596
+ raise TypeError(msg)
597
+
598
+ # Compatibility methods for code that uses IPy-style API
599
+ def net(self):
600
+ """Return the network address as an IPAddress object."""
601
+ return self.network
602
+
603
+ def strNormal(self, mode=0):
604
+ """Return string representation compatible with IPy's strNormal.
605
+
606
+ :param mode:
607
+ 0 = address without prefix for host, with prefix for network
608
+ 1 = address with prefix always
609
+ """
610
+ if mode == 0 and self.prefixlen in (32, 128):
611
+ return str(self.ip)
612
+ return f"{self.network}/{self.prefixlen}"
613
+
572
614
 
573
615
  class Comment:
574
616
  """Container for inline comments."""
@@ -1246,8 +1288,12 @@ class Matches(MyDict):
1246
1288
  return "%s-%s" % pair # noqa: UP031 # Tuples back to ranges.
1247
1289
  except TypeError:
1248
1290
  with contextlib.suppress(AttributeError):
1249
- # Make it print prefixes for /32, /128
1250
- pair.NoPrefixForSingleIp = False
1291
+ # Force prefix display for /32 and /128 in JunOS output
1292
+ if hasattr(pair, "prefixlen") and pair.prefixlen in (32, 128):
1293
+ result = f"{pair.network}/{pair.prefixlen}"
1294
+ if getattr(pair, "negated", False):
1295
+ result += " except"
1296
+ return result
1251
1297
  return str(pair)
1252
1298
 
1253
1299
  def ios_port_str(self, ports):
@@ -1286,12 +1332,12 @@ class Matches(MyDict):
1286
1332
  raise exceptions.VendorSupportLacking(
1287
1333
  msg,
1288
1334
  )
1289
- if addr.prefixlen() == 0:
1335
+ if addr.prefixlen == 0:
1290
1336
  a.append("any")
1291
- elif addr.prefixlen() == 32: # noqa: PLR2004
1337
+ elif addr.prefixlen == 32: # noqa: PLR2004
1292
1338
  a.append(f"host {addr.net()}")
1293
1339
  else:
1294
- inverse_mask = make_inverse_mask(addr.prefixlen())
1340
+ inverse_mask = make_inverse_mask(addr.prefixlen)
1295
1341
  a.append(f"{addr.net()} {inverse_mask}")
1296
1342
  return a
1297
1343
 
@@ -9,9 +9,8 @@ import tempfile
9
9
  from collections import defaultdict
10
10
  from pathlib import Path
11
11
 
12
- import IPy
13
-
14
12
  from trigger.acl.parser import * # noqa: F403
13
+ from trigger.acl.support import TIP
15
14
  from trigger.conf import settings
16
15
 
17
16
  # Defaults
@@ -667,10 +666,10 @@ class ACLScript:
667
666
  def _add_addr(self, to, src):
668
667
  if isinstance(src, list):
669
668
  for x in src:
670
- if IPy.IP(x) not in to:
671
- to.append(IPy.IP(x))
672
- elif IPy.IP(src) not in to:
673
- to.append(IPy.IP(src))
669
+ if TIP(x) not in to:
670
+ to.append(TIP(x))
671
+ elif TIP(src) not in to:
672
+ to.append(TIP(src))
674
673
 
675
674
  def _add_port(self, to, src):
676
675
  if isinstance(src, list):
@@ -11,11 +11,12 @@ import collections
11
11
  import itertools
12
12
  from xml.etree.ElementTree import Element, SubElement
13
13
 
14
- from IPy import IP
14
+ from netaddr import IPNetwork as IP
15
15
  from twisted.internet import defer, task
16
16
  from twisted.python import log
17
17
 
18
18
  from trigger import exceptions
19
+ from trigger.acl.support import TIP
19
20
  from trigger.conf import settings
20
21
  from trigger.netdevices import NetDevices
21
22
  from trigger.utils.templates import (
@@ -808,12 +809,13 @@ class NetACLInfo(Commando):
808
809
  super().__init__(**args)
809
810
 
810
811
  def IPsubnet(self, addr):
811
- """Given '172.20.1.4/24', return IP('172.20.1.0/24')."""
812
- return IP(addr, make_net=True)
812
+ """Given '172.20.1.4/24', return TIP('172.20.1.0/24')."""
813
+ net = IP(addr)
814
+ return TIP(f"{net.network}/{net.prefixlen}")
813
815
 
814
816
  def IPhost(self, addr):
815
- """Given '172.20.1.4/24', return IP('172.20.1.4/32')."""
816
- return IP(addr[: addr.index("/")]) # Only keep before "/"
817
+ """Given '172.20.1.4/24', return TIP('172.20.1.4/32')."""
818
+ return TIP(addr[: addr.index("/")]) # Only keep before "/"
817
819
 
818
820
  # =======================================
819
821
  # Vendor-specific generate (to_)/parse (from_) methods
@@ -1165,14 +1167,14 @@ def _make_ipy(nets):
1165
1167
  """Given a list of 2-tuples of (address, netmask), returns a list of
1166
1168
  IP address objects.
1167
1169
  """ # noqa: D205
1168
- return [IP(addr) for addr, mask in nets]
1170
+ return [TIP(addr) for addr, mask in nets]
1169
1171
 
1170
1172
 
1171
1173
  def _make_cidrs(nets):
1172
1174
  """Given a list of 2-tuples of (address, netmask), returns a list CIDR
1173
1175
  blocks.
1174
1176
  """ # noqa: D205
1175
- return [IP(addr).make_net(mask) for addr, mask in nets]
1177
+ return [TIP(IP(f"{addr}/{mask}").cidr) for addr, mask in nets]
1176
1178
 
1177
1179
 
1178
1180
  def _dump_interfaces(idict):
@@ -9,7 +9,7 @@ import os
9
9
  import socket
10
10
  from pathlib import Path
11
11
 
12
- import IPy
12
+ import netaddr
13
13
 
14
14
  # ===============================
15
15
  # Global Settings
@@ -67,9 +67,9 @@ TFTP_HOST = ""
67
67
  # Add internally owned networks here. All network blocks owned/operated and
68
68
  # considered part of your network should be included.
69
69
  INTERNAL_NETWORKS = [
70
- IPy.IP("10.0.0.0/8"),
71
- IPy.IP("172.16.0.0/12"),
72
- IPy.IP("192.168.0.0/16"),
70
+ netaddr.IPNetwork("10.0.0.0/8"),
71
+ netaddr.IPNetwork("172.16.0.0/12"),
72
+ netaddr.IPNetwork("192.168.0.0/16"),
73
73
  ]
74
74
 
75
75
  # A dictionary keyed by manufacturer name containing a list of the device types
@@ -3,7 +3,7 @@ Broken apart from acl.parser because the approaches are vastly different from ea
3
3
  other.
4
4
  """ # noqa: D205
5
5
 
6
- import IPy
6
+ import netaddr
7
7
 
8
8
  from trigger import exceptions
9
9
  from trigger.acl.parser import (
@@ -555,11 +555,11 @@ class NSAddressBook(NetScreen):
555
555
  self.any = NSAddress(name="ANY")
556
556
 
557
557
  def find(self, address, zone): # noqa: D102
558
- if not self.entries.has_key(zone):
558
+ if zone not in self.entries:
559
559
  return None
560
560
 
561
561
  for nsaddr in self.entries[zone]:
562
- if isinstance(address, IPy.IP):
562
+ if isinstance(address, netaddr.IPNetwork):
563
563
  if nsaddr.addr == address:
564
564
  return nsaddr
565
565
  elif isinstance(address, str):
@@ -646,7 +646,7 @@ class NSAddress(NetScreen):
646
646
  self.zone,
647
647
  self.name,
648
648
  self.addr.strNormal(0),
649
- self.addr.netmask(),
649
+ str(self.addr.netmask),
650
650
  self.comment,
651
651
  )
652
652
  return [output]
@@ -808,7 +808,7 @@ class NSPolicy(NetScreen):
808
808
  addr = TIP(address)
809
809
  found = address_book.find(addr, zone)
810
810
  if not found:
811
- if addr.prefixlen() == 32: # noqa: PLR2004
811
+ if addr.prefixlen == 32: # noqa: PLR2004
812
812
  name = f"h{addr.strNormal(0)}"
813
813
  else:
814
814
  name = f"n{addr.strNormal()}"
@@ -1495,18 +1495,21 @@ class TriggerSSHChannelBase(channel.SSHChannel, TimeoutMixin):
1495
1495
  send_enable(self)
1496
1496
  return
1497
1497
 
1498
- # Check for confirmation prompts
1499
- # If the prompt confirms set the index to the matched bytes
1498
+ # Check for confirmation prompts and auto-confirm with Enter
1500
1499
  if is_awaiting_confirmation(self.data):
1501
1500
  log.msg(f"[{self.device}] Got confirmation prompt: {self.data!r}")
1502
- prompt_idx = self.data.find(bytes)
1503
- else:
1501
+ log.msg(f"[{self.device}] Sending confirmation response")
1502
+ self.write(self.device.delimiter)
1503
+ self.data = ""
1504
+ self.resetTimeout()
1504
1505
  return
1505
- else:
1506
- # Or just use the matched regex object...
1507
- log.msg(f"[{self.device}] STATE: buffer {self.data!r}")
1508
- log.msg(f"[{self.device}] STATE: prompt {m.group()!r}")
1509
- prompt_idx = prompt_match_start(m)
1506
+
1507
+ return
1508
+
1509
+ # Or just use the matched regex object...
1510
+ log.msg(f"[{self.device}] STATE: buffer {self.data!r}")
1511
+ log.msg(f"[{self.device}] STATE: prompt {m.group()!r}")
1512
+ prompt_idx = prompt_match_start(m)
1510
1513
 
1511
1514
  # Strip the prompt from the match result
1512
1515
  result = self.data[:prompt_idx] # Cut the prompt out
@@ -2092,15 +2095,19 @@ class IoslikeSendExpect(protocol.Protocol, TimeoutMixin):
2092
2095
  # None
2093
2096
  m = self.prompt.search(self.data)
2094
2097
  if not m:
2095
- # If the prompt confirms set the index to the matched bytes,
2098
+ # Check for confirmation prompts and auto-confirm with Enter
2096
2099
  if is_awaiting_confirmation(self.data):
2097
2100
  log.msg(f"[{self.device}] Got confirmation prompt: {self.data!r}")
2098
- prompt_idx = self.data.find(bytes)
2099
- else:
2101
+ log.msg(f"[{self.device}] Sending confirmation response")
2102
+ self.write(self.device.delimiter)
2103
+ self.data = ""
2104
+ self.resetTimeout()
2100
2105
  return
2101
- else:
2102
- # Or just use the matched regex object...
2103
- prompt_idx = prompt_match_start(m)
2106
+
2107
+ return
2108
+
2109
+ # Or just use the matched regex object...
2110
+ prompt_idx = prompt_match_start(m)
2104
2111
 
2105
2112
  result = self.data[:prompt_idx]
2106
2113
  # Trim off the echoed-back command. This should *not* be necessary
@@ -648,15 +648,19 @@ class IoslikeSendExpect(protocol.Protocol, TimeoutMixin):
648
648
  # None
649
649
  m = self.prompt.search(self.data)
650
650
  if not m:
651
- # If the prompt confirms set the index to the matched bytes,
651
+ # Check for confirmation prompts and auto-confirm with Enter
652
652
  if is_awaiting_confirmation(self.data):
653
653
  log.msg(f"[{self.device}] Got confirmation prompt: {self.data!r}")
654
- prompt_idx = self.data.find(bytes)
655
- else:
654
+ log.msg(f"[{self.device}] Sending confirmation response")
655
+ self.transport.write(self.device.delimiter)
656
+ self.data = ""
657
+ self.resetTimeout()
656
658
  return
657
- else:
658
- # Or just use the matched regex object...
659
- prompt_idx = prompt_match_start(m)
659
+
660
+ return
661
+
662
+ # Or just use the matched regex object...
663
+ prompt_idx = prompt_match_start(m)
660
664
 
661
665
  result = self.data[:prompt_idx]
662
666
  # 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.5
3
+ Version: 2.3.0
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
@@ -13,7 +13,7 @@ Requires-Python: <3.12,>=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE.md
15
15
  License-File: AUTHORS.md
16
- Requires-Dist: IPy>=1.01
16
+ Requires-Dist: netaddr<2,>=1.0.0
17
17
  Requires-Dist: cryptography>=41.0.0
18
18
  Requires-Dist: Twisted>=22.10.0
19
19
  Requires-Dist: crochet>=2.0.0
@@ -1,4 +1,4 @@
1
- IPy>=1.01
1
+ netaddr<2,>=1.0.0
2
2
  cryptography>=41.0.0
3
3
  Twisted>=22.10.0
4
4
  crochet>=2.0.0
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