trigger 2.2.5__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.
- {trigger-2.2.5/trigger.egg-info → trigger-2.2.6}/PKG-INFO +1 -1
- {trigger-2.2.5 → trigger-2.2.6}/pyproject.toml +1 -1
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_twister.py +187 -1
- {trigger-2.2.5 → trigger-2.2.6}/trigger/twister.py +22 -15
- {trigger-2.2.5 → trigger-2.2.6}/trigger/twister2.py +10 -6
- {trigger-2.2.5 → trigger-2.2.6/trigger.egg-info}/PKG-INFO +1 -1
- {trigger-2.2.5 → trigger-2.2.6}/AUTHORS.md +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/LICENSE.md +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/README.md +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/setup.cfg +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_acl.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_acl_db.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_acl_queue.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_changemgmt.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_except.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_netdevices.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_scripts.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_tacacsrc.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_templates.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_twister2.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/tests/test_utils.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/autoacl.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/db.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/dicts.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/grammar.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/ios.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/junos.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/models.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/parser.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/queue.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/support.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/acl/tools.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/acl.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/acl_script.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/aclconv.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/check_access.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/check_syntax.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/fe.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/find_access.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/gnng.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/gong.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/load_acl.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/load_config.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/netdev.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/optimizer.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/bin/run_cmds.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/changemgmt/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/changemgmt/bounce.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/cmds.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/conf/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/conf/global_settings.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/contrib/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/exceptions.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/gorc.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/netdevices/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/netdevices/loader.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/netscreen.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/packages/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/packages/peewee.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/rancid.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/tacacsrc.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/__init__.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/cli.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/importlib.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/network.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/rcs.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/templates.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/url.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger/utils/xmltodict.py +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger.egg-info/SOURCES.txt +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger.egg-info/dependency_links.txt +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger.egg-info/entry_points.txt +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger.egg-info/requires.txt +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/trigger.egg-info/top_level.txt +0 -0
- {trigger-2.2.5 → trigger-2.2.6}/twisted/plugins/trigger_xmlrpc.py +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
|
|
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 == []
|
|
@@ -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
|
-
|
|
1503
|
-
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
2099
|
-
|
|
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
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
655
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|