pyinfra 3.4.1__py2.py3-none-any.whl → 3.5__py2.py3-none-any.whl
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.
- pyinfra/api/arguments.py +61 -0
- pyinfra/api/config.py +6 -0
- pyinfra/api/connect.py +19 -2
- pyinfra/api/operation.py +54 -1
- pyinfra/api/operations.py +119 -56
- pyinfra/api/state.py +10 -2
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +39 -7
- pyinfra/connectors/util.py +4 -0
- pyinfra/facts/dnf.py +8 -4
- pyinfra/facts/docker.py +28 -8
- pyinfra/facts/files.py +167 -26
- pyinfra/facts/server.py +55 -4
- pyinfra/facts/util/packaging.py +1 -0
- pyinfra/facts/yum.py +8 -4
- pyinfra/facts/zypper.py +3 -3
- pyinfra/operations/crontab.py +1 -1
- pyinfra/operations/docker.py +130 -29
- pyinfra/operations/files.py +162 -7
- pyinfra/operations/git.py +1 -1
- pyinfra/operations/openrc.py +13 -7
- pyinfra/operations/pip.py +6 -7
- pyinfra/operations/pipx.py +19 -7
- pyinfra/operations/util/docker.py +49 -1
- pyinfra/operations/util/files.py +70 -2
- pyinfra/operations/util/packaging.py +98 -55
- {pyinfra-3.4.1.dist-info → pyinfra-3.5.dist-info}/METADATA +3 -3
- {pyinfra-3.4.1.dist-info → pyinfra-3.5.dist-info}/RECORD +37 -35
- pyinfra_cli/main.py +39 -0
- pyinfra_cli/prints.py +4 -0
- tests/test_api/test_api_operations.py +348 -0
- tests/test_cli/test_cli.py +3 -0
- {pyinfra-3.4.1.dist-info → pyinfra-3.5.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.4.1.dist-info → pyinfra-3.5.dist-info}/WHEEL +0 -0
- {pyinfra-3.4.1.dist-info → pyinfra-3.5.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.4.1.dist-info → pyinfra-3.5.dist-info}/top_level.txt +0 -0
pyinfra_cli/main.py
CHANGED
|
@@ -67,6 +67,12 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
|
67
67
|
default=False,
|
|
68
68
|
help="Don't execute operations on the target hosts.",
|
|
69
69
|
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--diff",
|
|
72
|
+
is_flag=True,
|
|
73
|
+
default=False,
|
|
74
|
+
help="Show the differences when changing text files and templates.",
|
|
75
|
+
)
|
|
70
76
|
@click.option(
|
|
71
77
|
"-y",
|
|
72
78
|
"--yes",
|
|
@@ -132,6 +138,18 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
|
132
138
|
default=False,
|
|
133
139
|
help="Run operations in serial, host by host.",
|
|
134
140
|
)
|
|
141
|
+
@click.option(
|
|
142
|
+
"--retry",
|
|
143
|
+
type=int,
|
|
144
|
+
default=0,
|
|
145
|
+
help="Number of times to retry failed operations.",
|
|
146
|
+
)
|
|
147
|
+
@click.option(
|
|
148
|
+
"--retry-delay",
|
|
149
|
+
type=int,
|
|
150
|
+
default=5,
|
|
151
|
+
help="Delay in seconds between retry attempts.",
|
|
152
|
+
)
|
|
135
153
|
# SSH connector args
|
|
136
154
|
# TODO: remove the non-ssh-prefixed variants
|
|
137
155
|
@click.option("--ssh-user", "--user", "ssh_user", help="SSH user to connect as.")
|
|
@@ -267,10 +285,13 @@ def _main(
|
|
|
267
285
|
group_data,
|
|
268
286
|
config_filename: str,
|
|
269
287
|
dry: bool,
|
|
288
|
+
diff: bool,
|
|
270
289
|
yes: bool,
|
|
271
290
|
limit: Iterable,
|
|
272
291
|
no_wait: bool,
|
|
273
292
|
serial: bool,
|
|
293
|
+
retry: int,
|
|
294
|
+
retry_delay: int,
|
|
274
295
|
debug: bool,
|
|
275
296
|
debug_all: bool,
|
|
276
297
|
debug_facts: bool,
|
|
@@ -310,6 +331,9 @@ def _main(
|
|
|
310
331
|
shell_executable,
|
|
311
332
|
fail_percent,
|
|
312
333
|
yes,
|
|
334
|
+
diff,
|
|
335
|
+
retry,
|
|
336
|
+
retry_delay,
|
|
313
337
|
)
|
|
314
338
|
override_data = _set_override_data(
|
|
315
339
|
data,
|
|
@@ -549,6 +573,9 @@ def _set_config(
|
|
|
549
573
|
shell_executable,
|
|
550
574
|
fail_percent,
|
|
551
575
|
yes,
|
|
576
|
+
diff,
|
|
577
|
+
retry,
|
|
578
|
+
retry_delay,
|
|
552
579
|
):
|
|
553
580
|
logger.info("--> Loading config...")
|
|
554
581
|
|
|
@@ -583,6 +610,15 @@ def _set_config(
|
|
|
583
610
|
if fail_percent is not None:
|
|
584
611
|
config.FAIL_PERCENT = fail_percent
|
|
585
612
|
|
|
613
|
+
if diff:
|
|
614
|
+
config.DIFF = True
|
|
615
|
+
|
|
616
|
+
if retry is not None:
|
|
617
|
+
config.RETRY = retry
|
|
618
|
+
|
|
619
|
+
if retry_delay is not None:
|
|
620
|
+
config.RETRY_DELAY = retry_delay
|
|
621
|
+
|
|
586
622
|
return config
|
|
587
623
|
|
|
588
624
|
|
|
@@ -709,10 +745,13 @@ def _run_fact_operations(state, config, operations):
|
|
|
709
745
|
|
|
710
746
|
def _prepare_exec_operations(state, config, operations):
|
|
711
747
|
state.print_output = True
|
|
748
|
+
# Pass the retry settings from config to the shell operation
|
|
712
749
|
load_func(
|
|
713
750
|
state,
|
|
714
751
|
server.shell,
|
|
715
752
|
" ".join(operations),
|
|
753
|
+
_retries=config.RETRY,
|
|
754
|
+
_retry_delay=config.RETRY_DELAY,
|
|
716
755
|
)
|
|
717
756
|
return state
|
|
718
757
|
|
pyinfra_cli/prints.py
CHANGED
|
@@ -149,8 +149,12 @@ def print_support_info() -> None:
|
|
|
149
149
|
click.echo(" Machine: {0}".format(platform.uname()[4]), err=True)
|
|
150
150
|
click.echo(" pyinfra: v{0}".format(__version__), err=True)
|
|
151
151
|
|
|
152
|
+
seen_reqs: set[str] = set()
|
|
152
153
|
for requirement_string in sorted(requires("pyinfra") or []):
|
|
153
154
|
requirement = Requirement(requirement_string)
|
|
155
|
+
if requirement.name in seen_reqs:
|
|
156
|
+
continue
|
|
157
|
+
seen_reqs.add(requirement.name)
|
|
154
158
|
try:
|
|
155
159
|
click.echo(
|
|
156
160
|
" {0}: v{1}".format(requirement.name, version(requirement.name)),
|
|
@@ -19,6 +19,7 @@ from pyinfra.api.exceptions import PyinfraError
|
|
|
19
19
|
from pyinfra.api.operation import OperationMeta, add_op
|
|
20
20
|
from pyinfra.api.operations import run_ops
|
|
21
21
|
from pyinfra.api.state import StateOperationMeta
|
|
22
|
+
from pyinfra.connectors.util import CommandOutput, OutputLine
|
|
22
23
|
from pyinfra.context import ctx_host, ctx_state
|
|
23
24
|
from pyinfra.operations import files, python, server
|
|
24
25
|
|
|
@@ -576,4 +577,351 @@ class TestOperationOrdering(PatchSSHTestCase):
|
|
|
576
577
|
assert op_order[1] == second_op_hash
|
|
577
578
|
|
|
578
579
|
|
|
580
|
+
class TestOperationRetry(PatchSSHTestCase):
|
|
581
|
+
"""
|
|
582
|
+
Tests for the retry functionality in operations.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
@patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command")
|
|
586
|
+
def test_basic_retry_behavior(self, fake_run_command):
|
|
587
|
+
"""
|
|
588
|
+
Test that operations retry the correct number of times on failure.
|
|
589
|
+
"""
|
|
590
|
+
# Create inventory with just one host to simplify testing
|
|
591
|
+
inventory = make_inventory(hosts=("somehost",))
|
|
592
|
+
state = State(inventory, Config())
|
|
593
|
+
connect_all(state)
|
|
594
|
+
|
|
595
|
+
# Add operation with retry settings
|
|
596
|
+
add_op(
|
|
597
|
+
state,
|
|
598
|
+
server.shell,
|
|
599
|
+
'echo "testing retries"',
|
|
600
|
+
_retries=2,
|
|
601
|
+
_retry_delay=0.1, # Use small delay for tests
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Track how many times run_shell_command was called
|
|
605
|
+
call_count = 0
|
|
606
|
+
|
|
607
|
+
# First call fails, second succeeds
|
|
608
|
+
def side_effect(*args, **kwargs):
|
|
609
|
+
nonlocal call_count
|
|
610
|
+
call_count += 1
|
|
611
|
+
if call_count == 1:
|
|
612
|
+
# First call fails
|
|
613
|
+
fake_channel = FakeChannel(1)
|
|
614
|
+
return (False, FakeBuffer("", fake_channel))
|
|
615
|
+
else:
|
|
616
|
+
# Second call succeeds
|
|
617
|
+
fake_channel = FakeChannel(0)
|
|
618
|
+
return (True, FakeBuffer("success", fake_channel))
|
|
619
|
+
|
|
620
|
+
fake_run_command.side_effect = side_effect
|
|
621
|
+
|
|
622
|
+
# Run the operation
|
|
623
|
+
run_ops(state)
|
|
624
|
+
|
|
625
|
+
# Check that run_shell_command was called twice (original + 1 retry)
|
|
626
|
+
self.assertEqual(call_count, 2)
|
|
627
|
+
|
|
628
|
+
# Verify results
|
|
629
|
+
somehost = inventory.get_host("somehost")
|
|
630
|
+
|
|
631
|
+
# Operation should be successful (because the retry succeeded)
|
|
632
|
+
self.assertEqual(state.results[somehost].success_ops, 1)
|
|
633
|
+
self.assertEqual(state.results[somehost].error_ops, 0)
|
|
634
|
+
|
|
635
|
+
# Get the operation hash
|
|
636
|
+
op_hash = state.get_op_order()[0]
|
|
637
|
+
|
|
638
|
+
# Check retry info in OperationMeta
|
|
639
|
+
op_meta = state.ops[somehost][op_hash].operation_meta
|
|
640
|
+
self.assertEqual(op_meta.retry_attempts, 1)
|
|
641
|
+
self.assertEqual(op_meta.max_retries, 2)
|
|
642
|
+
self.assertTrue(op_meta.was_retried)
|
|
643
|
+
self.assertTrue(op_meta.retry_succeeded)
|
|
644
|
+
|
|
645
|
+
@patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command")
|
|
646
|
+
def test_retry_max_attempts_failure(self, fake_run_command):
|
|
647
|
+
"""
|
|
648
|
+
Test that operations stop retrying after max attempts and report failure.
|
|
649
|
+
"""
|
|
650
|
+
inventory = make_inventory(hosts=("somehost",))
|
|
651
|
+
state = State(inventory, Config())
|
|
652
|
+
connect_all(state)
|
|
653
|
+
|
|
654
|
+
# Add operation with retry settings
|
|
655
|
+
add_op(
|
|
656
|
+
state,
|
|
657
|
+
server.shell,
|
|
658
|
+
'echo "testing max retries"',
|
|
659
|
+
_retries=2,
|
|
660
|
+
_retry_delay=0.1,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Make all attempts fail
|
|
664
|
+
fake_channel = FakeChannel(1)
|
|
665
|
+
fake_run_command.return_value = (False, FakeBuffer("", fake_channel))
|
|
666
|
+
|
|
667
|
+
# This should fail after all retries
|
|
668
|
+
with self.assertRaises(PyinfraError) as e:
|
|
669
|
+
run_ops(state)
|
|
670
|
+
|
|
671
|
+
self.assertEqual(e.exception.args[0], "No hosts remaining!")
|
|
672
|
+
|
|
673
|
+
# Check that run_shell_command was called the right number of times (1 original + 2 retries)
|
|
674
|
+
self.assertEqual(fake_run_command.call_count, 3)
|
|
675
|
+
|
|
676
|
+
somehost = inventory.get_host("somehost")
|
|
677
|
+
|
|
678
|
+
# Operation should be marked as error
|
|
679
|
+
self.assertEqual(state.results[somehost].success_ops, 0)
|
|
680
|
+
self.assertEqual(state.results[somehost].error_ops, 1)
|
|
681
|
+
|
|
682
|
+
# Get the operation hash
|
|
683
|
+
op_hash = state.get_op_order()[0]
|
|
684
|
+
|
|
685
|
+
# Check retry info
|
|
686
|
+
op_meta = state.ops[somehost][op_hash].operation_meta
|
|
687
|
+
self.assertEqual(op_meta.retry_attempts, 2)
|
|
688
|
+
self.assertEqual(op_meta.max_retries, 2)
|
|
689
|
+
self.assertTrue(op_meta.was_retried)
|
|
690
|
+
self.assertFalse(op_meta.retry_succeeded)
|
|
691
|
+
|
|
692
|
+
@patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command")
|
|
693
|
+
@patch("time.sleep")
|
|
694
|
+
def test_retry_until_condition(self, fake_sleep, fake_run_command):
|
|
695
|
+
"""
|
|
696
|
+
Test that operations retry based on the retry_until callable condition.
|
|
697
|
+
"""
|
|
698
|
+
# Setup inventory and state using the utility function
|
|
699
|
+
inventory = make_inventory(hosts=("somehost",))
|
|
700
|
+
state = State(inventory, Config())
|
|
701
|
+
connect_all(state)
|
|
702
|
+
|
|
703
|
+
# Create a counter to track retry_until calls
|
|
704
|
+
call_counter = [0]
|
|
705
|
+
|
|
706
|
+
# Create a retry_until function that returns True (retry) for first two calls
|
|
707
|
+
def retry_until_func(output_data):
|
|
708
|
+
call_counter[0] += 1
|
|
709
|
+
return call_counter[0] < 3 # Retry twice, then stop
|
|
710
|
+
|
|
711
|
+
# Add operation with retry_until
|
|
712
|
+
add_op(
|
|
713
|
+
state,
|
|
714
|
+
server.shell,
|
|
715
|
+
'echo "test retry_until"',
|
|
716
|
+
_retries=3,
|
|
717
|
+
_retry_delay=0.1,
|
|
718
|
+
_retry_until=retry_until_func,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
# Set up fake command execution - always succeed but with proper output format
|
|
722
|
+
# Use the existing FakeBuffer/FakeChannel from test utils
|
|
723
|
+
|
|
724
|
+
# First two calls trigger retry_until, third doesn't
|
|
725
|
+
def command_side_effect(*args, **kwargs):
|
|
726
|
+
# Create proper CommandOutput for the retry_until function to process
|
|
727
|
+
lines = [OutputLine("stdout", "test output"), OutputLine("stderr", "no errors")]
|
|
728
|
+
return True, CommandOutput(lines)
|
|
729
|
+
|
|
730
|
+
fake_run_command.side_effect = command_side_effect
|
|
731
|
+
|
|
732
|
+
# Run the operations
|
|
733
|
+
run_ops(state)
|
|
734
|
+
|
|
735
|
+
# The command should be called 3 times total (initial + 2 retries)
|
|
736
|
+
self.assertEqual(fake_run_command.call_count, 3)
|
|
737
|
+
|
|
738
|
+
# The retry_until function should be called 3 times
|
|
739
|
+
self.assertEqual(call_counter[0], 3)
|
|
740
|
+
|
|
741
|
+
# Get the operation metadata to check retry info
|
|
742
|
+
somehost = inventory.get_host("somehost")
|
|
743
|
+
op_hash = state.get_op_order()[0]
|
|
744
|
+
op_meta = state.ops[somehost][op_hash].operation_meta
|
|
745
|
+
|
|
746
|
+
# Check retry metadata
|
|
747
|
+
self.assertEqual(op_meta.retry_attempts, 2)
|
|
748
|
+
self.assertEqual(op_meta.max_retries, 3)
|
|
749
|
+
self.assertTrue(op_meta.was_retried)
|
|
750
|
+
self.assertTrue(op_meta.retry_succeeded)
|
|
751
|
+
|
|
752
|
+
@patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command")
|
|
753
|
+
@patch("time.sleep")
|
|
754
|
+
def test_retry_delay(self, fake_sleep, fake_run_command):
|
|
755
|
+
"""
|
|
756
|
+
Test that retry delay is properly applied between attempts.
|
|
757
|
+
"""
|
|
758
|
+
inventory = make_inventory(hosts=("somehost",))
|
|
759
|
+
state = State(inventory, Config())
|
|
760
|
+
connect_all(state)
|
|
761
|
+
|
|
762
|
+
retry_delay = 5
|
|
763
|
+
|
|
764
|
+
# Add operation with retry settings
|
|
765
|
+
add_op(
|
|
766
|
+
state,
|
|
767
|
+
server.shell,
|
|
768
|
+
'echo "testing retry delay"',
|
|
769
|
+
_retries=2,
|
|
770
|
+
_retry_delay=retry_delay,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# Make first call fail, second succeed
|
|
774
|
+
call_count = 0
|
|
775
|
+
|
|
776
|
+
def side_effect(*args, **kwargs):
|
|
777
|
+
nonlocal call_count
|
|
778
|
+
call_count += 1
|
|
779
|
+
if call_count == 1:
|
|
780
|
+
fake_channel = FakeChannel(1)
|
|
781
|
+
return (False, FakeBuffer("", fake_channel))
|
|
782
|
+
else:
|
|
783
|
+
fake_channel = FakeChannel(0)
|
|
784
|
+
return (True, FakeBuffer("", fake_channel))
|
|
785
|
+
|
|
786
|
+
fake_run_command.side_effect = side_effect
|
|
787
|
+
|
|
788
|
+
# Run the operation
|
|
789
|
+
run_ops(state)
|
|
790
|
+
|
|
791
|
+
# Check that sleep was called with the correct delay
|
|
792
|
+
fake_sleep.assert_called_once_with(retry_delay)
|
|
793
|
+
|
|
794
|
+
@patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command")
|
|
795
|
+
@patch("time.sleep")
|
|
796
|
+
def test_retry_until_with_error_handling(self, fake_sleep, fake_run_command):
|
|
797
|
+
"""
|
|
798
|
+
Test that operations handle errors in retry_until functions gracefully.
|
|
799
|
+
"""
|
|
800
|
+
inventory = make_inventory(hosts=("somehost",))
|
|
801
|
+
state = State(inventory, Config())
|
|
802
|
+
connect_all(state)
|
|
803
|
+
|
|
804
|
+
# Create a retry_until function that raises an exception
|
|
805
|
+
def failing_retry_until_func(output_data):
|
|
806
|
+
raise ValueError("Test error in retry_until function")
|
|
807
|
+
|
|
808
|
+
# Add operation with failing retry_until
|
|
809
|
+
add_op(
|
|
810
|
+
state,
|
|
811
|
+
server.shell,
|
|
812
|
+
'echo "test failing retry_until"',
|
|
813
|
+
_retries=2,
|
|
814
|
+
_retry_delay=0.1,
|
|
815
|
+
_retry_until=failing_retry_until_func,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Set up fake command execution
|
|
819
|
+
|
|
820
|
+
def command_side_effect(*args, **kwargs):
|
|
821
|
+
lines = [OutputLine("stdout", "test output"), OutputLine("stderr", "no errors")]
|
|
822
|
+
return True, CommandOutput(lines)
|
|
823
|
+
|
|
824
|
+
fake_run_command.side_effect = command_side_effect
|
|
825
|
+
|
|
826
|
+
# Run the operations - should succeed despite retry_until error
|
|
827
|
+
run_ops(state)
|
|
828
|
+
|
|
829
|
+
# The command should be called only once (no retries due to error)
|
|
830
|
+
self.assertEqual(fake_run_command.call_count, 1)
|
|
831
|
+
|
|
832
|
+
# Verify operation completed successfully
|
|
833
|
+
somehost = inventory.get_host("somehost")
|
|
834
|
+
self.assertEqual(state.results[somehost].success_ops, 1)
|
|
835
|
+
self.assertEqual(state.results[somehost].error_ops, 0)
|
|
836
|
+
|
|
837
|
+
@patch("pyinfra.connectors.ssh.SSHConnector.run_shell_command")
|
|
838
|
+
@patch("time.sleep")
|
|
839
|
+
def test_retry_until_with_complex_output_parsing(self, fake_sleep, fake_run_command):
|
|
840
|
+
"""
|
|
841
|
+
Test retry_until with complex output parsing scenarios.
|
|
842
|
+
"""
|
|
843
|
+
inventory = make_inventory(hosts=("somehost",))
|
|
844
|
+
state = State(inventory, Config())
|
|
845
|
+
connect_all(state)
|
|
846
|
+
|
|
847
|
+
# Track what output we've seen
|
|
848
|
+
outputs_seen = []
|
|
849
|
+
|
|
850
|
+
def complex_retry_until_func(output_data):
|
|
851
|
+
# Store the output data for verification
|
|
852
|
+
outputs_seen.append(output_data)
|
|
853
|
+
|
|
854
|
+
# Check for specific patterns in stdout
|
|
855
|
+
stdout_text = " ".join(output_data["stdout_lines"])
|
|
856
|
+
|
|
857
|
+
# Continue retrying until we see "READY" in stdout
|
|
858
|
+
return "READY" not in stdout_text
|
|
859
|
+
|
|
860
|
+
# Add operation with complex retry_until
|
|
861
|
+
add_op(
|
|
862
|
+
state,
|
|
863
|
+
server.shell,
|
|
864
|
+
'echo "service status check"',
|
|
865
|
+
_retries=3,
|
|
866
|
+
_retry_delay=0.1,
|
|
867
|
+
_retry_until=complex_retry_until_func,
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Set up fake command execution with changing output
|
|
871
|
+
|
|
872
|
+
call_count = 0
|
|
873
|
+
|
|
874
|
+
def command_side_effect(*args, **kwargs):
|
|
875
|
+
nonlocal call_count
|
|
876
|
+
call_count += 1
|
|
877
|
+
|
|
878
|
+
if call_count == 1:
|
|
879
|
+
lines = [
|
|
880
|
+
OutputLine("stdout", "Service starting..."),
|
|
881
|
+
OutputLine("stderr", "Loading config"),
|
|
882
|
+
]
|
|
883
|
+
elif call_count == 2:
|
|
884
|
+
lines = [
|
|
885
|
+
OutputLine("stdout", "Service initializing..."),
|
|
886
|
+
OutputLine("stderr", "Connecting to database"),
|
|
887
|
+
]
|
|
888
|
+
else: # call_count == 3
|
|
889
|
+
lines = [
|
|
890
|
+
OutputLine("stdout", "Service READY"),
|
|
891
|
+
OutputLine("stderr", "All systems operational"),
|
|
892
|
+
]
|
|
893
|
+
|
|
894
|
+
return True, CommandOutput(lines)
|
|
895
|
+
|
|
896
|
+
fake_run_command.side_effect = command_side_effect
|
|
897
|
+
|
|
898
|
+
# Run the operations
|
|
899
|
+
run_ops(state)
|
|
900
|
+
|
|
901
|
+
# The command should be called 3 times
|
|
902
|
+
self.assertEqual(fake_run_command.call_count, 3)
|
|
903
|
+
|
|
904
|
+
# Verify retry_until was called 3 times with correct data
|
|
905
|
+
self.assertEqual(len(outputs_seen), 3)
|
|
906
|
+
|
|
907
|
+
# Check the output data structure
|
|
908
|
+
for output_data in outputs_seen:
|
|
909
|
+
self.assertIn("stdout_lines", output_data)
|
|
910
|
+
self.assertIn("stderr_lines", output_data)
|
|
911
|
+
self.assertIn("commands", output_data)
|
|
912
|
+
self.assertIn("executed_commands", output_data)
|
|
913
|
+
self.assertIn("host", output_data)
|
|
914
|
+
self.assertIn("operation", output_data)
|
|
915
|
+
|
|
916
|
+
# Verify operation metadata
|
|
917
|
+
somehost = inventory.get_host("somehost")
|
|
918
|
+
op_hash = state.get_op_order()[0]
|
|
919
|
+
op_meta = state.ops[somehost][op_hash].operation_meta
|
|
920
|
+
|
|
921
|
+
self.assertEqual(op_meta.retry_attempts, 2)
|
|
922
|
+
self.assertEqual(op_meta.max_retries, 3)
|
|
923
|
+
self.assertTrue(op_meta.was_retried)
|
|
924
|
+
self.assertTrue(op_meta.retry_succeeded)
|
|
925
|
+
|
|
926
|
+
|
|
579
927
|
this_filename = path.join("tests", "test_api", "test_api_operations.py")
|
tests/test_cli/test_cli.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|