pyinfra 2.9.2__py2.py3-none-any.whl → 3.0__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 (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.2.dist-info/RECORD +0 -170
  151. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,114 @@
1
+ from getpass import getpass
2
+ from os import path
3
+ from typing import TYPE_CHECKING, Type, Union
4
+
5
+ from paramiko import (
6
+ DSSKey,
7
+ ECDSAKey,
8
+ Ed25519Key,
9
+ PasswordRequiredException,
10
+ PKey,
11
+ RSAKey,
12
+ SSHException,
13
+ )
14
+
15
+ import pyinfra
16
+ from pyinfra.api.exceptions import ConnectError, PyinfraError
17
+
18
+ if TYPE_CHECKING:
19
+ from pyinfra.api.host import Host
20
+ from pyinfra.api.state import State
21
+
22
+
23
+ def raise_connect_error(host: "Host", message, data):
24
+ message = "{0} ({1})".format(message, data)
25
+ raise ConnectError(message)
26
+
27
+
28
+ def _load_private_key_file(filename: str, key_filename: str, key_password: str):
29
+ exception: Union[PyinfraError, SSHException] = PyinfraError("Invalid key: {0}".format(filename))
30
+
31
+ key_cls: Union[Type[RSAKey], Type[DSSKey], Type[ECDSAKey], Type[Ed25519Key]]
32
+
33
+ for key_cls in (RSAKey, DSSKey, ECDSAKey, Ed25519Key):
34
+ try:
35
+ return key_cls.from_private_key_file(
36
+ filename=filename,
37
+ )
38
+
39
+ except PasswordRequiredException:
40
+ if not key_password:
41
+ # If password is not provided, but we're in CLI mode, ask for it. I'm not a
42
+ # huge fan of having CLI specific code in here, but it doesn't really fit
43
+ # anywhere else without duplicating lots of key related code into cli.py.
44
+ if pyinfra.is_cli:
45
+ key_password = getpass(
46
+ "Enter password for private key: {0}: ".format(
47
+ key_filename,
48
+ ),
49
+ )
50
+
51
+ # API mode and no password? We can't continue!
52
+ else:
53
+ raise PyinfraError(
54
+ "Private key file ({0}) is encrypted, set ssh_key_password to "
55
+ "use this key".format(key_filename),
56
+ )
57
+
58
+ try:
59
+ return key_cls.from_private_key_file(
60
+ filename=filename,
61
+ password=key_password,
62
+ )
63
+ except SSHException as e: # key does not match key_cls type
64
+ exception = e
65
+ except SSHException as e: # key does not match key_cls type
66
+ exception = e
67
+ raise exception
68
+
69
+
70
+ def get_private_key(state: "State", key_filename: str, key_password: str) -> PKey:
71
+ if key_filename in state.private_keys:
72
+ return state.private_keys[key_filename]
73
+
74
+ ssh_key_filenames = [
75
+ # Global from executed directory
76
+ path.expanduser(key_filename),
77
+ ]
78
+
79
+ if state.cwd:
80
+ # Relative to the CWD
81
+ path.join(state.cwd, key_filename)
82
+
83
+ key = None
84
+ key_file_exists = False
85
+
86
+ for filename in ssh_key_filenames:
87
+ if not path.isfile(filename):
88
+ continue
89
+
90
+ key_file_exists = True
91
+
92
+ try:
93
+ key = _load_private_key_file(filename, key_filename, key_password)
94
+ break
95
+ except SSHException:
96
+ pass
97
+
98
+ # No break, so no key found
99
+ if not key:
100
+ if not key_file_exists:
101
+ raise PyinfraError("No such private key file: {0}".format(key_filename))
102
+ raise PyinfraError("Invalid private key file: {0}".format(key_filename))
103
+
104
+ # Load any certificate, names from OpenSSH:
105
+ # https://github.com/openssh/openssh-portable/blob/049297de975b92adcc2db77e3fb7046c0e3c695d/ssh-keygen.c#L2453 # noqa: E501
106
+ for certificate_filename in (
107
+ "{0}-cert.pub".format(key_filename),
108
+ "{0}.pub".format(key_filename),
109
+ ):
110
+ if path.isfile(certificate_filename):
111
+ key.load_certificate(certificate_filename)
112
+
113
+ state.private_keys[key_filename] = key
114
+ return key
@@ -166,12 +166,14 @@ class SSHClient(ParamikoClient):
166
166
  forward_agent = _pyinfra_ssh_forward_agent
167
167
 
168
168
  if forward_agent:
169
- # Enable SSH forwarding
170
- session = self.get_transport().open_session()
169
+ transport = self.get_transport()
170
+ assert transport is not None, "No transport"
171
+ session = transport.open_session()
171
172
  AgentRequestHandler(session)
172
173
 
173
174
  def gateway(self, hostname, host_port, target, target_port):
174
175
  transport = self.get_transport()
176
+ assert transport is not None, "No transport"
175
177
  return transport.open_channel(
176
178
  "direct-tcpip",
177
179
  (target, target_port),
@@ -185,7 +187,7 @@ class SSHClient(ParamikoClient):
185
187
  ssh_config_file=None,
186
188
  strict_host_key_checking=None,
187
189
  ):
188
- cfg = {"port": 22}
190
+ cfg: dict = {"port": 22}
189
191
  cfg.update(initial_cfg or {})
190
192
 
191
193
  forward_agent = False
@@ -1,37 +1,3 @@
1
- """
2
- .. warning::
3
- This connector is in alpha and may change in future releases.
4
-
5
- Generate one or more SSH hosts from a Terraform output variable. The variable
6
- must be a list of hostnames or IP addresses that ``pyinfra`` can connect to
7
- over SSH. Currently there is no support for specifying SSH user/pass/port/key
8
- from Terraform, these must be provided via ``pyinfra`` group data or ``--data``
9
- CLI flags.
10
-
11
- Output is fetched from a flattened JSON dictionary output from ``terraform output
12
- -json``. For example the following object:
13
-
14
- .. code:: json
15
-
16
- {
17
- "server_group": {
18
- "value": {
19
- "server_group_node_ips": [
20
- "1.2.3.4",
21
- "1.2.3.5",
22
- "1.2.3.6"
23
- ]
24
- }
25
- }
26
- }
27
-
28
- The IP list ``server_group_node_ips`` would be used like so:
29
-
30
- .. code:: python
31
-
32
- pyinfra @terraform/server_group.value.server_group_node_ips ...
33
- """
34
-
35
1
  import json
36
2
 
37
3
  from pyinfra import local, logger
@@ -39,6 +5,8 @@ from pyinfra.api.exceptions import InventoryError
39
5
  from pyinfra.api.util import memoize
40
6
  from pyinfra.progress import progress_spinner
41
7
 
8
+ from .base import BaseConnector
9
+
42
10
 
43
11
  @memoize
44
12
  def show_warning():
@@ -58,44 +26,97 @@ def _flatten_dict(d: dict, parent_key: str = "", sep: str = "."):
58
26
  return dict(_flatten_dict_gen(d, parent_key, sep))
59
27
 
60
28
 
61
- def make_names_data(output_key=None):
62
- show_warning()
29
+ class TerraformInventoryConnector(BaseConnector):
30
+ """
31
+ Generate one or more SSH hosts from a Terraform output variable. The variable
32
+ must be a list of hostnames or dictionaries.
63
33
 
64
- if not output_key:
65
- raise InventoryError("No Terraform output key!")
34
+ Output is fetched from a flattened JSON dictionary output from ``terraform output
35
+ -json``. For example the following object:
66
36
 
67
- with progress_spinner({"fetch terraform output"}):
68
- tf_output_raw = local.shell("terraform output -json")
37
+ .. code:: json
69
38
 
70
- tf_output = json.loads(tf_output_raw)
71
- tf_output = _flatten_dict(tf_output)
39
+ {
40
+ "server_group": {
41
+ "value": {
42
+ "server_group_node_ips": [
43
+ "1.2.3.4",
44
+ "1.2.3.5",
45
+ "1.2.3.6"
46
+ ]
47
+ }
48
+ }
49
+ }
72
50
 
73
- tf_output_value = tf_output.get(output_key)
74
- if tf_output_value is None:
75
- keys = "\n".join(f" - {k}" for k in tf_output.keys())
76
- raise InventoryError(f"No Terraform output with key: `{output_key}`, valid keys:\n{keys}")
51
+ The IP list ``server_group_node_ips`` would be used like so:
77
52
 
78
- if not isinstance(tf_output_value, list):
79
- raise InventoryError(
80
- "Invalid Terraform output type, should be `list`, got "
81
- f"`{type(tf_output_value).__name__}`",
82
- )
53
+ .. code:: sh
83
54
 
84
- for ssh_target in tf_output_value:
85
- if isinstance(ssh_target, dict):
86
- name = ssh_target.pop("name", ssh_target.get("ssh_hostname"))
87
- if name is None:
88
- raise InventoryError(
89
- "Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
90
- )
91
- yield f"@terraform/{name}", ssh_target, ["@terraform"]
55
+ pyinfra @terraform/server_group.value.server_group_node_ips ...
92
56
 
93
- elif isinstance(ssh_target, str):
94
- data = {"ssh_hostname": ssh_target}
95
- yield f"@terraform/{ssh_target}", data, ["@terraform"]
57
+ You can also specify dictionaries to include extra data with hosts:
96
58
 
97
- else:
59
+ .. code:: json
60
+
61
+ {
62
+ "server_group": {
63
+ "value": {
64
+ "server_group_node_ips": [
65
+ {
66
+ "ssh_hostname": "1.2.3.4",
67
+ "ssh_user": "ssh-user"
68
+ },
69
+ {
70
+ "ssh_hostname": "1.2.3.5",
71
+ "ssh_user": "ssh-user"
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ }
77
+
78
+ """
79
+
80
+ @staticmethod
81
+ def make_names_data(name=None):
82
+ show_warning()
83
+
84
+ if not name:
85
+ name = ""
86
+
87
+ with progress_spinner({"fetch terraform output"}):
88
+ tf_output_raw = local.shell("terraform output -json")
89
+
90
+ assert isinstance(tf_output_raw, str)
91
+ tf_output = json.loads(tf_output_raw)
92
+ tf_output = _flatten_dict(tf_output)
93
+
94
+ tf_output_value = tf_output.get(name)
95
+ if tf_output_value is None:
96
+ keys = "\n".join(f" - {k}" for k in tf_output.keys())
97
+ raise InventoryError(f"No Terraform output with key: `{name}`, valid keys:\n{keys}")
98
+
99
+ if not isinstance(tf_output_value, list):
98
100
  raise InventoryError(
99
- "Invalid Terraform list item, should be `dict` or `str` got "
100
- f"`{type(ssh_target).__name__}`",
101
+ "Invalid Terraform output type, should be `list`, got "
102
+ f"`{type(tf_output_value).__name__}`",
101
103
  )
104
+
105
+ for ssh_target in tf_output_value:
106
+ if isinstance(ssh_target, dict):
107
+ name = ssh_target.pop("name", ssh_target.get("ssh_hostname"))
108
+ if name is None:
109
+ raise InventoryError(
110
+ "Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
111
+ )
112
+ yield f"@terraform/{name}", ssh_target, ["@terraform"]
113
+
114
+ elif isinstance(ssh_target, str):
115
+ data = {"ssh_hostname": ssh_target}
116
+ yield f"@terraform/{ssh_target}", data, ["@terraform"]
117
+
118
+ else:
119
+ raise InventoryError(
120
+ "Invalid Terraform list item, should be `dict` or `str` got "
121
+ f"`{type(ssh_target).__name__}`",
122
+ )