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_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")
@@ -188,5 +188,8 @@ class TestDirectMainExecution(PatchSSHTestCase):
188
188
  debug_all=False,
189
189
  debug_operations=False,
190
190
  config_filename="config.py",
191
+ diff=True,
192
+ retry=0,
193
+ retry_delay=5,
191
194
  )
192
195
  assert e.args == (0,)
File without changes