pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.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.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +115 -97
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +5 -3
- pyinfra/api/config.py +139 -39
- pyinfra/api/connectors.py +5 -2
- pyinfra/api/deploy.py +19 -19
- pyinfra/api/exceptions.py +35 -4
- pyinfra/api/facts.py +62 -86
- pyinfra/api/host.py +102 -15
- pyinfra/api/inventory.py +4 -0
- pyinfra/api/operation.py +188 -120
- pyinfra/api/operations.py +66 -113
- pyinfra/api/state.py +53 -34
- pyinfra/api/util.py +64 -33
- pyinfra/connectors/base.py +65 -20
- pyinfra/connectors/chroot.py +15 -13
- pyinfra/connectors/docker.py +62 -72
- pyinfra/connectors/dockerssh.py +20 -19
- pyinfra/connectors/local.py +32 -22
- pyinfra/connectors/ssh.py +162 -86
- pyinfra/connectors/sshuserclient/client.py +1 -1
- pyinfra/connectors/terraform.py +57 -39
- pyinfra/connectors/util.py +26 -27
- pyinfra/connectors/vagrant.py +27 -26
- pyinfra/context.py +1 -0
- pyinfra/facts/apk.py +7 -2
- pyinfra/facts/apt.py +15 -7
- pyinfra/facts/brew.py +28 -13
- pyinfra/facts/bsdinit.py +9 -6
- pyinfra/facts/cargo.py +6 -3
- pyinfra/facts/choco.py +8 -4
- pyinfra/facts/deb.py +21 -9
- pyinfra/facts/dnf.py +11 -6
- pyinfra/facts/docker.py +30 -5
- pyinfra/facts/files.py +49 -33
- pyinfra/facts/gem.py +7 -2
- pyinfra/facts/git.py +14 -21
- pyinfra/facts/gpg.py +4 -1
- pyinfra/facts/hardware.py +186 -138
- pyinfra/facts/launchd.py +7 -2
- pyinfra/facts/lxd.py +8 -2
- pyinfra/facts/mysql.py +19 -12
- pyinfra/facts/npm.py +3 -1
- pyinfra/facts/openrc.py +8 -2
- pyinfra/facts/pacman.py +13 -5
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +5 -1
- pyinfra/facts/pkgin.py +7 -2
- pyinfra/facts/postgres.py +170 -0
- pyinfra/facts/postgresql.py +5 -162
- pyinfra/facts/rpm.py +21 -15
- pyinfra/facts/runit.py +70 -0
- pyinfra/facts/selinux.py +12 -4
- pyinfra/facts/server.py +240 -82
- pyinfra/facts/snap.py +8 -2
- pyinfra/facts/systemd.py +37 -13
- pyinfra/facts/sysvinit.py +7 -4
- pyinfra/facts/upstart.py +7 -2
- pyinfra/facts/util/packaging.py +3 -2
- pyinfra/facts/vzctl.py +8 -4
- pyinfra/facts/xbps.py +7 -2
- pyinfra/facts/yum.py +10 -5
- pyinfra/facts/zypper.py +9 -4
- pyinfra/operations/apk.py +5 -3
- pyinfra/operations/apt.py +28 -25
- pyinfra/operations/brew.py +60 -29
- pyinfra/operations/bsdinit.py +6 -4
- pyinfra/operations/cargo.py +3 -1
- pyinfra/operations/choco.py +3 -1
- pyinfra/operations/dnf.py +16 -20
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +187 -168
- pyinfra/operations/gem.py +3 -1
- pyinfra/operations/git.py +23 -25
- pyinfra/operations/iptables.py +33 -25
- pyinfra/operations/launchd.py +5 -6
- pyinfra/operations/lxd.py +7 -4
- pyinfra/operations/mysql.py +59 -55
- pyinfra/operations/npm.py +8 -1
- pyinfra/operations/openrc.py +5 -3
- pyinfra/operations/pacman.py +6 -7
- pyinfra/operations/pip.py +19 -12
- pyinfra/operations/pkg.py +3 -1
- pyinfra/operations/pkgin.py +5 -3
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -335
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -29
- pyinfra/operations/server.py +138 -67
- pyinfra/operations/snap.py +3 -1
- pyinfra/operations/ssh.py +18 -16
- pyinfra/operations/systemd.py +18 -12
- pyinfra/operations/sysvinit.py +7 -5
- pyinfra/operations/upstart.py +7 -5
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +177 -0
- pyinfra/operations/util/files.py +24 -16
- pyinfra/operations/util/packaging.py +54 -38
- pyinfra/operations/util/service.py +39 -47
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +5 -3
- pyinfra/operations/yum.py +15 -19
- pyinfra/operations/zypper.py +9 -10
- pyinfra/version.py +5 -2
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/METADATA +51 -58
- pyinfra-3.0.2.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/entry_points.txt +0 -3
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +3 -2
- pyinfra_cli/exceptions.py +75 -43
- pyinfra_cli/inventory.py +52 -31
- pyinfra_cli/log.py +10 -2
- pyinfra_cli/main.py +88 -65
- pyinfra_cli/prints.py +37 -109
- pyinfra_cli/util.py +15 -10
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +9 -9
- tests/test_api/test_api_deploys.py +15 -19
- tests/test_api/test_api_facts.py +4 -5
- tests/test_api/test_api_operations.py +18 -20
- tests/test_api/test_api_util.py +41 -2
- tests/test_cli/test_cli.py +14 -50
- tests/test_cli/test_cli_deploy.py +17 -14
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/test_cli_inventory.py +66 -0
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_dockerssh.py +11 -8
- tests/test_connectors/test_ssh.py +88 -23
- tests/test_connectors/test_sshuserclient.py +1 -1
- tests/test_connectors/test_terraform.py +11 -8
- tests/test_connectors/test_vagrant.py +6 -6
- pyinfra/connectors/ansible.py +0 -175
- pyinfra/connectors/mech.py +0 -189
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -312
- pyinfra/facts/windows.py +0 -366
- pyinfra/facts/windows_files.py +0 -90
- pyinfra/operations/windows.py +0 -59
- pyinfra/operations/windows_files.py +0 -538
- pyinfra-3.0.dev0.dist-info/RECORD +0 -170
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.2.dist-info}/top_level.txt +0 -0
pyinfra/connectors/ssh.py
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Connect to hosts over SSH. This is the default connector and all targets default
|
|
3
|
-
to this meaning you do not need to specify it - ie the following two commands
|
|
4
|
-
are identical:
|
|
5
|
-
|
|
6
|
-
.. code:: shell
|
|
7
|
-
|
|
8
|
-
pyinfra my-host.net ...
|
|
9
|
-
pyinfra @ssh/my-host.net ...
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
1
|
from __future__ import annotations
|
|
13
2
|
|
|
14
3
|
import shlex
|
|
15
|
-
from
|
|
4
|
+
from random import uniform
|
|
5
|
+
from shutil import which
|
|
16
6
|
from socket import error as socket_error, gaierror
|
|
7
|
+
from time import sleep
|
|
17
8
|
from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
|
|
18
9
|
|
|
19
10
|
import click
|
|
20
11
|
from paramiko import AuthenticationException, BadHostKeyException, SFTPClient, SSHException
|
|
21
|
-
from typing_extensions import Unpack
|
|
12
|
+
from typing_extensions import TypedDict, Unpack
|
|
22
13
|
|
|
23
14
|
from pyinfra import logger
|
|
24
15
|
from pyinfra.api.command import QuoteString, StringCommand
|
|
25
16
|
from pyinfra.api.exceptions import ConnectError
|
|
26
17
|
from pyinfra.api.util import get_file_io, memoize
|
|
27
18
|
|
|
28
|
-
from .base import BaseConnector,
|
|
19
|
+
from .base import BaseConnector, DataMeta
|
|
29
20
|
from .ssh_util import get_private_key, raise_connect_error
|
|
30
21
|
from .sshuserclient import SSHClient
|
|
31
22
|
from .util import (
|
|
@@ -41,94 +32,197 @@ if TYPE_CHECKING:
|
|
|
41
32
|
from pyinfra.api.arguments import ConnectorArguments
|
|
42
33
|
|
|
43
34
|
|
|
44
|
-
class
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
class ConnectorData(TypedDict):
|
|
36
|
+
ssh_hostname: str
|
|
37
|
+
ssh_port: int
|
|
38
|
+
ssh_user: str
|
|
39
|
+
ssh_password: str
|
|
40
|
+
ssh_key: str
|
|
41
|
+
ssh_key_password: str
|
|
42
|
+
|
|
43
|
+
ssh_allow_agent: bool
|
|
44
|
+
ssh_look_for_keys: bool
|
|
45
|
+
ssh_forward_agent: bool
|
|
46
|
+
|
|
47
|
+
ssh_config_file: str
|
|
48
|
+
ssh_known_hosts_file: str
|
|
49
|
+
ssh_strict_host_key_checking: str
|
|
50
|
+
|
|
51
|
+
ssh_paramiko_connect_kwargs: dict
|
|
52
|
+
|
|
53
|
+
ssh_connect_retries: int
|
|
54
|
+
ssh_connect_retry_min_delay: float
|
|
55
|
+
ssh_connect_retry_max_delay: float
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
connector_data_meta: dict[str, DataMeta] = {
|
|
59
|
+
"ssh_hostname": DataMeta("SSH hostname"),
|
|
60
|
+
"ssh_port": DataMeta("SSH port"),
|
|
61
|
+
"ssh_user": DataMeta("SSH user"),
|
|
62
|
+
"ssh_password": DataMeta("SSH password"),
|
|
63
|
+
"ssh_key": DataMeta("SSH key filename"),
|
|
64
|
+
"ssh_key_password": DataMeta("SSH key password"),
|
|
65
|
+
"ssh_allow_agent": DataMeta(
|
|
66
|
+
"Whether to use any active SSH agent",
|
|
67
|
+
True,
|
|
68
|
+
),
|
|
69
|
+
"ssh_look_for_keys": DataMeta(
|
|
70
|
+
"Whether to look for private keys",
|
|
71
|
+
True,
|
|
72
|
+
),
|
|
73
|
+
"ssh_forward_agent": DataMeta(
|
|
74
|
+
"Whether to enable SSH forward agent",
|
|
75
|
+
False,
|
|
76
|
+
),
|
|
77
|
+
"ssh_config_file": DataMeta("SSH config filename"),
|
|
78
|
+
"ssh_known_hosts_file": DataMeta("SSH known_hosts filename"),
|
|
79
|
+
"ssh_strict_host_key_checking": DataMeta(
|
|
80
|
+
"SSH strict host key checking",
|
|
81
|
+
"accept-new",
|
|
82
|
+
),
|
|
83
|
+
"ssh_paramiko_connect_kwargs": DataMeta(
|
|
84
|
+
"Override keyword arguments passed into Paramiko's ``SSHClient.connect``"
|
|
85
|
+
),
|
|
86
|
+
"ssh_connect_retries": DataMeta("Number of tries to connect via ssh", 0),
|
|
87
|
+
"ssh_connect_retry_min_delay": DataMeta(
|
|
88
|
+
"Lower bound for random delay between retries",
|
|
89
|
+
0.1,
|
|
90
|
+
),
|
|
91
|
+
"ssh_connect_retry_max_delay": DataMeta(
|
|
92
|
+
"Upper bound for random delay between retries",
|
|
93
|
+
0.5,
|
|
94
|
+
),
|
|
95
|
+
}
|
|
47
96
|
|
|
48
|
-
user = "User to SSH as"
|
|
49
|
-
password = "Password to use for authentication"
|
|
50
|
-
key = "Key file to use for authentication"
|
|
51
|
-
key_password = "Key file password"
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
class SSHConnector(BaseConnector):
|
|
99
|
+
"""
|
|
100
|
+
Connect to hosts over SSH. This is the default connector and all targets default
|
|
101
|
+
to this meaning you do not need to specify it - ie the following two commands
|
|
102
|
+
are identical:
|
|
55
103
|
|
|
56
|
-
|
|
57
|
-
config_file = "Custom SSH config file"
|
|
58
|
-
known_hosts_file = "Custom SSH known hosts file"
|
|
59
|
-
strict_host_key_checking = "Override strict host keys check setting"
|
|
104
|
+
.. code:: shell
|
|
60
105
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
106
|
+
pyinfra my-host.net ...
|
|
107
|
+
pyinfra @ssh/my-host.net ...
|
|
108
|
+
"""
|
|
64
109
|
|
|
110
|
+
__examples_doc__ = """
|
|
111
|
+
An inventory file (``inventory.py``) containing a single SSH target with SSH
|
|
112
|
+
forward agent enabled:
|
|
65
113
|
|
|
66
|
-
|
|
114
|
+
.. code:: python
|
|
67
115
|
|
|
116
|
+
hosts = [
|
|
117
|
+
("my-host.net", {"ssh_forward_agent": True}),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
Multiple hosts sharing the same SSH username:
|
|
121
|
+
|
|
122
|
+
.. code:: python
|
|
123
|
+
|
|
124
|
+
hosts = (
|
|
125
|
+
["my-host-1.net", "my-host-2.net"],
|
|
126
|
+
{"ssh_user": "ssh-user"},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
Multiple hosts with different SSH usernames:
|
|
130
|
+
|
|
131
|
+
.. code:: python
|
|
132
|
+
|
|
133
|
+
hosts = [
|
|
134
|
+
("my-host-1.net", {"ssh_user": "ssh-user"}),
|
|
135
|
+
("my-host-2.net", {"ssh_user": "other-user"}),
|
|
136
|
+
]
|
|
137
|
+
"""
|
|
68
138
|
|
|
69
|
-
class SSHConnector(BaseConnector):
|
|
70
139
|
handles_execution = True
|
|
140
|
+
|
|
141
|
+
data_cls = ConnectorData
|
|
142
|
+
data_meta = connector_data_meta
|
|
143
|
+
data: ConnectorData
|
|
144
|
+
|
|
71
145
|
client: Optional[SSHClient] = None
|
|
72
146
|
|
|
73
|
-
|
|
74
|
-
|
|
147
|
+
@staticmethod
|
|
148
|
+
def make_names_data(name):
|
|
149
|
+
yield "@ssh/{0}".format(name), {"ssh_hostname": name}, []
|
|
75
150
|
|
|
76
151
|
def make_paramiko_kwargs(self) -> dict[str, Any]:
|
|
77
152
|
kwargs = {
|
|
78
153
|
"allow_agent": False,
|
|
79
154
|
"look_for_keys": False,
|
|
80
|
-
"hostname": self.
|
|
155
|
+
"hostname": self.data["ssh_hostname"] or self.host.name,
|
|
81
156
|
# Overrides of SSH config via pyinfra host data
|
|
82
|
-
"_pyinfra_ssh_forward_agent": self.
|
|
83
|
-
"_pyinfra_ssh_config_file": self.
|
|
84
|
-
"_pyinfra_ssh_known_hosts_file": self.
|
|
85
|
-
"_pyinfra_ssh_strict_host_key_checking": self.
|
|
86
|
-
|
|
87
|
-
),
|
|
88
|
-
"_pyinfra_ssh_paramiko_connect_kwargs": self.host.data.get(
|
|
89
|
-
DATA_KEYS.paramiko_connect_kwargs
|
|
90
|
-
),
|
|
157
|
+
"_pyinfra_ssh_forward_agent": self.data["ssh_forward_agent"],
|
|
158
|
+
"_pyinfra_ssh_config_file": self.data["ssh_config_file"],
|
|
159
|
+
"_pyinfra_ssh_known_hosts_file": self.data["ssh_known_hosts_file"],
|
|
160
|
+
"_pyinfra_ssh_strict_host_key_checking": self.data["ssh_strict_host_key_checking"],
|
|
161
|
+
"_pyinfra_ssh_paramiko_connect_kwargs": self.data["ssh_paramiko_connect_kwargs"],
|
|
91
162
|
}
|
|
92
163
|
|
|
93
164
|
for key, value in (
|
|
94
|
-
("username", self.
|
|
95
|
-
("port", int(self.
|
|
165
|
+
("username", self.data["ssh_user"]),
|
|
166
|
+
("port", int(self.data["ssh_port"] or 0)),
|
|
96
167
|
("timeout", self.state.config.CONNECT_TIMEOUT),
|
|
97
168
|
):
|
|
98
169
|
if value:
|
|
99
170
|
kwargs[key] = value
|
|
100
171
|
|
|
101
172
|
# Password auth (boo!)
|
|
102
|
-
ssh_password = self.
|
|
173
|
+
ssh_password = self.data["ssh_password"]
|
|
103
174
|
if ssh_password:
|
|
104
175
|
kwargs["password"] = ssh_password
|
|
105
176
|
|
|
106
177
|
# Key auth!
|
|
107
|
-
ssh_key = self.
|
|
178
|
+
ssh_key = self.data["ssh_key"]
|
|
108
179
|
if ssh_key:
|
|
109
180
|
kwargs["pkey"] = get_private_key(
|
|
110
181
|
self.state,
|
|
111
182
|
key_filename=ssh_key,
|
|
112
|
-
key_password=self.
|
|
183
|
+
key_password=self.data["ssh_key_password"],
|
|
113
184
|
)
|
|
114
185
|
|
|
115
186
|
# No key or password, so let's have paramiko look for SSH agents and user keys
|
|
116
187
|
# unless disabled by the user.
|
|
117
188
|
else:
|
|
118
|
-
kwargs["allow_agent"] = self.
|
|
119
|
-
kwargs["look_for_keys"] = self.
|
|
189
|
+
kwargs["allow_agent"] = self.data["ssh_allow_agent"]
|
|
190
|
+
kwargs["look_for_keys"] = self.data["ssh_look_for_keys"]
|
|
120
191
|
|
|
121
192
|
return kwargs
|
|
122
193
|
|
|
123
194
|
def connect(self) -> None:
|
|
195
|
+
retries = self.data["ssh_connect_retries"]
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
while True:
|
|
199
|
+
try:
|
|
200
|
+
return self._connect()
|
|
201
|
+
except (SSHException, gaierror, socket_error, EOFError):
|
|
202
|
+
if retries == 0:
|
|
203
|
+
raise
|
|
204
|
+
retries -= 1
|
|
205
|
+
min_delay = self.data["ssh_connect_retry_min_delay"]
|
|
206
|
+
max_delay = self.data["ssh_connect_retry_max_delay"]
|
|
207
|
+
sleep(uniform(min_delay, max_delay))
|
|
208
|
+
except SSHException as e:
|
|
209
|
+
raise_connect_error(self.host, "SSH error", e)
|
|
210
|
+
except gaierror as e:
|
|
211
|
+
raise_connect_error(self.host, "Could not resolve hostname", e)
|
|
212
|
+
except socket_error as e:
|
|
213
|
+
raise_connect_error(self.host, "Could not connect", e)
|
|
214
|
+
except EOFError as e:
|
|
215
|
+
raise_connect_error(self.host, "EOF error", e)
|
|
216
|
+
|
|
217
|
+
def _connect(self) -> None:
|
|
124
218
|
"""
|
|
125
219
|
Connect to a single host. Returns the SSH client if successful. Stateless by
|
|
126
220
|
design so can be run in parallel.
|
|
127
221
|
"""
|
|
128
222
|
|
|
129
223
|
kwargs = self.make_paramiko_kwargs()
|
|
130
|
-
logger.debug("Connecting to: %s (%r)", self.host.name, kwargs)
|
|
131
224
|
hostname = kwargs.pop("hostname")
|
|
225
|
+
logger.debug("Connecting to: %s (%r)", hostname, kwargs)
|
|
132
226
|
|
|
133
227
|
self.client = SSHClient()
|
|
134
228
|
|
|
@@ -143,7 +237,7 @@ class SSHConnector(BaseConnector):
|
|
|
143
237
|
continue
|
|
144
238
|
|
|
145
239
|
if key == "pkey" and value:
|
|
146
|
-
auth_kwargs["key"] = self.
|
|
240
|
+
auth_kwargs["key"] = self.data["ssh_key"]
|
|
147
241
|
|
|
148
242
|
auth_args = ", ".join(
|
|
149
243
|
"{0}={1}".format(key, value) for key, value in auth_kwargs.items()
|
|
@@ -170,18 +264,6 @@ class SSHConnector(BaseConnector):
|
|
|
170
264
|
f"Host key for {e.hostname} does not match.",
|
|
171
265
|
)
|
|
172
266
|
|
|
173
|
-
except SSHException as e:
|
|
174
|
-
raise_connect_error(self.host, "SSH error", e)
|
|
175
|
-
|
|
176
|
-
except gaierror:
|
|
177
|
-
raise_connect_error(self.host, "Could not resolve hostname", hostname)
|
|
178
|
-
|
|
179
|
-
except socket_error as e:
|
|
180
|
-
raise_connect_error(self.host, "Could not connect", e)
|
|
181
|
-
|
|
182
|
-
except EOFError as e:
|
|
183
|
-
raise_connect_error(self.host, "EOF error", e)
|
|
184
|
-
|
|
185
267
|
def run_shell_command(
|
|
186
268
|
self,
|
|
187
269
|
command: StringCommand,
|
|
@@ -303,7 +385,7 @@ class SSHConnector(BaseConnector):
|
|
|
303
385
|
|
|
304
386
|
if _sudo or _su_user:
|
|
305
387
|
# Get temp file location
|
|
306
|
-
temp_file = remote_temp_filename or self.
|
|
388
|
+
temp_file = remote_temp_filename or self.host.get_temp_filename(remote_filename)
|
|
307
389
|
|
|
308
390
|
# Copy the file to the tempfile location and add read permissions
|
|
309
391
|
command = StringCommand(
|
|
@@ -395,20 +477,14 @@ class SSHConnector(BaseConnector):
|
|
|
395
477
|
# user connected, so upload to tmp and copy/chown w/sudo and/or su_user
|
|
396
478
|
if _sudo or _doas or _su_user:
|
|
397
479
|
# Get temp file location
|
|
398
|
-
temp_file = remote_temp_filename or self.
|
|
480
|
+
temp_file = remote_temp_filename or self.host.get_temp_filename(remote_filename)
|
|
399
481
|
self._put_file(filename_or_io, temp_file)
|
|
400
482
|
|
|
401
483
|
# Make sure our sudo/su user can access the file
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
elif _sudo_user:
|
|
405
|
-
command = StringCommand("setfacl", "-m", "u:{0}:r".format(_sudo_user), temp_file)
|
|
406
|
-
elif _doas_user:
|
|
407
|
-
command = StringCommand("setfacl", "-m", "u:{0}:r".format(_doas_user), temp_file)
|
|
408
|
-
|
|
409
|
-
if _su_user or _sudo_user or _doas_user:
|
|
484
|
+
other_user = _su_user or _sudo_user or _doas_user
|
|
485
|
+
if other_user:
|
|
410
486
|
status, output = self.run_shell_command(
|
|
411
|
-
|
|
487
|
+
StringCommand("setfacl", "-m", f"u:{other_user}:r", temp_file),
|
|
412
488
|
print_output=print_output,
|
|
413
489
|
print_input=print_input,
|
|
414
490
|
**arguments,
|
|
@@ -459,15 +535,15 @@ class SSHConnector(BaseConnector):
|
|
|
459
535
|
return True
|
|
460
536
|
|
|
461
537
|
def check_can_rsync(self):
|
|
462
|
-
if self.
|
|
538
|
+
if self.data["ssh_key_password"]:
|
|
463
539
|
raise NotImplementedError(
|
|
464
540
|
"Rsync does not currently work with SSH keys needing passwords."
|
|
465
541
|
)
|
|
466
542
|
|
|
467
|
-
if self.
|
|
543
|
+
if self.data["ssh_password"]:
|
|
468
544
|
raise NotImplementedError("Rsync does not currently work with SSH passwords.")
|
|
469
545
|
|
|
470
|
-
if not
|
|
546
|
+
if not which("rsync"):
|
|
471
547
|
raise NotImplementedError("The `rsync` binary is not available on this system.")
|
|
472
548
|
|
|
473
549
|
def rsync(
|
|
@@ -482,8 +558,8 @@ class SSHConnector(BaseConnector):
|
|
|
482
558
|
_sudo = arguments.pop("_sudo", False)
|
|
483
559
|
_sudo_user = arguments.pop("_sudo_user", False)
|
|
484
560
|
|
|
485
|
-
hostname = self.
|
|
486
|
-
user = self.
|
|
561
|
+
hostname = self.data["ssh_hostname"] or self.host.name
|
|
562
|
+
user = self.data["ssh_user"]
|
|
487
563
|
if user:
|
|
488
564
|
user = "{0}@".format(user)
|
|
489
565
|
|
|
@@ -491,27 +567,27 @@ class SSHConnector(BaseConnector):
|
|
|
491
567
|
# To avoid asking for interactive input, specify BatchMode=yes
|
|
492
568
|
ssh_flags.append("-o BatchMode=yes")
|
|
493
569
|
|
|
494
|
-
known_hosts_file = self.
|
|
570
|
+
known_hosts_file = self.data["ssh_known_hosts_file"]
|
|
495
571
|
if known_hosts_file:
|
|
496
572
|
ssh_flags.append(
|
|
497
573
|
'-o \\"UserKnownHostsFile={0}\\"'.format(shlex.quote(known_hosts_file))
|
|
498
574
|
) # never trust users
|
|
499
575
|
|
|
500
|
-
strict_host_key_checking = self.
|
|
576
|
+
strict_host_key_checking = self.data["ssh_strict_host_key_checking"]
|
|
501
577
|
if strict_host_key_checking:
|
|
502
578
|
ssh_flags.append(
|
|
503
579
|
'-o \\"StrictHostKeyChecking={0}\\"'.format(shlex.quote(strict_host_key_checking))
|
|
504
580
|
)
|
|
505
581
|
|
|
506
|
-
ssh_config_file = self.
|
|
582
|
+
ssh_config_file = self.data["ssh_config_file"]
|
|
507
583
|
if ssh_config_file:
|
|
508
584
|
ssh_flags.append("-F {0}".format(shlex.quote(ssh_config_file)))
|
|
509
585
|
|
|
510
|
-
port = self.
|
|
586
|
+
port = self.data["ssh_port"]
|
|
511
587
|
if port:
|
|
512
588
|
ssh_flags.append("-p {0}".format(port))
|
|
513
589
|
|
|
514
|
-
ssh_key = self.
|
|
590
|
+
ssh_key = self.data["ssh_key"]
|
|
515
591
|
if ssh_key:
|
|
516
592
|
ssh_flags.append("-i {0}".format(ssh_key))
|
|
517
593
|
|
pyinfra/connectors/terraform.py
CHANGED
|
@@ -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
|
|
@@ -61,22 +27,74 @@ def _flatten_dict(d: dict, parent_key: str = "", sep: str = "."):
|
|
|
61
27
|
|
|
62
28
|
|
|
63
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.
|
|
33
|
+
|
|
34
|
+
Output is fetched from a flattened JSON dictionary output from ``terraform output
|
|
35
|
+
-json``. For example the following object:
|
|
36
|
+
|
|
37
|
+
.. code:: json
|
|
38
|
+
|
|
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
|
+
}
|
|
50
|
+
|
|
51
|
+
The IP list ``server_group_node_ips`` would be used like so:
|
|
52
|
+
|
|
53
|
+
.. code:: sh
|
|
54
|
+
|
|
55
|
+
pyinfra @terraform/server_group.value.server_group_node_ips ...
|
|
56
|
+
|
|
57
|
+
You can also specify dictionaries to include extra data with hosts:
|
|
58
|
+
|
|
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
|
+
|
|
64
80
|
@staticmethod
|
|
65
|
-
def make_names_data(
|
|
81
|
+
def make_names_data(name=None):
|
|
66
82
|
show_warning()
|
|
67
83
|
|
|
68
|
-
if not
|
|
69
|
-
|
|
84
|
+
if not name:
|
|
85
|
+
name = ""
|
|
70
86
|
|
|
71
87
|
with progress_spinner({"fetch terraform output"}):
|
|
72
88
|
tf_output_raw = local.shell("terraform output -json")
|
|
73
89
|
|
|
90
|
+
assert isinstance(tf_output_raw, str)
|
|
74
91
|
tf_output = json.loads(tf_output_raw)
|
|
75
92
|
tf_output = _flatten_dict(tf_output)
|
|
76
93
|
|
|
77
|
-
tf_output_value = tf_output.get(
|
|
94
|
+
tf_output_value = tf_output.get(name)
|
|
78
95
|
if tf_output_value is None:
|
|
79
|
-
|
|
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}")
|
|
80
98
|
|
|
81
99
|
if not isinstance(tf_output_value, list):
|
|
82
100
|
raise InventoryError(
|
pyinfra/connectors/util.py
CHANGED
|
@@ -40,7 +40,7 @@ def run_local_process(
|
|
|
40
40
|
stdin=None,
|
|
41
41
|
timeout: Optional[int] = None,
|
|
42
42
|
print_output: bool = False,
|
|
43
|
-
print_prefix=
|
|
43
|
+
print_prefix: str = "",
|
|
44
44
|
) -> tuple[int, "CommandOutput"]:
|
|
45
45
|
process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
|
|
46
46
|
|
|
@@ -201,7 +201,10 @@ def execute_command_with_sudo_retry(
|
|
|
201
201
|
if return_code != 0 and output and output.combined_lines:
|
|
202
202
|
last_line = output.combined_lines[-1].line
|
|
203
203
|
if last_line.strip() == "sudo: a password is required":
|
|
204
|
-
|
|
204
|
+
# If we need a password, ask the user for it and attach to the host
|
|
205
|
+
# internal connector data for use when executing future commands.
|
|
206
|
+
sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
|
|
207
|
+
host.connector_data["prompted_sudo_password"] = sudo_password
|
|
205
208
|
return_code, output = execute_command()
|
|
206
209
|
|
|
207
210
|
return return_code, output
|
|
@@ -221,25 +224,6 @@ def write_stdin(stdin, buffer):
|
|
|
221
224
|
buffer.close()
|
|
222
225
|
|
|
223
226
|
|
|
224
|
-
def _get_sudo_password(host: "Host", use_sudo_password) -> str:
|
|
225
|
-
if not host.connector_data.get("sudo_askpass_path"):
|
|
226
|
-
_, output = host.run_shell_command(SUDO_ASKPASS_COMMAND)
|
|
227
|
-
host.connector_data["sudo_askpass_path"] = shlex.quote(output.stdout_lines[0])
|
|
228
|
-
|
|
229
|
-
if use_sudo_password is True:
|
|
230
|
-
sudo_password = host.connector_data.get("sudo_password")
|
|
231
|
-
if not sudo_password:
|
|
232
|
-
sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
|
|
233
|
-
host.connector_data["sudo_password"] = sudo_password
|
|
234
|
-
sudo_password = sudo_password
|
|
235
|
-
elif callable(use_sudo_password):
|
|
236
|
-
sudo_password = use_sudo_password()
|
|
237
|
-
else:
|
|
238
|
-
sudo_password = use_sudo_password
|
|
239
|
-
|
|
240
|
-
return shlex.quote(sudo_password)
|
|
241
|
-
|
|
242
|
-
|
|
243
227
|
def remove_any_sudo_askpass_file(host) -> None:
|
|
244
228
|
sudo_askpass_path = host.connector_data.get("sudo_askpass_path")
|
|
245
229
|
if sudo_askpass_path:
|
|
@@ -273,17 +257,32 @@ def extract_control_arguments(arguments: "ConnectorArguments") -> "ConnectorArgu
|
|
|
273
257
|
return control_arguments
|
|
274
258
|
|
|
275
259
|
|
|
260
|
+
def _ensure_sudo_askpass_set_for_host(host: "Host"):
|
|
261
|
+
if host.connector_data.get("sudo_askpass_path"):
|
|
262
|
+
return
|
|
263
|
+
_, output = host.run_shell_command(SUDO_ASKPASS_COMMAND)
|
|
264
|
+
host.connector_data["sudo_askpass_path"] = shlex.quote(output.stdout_lines[0])
|
|
265
|
+
|
|
266
|
+
|
|
276
267
|
def make_unix_command_for_host(
|
|
277
268
|
state: "State",
|
|
278
269
|
host: "Host",
|
|
279
270
|
command: StringCommand,
|
|
280
271
|
**command_arguments,
|
|
281
272
|
) -> StringCommand:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
273
|
+
if not command_arguments.get("_sudo"):
|
|
274
|
+
# If no sudo, we've nothing to do here
|
|
275
|
+
return make_unix_command(command, **command_arguments)
|
|
276
|
+
|
|
277
|
+
# If the sudo password is not set in the direct arguments,
|
|
278
|
+
# set it from the connector data value.
|
|
279
|
+
if "_sudo_password" not in command_arguments or not command_arguments["_sudo_password"]:
|
|
280
|
+
command_arguments["_sudo_password"] = host.connector_data.get("prompted_sudo_password")
|
|
281
|
+
|
|
282
|
+
if command_arguments["_sudo_password"]:
|
|
283
|
+
# Ensure the askpass path is correctly set and passed through
|
|
284
|
+
_ensure_sudo_askpass_set_for_host(host)
|
|
285
|
+
command_arguments["_sudo_askpass_path"] = host.connector_data["sudo_askpass_path"]
|
|
287
286
|
return make_unix_command(command, **command_arguments)
|
|
288
287
|
|
|
289
288
|
|
|
@@ -339,7 +338,7 @@ def make_unix_command(
|
|
|
339
338
|
[
|
|
340
339
|
"env",
|
|
341
340
|
"SUDO_ASKPASS={0}".format(_sudo_askpass_path),
|
|
342
|
-
MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, _sudo_password)),
|
|
341
|
+
MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, shlex.quote(_sudo_password))),
|
|
343
342
|
],
|
|
344
343
|
)
|
|
345
344
|
|