pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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 (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +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
@@ -0,0 +1,309 @@
1
+ """
2
+ This file as originally part of the "sshuserclient" pypi package. The GitHub
3
+ source has now vanished (https://github.com/tobald/sshuserclient).
4
+ """
5
+
6
+ from os import path
7
+
8
+ from gevent.lock import BoundedSemaphore
9
+ from paramiko import (
10
+ HostKeys,
11
+ MissingHostKeyPolicy,
12
+ ProxyCommand,
13
+ SSHClient as ParamikoClient,
14
+ SSHException,
15
+ )
16
+ from paramiko.agent import AgentRequestHandler
17
+ from paramiko.hostkeys import HostKeyEntry
18
+ from typing_extensions import override
19
+
20
+ from pyinfra import logger
21
+ from pyinfra.api.util import memoize
22
+
23
+ from .config import SSHConfig
24
+
25
+ HOST_KEYS_LOCK = BoundedSemaphore()
26
+
27
+
28
+ class StrictPolicy(MissingHostKeyPolicy):
29
+ @override
30
+ def missing_host_key(self, client, hostname, key):
31
+ logger.error("No host key for {0} found in known_hosts".format(hostname))
32
+ raise SSHException(
33
+ "StrictPolicy: No host key for {0} found in known_hosts".format(hostname),
34
+ )
35
+
36
+
37
+ def append_hostkey(client, hostname, key):
38
+ """Append hostname to the clients host_keys_file"""
39
+
40
+ with HOST_KEYS_LOCK:
41
+ # The paramiko client saves host keys incorrectly whereas the host keys object does
42
+ # this correctly, so use that with the client filename variable.
43
+ # See: https://github.com/paramiko/paramiko/pull/1989
44
+ host_key_entry = HostKeyEntry([hostname], key)
45
+ if host_key_entry is None:
46
+ raise SSHException(
47
+ "Append Hostkey: Failed to parse host {0}, could not append to hostfile".format(
48
+ hostname
49
+ ),
50
+ )
51
+ with open(client._host_keys_filename, "a") as host_keys_file:
52
+ hk_entry = host_key_entry.to_line()
53
+ if hk_entry is None:
54
+ raise SSHException(f"Append Hostkey: Failed to append hostkey ({host_key_entry})")
55
+
56
+ host_keys_file.write(hk_entry)
57
+
58
+
59
+ class AcceptNewPolicy(MissingHostKeyPolicy):
60
+ @override
61
+ def missing_host_key(self, client, hostname, key):
62
+ logger.warning(
63
+ (
64
+ f"No host key for {hostname} found in known_hosts, "
65
+ "accepting & adding to host keys file"
66
+ ),
67
+ )
68
+
69
+ append_hostkey(client, hostname, key)
70
+ logger.warning("Added host key for {0} to known_hosts".format(hostname))
71
+
72
+
73
+ class AskPolicy(MissingHostKeyPolicy):
74
+ @override
75
+ def missing_host_key(self, client, hostname, key):
76
+ should_continue = input(
77
+ "No host key for {0} found in known_hosts, do you want to continue [y/n] ".format(
78
+ hostname,
79
+ ),
80
+ )
81
+ if should_continue.lower() != "y":
82
+ raise SSHException(
83
+ "AskPolicy: No host key for {0} found in known_hosts".format(hostname),
84
+ )
85
+ append_hostkey(client, hostname, key)
86
+ logger.warning("Added host key for {0} to known_hosts".format(hostname))
87
+ return
88
+
89
+
90
+ class WarningPolicy(MissingHostKeyPolicy):
91
+ @override
92
+ def missing_host_key(self, client, hostname, key):
93
+ logger.warning("No host key for {0} found in known_hosts".format(hostname))
94
+
95
+
96
+ def get_missing_host_key_policy(policy):
97
+ if policy is None or policy == "ask":
98
+ return AskPolicy()
99
+ if policy == "no" or policy == "off":
100
+ return WarningPolicy()
101
+ if policy == "yes":
102
+ return StrictPolicy()
103
+ if policy == "accept-new":
104
+ return AcceptNewPolicy()
105
+ raise SSHException("Invalid value StrictHostKeyChecking={}".format(policy))
106
+
107
+
108
+ @memoize
109
+ def get_ssh_config(user_config_file=None):
110
+ logger.debug("Loading SSH config: %s", user_config_file)
111
+
112
+ if user_config_file is None:
113
+ user_config_file = path.expanduser("~/.ssh/config")
114
+
115
+ if path.exists(user_config_file):
116
+ with open(user_config_file, encoding="utf-8") as f:
117
+ ssh_config = SSHConfig()
118
+ ssh_config.parse(f)
119
+ return ssh_config
120
+
121
+
122
+ @memoize
123
+ def get_host_keys(filename):
124
+ with HOST_KEYS_LOCK:
125
+ host_keys = HostKeys()
126
+
127
+ try:
128
+ host_keys.load(filename)
129
+ # When paramiko encounters a bad host keys line it sometimes bails the
130
+ # entire load incorrectly.
131
+ # See: https://github.com/paramiko/paramiko/pull/1990
132
+ except Exception as e:
133
+ logger.warning("Failed to load host keys from {0}: {1}".format(filename, e))
134
+
135
+ return host_keys
136
+
137
+
138
+ class SSHClient(ParamikoClient):
139
+ """
140
+ An SSHClient which honors ssh_config and supports proxyjumping
141
+ original idea at http://bitprophet.org/blog/2012/11/05/gateway-solutions/.
142
+ """
143
+
144
+ @override
145
+ def connect( # type: ignore[override]
146
+ self,
147
+ hostname,
148
+ _pyinfra_ssh_forward_agent=None,
149
+ _pyinfra_ssh_config_file=None,
150
+ _pyinfra_ssh_known_hosts_file=None,
151
+ _pyinfra_ssh_strict_host_key_checking=None,
152
+ _pyinfra_ssh_paramiko_connect_kwargs=None,
153
+ **kwargs,
154
+ ):
155
+ (
156
+ hostname,
157
+ config,
158
+ forward_agent,
159
+ missing_host_key_policy,
160
+ host_keys_file,
161
+ keep_alive,
162
+ ) = self.parse_config(
163
+ hostname,
164
+ kwargs,
165
+ ssh_config_file=_pyinfra_ssh_config_file,
166
+ strict_host_key_checking=_pyinfra_ssh_strict_host_key_checking,
167
+ )
168
+ self.set_missing_host_key_policy(missing_host_key_policy)
169
+ config.update(kwargs)
170
+
171
+ if _pyinfra_ssh_known_hosts_file:
172
+ host_keys_file = _pyinfra_ssh_known_hosts_file
173
+
174
+ # Overwrite paramiko empty defaults with @memoize-d host keys object
175
+ self._host_keys = get_host_keys(host_keys_file)
176
+ self._host_keys_filename = host_keys_file
177
+
178
+ if _pyinfra_ssh_paramiko_connect_kwargs:
179
+ config.update(_pyinfra_ssh_paramiko_connect_kwargs)
180
+
181
+ self._ssh_config = config
182
+ super().connect(hostname, **config)
183
+
184
+ if _pyinfra_ssh_forward_agent is not None:
185
+ forward_agent = _pyinfra_ssh_forward_agent
186
+
187
+ if keep_alive:
188
+ transport = self.get_transport()
189
+ assert transport is not None, "No transport"
190
+ transport.set_keepalive(keep_alive)
191
+
192
+ if forward_agent:
193
+ transport = self.get_transport()
194
+ assert transport is not None, "No transport"
195
+ session = transport.open_session()
196
+ AgentRequestHandler(session)
197
+
198
+ def gateway(self, hostname, host_port, target, target_port):
199
+ transport = self.get_transport()
200
+ assert transport is not None, "No transport"
201
+ return transport.open_channel(
202
+ "direct-tcpip",
203
+ (target, target_port),
204
+ (hostname, host_port),
205
+ )
206
+
207
+ def parse_config(
208
+ self,
209
+ hostname,
210
+ initial_cfg=None,
211
+ ssh_config_file=None,
212
+ strict_host_key_checking=None,
213
+ ):
214
+ cfg: dict = {"port": 22}
215
+ cfg.update(initial_cfg or {})
216
+
217
+ keep_alive = 0
218
+ forward_agent = False
219
+ missing_host_key_policy = get_missing_host_key_policy(strict_host_key_checking)
220
+ host_keys_file = path.expanduser("~/.ssh/known_hosts") # OpenSSH default
221
+
222
+ ssh_config = get_ssh_config(ssh_config_file)
223
+ if not ssh_config:
224
+ return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
225
+
226
+ host_config = ssh_config.lookup(hostname)
227
+ forward_agent = host_config.get("forwardagent") == "yes"
228
+
229
+ # If not overridden, apply any StrictHostKeyChecking
230
+ if strict_host_key_checking is None and "stricthostkeychecking" in host_config:
231
+ missing_host_key_policy = get_missing_host_key_policy(
232
+ host_config["stricthostkeychecking"],
233
+ )
234
+
235
+ if "userknownhostsfile" in host_config:
236
+ host_keys_file = path.expanduser(host_config["userknownhostsfile"])
237
+
238
+ if "hostname" in host_config:
239
+ hostname = host_config["hostname"]
240
+
241
+ if "user" in host_config:
242
+ cfg["username"] = host_config["user"]
243
+
244
+ if "identityfile" in host_config:
245
+ cfg["key_filename"] = host_config["identityfile"]
246
+
247
+ if "port" in host_config:
248
+ cfg["port"] = int(host_config["port"])
249
+
250
+ if "serveraliveinterval" in host_config:
251
+ keep_alive = int(host_config["serveraliveinterval"])
252
+
253
+ if "proxycommand" in host_config:
254
+ cfg["sock"] = ProxyCommand(host_config["proxycommand"])
255
+
256
+ elif "proxyjump" in host_config:
257
+ hops = host_config["proxyjump"].split(",")
258
+ sock = None
259
+
260
+ for i, hop in enumerate(hops):
261
+ hop_hostname, hop_config = self.derive_shorthand(ssh_config, hop)
262
+ logger.debug("SSH ProxyJump through %s:%s", hop_hostname, hop_config["port"])
263
+
264
+ c = SSHClient()
265
+ c.connect(
266
+ hop_hostname, _pyinfra_ssh_config_file=ssh_config_file, sock=sock, **hop_config
267
+ )
268
+
269
+ if i == len(hops) - 1:
270
+ target = hostname
271
+ target_config = {"port": cfg["port"]}
272
+ else:
273
+ target, target_config = self.derive_shorthand(ssh_config, hops[i + 1])
274
+
275
+ sock = c.gateway(hostname, cfg["port"], target, target_config["port"])
276
+ cfg["sock"] = sock
277
+
278
+ return hostname, cfg, forward_agent, missing_host_key_policy, host_keys_file, keep_alive
279
+
280
+ @staticmethod
281
+ def derive_shorthand(ssh_config, host_string):
282
+ shorthand_config = {}
283
+ user_hostport = host_string.rsplit("@", 1)
284
+ hostport = user_hostport.pop()
285
+ user = user_hostport[0] if user_hostport and user_hostport[0] else None
286
+ if user:
287
+ shorthand_config["username"] = user
288
+
289
+ # IPv6: can't reliably tell where addr ends and port begins, so don't
290
+ # try (and don't bother adding special syntax either, user should avoid
291
+ # this situation by using port=).
292
+ if hostport.count(":") > 1:
293
+ hostname = hostport
294
+ # IPv4: can split on ':' reliably.
295
+ else:
296
+ host_port = hostport.rsplit(":", 1)
297
+ hostname = host_port.pop(0) or None
298
+ if host_port and host_port[0]:
299
+ shorthand_config["port"] = int(host_port[0])
300
+
301
+ base_config = ssh_config.lookup(hostname)
302
+
303
+ config = {
304
+ "port": base_config.get("port", 22),
305
+ "username": base_config.get("user"),
306
+ }
307
+ config.update(shorthand_config)
308
+
309
+ return hostname, config
@@ -0,0 +1,102 @@
1
+ """
2
+ This file as originally part of the "sshuserclient" pypi package. The GitHub
3
+ source has now vanished (https://github.com/tobald/sshuserclient).
4
+ """
5
+
6
+ import glob
7
+ import re
8
+ from os import environ, path
9
+
10
+ import paramiko.config
11
+ from gevent.subprocess import CalledProcessError, check_call
12
+ from paramiko import SSHConfig as ParamikoSSHConfig
13
+ from typing_extensions import override
14
+
15
+ from pyinfra import logger
16
+
17
+ SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)")
18
+
19
+
20
+ class FakeInvokeResult:
21
+ ok = False
22
+
23
+
24
+ class FakeInvoke:
25
+ @staticmethod
26
+ def run(cmd, *args, **kwargs):
27
+ result = FakeInvokeResult()
28
+
29
+ try:
30
+ cmd = [environ["SHELL"], cmd]
31
+ try:
32
+ code = check_call(cmd)
33
+ except CalledProcessError as e:
34
+ code = e.returncode
35
+ result.ok = code == 0
36
+ except Exception as e:
37
+ logger.warning(
38
+ ("pyinfra encountered an error loading SSH config match exec {0}: {1}").format(
39
+ cmd,
40
+ e,
41
+ ),
42
+ )
43
+
44
+ return result
45
+
46
+
47
+ paramiko.config.invoke = FakeInvoke # type: ignore
48
+
49
+
50
+ def _expand_include_statements(file_obj, parsed_files=None):
51
+ parsed_lines = []
52
+
53
+ for line in file_obj.readlines():
54
+ line = line.strip()
55
+ if not line or line.startswith("#"):
56
+ continue
57
+
58
+ match = re.match(SETTINGS_REGEX, line)
59
+ if not match:
60
+ parsed_lines.append(line)
61
+ continue
62
+
63
+ key = match.group(1).lower()
64
+ value = match.group(2)
65
+
66
+ if key != "include":
67
+ parsed_lines.append(line)
68
+ continue
69
+
70
+ if parsed_files is None:
71
+ parsed_files = []
72
+
73
+ # The path can be relative to its parent configuration file
74
+ if path.isabs(value) is False and value[0] != "~":
75
+ folder = path.dirname(file_obj.name)
76
+ value = path.join(folder, value)
77
+
78
+ value = path.expanduser(value)
79
+
80
+ for filename in glob.iglob(value):
81
+ if path.isfile(filename):
82
+ if filename in parsed_files:
83
+ raise Exception(
84
+ "Include loop detected in ssh config file: %s" % filename,
85
+ )
86
+ with open(filename, encoding="utf-8") as fd:
87
+ parsed_files.append(filename)
88
+ parsed_lines.extend(_expand_include_statements(fd, parsed_files))
89
+
90
+ return parsed_lines
91
+
92
+
93
+ class SSHConfig(ParamikoSSHConfig):
94
+ """
95
+ an SSHConfig that supports includes directives
96
+ https://github.com/paramiko/paramiko/pull/1194
97
+ """
98
+
99
+ @override
100
+ def parse(self, file_obj):
101
+ file_obj = _expand_include_statements(file_obj)
102
+ return super().parse(file_obj)
@@ -0,0 +1,135 @@
1
+ import json
2
+
3
+ from typing_extensions import override
4
+
5
+ from pyinfra import local, logger
6
+ from pyinfra.api.exceptions import InventoryError
7
+ from pyinfra.api.util import memoize
8
+ from pyinfra.progress import progress_spinner
9
+
10
+ from .base import BaseConnector
11
+
12
+
13
+ @memoize
14
+ def show_warning() -> None:
15
+ logger.warning("The @terraform connector is in beta!")
16
+
17
+
18
+ def _flatten_dict_gen(d, parent_key, sep):
19
+ for k, v in d.items():
20
+ new_key = parent_key + sep + k if parent_key else k
21
+ yield new_key, v
22
+ if isinstance(v, dict):
23
+ yield from _flatten_dict(v, new_key, sep=sep).items()
24
+
25
+
26
+ def _flatten_dict(d: dict, parent_key: str = "", sep: str = "."):
27
+ return dict(_flatten_dict_gen(d, parent_key, sep))
28
+
29
+
30
+ class TerraformInventoryConnector(BaseConnector):
31
+ """
32
+ Generate one or more SSH hosts from a Terraform output variable. The variable
33
+ must be a list of hostnames or dictionaries.
34
+
35
+ Output is fetched from a flattened JSON dictionary output from ``terraform output
36
+ -json``. For example the following object:
37
+
38
+ .. code:: json
39
+
40
+ {
41
+ "server_group": {
42
+ "value": {
43
+ "server_group_node_ips": [
44
+ "1.2.3.4",
45
+ "1.2.3.5",
46
+ "1.2.3.6"
47
+ ]
48
+ }
49
+ }
50
+ }
51
+
52
+ The IP list ``server_group_node_ips`` would be used like so:
53
+
54
+ .. code:: sh
55
+
56
+ pyinfra @terraform/server_group.value.server_group_node_ips ...
57
+
58
+ You can also specify dictionaries to include extra data with hosts:
59
+
60
+ .. code:: json
61
+
62
+ {
63
+ "server_group": {
64
+ "value": {
65
+ "server_group_node_ips": [
66
+ {
67
+ "ssh_hostname": "1.2.3.4",
68
+ "ssh_user": "ssh-user"
69
+ },
70
+ {
71
+ "ssh_hostname": "1.2.3.5",
72
+ "ssh_user": "ssh-user"
73
+ }
74
+ ]
75
+ }
76
+ }
77
+ }
78
+
79
+ """
80
+
81
+ @override
82
+ @staticmethod
83
+ def make_names_data(name=None):
84
+ show_warning()
85
+
86
+ if not name:
87
+ # This is the default which allows one to create a Terraform output
88
+ # "pyinfra" and directly call: pyinfra @terraform ...
89
+ name = "pyinfra_inventory.value"
90
+
91
+ with progress_spinner({"fetch terraform output"}):
92
+ tf_output_raw = local.shell("terraform output -json")
93
+
94
+ assert isinstance(tf_output_raw, str)
95
+ tf_output = json.loads(tf_output_raw)
96
+ tf_output = _flatten_dict(tf_output)
97
+
98
+ tf_output_value = tf_output.get(name)
99
+ if tf_output_value is None:
100
+ keys = "\n".join(f" - {k}" for k in tf_output.keys())
101
+ raise InventoryError(f"No Terraform output with key: `{name}`, valid keys:\n{keys}")
102
+
103
+ if not isinstance(tf_output_value, (list, dict)):
104
+ raise InventoryError(
105
+ "Invalid Terraform output type, should be `list`, got "
106
+ f"`{type(tf_output_value).__name__}`",
107
+ )
108
+
109
+ if isinstance(tf_output_value, list):
110
+ tf_output_value = {
111
+ "all": tf_output_value,
112
+ }
113
+
114
+ for group_name, hosts in tf_output_value.items():
115
+ if not isinstance(hosts, list):
116
+ raise InventoryError(
117
+ "Invalid Terraform map value type, all values should be `list`, got "
118
+ f"`{type(hosts).__name__}`",
119
+ )
120
+ for host in hosts:
121
+ if isinstance(host, dict):
122
+ name = host.pop("name", host.get("ssh_hostname"))
123
+ if name is None:
124
+ raise InventoryError(
125
+ "Invalid Terraform list item, missing `name` or `ssh_hostname` keys",
126
+ )
127
+ yield f"@terraform/{name}", host, ["@terraform", group_name]
128
+ elif isinstance(host, str):
129
+ data = {"ssh_hostname": host}
130
+ yield f"@terraform/{host}", data, ["@terraform", group_name]
131
+ else:
132
+ raise InventoryError(
133
+ "Invalid Terraform list item, should be `dict` or `str` got "
134
+ f"`{type(host).__name__}`",
135
+ )