trigger 2.2.4__tar.gz → 2.2.6__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.4/trigger.egg-info → trigger-2.2.6}/PKG-INFO +1 -1
  2. {trigger-2.2.4 → trigger-2.2.6}/pyproject.toml +1 -1
  3. {trigger-2.2.4 → trigger-2.2.6}/tests/test_twister.py +187 -1
  4. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/acl.py +26 -2
  5. {trigger-2.2.4 → trigger-2.2.6}/trigger/twister.py +22 -15
  6. {trigger-2.2.4 → trigger-2.2.6}/trigger/twister2.py +10 -6
  7. {trigger-2.2.4 → trigger-2.2.6/trigger.egg-info}/PKG-INFO +1 -1
  8. {trigger-2.2.4 → trigger-2.2.6}/AUTHORS.md +0 -0
  9. {trigger-2.2.4 → trigger-2.2.6}/LICENSE.md +0 -0
  10. {trigger-2.2.4 → trigger-2.2.6}/README.md +0 -0
  11. {trigger-2.2.4 → trigger-2.2.6}/setup.cfg +0 -0
  12. {trigger-2.2.4 → trigger-2.2.6}/tests/test_acl.py +0 -0
  13. {trigger-2.2.4 → trigger-2.2.6}/tests/test_acl_db.py +0 -0
  14. {trigger-2.2.4 → trigger-2.2.6}/tests/test_acl_queue.py +0 -0
  15. {trigger-2.2.4 → trigger-2.2.6}/tests/test_changemgmt.py +0 -0
  16. {trigger-2.2.4 → trigger-2.2.6}/tests/test_except.py +0 -0
  17. {trigger-2.2.4 → trigger-2.2.6}/tests/test_netdevices.py +0 -0
  18. {trigger-2.2.4 → trigger-2.2.6}/tests/test_scripts.py +0 -0
  19. {trigger-2.2.4 → trigger-2.2.6}/tests/test_tacacsrc.py +0 -0
  20. {trigger-2.2.4 → trigger-2.2.6}/tests/test_templates.py +0 -0
  21. {trigger-2.2.4 → trigger-2.2.6}/tests/test_twister2.py +0 -0
  22. {trigger-2.2.4 → trigger-2.2.6}/tests/test_utils.py +0 -0
  23. {trigger-2.2.4 → trigger-2.2.6}/trigger/__init__.py +0 -0
  24. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/__init__.py +0 -0
  25. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/autoacl.py +0 -0
  26. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/db.py +0 -0
  27. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/dicts.py +0 -0
  28. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/grammar.py +0 -0
  29. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/ios.py +0 -0
  30. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/junos.py +0 -0
  31. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/models.py +0 -0
  32. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/parser.py +0 -0
  33. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/queue.py +0 -0
  34. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/support.py +0 -0
  35. {trigger-2.2.4 → trigger-2.2.6}/trigger/acl/tools.py +0 -0
  36. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/__init__.py +0 -0
  37. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/acl_script.py +0 -0
  38. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/aclconv.py +0 -0
  39. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/check_access.py +0 -0
  40. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/check_syntax.py +0 -0
  41. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/fe.py +0 -0
  42. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/find_access.py +0 -0
  43. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/gnng.py +0 -0
  44. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/gong.py +0 -0
  45. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/load_acl.py +0 -0
  46. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/load_config.py +0 -0
  47. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/netdev.py +0 -0
  48. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/optimizer.py +0 -0
  49. {trigger-2.2.4 → trigger-2.2.6}/trigger/bin/run_cmds.py +0 -0
  50. {trigger-2.2.4 → trigger-2.2.6}/trigger/changemgmt/__init__.py +0 -0
  51. {trigger-2.2.4 → trigger-2.2.6}/trigger/changemgmt/bounce.py +0 -0
  52. {trigger-2.2.4 → trigger-2.2.6}/trigger/cmds.py +0 -0
  53. {trigger-2.2.4 → trigger-2.2.6}/trigger/conf/__init__.py +0 -0
  54. {trigger-2.2.4 → trigger-2.2.6}/trigger/conf/global_settings.py +0 -0
  55. {trigger-2.2.4 → trigger-2.2.6}/trigger/contrib/__init__.py +0 -0
  56. {trigger-2.2.4 → trigger-2.2.6}/trigger/exceptions.py +0 -0
  57. {trigger-2.2.4 → trigger-2.2.6}/trigger/gorc.py +0 -0
  58. {trigger-2.2.4 → trigger-2.2.6}/trigger/netdevices/__init__.py +0 -0
  59. {trigger-2.2.4 → trigger-2.2.6}/trigger/netdevices/loader.py +0 -0
  60. {trigger-2.2.4 → trigger-2.2.6}/trigger/netscreen.py +0 -0
  61. {trigger-2.2.4 → trigger-2.2.6}/trigger/packages/__init__.py +0 -0
  62. {trigger-2.2.4 → trigger-2.2.6}/trigger/packages/peewee.py +0 -0
  63. {trigger-2.2.4 → trigger-2.2.6}/trigger/rancid.py +0 -0
  64. {trigger-2.2.4 → trigger-2.2.6}/trigger/tacacsrc.py +0 -0
  65. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/__init__.py +0 -0
  66. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/cli.py +0 -0
  67. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/importlib.py +0 -0
  68. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/network.py +0 -0
  69. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/rcs.py +0 -0
  70. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/templates.py +0 -0
  71. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/url.py +0 -0
  72. {trigger-2.2.4 → trigger-2.2.6}/trigger/utils/xmltodict.py +0 -0
  73. {trigger-2.2.4 → trigger-2.2.6}/trigger.egg-info/SOURCES.txt +0 -0
  74. {trigger-2.2.4 → trigger-2.2.6}/trigger.egg-info/dependency_links.txt +0 -0
  75. {trigger-2.2.4 → trigger-2.2.6}/trigger.egg-info/entry_points.txt +0 -0
  76. {trigger-2.2.4 → trigger-2.2.6}/trigger.egg-info/requires.txt +0 -0
  77. {trigger-2.2.4 → trigger-2.2.6}/trigger.egg-info/top_level.txt +0 -0
  78. {trigger-2.2.4 → trigger-2.2.6}/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.4
3
+ Version: 2.2.6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trigger"
7
- version = "2.2.4"
7
+ version = "2.2.6"
8
8
  description = "Network automation toolkit for managing network devices"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -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 == []
@@ -10,6 +10,7 @@ and to manage the ACL task queue.
10
10
  import optparse
11
11
  import sys
12
12
  from collections import defaultdict
13
+ from pathlib import Path
13
14
  from textwrap import wrap
14
15
 
15
16
  from trigger import __version__, exceptions
@@ -24,7 +25,8 @@ def parse_args(argv, optp): # noqa: D103
24
25
  %prog --display [--exact | --device-name-only] (<acl_name> | <device>)
25
26
  %prog (--add | --remove) <acl_name> [<device> [<device> ...]]
26
27
  %prog (--clear | --inject) [--quiet] [<acl_name> [<acl_name> ...]]
27
- %prog (--list | --listmanual)"""
28
+ %prog (--list | --listmanual)
29
+ %prog --staged"""
28
30
 
29
31
  # Parse arguments.
30
32
  optp.usage = usage
@@ -46,6 +48,14 @@ def parse_args(argv, optp): # noqa: D103
46
48
  const="listmanual",
47
49
  dest="mode",
48
50
  )
51
+ optp.add_option(
52
+ "-s",
53
+ "--staged",
54
+ help="list currently staged ACLs",
55
+ action="store_const",
56
+ const="staged",
57
+ dest="mode",
58
+ )
49
59
  optp.add_option(
50
60
  "-i",
51
61
  "--inject",
@@ -128,7 +138,7 @@ def main(): # noqa: PLR0912, PLR0915
128
138
  # Setup
129
139
  aclsdb = AclsDB()
130
140
  term_width = get_terminal_width() # How wide is your term!
131
- valid_modes = ["list", "listmanual"] # Valid listing modes
141
+ valid_modes = ["list", "listmanual", "staged"] # Valid listing modes
132
142
 
133
143
  optp = optparse.OptionParser()
134
144
  opts, args = parse_args(sys.argv, optp)
@@ -167,6 +177,20 @@ def main(): # noqa: PLR0912, PLR0915
167
177
  if not queue.list(queue="manual"):
168
178
  print("Nothing in the manual queue.")
169
179
 
180
+ elif opts.mode == "staged":
181
+ tftproot = settings.TFTPROOT_DIR
182
+ tftp_path = Path(tftproot)
183
+ if not tftp_path.is_dir():
184
+ print(f"TFTPROOT_DIR directory not found: {tftproot}")
185
+ sys.exit(1)
186
+ staged_files = sorted(p.name for p in tftp_path.iterdir() if p.is_file())
187
+ if staged_files:
188
+ print(f"Access-lists currently staged in {tftproot}:")
189
+ for filename in staged_files:
190
+ print(f" {filename}")
191
+ else:
192
+ print(f"No ACLs currently staged in {tftproot}.")
193
+
170
194
  elif opts.mode == "inject":
171
195
  for arg in args:
172
196
  devs = [dev[0] for dev in get_matching_acls([arg])]
@@ -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.4
3
+ Version: 2.2.6
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
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