pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__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.
Files changed (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +184 -118
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.1.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +10 -12
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,14 @@ from .util import run_cli
9
9
 
10
10
 
11
11
  class TestCliDeployState(PatchSSHTestCase):
12
+ def _run_cli(self, hosts, filename):
13
+ return run_cli(
14
+ "-y",
15
+ ",".join(hosts),
16
+ path.join("tests", "test_cli", "deploy", filename),
17
+ f'--chdir={path.join("tests", "test_cli", "deploy")}',
18
+ )
19
+
12
20
  def _assert_op_data(self, correct_op_name_and_host_names):
13
21
  op_order = state.get_op_order()
14
22
 
@@ -74,12 +82,7 @@ class TestCliDeployState(PatchSSHTestCase):
74
82
  hosts = ["somehost", "anotherhost", "someotherhost"]
75
83
  shuffle(hosts)
76
84
 
77
- result = run_cli(
78
- "-y",
79
- ",".join(hosts),
80
- path.join("tests", "deploy", "deploy.py"),
81
- f'--chdir={path.join("tests", "deploy")}',
82
- )
85
+ result = self._run_cli(hosts, "deploy.py")
83
86
  assert result.exit_code == 0, result.stdout
84
87
 
85
88
  self._assert_op_data(correct_op_name_and_host_names)
@@ -104,12 +107,7 @@ class TestCliDeployState(PatchSSHTestCase):
104
107
  hosts = ["somehost", "anotherhost", "someotherhost"]
105
108
  shuffle(hosts)
106
109
 
107
- result = run_cli(
108
- "-y",
109
- ",".join(hosts),
110
- path.join("tests", "deploy", "deploy_random.py"),
111
- f'--chdir={path.join("tests", "deploy")}',
112
- )
110
+ result = self._run_cli(hosts, "deploy_random.py")
113
111
  assert result.exit_code == 0, result.stdout
114
112
 
115
113
  self._assert_op_data(correct_op_name_and_host_names)
@@ -1,34 +1,27 @@
1
+ import sys
2
+ from os import path
1
3
  from unittest import TestCase
2
4
 
5
+ import pytest
3
6
  from click.testing import CliRunner
4
7
 
5
- from pyinfra_cli.exceptions import CliError
8
+ from pyinfra.api import OperationError
9
+ from pyinfra.api.exceptions import ArgumentTypeError
10
+ from pyinfra_cli.exceptions import CliError, UnexpectedExternalError, WrappedError
6
11
  from pyinfra_cli.main import cli
7
12
 
13
+ from .util import run_cli
14
+
8
15
 
9
16
  class TestCliExceptions(TestCase):
10
17
  @classmethod
11
18
  def setUpClass(cls):
12
- cls.test_cli = CliRunner()
13
- cls.old_cli_show = CliError.show
14
-
15
- @classmethod
16
- def tearDownClass(cls):
17
- CliError.show = cls.old_cli_show
18
-
19
- def setUp(self):
20
- self.exception = None
21
- CliError.show = lambda e: self.capture_cli_error(e)
22
-
23
- def capture_cli_error(self, e):
24
- self.exception = e
25
- self.old_cli_show()
19
+ cls.runner = CliRunner()
26
20
 
27
21
  def assert_cli_exception(self, args, message):
28
- self.test_cli.invoke(cli, args)
29
-
30
- self.assertIsInstance(self.exception, CliError)
31
- assert self.exception.message == message
22
+ result = self.runner.invoke(cli, args, standalone_mode=False)
23
+ self.assertIsInstance(result.exception, CliError)
24
+ assert getattr(result.exception, "message") == message
32
25
 
33
26
  def test_bad_deploy_file(self):
34
27
  self.assert_cli_exception(
@@ -53,3 +46,41 @@ class TestCliExceptions(TestCase):
53
46
  ["my-server.net", "fact", "server.NotAFact"],
54
47
  "No such attribute in module pyinfra.facts.server: NotAFact",
55
48
  )
49
+
50
+
51
+ class TestCliDeployExceptions(TestCase):
52
+ def _run_cli(self, hosts, filename):
53
+ return run_cli(
54
+ "-y",
55
+ ",".join(hosts),
56
+ path.join("tests", "test_cli", "deploy_fails", filename),
57
+ f'--chdir={path.join("tests", "test_cli", "deploy_fails")}',
58
+ )
59
+
60
+ def test_invalid_argument_type(self):
61
+ result = self._run_cli(["@local"], "invalid_argument_type.py")
62
+ assert isinstance(result.exception, WrappedError)
63
+ assert isinstance(result.exception.exception, ArgumentTypeError)
64
+ assert (
65
+ result.exception.exception.args[0]
66
+ == "Invalid argument `_sudo`:: None is not an instance of bool"
67
+ )
68
+
69
+ def test_invalid_operation_arg(self):
70
+ result = self._run_cli(["@local"], "invalid_operation_arg.py")
71
+ assert isinstance(result.exception, UnexpectedExternalError)
72
+ assert isinstance(result.exception.exception, TypeError)
73
+ assert result.exception.filename == "invalid_operation_arg.py"
74
+ assert result.exception.exception.args[0] == "missing a required argument: 'commands'"
75
+
76
+ @pytest.mark.skipif(
77
+ sys.platform.startswith("win"),
78
+ reason="The operation is not compatible with Windows",
79
+ )
80
+ def test_operation_error(self):
81
+ result = self._run_cli(["@local"], "operation_error.py")
82
+ assert isinstance(result.exception, WrappedError)
83
+ assert isinstance(result.exception.exception, OperationError)
84
+ assert (
85
+ result.exception.exception.args[0] == "operation_error.py exists and is not a directory"
86
+ )
@@ -0,0 +1,66 @@
1
+ from os import path
2
+
3
+ from pyinfra import inventory
4
+ from pyinfra.context import ctx_inventory, ctx_state
5
+
6
+ from ..paramiko_util import PatchSSHTestCase
7
+ from .util import run_cli
8
+
9
+
10
+ class TestCliInventory(PatchSSHTestCase):
11
+ def test_load_deploy_group_data(self):
12
+ ctx_state.reset()
13
+ ctx_inventory.reset()
14
+
15
+ hosts = ["somehost", "anotherhost", "someotherhost"]
16
+ result = run_cli(
17
+ "-y",
18
+ ",".join(hosts),
19
+ path.join("tests", "test_cli", "deploy", "deploy.py"),
20
+ f'--chdir={path.join("tests", "test_cli", "deploy")}',
21
+ )
22
+ assert result.exit_code == 0, result.stdout
23
+
24
+ assert inventory.data.get("hello") == "world"
25
+ assert "leftover_data" in inventory.group_data
26
+ assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
27
+ assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
28
+
29
+ def test_load_group_data(self):
30
+ ctx_state.reset()
31
+ ctx_inventory.reset()
32
+
33
+ hosts = ["somehost", "anotherhost", "someotherhost"]
34
+ result = run_cli(
35
+ "-y",
36
+ ",".join(hosts),
37
+ f'--group-data={path.join("tests", "test_cli", "deploy", "group_data")}',
38
+ "exec",
39
+ "uptime",
40
+ )
41
+ assert result.exit_code == 0, result.stdout
42
+
43
+ assert inventory.data.get("hello") == "world"
44
+ assert "leftover_data" in inventory.group_data
45
+ assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
46
+ assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
47
+
48
+ def test_load_group_data_file(self):
49
+ ctx_state.reset()
50
+ ctx_inventory.reset()
51
+
52
+ hosts = ["somehost", "anotherhost", "someotherhost"]
53
+ filename = path.join("tests", "test_cli", "deploy", "group_data", "leftover_data.py")
54
+ result = run_cli(
55
+ "-y",
56
+ ",".join(hosts),
57
+ f"--group-data={filename}",
58
+ "exec",
59
+ "uptime",
60
+ )
61
+ assert result.exit_code == 0, result.stdout
62
+
63
+ assert "hello" not in inventory.data
64
+ assert "leftover_data" in inventory.group_data
65
+ assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
66
+ assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
tests/test_cli/util.py CHANGED
@@ -10,7 +10,7 @@ def run_cli(*arguments):
10
10
  cwd = getcwd()
11
11
  pyinfra.is_cli = True
12
12
  runner = CliRunner()
13
- result = runner.invoke(cli, arguments)
13
+ result = runner.invoke(cli, arguments, standalone_mode=False)
14
14
  pyinfra.is_cli = False
15
15
  chdir(cwd)
16
16
  return result
@@ -31,6 +31,9 @@ def fake_ssh_docker_shell(
31
31
  if str(command).startswith("rm -f"):
32
32
  return (True, CommandOutput([]))
33
33
 
34
+ if "$TMPDIR" in str(command):
35
+ return (True, CommandOutput([]))
36
+
34
37
  # This is a bit messy. But it's easier than trying to swap out a mock
35
38
  # when it needs to be used...
36
39
  if fake_ssh_docker_shell.custom_command:
@@ -150,10 +153,10 @@ class TestDockerSSHConnector(TestCase):
150
153
  ]
151
154
 
152
155
  inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
153
- state = State(inventory, Config())
154
- state.get_temp_filename = lambda _: "remote_tempfile"
156
+ State(inventory, Config())
155
157
 
156
158
  host = inventory.get_host("@dockerssh/somehost:not-an-image")
159
+ host.get_temp_filename = lambda _: "remote_tempfile"
157
160
  host.connect()
158
161
 
159
162
  host.put_file("not-a-file", "not-another-file", print_output=True)
@@ -167,10 +170,10 @@ class TestDockerSSHConnector(TestCase):
167
170
  @patch("pyinfra.connectors.ssh.SSHConnector.put_file")
168
171
  def test_put_file_error(self, fake_put_file):
169
172
  inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
170
- state = State(inventory, Config())
171
- state.get_temp_filename = lambda _: "remote_tempfile"
173
+ State(inventory, Config())
172
174
 
173
175
  host = inventory.get_host("@dockerssh/somehost:not-an-image")
176
+ host.get_temp_filename = lambda _: "remote_tempfile"
174
177
  host.connect()
175
178
 
176
179
  # SSH error
@@ -199,10 +202,10 @@ class TestDockerSSHConnector(TestCase):
199
202
  ]
200
203
 
201
204
  inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
202
- state = State(inventory, Config())
203
- state.get_temp_filename = lambda _: "remote_tempfile"
205
+ State(inventory, Config())
204
206
 
205
207
  host = inventory.get_host("@dockerssh/somehost:not-an-image")
208
+ host.get_temp_filename = lambda _: "remote_tempfile"
206
209
  host.connect()
207
210
 
208
211
  host.get_file("not-a-file", "not-another-file", print_output=True)
@@ -222,10 +225,10 @@ class TestDockerSSHConnector(TestCase):
222
225
  ]
223
226
 
224
227
  inventory = make_inventory(hosts=("@dockerssh/somehost:not-an-image",))
225
- state = State(inventory, Config())
226
- state.get_temp_filename = lambda _: "remote_tempfile"
228
+ State(inventory, Config())
227
229
 
228
230
  host = inventory.get_host("@dockerssh/somehost:not-an-image")
231
+ host.get_temp_filename = lambda _: "remote_tempfile"
229
232
  host.connect()
230
233
 
231
234
  fake_get_file.return_value = True
@@ -102,10 +102,10 @@ class TestSSHConnector(TestCase):
102
102
  pkey=fake_key,
103
103
  timeout=10,
104
104
  username="vagrant",
105
- _pyinfra_ssh_forward_agent=None,
105
+ _pyinfra_ssh_forward_agent=False,
106
106
  _pyinfra_ssh_config_file=None,
107
107
  _pyinfra_ssh_known_hosts_file=None,
108
- _pyinfra_ssh_strict_host_key_checking=None,
108
+ _pyinfra_ssh_strict_host_key_checking="accept-new",
109
109
  _pyinfra_ssh_paramiko_connect_kwargs=None,
110
110
  )
111
111
 
@@ -257,10 +257,10 @@ class TestSSHConnector(TestCase):
257
257
  pkey=fake_key,
258
258
  timeout=10,
259
259
  username="vagrant",
260
- _pyinfra_ssh_forward_agent=None,
260
+ _pyinfra_ssh_forward_agent=False,
261
261
  _pyinfra_ssh_config_file=None,
262
262
  _pyinfra_ssh_known_hosts_file=None,
263
- _pyinfra_ssh_strict_host_key_checking=None,
263
+ _pyinfra_ssh_strict_host_key_checking="accept-new",
264
264
  _pyinfra_ssh_paramiko_connect_kwargs=None,
265
265
  )
266
266
 
@@ -316,10 +316,10 @@ class TestSSHConnector(TestCase):
316
316
  pkey=fake_dss_key,
317
317
  timeout=10,
318
318
  username="vagrant",
319
- _pyinfra_ssh_forward_agent=None,
319
+ _pyinfra_ssh_forward_agent=False,
320
320
  _pyinfra_ssh_config_file=None,
321
321
  _pyinfra_ssh_known_hosts_file=None,
322
- _pyinfra_ssh_strict_host_key_checking=None,
322
+ _pyinfra_ssh_strict_host_key_checking="accept-new",
323
323
  _pyinfra_ssh_paramiko_connect_kwargs=None,
324
324
  )
325
325
 
@@ -451,14 +451,24 @@ class TestSSHConnector(TestCase):
451
451
 
452
452
  @patch("pyinfra.connectors.util.getpass")
453
453
  @patch("pyinfra.connectors.ssh.SSHClient")
454
- def test_run_shell_command_sudo_password_prompt(
454
+ def test_run_shell_command_sudo_password_automatic_prompt(
455
455
  self,
456
456
  fake_ssh_client,
457
457
  fake_getpass,
458
458
  ):
459
459
  fake_ssh = MagicMock()
460
- fake_stdout = MagicMock()
461
- fake_ssh.exec_command.return_value = MagicMock(), fake_stdout, MagicMock()
460
+ first_fake_stdout = MagicMock()
461
+ second_fake_stdout = MagicMock()
462
+ third_fake_stdout = MagicMock()
463
+
464
+ first_fake_stdout.__iter__.return_value = ["sudo: a password is required\r"]
465
+ second_fake_stdout.__iter__.return_value = ["/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"]
466
+
467
+ fake_ssh.exec_command.side_effect = [
468
+ (MagicMock(), first_fake_stdout, MagicMock()), # command w/o sudo password
469
+ (MagicMock(), second_fake_stdout, MagicMock()), # SUDO_ASKPASS_COMMAND
470
+ (MagicMock(), third_fake_stdout, MagicMock()), # command with sudo pw
471
+ ]
462
472
 
463
473
  fake_ssh_client.return_value = fake_ssh
464
474
  fake_getpass.return_value = "password"
@@ -469,17 +479,18 @@ class TestSSHConnector(TestCase):
469
479
  host.connect()
470
480
 
471
481
  command = "echo Šablony"
472
- fake_stdout.__iter__.return_value = ["/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"]
473
- fake_stdout.channel.recv_exit_status.return_value = 0
482
+ first_fake_stdout.channel.recv_exit_status.return_value = 1
483
+ second_fake_stdout.channel.recv_exit_status.return_value = 0
484
+ third_fake_stdout.channel.recv_exit_status.return_value = 0
474
485
 
475
- out = host.run_shell_command(
476
- command, _sudo=True, _use_sudo_password=True, print_output=True
477
- )
486
+ out = host.run_shell_command(command, _sudo=True, print_output=True)
478
487
  assert len(out) == 2
479
488
 
480
489
  status, output = out
481
490
  assert status is True
482
491
 
492
+ fake_ssh.exec_command.assert_any_call(("sudo -H -n sh -c 'echo Šablony'"), get_pty=False)
493
+
483
494
  fake_ssh.exec_command.assert_called_with(
484
495
  (
485
496
  "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
@@ -491,7 +502,7 @@ class TestSSHConnector(TestCase):
491
502
 
492
503
  @patch("pyinfra.connectors.util.getpass")
493
504
  @patch("pyinfra.connectors.ssh.SSHClient")
494
- def test_run_shell_command_sudo_password_automatic_prompt(
505
+ def test_run_shell_command_sudo_password_automatic_prompt_with_special_chars_in_password(
495
506
  self,
496
507
  fake_ssh_client,
497
508
  fake_getpass,
@@ -511,7 +522,7 @@ class TestSSHConnector(TestCase):
511
522
  ]
512
523
 
513
524
  fake_ssh_client.return_value = fake_ssh
514
- fake_getpass.return_value = "password"
525
+ fake_getpass.return_value = "p@ss'word';"
515
526
 
516
527
  inventory = make_inventory(hosts=("somehost",))
517
528
  State(inventory, Config())
@@ -534,7 +545,7 @@ class TestSSHConnector(TestCase):
534
545
  fake_ssh.exec_command.assert_called_with(
535
546
  (
536
547
  "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
537
- "PYINFRA_SUDO_PASSWORD=password "
548
+ """PYINFRA_SUDO_PASSWORD='p@ss'"'"'word'"'"';' """
538
549
  "sudo -H -A -k sh -c 'echo Šablony'"
539
550
  ),
540
551
  get_pty=False,
@@ -544,13 +555,13 @@ class TestSSHConnector(TestCase):
544
555
  #
545
556
 
546
557
  @patch("pyinfra.connectors.ssh.SSHClient")
547
- @patch("pyinfra.connectors.util._get_sudo_password")
558
+ @patch("pyinfra.connectors.util.getpass")
548
559
  def test_run_shell_command_retry_for_sudo_password(
549
560
  self,
550
- fake_get_sudo_password,
561
+ fake_getpass,
551
562
  fake_ssh_client,
552
563
  ):
553
- fake_get_sudo_password.return_value = "PASSWORD"
564
+ fake_getpass.return_value = "PASSWORD"
554
565
 
555
566
  fake_ssh = MagicMock()
556
567
  fake_stdin = MagicMock()
@@ -570,13 +581,13 @@ class TestSSHConnector(TestCase):
570
581
  return_values = [1, 0] # return 0 on the second call
571
582
  fake_stdout.channel.recv_exit_status.side_effect = lambda: return_values.pop(0)
572
583
 
573
- out = host.run_shell_command(command)
584
+ out = host.run_shell_command(command, _sudo=True)
574
585
  assert len(out) == 2
575
586
  assert out[0] is True
576
- assert fake_get_sudo_password.called
587
+ assert fake_getpass.called
577
588
  fake_ssh.exec_command.assert_called_with(
578
589
  "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
579
- "PYINFRA_SUDO_PASSWORD=PASSWORD sh -c 'echo hi'",
590
+ "PYINFRA_SUDO_PASSWORD=PASSWORD sudo -H -A -k sh -c 'echo hi'",
580
591
  get_pty=False,
581
592
  )
582
593
 
@@ -1041,3 +1052,57 @@ class TestSSHConnector(TestCase):
1041
1052
  "not-another-file",
1042
1053
  print_output=True,
1043
1054
  )
1055
+
1056
+ @patch("pyinfra.connectors.ssh.SSHClient")
1057
+ @patch("pyinfra.connectors.ssh.sleep")
1058
+ def test_ssh_connect_fail_retry(self, fake_sleep, fake_ssh_client):
1059
+ for exception_class in (
1060
+ SSHException,
1061
+ gaierror,
1062
+ socket_error,
1063
+ EOFError,
1064
+ ):
1065
+ fake_sleep.reset_mock()
1066
+ fake_ssh_client.reset_mock()
1067
+
1068
+ inventory = make_inventory(
1069
+ hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1}
1070
+ )
1071
+ State(inventory, Config())
1072
+
1073
+ unresposivehost = inventory.get_host("unresposivehost")
1074
+ assert unresposivehost.data.ssh_connect_retries == 1
1075
+
1076
+ fake_ssh_client().connect.side_effect = exception_class()
1077
+
1078
+ with self.assertRaises(ConnectError):
1079
+ unresposivehost.connect(show_errors=False, raise_exceptions=True)
1080
+
1081
+ fake_sleep.assert_called_once()
1082
+ assert fake_ssh_client().connect.call_count == 2
1083
+
1084
+ @patch("pyinfra.connectors.ssh.SSHClient")
1085
+ @patch("pyinfra.connectors.ssh.sleep")
1086
+ def test_ssh_connect_fail_success(self, fake_sleep, fake_ssh_client):
1087
+ for exception_class in (
1088
+ SSHException,
1089
+ gaierror,
1090
+ socket_error,
1091
+ EOFError,
1092
+ ):
1093
+ fake_sleep.reset_mock()
1094
+ fake_ssh_client.reset_mock()
1095
+
1096
+ inventory = make_inventory(
1097
+ hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1}
1098
+ )
1099
+ State(inventory, Config())
1100
+
1101
+ unresposivehost = inventory.get_host("unresposivehost")
1102
+ assert unresposivehost.data.ssh_connect_retries == 1
1103
+
1104
+ fake_ssh_client().connect.side_effect = [exception_class(), MagicMock()]
1105
+
1106
+ unresposivehost.connect(show_errors=False, raise_exceptions=True)
1107
+ fake_sleep.assert_called_once()
1108
+ assert fake_ssh_client().connect.call_count == 2
@@ -21,7 +21,7 @@ Include other_file
21
21
  SSH_CONFIG_OTHER_FILE = """
22
22
  Host 192.168.1.1
23
23
  User "otheruser"
24
- ProxyCommand None
24
+ ProxyCommand None # Commented to get test passing with Paramiko > 3
25
25
  ForwardAgent yes
26
26
  UserKnownHostsFile ~/.ssh/test3
27
27
  """
@@ -7,20 +7,23 @@ from pyinfra.connectors.terraform import TerraformInventoryConnector
7
7
 
8
8
 
9
9
  class TestTerraformConnector(TestCase):
10
- def test_make_names_data_no_output_key(self):
11
- with self.assertRaises(InventoryError) as context:
12
- list(TerraformInventoryConnector.make_names_data())
13
-
14
- assert context.exception.args[0] == "No Terraform output key!"
15
-
16
10
  @patch("pyinfra.connectors.terraform.local.shell")
17
11
  def test_make_names_data_no_output(self, fake_shell):
18
- fake_shell.return_value = json.dumps({})
12
+ fake_shell.return_value = json.dumps(
13
+ {
14
+ "hello": {
15
+ "world": [],
16
+ },
17
+ },
18
+ )
19
19
 
20
20
  with self.assertRaises(InventoryError) as context:
21
21
  list(TerraformInventoryConnector.make_names_data("output_key"))
22
22
 
23
- assert context.exception.args[0] == "No Terraform output with key: `output_key`"
23
+ assert (
24
+ context.exception.args[0]
25
+ == "No Terraform output with key: `output_key`, valid keys:\n - hello.world"
26
+ )
24
27
 
25
28
  @patch("pyinfra.connectors.terraform.local.shell")
26
29
  def test_make_names_data_invalid_output(self, fake_shell):
@@ -69,13 +69,13 @@ class TestVagrantConnector(TestCase):
69
69
  )
70
70
  @patch("pyinfra.connectors.vagrant.path.exists", lambda path: True)
71
71
  def test_make_names_data_with_options(self):
72
- data = VagrantInventoryConnector.make_names_data()
72
+ data = list(VagrantInventoryConnector.make_names_data())
73
73
 
74
74
  assert data == [
75
75
  (
76
76
  "@vagrant/ubuntu16",
77
77
  {
78
- "ssh_port": "2222",
78
+ "ssh_port": 2222,
79
79
  "ssh_user": "vagrant",
80
80
  "ssh_hostname": "127.0.0.1",
81
81
  "ssh_key": "path/to/key",
@@ -85,7 +85,7 @@ class TestVagrantConnector(TestCase):
85
85
  (
86
86
  "@vagrant/centos7",
87
87
  {
88
- "ssh_port": "2200",
88
+ "ssh_port": 2200,
89
89
  "ssh_user": "vagrant",
90
90
  "ssh_hostname": "127.0.0.1",
91
91
  "ssh_key": "path/to/key",
@@ -103,13 +103,13 @@ class TestVagrantConnector(TestCase):
103
103
  ]
104
104
 
105
105
  def test_make_names_data_with_limit(self):
106
- data = VagrantInventoryConnector.make_names_data(limit=("ubuntu16",))
106
+ data = list(VagrantInventoryConnector.make_names_data(name=("ubuntu16",)))
107
107
 
108
108
  assert data == [
109
109
  (
110
110
  "@vagrant/ubuntu16",
111
111
  {
112
- "ssh_port": "2222",
112
+ "ssh_port": 2222,
113
113
  "ssh_user": "vagrant",
114
114
  "ssh_hostname": "127.0.0.1",
115
115
  "ssh_key": "path/to/key",
@@ -120,4 +120,4 @@ class TestVagrantConnector(TestCase):
120
120
 
121
121
  def test_make_names_data_no_matches(self):
122
122
  with self.assertRaises(InventoryError):
123
- VagrantInventoryConnector.make_names_data(limit="nope")
123
+ list(VagrantInventoryConnector.make_names_data(name="nope"))