pyinfra 3.0b0__py2.py3-none-any.whl → 3.0b1__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 (45) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +5 -4
  3. pyinfra/api/arguments_typed.py +12 -2
  4. pyinfra/api/exceptions.py +19 -0
  5. pyinfra/api/facts.py +1 -1
  6. pyinfra/api/host.py +46 -7
  7. pyinfra/api/operation.py +77 -39
  8. pyinfra/api/operations.py +10 -11
  9. pyinfra/api/state.py +11 -2
  10. pyinfra/connectors/base.py +1 -1
  11. pyinfra/connectors/chroot.py +5 -6
  12. pyinfra/connectors/docker.py +11 -10
  13. pyinfra/connectors/dockerssh.py +5 -4
  14. pyinfra/connectors/local.py +5 -5
  15. pyinfra/connectors/ssh.py +44 -23
  16. pyinfra/connectors/terraform.py +9 -6
  17. pyinfra/connectors/util.py +1 -1
  18. pyinfra/connectors/vagrant.py +6 -5
  19. pyinfra/facts/choco.py +1 -1
  20. pyinfra/facts/deb.py +2 -2
  21. pyinfra/facts/postgres.py +168 -0
  22. pyinfra/facts/postgresql.py +5 -164
  23. pyinfra/facts/systemd.py +26 -10
  24. pyinfra/operations/files.py +5 -3
  25. pyinfra/operations/iptables.py +6 -0
  26. pyinfra/operations/pip.py +5 -0
  27. pyinfra/operations/postgres.py +347 -0
  28. pyinfra/operations/postgresql.py +17 -336
  29. pyinfra/operations/systemd.py +5 -3
  30. {pyinfra-3.0b0.dist-info → pyinfra-3.0b1.dist-info}/METADATA +6 -6
  31. {pyinfra-3.0b0.dist-info → pyinfra-3.0b1.dist-info}/RECORD +44 -43
  32. pyinfra_cli/commands.py +3 -2
  33. pyinfra_cli/exceptions.py +5 -0
  34. pyinfra_cli/main.py +2 -0
  35. pyinfra_cli/prints.py +22 -104
  36. tests/test_api/test_api_deploys.py +5 -5
  37. tests/test_api/test_api_operations.py +4 -4
  38. tests/test_connectors/test_ssh.py +52 -0
  39. tests/test_connectors/test_terraform.py +11 -8
  40. tests/test_connectors/test_vagrant.py +3 -3
  41. pyinfra_cli/inventory_dsl.py +0 -23
  42. {pyinfra-3.0b0.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
  43. {pyinfra-3.0b0.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
  44. {pyinfra-3.0b0.dist-info → pyinfra-3.0b1.dist-info}/entry_points.txt +0 -0
  45. {pyinfra-3.0b0.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
pyinfra_cli/prints.py CHANGED
@@ -217,7 +217,7 @@ def pretty_op_name(op_meta):
217
217
 
218
218
  def print_meta(state: "State"):
219
219
  rows: List[Tuple[Callable, Union[List[str], str]]] = [
220
- (logger.info, ["Operation", "Hosts"]),
220
+ (logger.info, ["Operation", "Change", "Conditional Change"]),
221
221
  ]
222
222
 
223
223
  for op_hash in state.get_op_order():
@@ -237,14 +237,18 @@ def print_meta(state: "State"):
237
237
  logger.info,
238
238
  [
239
239
  pretty_op_name(state.op_meta[op_hash]),
240
- "{0}{1}/{2} ({3})".format(
240
+ "-"
241
+ if len(hosts_in_op) == 0
242
+ else "{0} ({1})".format(
241
243
  len(hosts_in_op),
242
- f"-{len(hosts_maybe_in_op)}" if hosts_maybe_in_op else "",
243
- len(state.inventory),
244
- truncate(", ".join(sorted(hosts_in_op + hosts_maybe_in_op)), 48),
245
- )
246
- if hosts_in_op or hosts_maybe_in_op
247
- else "No hosts with changes at this time",
244
+ truncate(", ".join(sorted(hosts_in_op)), 48),
245
+ ),
246
+ "-"
247
+ if len(hosts_maybe_in_op) == 0
248
+ else "{0} ({1})".format(
249
+ len(hosts_maybe_in_op),
250
+ truncate(", ".join(sorted(hosts_maybe_in_op)), 48),
251
+ ),
248
252
  ],
249
253
  )
250
254
  )
@@ -261,23 +265,21 @@ def print_results(state: "State"):
261
265
  hosts_in_op = 0
262
266
  hosts_in_op_success: list[str] = []
263
267
  hosts_in_op_error: list[str] = []
264
- hosts_in_op_no_attempt: list[str] = []
268
+ hosts_in_op_no_change: list[str] = []
265
269
  for host in state.inventory.iter_activated_hosts():
266
270
  if op_hash not in state.ops[host]:
267
271
  continue
268
272
 
269
273
  hosts_in_op += 1
270
274
 
271
- result = state.ops[host][op_hash].operation_meta.did_succeed()
272
- if result is True:
273
- hosts_in_op_success.append(host.name)
274
- elif result is False:
275
- hosts_in_op_error.append(host.name)
275
+ op_meta = state.ops[host][op_hash].operation_meta
276
+ if op_meta.did_succeed():
277
+ if op_meta._did_change():
278
+ hosts_in_op_success.append(host.name)
279
+ else:
280
+ hosts_in_op_no_change.append(host.name)
276
281
  else:
277
- hosts_in_op_no_attempt.append(host.name)
278
-
279
- # if not hosts_in_op:
280
- # continue
282
+ hosts_in_op_error.append(host.name)
281
283
 
282
284
  row = [
283
285
  pretty_op_name(state.op_meta[op_hash]),
@@ -292,95 +294,11 @@ def print_results(state: "State"):
292
294
  row.append(f"{len(hosts_in_op_error)}")
293
295
  else:
294
296
  row.append("-")
295
- if hosts_in_op_no_attempt:
296
- row.append(f"{len(hosts_in_op_no_attempt)}")
297
+ if hosts_in_op_no_change:
298
+ row.append(f"{len(hosts_in_op_no_change)}")
297
299
  else:
298
300
  row.append("-")
299
301
 
300
302
  rows.append((logger.info, row))
301
303
 
302
304
  print_rows(rows)
303
-
304
-
305
- def get_fucked(state: "State"):
306
- group_combinations = _get_group_combinations(state.inventory.iter_activated_hosts())
307
- rows: List[Tuple[Callable, Union[List[str], str]]] = []
308
-
309
- for i, (groups, hosts) in enumerate(group_combinations.items(), 1):
310
- if not hosts:
311
- continue
312
-
313
- if groups:
314
- rows.append(
315
- (
316
- logger.info,
317
- "Groups: {0}".format(
318
- click.style(" / ".join(groups), bold=True),
319
- ),
320
- ),
321
- )
322
- else:
323
- rows.append((logger.info, "Ungrouped:"))
324
-
325
- for host in hosts:
326
- # Didn't connect to this host?
327
- if host not in state.activated_hosts:
328
- rows.append(
329
- (
330
- logger.info,
331
- [
332
- host.style_print_prefix("red", bold=True),
333
- click.style("No connection", "red"),
334
- ],
335
- ),
336
- )
337
- continue
338
-
339
- results = state.results[host]
340
-
341
- meta = state.meta[host]
342
- success_ops = results.success_ops
343
- partial_ops = results.partial_ops
344
- # TODO: type meta object
345
- changed_ops = success_ops - meta.ops_no_change # type: ignore
346
- error_ops = results.error_ops
347
- ignored_error_ops = results.ignored_error_ops
348
-
349
- host_args = ("green",)
350
- host_kwargs = {}
351
-
352
- # If all ops got complete
353
- if results.ops == meta.ops:
354
- # We had some errors - but we ignored them - so "warning" color
355
- if error_ops != 0:
356
- host_args = ("yellow",)
357
-
358
- # Ops did not complete!
359
- else:
360
- host_args = ("red",)
361
- host_kwargs["bold"] = True
362
-
363
- changed_str = "Changed: {0}".format(click.style(f"{changed_ops}", bold=True))
364
- if partial_ops:
365
- changed_str = f"{changed_str} ({partial_ops} partial)"
366
-
367
- error_str = "Errors: {0}".format(click.style(f"{error_ops}", bold=True))
368
- if ignored_error_ops:
369
- error_str = f"{error_str} ({ignored_error_ops} ignored)"
370
-
371
- rows.append(
372
- (
373
- logger.info,
374
- [
375
- host.style_print_prefix(*host_args, **host_kwargs),
376
- changed_str,
377
- "No change: {0}".format(click.style(f"{meta.ops_no_change}", bold=True)),
378
- error_str,
379
- ],
380
- ),
381
- )
382
-
383
- if i != len(group_combinations):
384
- rows.append((lambda m: click.echo(m, err=True), []))
385
-
386
- print_rows(rows)
@@ -40,7 +40,7 @@ class TestDeploysApi(PatchSSHTestCase):
40
40
  run_ops(state)
41
41
 
42
42
  first_op_hash = op_order[0]
43
- assert state.op_meta[first_op_hash].names == {"test_deploy | Server/Shell"}
43
+ assert state.op_meta[first_op_hash].names == {"test_deploy | server.shell"}
44
44
  assert state.ops[somehost][first_op_hash].operation_meta._commands == [
45
45
  StringCommand("echo first command"),
46
46
  ]
@@ -49,7 +49,7 @@ class TestDeploysApi(PatchSSHTestCase):
49
49
  ]
50
50
 
51
51
  second_op_hash = op_order[1]
52
- assert state.op_meta[second_op_hash].names == {"test_deploy | Server/Shell"}
52
+ assert state.op_meta[second_op_hash].names == {"test_deploy | server.shell"}
53
53
  assert state.ops[somehost][second_op_hash].operation_meta._commands == [
54
54
  StringCommand("echo second command"),
55
55
  ]
@@ -104,21 +104,21 @@ class TestDeploysApi(PatchSSHTestCase):
104
104
  run_ops(state)
105
105
 
106
106
  first_op_hash = op_order[0]
107
- assert state.op_meta[first_op_hash].names == {"test_deploy | Server/Shell"}
107
+ assert state.op_meta[first_op_hash].names == {"test_deploy | server.shell"}
108
108
  assert state.ops[somehost][first_op_hash].operation_meta._commands == [
109
109
  StringCommand("echo first command"),
110
110
  ]
111
111
 
112
112
  second_op_hash = op_order[1]
113
113
  assert state.op_meta[second_op_hash].names == {
114
- "test_deploy | test_nested_deploy | Server/Shell",
114
+ "test_deploy | test_nested_deploy | server.shell",
115
115
  }
116
116
  assert state.ops[somehost][second_op_hash].operation_meta._commands == [
117
117
  StringCommand("echo nested command"),
118
118
  ]
119
119
 
120
120
  third_op_hash = op_order[2]
121
- assert state.op_meta[third_op_hash].names == {"test_deploy | Server/Shell"}
121
+ assert state.op_meta[third_op_hash].names == {"test_deploy | server.shell"}
122
122
  assert state.ops[somehost][third_op_hash].operation_meta._commands == [
123
123
  StringCommand("echo second command"),
124
124
  ]
@@ -78,7 +78,7 @@ class TestOperationsApi(PatchSSHTestCase):
78
78
  first_op_hash = op_order[0]
79
79
 
80
80
  # Ensure the op name
81
- assert state.op_meta[first_op_hash].names == {"Files/File"}
81
+ assert state.op_meta[first_op_hash].names == {"files.file"}
82
82
 
83
83
  # Ensure the global kwargs (same for both hosts)
84
84
  somehost_global_arguments = state.ops[somehost][first_op_hash].global_arguments
@@ -433,11 +433,11 @@ class TestNestedOperationsApi(PatchSSHTestCase):
433
433
 
434
434
  try:
435
435
  outer_result = server.shell(commands="echo outer")
436
- assert outer_result._combined_output_lines is None
436
+ assert outer_result._combined_output is None
437
437
 
438
438
  def callback():
439
439
  inner_result = server.shell(commands="echo inner")
440
- assert inner_result._combined_output_lines is not None
440
+ assert inner_result._combined_output is not None
441
441
 
442
442
  python.call(function=callback)
443
443
 
@@ -447,7 +447,7 @@ class TestNestedOperationsApi(PatchSSHTestCase):
447
447
 
448
448
  assert len(state.get_op_order()) == 3
449
449
  assert state.results[somehost].success_ops == 3
450
- assert outer_result._combined_output_lines is not None
450
+ assert outer_result._combined_output is not None
451
451
 
452
452
  disconnect_all(state)
453
453
  finally:
@@ -1001,3 +1001,55 @@ class TestSSHConnector(TestCase):
1001
1001
  "not-another-file",
1002
1002
  print_output=True,
1003
1003
  )
1004
+
1005
+ @patch("pyinfra.connectors.ssh.SSHClient")
1006
+ @patch("time.sleep")
1007
+ def test_ssh_connect_fail_retry(self, fake_sleep, fake_ssh_client):
1008
+ for exception_class in (
1009
+ SSHException,
1010
+ gaierror,
1011
+ socket_error,
1012
+ EOFError,
1013
+ ):
1014
+ inventory = make_inventory(
1015
+ hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1}
1016
+ )
1017
+ State(inventory, Config())
1018
+
1019
+ unresposivehost = inventory.get_host("unresposivehost")
1020
+ assert unresposivehost.data.ssh_connect_retries == 1
1021
+
1022
+ fake_ssh = MagicMock()
1023
+ fake_ssh.connect.side_effect = exception_class()
1024
+ fake_ssh_client.return_value = fake_ssh
1025
+
1026
+ with self.assertRaises(ConnectError):
1027
+ unresposivehost.connect(show_errors=False, raise_exceptions=True)
1028
+ assert fake_sleep.called_once()
1029
+ assert fake_ssh_client.connect.called_twice()
1030
+
1031
+ @patch("pyinfra.connectors.ssh.SSHClient")
1032
+ @patch("time.sleep")
1033
+ def test_ssh_connect_fail_success(self, fake_sleep, fake_ssh_client):
1034
+ for exception_class in (
1035
+ SSHException,
1036
+ gaierror,
1037
+ socket_error,
1038
+ EOFError,
1039
+ ):
1040
+ inventory = make_inventory(
1041
+ hosts=("unresposivehost",), override_data={"ssh_connect_retries": 1}
1042
+ )
1043
+ State(inventory, Config())
1044
+
1045
+ unresposivehost = inventory.get_host("unresposivehost")
1046
+ assert unresposivehost.data.ssh_connect_retries == 1
1047
+
1048
+ connection = MagicMock()
1049
+ fake_ssh = MagicMock()
1050
+ fake_ssh.connect.side_effect = [exception_class(), connection]
1051
+ fake_ssh_client.return_value = fake_ssh
1052
+
1053
+ unresposivehost.connect(show_errors=False, raise_exceptions=True)
1054
+ assert fake_sleep.called_once()
1055
+ assert fake_ssh_client.connect.called_twice()
@@ -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,7 +69,7 @@ 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
  (
@@ -103,7 +103,7 @@ 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
  (
@@ -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"))
@@ -1,23 +0,0 @@
1
- class Host:
2
- name: str
3
- data: dict
4
-
5
- def __init__(self, name, **data) -> None:
6
- self.name = name
7
- self.data = data
8
-
9
-
10
- class Group:
11
- hosts: list[Host]
12
- data: dict
13
-
14
- def __init__(self, *hosts: Host, **data) -> None:
15
- self.hosts = list(hosts)
16
- self.data = data
17
-
18
- def __iter__(self):
19
- for host in self.hosts:
20
- yield host
21
-
22
- def append(self, *hosts: Host) -> None:
23
- self.hosts.extend(hosts)