pyinfra 3.1__py2.py3-none-any.whl → 3.2__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 (52) hide show
  1. pyinfra/api/arguments.py +10 -3
  2. pyinfra/api/deploy.py +12 -2
  3. pyinfra/api/host.py +7 -4
  4. pyinfra/connectors/chroot.py +1 -1
  5. pyinfra/connectors/docker.py +17 -6
  6. pyinfra/connectors/local.py +1 -1
  7. pyinfra/connectors/ssh.py +3 -0
  8. pyinfra/connectors/sshuserclient/client.py +26 -14
  9. pyinfra/facts/apk.py +3 -1
  10. pyinfra/facts/apt.py +62 -2
  11. pyinfra/facts/crontab.py +190 -0
  12. pyinfra/facts/docker.py +6 -0
  13. pyinfra/facts/efibootmgr.py +108 -0
  14. pyinfra/facts/files.py +93 -6
  15. pyinfra/facts/git.py +3 -2
  16. pyinfra/facts/hardware.py +1 -0
  17. pyinfra/facts/mysql.py +1 -2
  18. pyinfra/facts/opkg.py +233 -0
  19. pyinfra/facts/pipx.py +74 -0
  20. pyinfra/facts/podman.py +47 -0
  21. pyinfra/facts/postgres.py +2 -0
  22. pyinfra/facts/selinux.py +3 -1
  23. pyinfra/facts/server.py +39 -77
  24. pyinfra/facts/util/units.py +30 -0
  25. pyinfra/facts/zfs.py +22 -19
  26. pyinfra/local.py +3 -2
  27. pyinfra/operations/apt.py +29 -21
  28. pyinfra/operations/crontab.py +189 -0
  29. pyinfra/operations/docker.py +13 -12
  30. pyinfra/operations/files.py +20 -2
  31. pyinfra/operations/git.py +48 -9
  32. pyinfra/operations/opkg.py +88 -0
  33. pyinfra/operations/pip.py +3 -2
  34. pyinfra/operations/pipx.py +90 -0
  35. pyinfra/operations/postgres.py +15 -11
  36. pyinfra/operations/runit.py +2 -0
  37. pyinfra/operations/server.py +4 -178
  38. pyinfra/operations/zfs.py +14 -14
  39. {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/METADATA +11 -12
  40. {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/RECORD +52 -43
  41. pyinfra_cli/inventory.py +26 -9
  42. pyinfra_cli/prints.py +18 -3
  43. pyinfra_cli/util.py +5 -2
  44. tests/test_cli/test_cli_deploy.py +15 -13
  45. tests/test_cli/test_cli_exceptions.py +2 -2
  46. tests/test_cli/test_cli_inventory.py +53 -0
  47. tests/test_cli/test_cli_util.py +2 -4
  48. tests/test_connectors/test_sshuserclient.py +68 -1
  49. {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/LICENSE.md +0 -0
  50. {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/WHEEL +0 -0
  51. {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/entry_points.txt +0 -0
  52. {pyinfra-3.1.dist-info → pyinfra-3.2.dist-info}/top_level.txt +0 -0
pyinfra_cli/prints.py CHANGED
@@ -281,6 +281,8 @@ def print_results(state: "State"):
281
281
  (logger.info, ["Operation", "Hosts", "Success", "Error", "No Change"]),
282
282
  ]
283
283
 
284
+ totals = {"hosts": 0, "success": 0, "error": 0, "no_change": 0}
285
+
284
286
  for op_hash in state.get_op_order():
285
287
  hosts_in_op = 0
286
288
  hosts_in_op_success: list[str] = []
@@ -306,19 +308,32 @@ def print_results(state: "State"):
306
308
  str(hosts_in_op),
307
309
  ]
308
310
 
311
+ totals["hosts"] += hosts_in_op
312
+
309
313
  if hosts_in_op_success:
310
- row.append(f"{len(hosts_in_op_success)}")
314
+ num_hosts_in_op_success = len(hosts_in_op_success)
315
+ row.append(str(num_hosts_in_op_success))
316
+ totals["success"] += num_hosts_in_op_success
311
317
  else:
312
318
  row.append("-")
319
+
313
320
  if hosts_in_op_error:
314
- row.append(f"{len(hosts_in_op_error)}")
321
+ num_hosts_in_op_error = len(hosts_in_op_error)
322
+ row.append(str(num_hosts_in_op_error))
323
+ totals["error"] += num_hosts_in_op_error
315
324
  else:
316
325
  row.append("-")
326
+
317
327
  if hosts_in_op_no_change:
318
- row.append(f"{len(hosts_in_op_no_change)}")
328
+ num_hosts_in_op_no_change = len(hosts_in_op_no_change)
329
+ row.append(str(num_hosts_in_op_no_change))
330
+ totals["no_change"] += num_hosts_in_op_no_change
319
331
  else:
320
332
  row.append("-")
321
333
 
322
334
  rows.append((logger.info, row))
323
335
 
336
+ totals_row = ["Grand total"] + [str(i) if i else "-" for i in totals.values()]
337
+ rows.append((logger.info, totals_row))
338
+
324
339
  print_rows(rows)
pyinfra_cli/util.py CHANGED
@@ -124,6 +124,9 @@ def json_encode(obj):
124
124
  if isinstance(obj, bytes):
125
125
  return obj.decode()
126
126
 
127
+ if hasattr(obj, "to_json"):
128
+ return obj.to_json()
129
+
127
130
  raise TypeError("Cannot serialize: {0} ({1})".format(type(obj), obj))
128
131
 
129
132
 
@@ -180,13 +183,13 @@ def try_import_module_attribute(path, prefix=None, raise_for_none=True):
180
183
 
181
184
  if module is None:
182
185
  if raise_for_none:
183
- raise CliError(f"No such module: {possible_modules[-1]}")
186
+ raise CliError(f"No such module: {possible_modules[0]}")
184
187
  return
185
188
 
186
189
  attr = getattr(module, attr_name, None)
187
190
  if attr is None:
188
191
  if raise_for_none:
189
- raise CliError(f"No such attribute in module {possible_modules[-1]}: {attr_name}")
192
+ raise CliError(f"No such attribute in module {possible_modules[0]}: {attr_name}")
190
193
  return
191
194
 
192
195
  return attr
@@ -43,26 +43,28 @@ class TestCliDeployState(PatchSSHTestCase):
43
43
  assert executed is False
44
44
 
45
45
  def test_deploy(self):
46
- task_file_path = path.join("tasks", "a_task.py")
46
+ a_task_file_path = path.join("tasks", "a_task.py")
47
+ b_task_file_path = path.join("tasks", "b_task.py")
47
48
  nested_task_path = path.join("tasks", "another_task.py")
48
49
  correct_op_name_and_host_names = [
49
50
  ("First main operation", True), # true for all hosts
50
51
  ("Second main operation", ("somehost",)),
51
- ("{0} | First task operation".format(task_file_path), ("anotherhost",)),
52
- ("{0} | Task order loop 1".format(task_file_path), ("anotherhost",)),
53
- ("{0} | 2nd Task order loop 1".format(task_file_path), ("anotherhost",)),
54
- ("{0} | Task order loop 2".format(task_file_path), ("anotherhost",)),
55
- ("{0} | 2nd Task order loop 2".format(task_file_path), ("anotherhost",)),
52
+ ("{0} | First task operation".format(a_task_file_path), ("anotherhost",)),
53
+ ("{0} | Task order loop 1".format(a_task_file_path), ("anotherhost",)),
54
+ ("{0} | 2nd Task order loop 1".format(a_task_file_path), ("anotherhost",)),
55
+ ("{0} | Task order loop 2".format(a_task_file_path), ("anotherhost",)),
56
+ ("{0} | 2nd Task order loop 2".format(a_task_file_path), ("anotherhost",)),
56
57
  (
57
- "{0} | {1} | Second task operation".format(task_file_path, nested_task_path),
58
+ "{0} | {1} | Second task operation".format(a_task_file_path, nested_task_path),
58
59
  ("anotherhost",),
59
60
  ),
60
- ("{0} | First task operation".format(task_file_path), True),
61
- ("{0} | Task order loop 1".format(task_file_path), True),
62
- ("{0} | 2nd Task order loop 1".format(task_file_path), True),
63
- ("{0} | Task order loop 2".format(task_file_path), True),
64
- ("{0} | 2nd Task order loop 2".format(task_file_path), True),
65
- ("{0} | {1} | Second task operation".format(task_file_path, nested_task_path), True),
61
+ ("{0} | First task operation".format(a_task_file_path), True),
62
+ ("{0} | Task order loop 1".format(a_task_file_path), True),
63
+ ("{0} | 2nd Task order loop 1".format(a_task_file_path), True),
64
+ ("{0} | Task order loop 2".format(a_task_file_path), True),
65
+ ("{0} | 2nd Task order loop 2".format(a_task_file_path), True),
66
+ ("{0} | {1} | Second task operation".format(a_task_file_path, nested_task_path), True),
67
+ ("{0} | Important task operation".format(b_task_file_path), True),
66
68
  ("My deploy | First deploy operation", True),
67
69
  ("My deploy | My nested deploy | First nested deploy operation", True),
68
70
  ("My deploy | Second deploy operation", True),
@@ -38,13 +38,13 @@ class TestCliExceptions(TestCase):
38
38
  def test_no_fact_module(self):
39
39
  self.assert_cli_exception(
40
40
  ["my-server.net", "fact", "not_a_module.SomeFact"],
41
- "No such module: pyinfra.facts.not_a_module",
41
+ "No such module: not_a_module",
42
42
  )
43
43
 
44
44
  def test_no_fact_cls(self):
45
45
  self.assert_cli_exception(
46
46
  ["my-server.net", "fact", "server.NotAFact"],
47
- "No such attribute in module pyinfra.facts.server: NotAFact",
47
+ "No such attribute in module server: NotAFact",
48
48
  )
49
49
 
50
50
 
@@ -64,3 +64,56 @@ class TestCliInventory(PatchSSHTestCase):
64
64
  assert "leftover_data" in inventory.group_data
65
65
  assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
66
66
  assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
67
+
68
+ def test_ignores_variables_with_leading_underscore(self):
69
+ ctx_state.reset()
70
+ ctx_inventory.reset()
71
+
72
+ result = run_cli(
73
+ path.join("tests", "test_cli", "inventories", "invalid.py"),
74
+ "exec",
75
+ "--debug",
76
+ "--",
77
+ "echo hi",
78
+ )
79
+
80
+ assert result.exit_code == 0, result.stdout
81
+ assert (
82
+ 'Ignoring variable "_hosts" in inventory file since it starts with a leading underscore'
83
+ in result.stdout
84
+ )
85
+ assert inventory.hosts == {}
86
+
87
+ def test_only_supports_list_and_tuples(self):
88
+ ctx_state.reset()
89
+ ctx_inventory.reset()
90
+
91
+ result = run_cli(
92
+ path.join("tests", "test_cli", "inventories", "invalid.py"),
93
+ "exec",
94
+ "--debug",
95
+ "--",
96
+ "echo hi",
97
+ )
98
+
99
+ assert result.exit_code == 0, result.stdout
100
+ assert 'Ignoring variable "dict_hosts" in inventory file' in result.stdout, result.stdout
101
+ assert (
102
+ 'Ignoring variable "generator_hosts" in inventory file' in result.stdout
103
+ ), result.stdout
104
+ assert inventory.hosts == {}
105
+
106
+ def test_host_groups_may_only_contain_strings_or_tuples(self):
107
+ ctx_state.reset()
108
+ ctx_inventory.reset()
109
+
110
+ result = run_cli(
111
+ path.join("tests", "test_cli", "inventories", "invalid.py"),
112
+ "exec",
113
+ "--",
114
+ "echo hi",
115
+ )
116
+
117
+ assert result.exit_code == 0, result.stdout
118
+ assert 'Ignoring host group "issue_662"' in result.stdout, result.stdout
119
+ assert inventory.hosts == {}
@@ -30,15 +30,13 @@ class TestCliUtil(TestCase):
30
30
  def test_setup_no_module(self):
31
31
  with self.assertRaises(CliError) as context:
32
32
  get_func_and_args(("no.op",))
33
- assert context.exception.message == "No such module: pyinfra.operations.no"
33
+ assert context.exception.message == "No such module: no"
34
34
 
35
35
  def test_setup_no_op(self):
36
36
  with self.assertRaises(CliError) as context:
37
37
  get_func_and_args(("server.no",))
38
38
 
39
- assert (
40
- context.exception.message == "No such attribute in module pyinfra.operations.server: no"
41
- )
39
+ assert context.exception.message == "No such attribute in module server: no"
42
40
 
43
41
  def test_setup_op_and_args(self):
44
42
  commands = ("pyinfra.operations.server.user", "one", "two", "hello=world")
@@ -1,7 +1,8 @@
1
+ from base64 import b64decode
1
2
  from unittest import TestCase
2
3
  from unittest.mock import mock_open, patch
3
4
 
4
- from paramiko import ProxyCommand
5
+ from paramiko import PKey, ProxyCommand, SSHException
5
6
 
6
7
  from pyinfra.connectors.sshuserclient import SSHClient
7
8
  from pyinfra.connectors.sshuserclient.client import AskPolicy, get_ssh_config
@@ -41,6 +42,30 @@ LOOPING_SSH_CONFIG_DATA = """
41
42
  Include other_file
42
43
  """
43
44
 
45
+ # To ensure that we don't remove things from users hostfiles
46
+ # we should test that all modifications only append to the
47
+ # hostfile, and don't delete any data or comments.
48
+ EXAMPLE_KEY_1 = (
49
+ "AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+"
50
+ "VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/"
51
+ "C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXk"
52
+ "E2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMj"
53
+ "A2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIE"
54
+ "s4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+Ej"
55
+ "qoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/Wnw"
56
+ "H6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk="
57
+ )
58
+
59
+ KNOWN_HOSTS_EXAMPLE_DATA = f"""
60
+ # this is an important comment
61
+
62
+ # another comment after the newline
63
+
64
+ @cert-authority example-domain.lan ssh-rsa {EXAMPLE_KEY_1}
65
+
66
+ 192.168.1.222 ssh-rsa {EXAMPLE_KEY_1}
67
+ """
68
+
44
69
 
45
70
  class TestSSHUserConfigMissing(TestCase):
46
71
  def setUp(self):
@@ -199,3 +224,45 @@ class TestSSHUserConfig(TestCase):
199
224
  port=22,
200
225
  test="kwarg",
201
226
  )
227
+
228
+ def test_missing_hostkey(self):
229
+ client = SSHClient()
230
+ policy = AskPolicy()
231
+ example_hostname = "new_host"
232
+ example_keytype = "ecdsa-sha2-nistp256"
233
+ example_key = (
234
+ "AAAAE2VjZHNhLXNoYTItbmlzdHAyNT"
235
+ "YAAAAIbmlzdHAyNTYAAABBBHNp1NM"
236
+ "ZjxPBuuKwIPfkVJqWaH3oUtW137kIW"
237
+ "P4PlCyACt8zVIIimFhIpwRUidcf7jw"
238
+ "VWPAJvfBjEPqewDApnZQ="
239
+ )
240
+
241
+ key = PKey.from_type_string(
242
+ example_keytype,
243
+ b64decode(example_key),
244
+ )
245
+
246
+ # Check if AskPolicy respects not importing and properly raises SSHException
247
+ with self.subTest("Check user 'no'"):
248
+ with patch("builtins.input", return_value="n"):
249
+ self.assertRaises(
250
+ SSHException, lambda: policy.missing_host_key(client, example_hostname, key)
251
+ )
252
+
253
+ # Check if AskPolicy properly appends to hostfile
254
+ with self.subTest("Check user 'yes'"):
255
+ mock_data = mock_open(read_data=KNOWN_HOSTS_EXAMPLE_DATA)
256
+ # Read mock hostfile
257
+ with patch("pyinfra.connectors.sshuserclient.client.open", mock_data):
258
+ with patch("paramiko.hostkeys.open", mock_data):
259
+ with patch("builtins.input", return_value="y"):
260
+ policy.missing_host_key(client, "new_host", key)
261
+
262
+ # Assert that we appended correctly to the file
263
+ write_call_args = mock_data.return_value.write.call_args
264
+ # Ensure we only wrote once and then closed the handle.
265
+ assert len(write_call_args) == 2
266
+ # Ensure we wrote the correct content
267
+ correct_output = f"{example_hostname} {example_keytype} {example_key}\n"
268
+ assert write_call_args[0][0] == correct_output
File without changes