pyinfra 2.9.2__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.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +261 -255
- pyinfra/api/arguments_typed.py +77 -0
- pyinfra/api/command.py +66 -53
- pyinfra/api/config.py +27 -22
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +2 -24
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +77 -113
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +17 -25
- pyinfra/api/operation.py +232 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +55 -70
- pyinfra/connectors/base.py +150 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +227 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +195 -207
- pyinfra/connectors/ssh.py +528 -615
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +212 -137
- pyinfra/connectors/vagrant.py +55 -48
- pyinfra/context.py +3 -2
- pyinfra/facts/docker.py +1 -0
- pyinfra/facts/files.py +45 -32
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +1 -1
- pyinfra/facts/hardware.py +4 -2
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/mysql.py +1 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +5 -161
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +77 -30
- pyinfra/facts/systemd.py +29 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/util/packaging.py +4 -2
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +3 -3
- pyinfra/operations/apt.py +25 -47
- pyinfra/operations/brew.py +7 -14
- pyinfra/operations/bsdinit.py +4 -4
- pyinfra/operations/cargo.py +1 -1
- pyinfra/operations/choco.py +1 -1
- pyinfra/operations/dnf.py +4 -4
- pyinfra/operations/files.py +108 -321
- pyinfra/operations/gem.py +1 -1
- pyinfra/operations/git.py +6 -37
- pyinfra/operations/iptables.py +2 -10
- pyinfra/operations/launchd.py +1 -1
- pyinfra/operations/lxd.py +1 -9
- pyinfra/operations/mysql.py +5 -28
- pyinfra/operations/npm.py +1 -1
- pyinfra/operations/openrc.py +1 -1
- pyinfra/operations/pacman.py +3 -3
- pyinfra/operations/pip.py +14 -15
- pyinfra/operations/pkg.py +1 -1
- pyinfra/operations/pkgin.py +3 -3
- pyinfra/operations/postgres.py +347 -0
- pyinfra/operations/postgresql.py +17 -380
- pyinfra/operations/python.py +2 -17
- pyinfra/operations/selinux.py +5 -28
- pyinfra/operations/server.py +59 -84
- pyinfra/operations/snap.py +1 -3
- pyinfra/operations/ssh.py +8 -23
- pyinfra/operations/systemd.py +7 -7
- pyinfra/operations/sysvinit.py +3 -12
- pyinfra/operations/upstart.py +4 -4
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/files.py +2 -2
- pyinfra/operations/util/packaging.py +6 -24
- pyinfra/operations/util/service.py +18 -37
- pyinfra/operations/vzctl.py +2 -2
- pyinfra/operations/xbps.py +3 -3
- pyinfra/operations/yum.py +4 -4
- pyinfra/operations/zypper.py +4 -4
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
- pyinfra-3.0b1.dist-info/RECORD +163 -0
- pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +2 -0
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +83 -42
- pyinfra_cli/inventory.py +19 -4
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +93 -129
- pyinfra_cli/util.py +60 -29
- tests/test_api/test_api.py +2 -0
- tests/test_api/test_api_arguments.py +13 -13
- tests/test_api/test_api_deploys.py +28 -29
- tests/test_api/test_api_facts.py +60 -98
- tests/test_api/test_api_operations.py +100 -200
- tests/test_cli/test_cli.py +18 -49
- tests/test_cli/test_cli_deploy.py +11 -37
- tests/test_cli/test_cli_exceptions.py +50 -19
- tests/test_cli/util.py +1 -1
- tests/test_connectors/test_chroot.py +6 -6
- tests/test_connectors/test_docker.py +4 -4
- tests/test_connectors/test_dockerssh.py +38 -50
- tests/test_connectors/test_local.py +11 -12
- tests/test_connectors/test_ssh.py +66 -107
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +4 -4
- pyinfra/api/operation.pyi +0 -117
- pyinfra/connectors/ansible.py +0 -171
- pyinfra/connectors/mech.py +0 -186
- pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
- pyinfra/connectors/winrm.py +0 -320
- 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 -551
- pyinfra-2.9.2.dist-info/RECORD +0 -170
- pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
- tests/test_connectors/test_ansible.py +0 -64
- tests/test_connectors/test_mech.py +0 -126
- tests/test_connectors/test_winrm.py +0 -76
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
- {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
pyinfra_cli/prints.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import platform
|
|
3
5
|
import re
|
|
@@ -50,12 +52,6 @@ def jsonify(data, *args, **kwargs):
|
|
|
50
52
|
return json.dumps(data, *args, **kwargs)
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
def print_state_facts(state: "State"):
|
|
54
|
-
click.echo(err=True)
|
|
55
|
-
click.echo("--> Facts:", err=True)
|
|
56
|
-
click.echo(jsonify(state.facts, indent=4, default=json_encode), err=True)
|
|
57
|
-
|
|
58
|
-
|
|
59
55
|
def print_state_operations(state: "State"):
|
|
60
56
|
state_ops = {host: ops for host, ops in state.ops.items() if state.is_host_in_limit(host)}
|
|
61
57
|
|
|
@@ -76,7 +72,7 @@ def print_state_operations(state: "State"):
|
|
|
76
72
|
click.echo(
|
|
77
73
|
" {0} (names={1}, hosts={2})".format(
|
|
78
74
|
op_hash,
|
|
79
|
-
meta
|
|
75
|
+
meta.names,
|
|
80
76
|
hosts,
|
|
81
77
|
),
|
|
82
78
|
err=True,
|
|
@@ -161,22 +157,22 @@ def print_support_info():
|
|
|
161
157
|
|
|
162
158
|
def print_rows(rows):
|
|
163
159
|
# Go through the rows and work out all the widths in each column
|
|
164
|
-
|
|
160
|
+
row_column_widths: list[list[int]] = []
|
|
165
161
|
|
|
166
162
|
for _, columns in rows:
|
|
167
163
|
if isinstance(columns, str):
|
|
168
164
|
continue
|
|
169
165
|
|
|
170
166
|
for i, column in enumerate(columns):
|
|
171
|
-
if i >= len(
|
|
172
|
-
|
|
167
|
+
if i >= len(row_column_widths):
|
|
168
|
+
row_column_widths.append([])
|
|
173
169
|
|
|
174
170
|
# Length of the column (with ansi codes removed)
|
|
175
171
|
width = len(_strip_ansi(column.strip()))
|
|
176
|
-
|
|
172
|
+
row_column_widths[i].append(width)
|
|
177
173
|
|
|
178
174
|
# Get the max width of each column and add 4 padding spaces
|
|
179
|
-
column_widths = [max(widths) + 4 for widths in
|
|
175
|
+
column_widths = [max(widths) + 4 for widths in row_column_widths]
|
|
180
176
|
|
|
181
177
|
# Now print each column, keeping text justified to the widths above
|
|
182
178
|
for func, columns in rows:
|
|
@@ -202,139 +198,107 @@ def print_rows(rows):
|
|
|
202
198
|
func(line)
|
|
203
199
|
|
|
204
200
|
|
|
205
|
-
def
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
def truncate(text, max_length):
|
|
202
|
+
if len(text) <= max_length:
|
|
203
|
+
return text
|
|
208
204
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
if groups:
|
|
214
|
-
rows.append(
|
|
215
|
-
(
|
|
216
|
-
logger.info,
|
|
217
|
-
"Groups: {0}".format(
|
|
218
|
-
click.style(" / ".join(groups), bold=True),
|
|
219
|
-
),
|
|
220
|
-
),
|
|
221
|
-
)
|
|
222
|
-
else:
|
|
223
|
-
rows.append((logger.info, "Ungrouped:"))
|
|
224
|
-
|
|
225
|
-
for host in hosts:
|
|
226
|
-
meta = state.meta[host]
|
|
227
|
-
|
|
228
|
-
# Didn't connect to this host?
|
|
229
|
-
if host not in state.activated_hosts:
|
|
230
|
-
rows.append(
|
|
231
|
-
(
|
|
232
|
-
logger.info,
|
|
233
|
-
[
|
|
234
|
-
host.style_print_prefix("red", bold=True),
|
|
235
|
-
click.style("No connection", "red"),
|
|
236
|
-
],
|
|
237
|
-
),
|
|
238
|
-
)
|
|
239
|
-
continue
|
|
205
|
+
text = text[: max_length - 3]
|
|
206
|
+
return f"{text}..."
|
|
240
207
|
|
|
241
|
-
rows.append(
|
|
242
|
-
(
|
|
243
|
-
logger.info,
|
|
244
|
-
[
|
|
245
|
-
host.print_prefix,
|
|
246
|
-
"Operations: {0}".format(meta["ops"]),
|
|
247
|
-
"Change: {0}".format(meta["ops_change"]),
|
|
248
|
-
"No change: {0}".format(meta["ops_no_change"]),
|
|
249
|
-
],
|
|
250
|
-
),
|
|
251
|
-
)
|
|
252
208
|
|
|
253
|
-
|
|
254
|
-
|
|
209
|
+
def pretty_op_name(op_meta):
|
|
210
|
+
name = list(op_meta.names)[0]
|
|
255
211
|
|
|
256
|
-
|
|
212
|
+
if op_meta.args:
|
|
213
|
+
name = "{0} ({1})".format(name, ", ".join(str(arg) for arg in op_meta.args))
|
|
257
214
|
|
|
215
|
+
return name
|
|
258
216
|
|
|
259
|
-
def print_results(state: "State"):
|
|
260
|
-
group_combinations = _get_group_combinations(state.inventory.iter_activated_hosts())
|
|
261
|
-
rows: List[Tuple[Callable, Union[List[str], str]]] = []
|
|
262
217
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
218
|
+
def print_meta(state: "State"):
|
|
219
|
+
rows: List[Tuple[Callable, Union[List[str], str]]] = [
|
|
220
|
+
(logger.info, ["Operation", "Change", "Conditional Change"]),
|
|
221
|
+
]
|
|
266
222
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
223
|
+
for op_hash in state.get_op_order():
|
|
224
|
+
hosts_in_op = []
|
|
225
|
+
hosts_maybe_in_op = []
|
|
226
|
+
for host in state.inventory.iter_activated_hosts():
|
|
227
|
+
if op_hash in state.ops[host]:
|
|
228
|
+
op_data = state.get_op_data_for_host(host, op_hash)
|
|
229
|
+
if op_data.operation_meta._maybe_is_change:
|
|
230
|
+
if op_data.global_arguments["_if"]:
|
|
231
|
+
hosts_maybe_in_op.append(host.name)
|
|
232
|
+
else:
|
|
233
|
+
hosts_in_op.append(host.name)
|
|
234
|
+
|
|
235
|
+
rows.append(
|
|
236
|
+
(
|
|
237
|
+
logger.info,
|
|
238
|
+
[
|
|
239
|
+
pretty_op_name(state.op_meta[op_hash]),
|
|
240
|
+
"-"
|
|
241
|
+
if len(hosts_in_op) == 0
|
|
242
|
+
else "{0} ({1})".format(
|
|
243
|
+
len(hosts_in_op),
|
|
244
|
+
truncate(", ".join(sorted(hosts_in_op)), 48),
|
|
273
245
|
),
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
for host in hosts:
|
|
280
|
-
# Didn't connect to this host?
|
|
281
|
-
if host not in state.activated_hosts:
|
|
282
|
-
rows.append(
|
|
283
|
-
(
|
|
284
|
-
logger.info,
|
|
285
|
-
[
|
|
286
|
-
host.style_print_prefix("red", bold=True),
|
|
287
|
-
click.style("No connection", "red"),
|
|
288
|
-
],
|
|
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),
|
|
289
251
|
),
|
|
290
|
-
|
|
291
|
-
|
|
252
|
+
],
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
print_rows(rows)
|
|
292
257
|
|
|
293
|
-
results = state.results[host]
|
|
294
258
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
changed_ops = success_ops - meta["ops_no_change"] # type: ignore
|
|
300
|
-
error_ops = results["error_ops"]
|
|
301
|
-
ignored_error_ops = results["ignored_error_ops"]
|
|
259
|
+
def print_results(state: "State"):
|
|
260
|
+
rows: List[Tuple[Callable, Union[List[str], str]]] = [
|
|
261
|
+
(logger.info, ["Operation", "Hosts", "Success", "Error", "No Change"]),
|
|
262
|
+
]
|
|
302
263
|
|
|
303
|
-
|
|
304
|
-
|
|
264
|
+
for op_hash in state.get_op_order():
|
|
265
|
+
hosts_in_op = 0
|
|
266
|
+
hosts_in_op_success: list[str] = []
|
|
267
|
+
hosts_in_op_error: list[str] = []
|
|
268
|
+
hosts_in_op_no_change: list[str] = []
|
|
269
|
+
for host in state.inventory.iter_activated_hosts():
|
|
270
|
+
if op_hash not in state.ops[host]:
|
|
271
|
+
continue
|
|
305
272
|
|
|
306
|
-
|
|
307
|
-
if results["ops"] == meta["ops"]:
|
|
308
|
-
# We had some errors - but we ignored them - so "warning" color
|
|
309
|
-
if error_ops != 0:
|
|
310
|
-
host_args = ("yellow",)
|
|
273
|
+
hosts_in_op += 1
|
|
311
274
|
|
|
312
|
-
|
|
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)
|
|
313
281
|
else:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
],
|
|
334
|
-
),
|
|
335
|
-
)
|
|
282
|
+
hosts_in_op_error.append(host.name)
|
|
283
|
+
|
|
284
|
+
row = [
|
|
285
|
+
pretty_op_name(state.op_meta[op_hash]),
|
|
286
|
+
str(hosts_in_op),
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
if hosts_in_op_success:
|
|
290
|
+
row.append(f"{len(hosts_in_op_success)}")
|
|
291
|
+
else:
|
|
292
|
+
row.append("-")
|
|
293
|
+
if hosts_in_op_error:
|
|
294
|
+
row.append(f"{len(hosts_in_op_error)}")
|
|
295
|
+
else:
|
|
296
|
+
row.append("-")
|
|
297
|
+
if hosts_in_op_no_change:
|
|
298
|
+
row.append(f"{len(hosts_in_op_no_change)}")
|
|
299
|
+
else:
|
|
300
|
+
row.append("-")
|
|
336
301
|
|
|
337
|
-
|
|
338
|
-
rows.append((lambda m: click.echo(m, err=True), []))
|
|
302
|
+
rows.append((logger.info, row))
|
|
339
303
|
|
|
340
304
|
print_rows(rows)
|
pyinfra_cli/util.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import os
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from importlib import import_module
|
|
7
|
+
from importlib.util import find_spec
|
|
5
8
|
from io import IOBase
|
|
6
9
|
from os import path
|
|
7
10
|
from pathlib import Path
|
|
8
|
-
from types import FunctionType, ModuleType
|
|
9
|
-
from typing import
|
|
11
|
+
from types import CodeType, FunctionType, ModuleType
|
|
12
|
+
from typing import Callable
|
|
10
13
|
|
|
11
14
|
import click
|
|
12
15
|
import gevent
|
|
@@ -16,16 +19,20 @@ from pyinfra.api.command import PyinfraCommand
|
|
|
16
19
|
from pyinfra.api.exceptions import PyinfraError
|
|
17
20
|
from pyinfra.api.host import HostData
|
|
18
21
|
from pyinfra.api.operation import OperationMeta
|
|
22
|
+
from pyinfra.api.state import (
|
|
23
|
+
State,
|
|
24
|
+
StateHostMeta,
|
|
25
|
+
StateHostResults,
|
|
26
|
+
StateOperationHostData,
|
|
27
|
+
StateOperationMeta,
|
|
28
|
+
)
|
|
19
29
|
from pyinfra.context import ctx_config, ctx_host
|
|
20
30
|
from pyinfra.progress import progress_spinner
|
|
21
31
|
|
|
22
32
|
from .exceptions import CliError, UnexpectedExternalError
|
|
23
33
|
|
|
24
|
-
if TYPE_CHECKING:
|
|
25
|
-
from pyinfra.api.state import State
|
|
26
|
-
|
|
27
34
|
# Cache for compiled Python deploy code
|
|
28
|
-
PYTHON_CODES = {}
|
|
35
|
+
PYTHON_CODES: dict[str, CodeType] = {}
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
def is_subdir(child, parent):
|
|
@@ -45,25 +52,26 @@ def exec_file(filename, return_locals: bool = False, is_deploy_code: bool = Fals
|
|
|
45
52
|
|
|
46
53
|
if filename not in PYTHON_CODES:
|
|
47
54
|
with open(filename, "r", encoding="utf-8") as f:
|
|
48
|
-
|
|
55
|
+
code_str = f.read()
|
|
49
56
|
|
|
50
|
-
code = compile(
|
|
57
|
+
code = compile(code_str, filename, "exec")
|
|
51
58
|
PYTHON_CODES[filename] = code
|
|
52
59
|
|
|
53
60
|
# Create some base attributes for our "module"
|
|
54
|
-
data = {
|
|
55
|
-
"__file__": filename,
|
|
56
|
-
}
|
|
61
|
+
data = {"__file__": filename}
|
|
57
62
|
|
|
58
63
|
# Execute the code with locals/globals going into the dict above
|
|
59
64
|
try:
|
|
60
65
|
exec(PYTHON_CODES[filename], data)
|
|
66
|
+
except PyinfraError:
|
|
67
|
+
# Raise pyinfra errors as-is
|
|
68
|
+
raise
|
|
61
69
|
except Exception as e:
|
|
62
|
-
|
|
63
|
-
raise
|
|
70
|
+
# Wrap & re-raise errors in user code so we highlight filename/etc
|
|
64
71
|
raise UnexpectedExternalError(e, filename)
|
|
72
|
+
finally:
|
|
73
|
+
state.current_exec_filename = old_current_exec_filename
|
|
65
74
|
|
|
66
|
-
state.current_exec_filename = old_current_exec_filename
|
|
67
75
|
return data
|
|
68
76
|
|
|
69
77
|
|
|
@@ -75,7 +83,16 @@ def json_encode(obj):
|
|
|
75
83
|
if isinstance(obj, PyinfraCommand):
|
|
76
84
|
return repr(obj)
|
|
77
85
|
|
|
78
|
-
if isinstance(
|
|
86
|
+
if isinstance(
|
|
87
|
+
obj,
|
|
88
|
+
(
|
|
89
|
+
OperationMeta,
|
|
90
|
+
StateOperationMeta,
|
|
91
|
+
StateOperationHostData,
|
|
92
|
+
StateHostMeta,
|
|
93
|
+
StateHostResults,
|
|
94
|
+
),
|
|
95
|
+
):
|
|
79
96
|
return repr(obj)
|
|
80
97
|
|
|
81
98
|
# Python types
|
|
@@ -133,28 +150,42 @@ def parse_cli_arg(arg):
|
|
|
133
150
|
return arg
|
|
134
151
|
|
|
135
152
|
|
|
136
|
-
def try_import_module_attribute(path, prefix=None):
|
|
137
|
-
|
|
138
|
-
|
|
153
|
+
def try_import_module_attribute(path, prefix=None, raise_for_none=True):
|
|
154
|
+
if ":" in path:
|
|
155
|
+
# Allow a.module.name:function syntax
|
|
156
|
+
mod_path, attr_name = path.rsplit(":", 1)
|
|
157
|
+
else:
|
|
158
|
+
# And also a.module.name.function
|
|
159
|
+
mod_path, attr_name = path.rsplit(".", 1)
|
|
139
160
|
|
|
161
|
+
possible_modules = [mod_path]
|
|
140
162
|
if prefix:
|
|
141
|
-
|
|
163
|
+
possible_modules.append(f"{prefix}.{mod_path}")
|
|
164
|
+
|
|
165
|
+
module = None
|
|
166
|
+
|
|
167
|
+
for possible in possible_modules:
|
|
142
168
|
try:
|
|
143
|
-
module
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
# First use find_spec which checks if the possible module exists *without* importing
|
|
170
|
+
# it, thus any import errors it contains still get properly raised to the user.
|
|
171
|
+
spec = find_spec(possible)
|
|
172
|
+
except ModuleNotFoundError:
|
|
173
|
+
continue
|
|
174
|
+
else:
|
|
175
|
+
if spec is not None:
|
|
176
|
+
module = import_module(possible)
|
|
177
|
+
break
|
|
148
178
|
|
|
149
179
|
if module is None:
|
|
150
|
-
|
|
151
|
-
module
|
|
152
|
-
|
|
153
|
-
raise CliError(f"No such module: {full_path}")
|
|
180
|
+
if raise_for_none:
|
|
181
|
+
raise CliError(f"No such module: {possible_modules[-1]}")
|
|
182
|
+
return
|
|
154
183
|
|
|
155
184
|
attr = getattr(module, attr_name, None)
|
|
156
185
|
if attr is None:
|
|
157
|
-
|
|
186
|
+
if raise_for_none:
|
|
187
|
+
raise CliError(f"No such attribute in module {possible_modules[-1]}: {attr_name}")
|
|
188
|
+
return
|
|
158
189
|
|
|
159
190
|
return attr
|
|
160
191
|
|
tests/test_api/test_api.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
|
+
from unittest.mock import patch
|
|
2
3
|
|
|
3
4
|
from paramiko import SSHException
|
|
4
5
|
|
|
@@ -60,6 +61,7 @@ class TestInventoryApi(TestCase):
|
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
class TestStateApi(PatchSSHTestCase):
|
|
64
|
+
@patch("pyinfra.connectors.base.raise_if_bad_type", lambda *args, **kwargs: None)
|
|
63
65
|
def test_fail_percent(self):
|
|
64
66
|
inventory = make_inventory(
|
|
65
67
|
(
|
|
@@ -12,44 +12,44 @@ class TestOperationKwargs(TestCase):
|
|
|
12
12
|
state = State(config=config, inventory=inventory)
|
|
13
13
|
|
|
14
14
|
kwargs, keys = pop_global_arguments({}, state=state, host=inventory.get_host("somehost"))
|
|
15
|
-
assert kwargs["
|
|
15
|
+
assert kwargs["_sudo"] == "config-value"
|
|
16
16
|
|
|
17
17
|
def test_get_from_host(self):
|
|
18
18
|
config = Config(SUDO="config-value")
|
|
19
|
-
inventory = Inventory(([("somehost", {"
|
|
19
|
+
inventory = Inventory(([("somehost", {"_sudo": True})], {}))
|
|
20
20
|
|
|
21
21
|
state = State(config=config, inventory=inventory)
|
|
22
22
|
|
|
23
23
|
kwargs, keys = pop_global_arguments({}, state=state, host=inventory.get_host("somehost"))
|
|
24
|
-
assert kwargs["
|
|
24
|
+
assert kwargs["_sudo"] is True
|
|
25
25
|
|
|
26
26
|
def test_get_from_state_deploy_kwargs(self):
|
|
27
27
|
config = Config(SUDO="config-value")
|
|
28
|
-
inventory = Inventory(([("somehost", {"
|
|
28
|
+
inventory = Inventory(([("somehost", {"_sudo": False})], {}))
|
|
29
29
|
somehost = inventory.get_host("somehost")
|
|
30
30
|
|
|
31
31
|
state = State(config=config, inventory=inventory)
|
|
32
|
-
somehost.current_deploy_kwargs = {"
|
|
32
|
+
somehost.current_deploy_kwargs = {"_sudo": True}
|
|
33
33
|
|
|
34
34
|
kwargs, keys = pop_global_arguments({}, state=state, host=somehost)
|
|
35
|
-
assert kwargs["
|
|
35
|
+
assert kwargs["_sudo"] is True
|
|
36
36
|
|
|
37
37
|
def test_get_from_kwargs(self):
|
|
38
38
|
config = Config(SUDO="config-value")
|
|
39
|
-
inventory = Inventory(([("somehost", {"
|
|
39
|
+
inventory = Inventory(([("somehost", {"_sudo": False})], {}))
|
|
40
40
|
somehost = inventory.get_host("somehost")
|
|
41
41
|
|
|
42
42
|
state = State(config=config, inventory=inventory)
|
|
43
43
|
somehost.current_deploy_kwargs = {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
44
|
+
"_sudo": False,
|
|
45
|
+
"_sudo_user": "deploy-kwarg-user",
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
kwargs, keys = pop_global_arguments(
|
|
49
|
-
{"
|
|
49
|
+
{"_sudo": True},
|
|
50
50
|
state=state,
|
|
51
51
|
host=somehost,
|
|
52
52
|
)
|
|
53
|
-
assert kwargs["
|
|
54
|
-
assert kwargs["
|
|
55
|
-
assert "
|
|
53
|
+
assert kwargs["_sudo"] is True
|
|
54
|
+
assert kwargs["_sudo_user"] == "deploy-kwarg-user"
|
|
55
|
+
assert "_sudo" in keys
|
|
@@ -24,7 +24,7 @@ class TestDeploysApi(PatchSSHTestCase):
|
|
|
24
24
|
|
|
25
25
|
connect_all(state)
|
|
26
26
|
|
|
27
|
-
@deploy
|
|
27
|
+
@deploy()
|
|
28
28
|
def test_deploy(state=None, host=None):
|
|
29
29
|
server.shell(commands=["echo first command"])
|
|
30
30
|
server.shell(commands=["echo second command"])
|
|
@@ -36,40 +36,36 @@ class TestDeploysApi(PatchSSHTestCase):
|
|
|
36
36
|
# Ensure we have an op
|
|
37
37
|
assert len(op_order) == 2
|
|
38
38
|
|
|
39
|
+
# Ensure run ops works
|
|
40
|
+
run_ops(state)
|
|
41
|
+
|
|
39
42
|
first_op_hash = op_order[0]
|
|
40
|
-
assert state.op_meta[first_op_hash]
|
|
41
|
-
assert state.ops[somehost][first_op_hash]
|
|
43
|
+
assert state.op_meta[first_op_hash].names == {"test_deploy | server.shell"}
|
|
44
|
+
assert state.ops[somehost][first_op_hash].operation_meta._commands == [
|
|
42
45
|
StringCommand("echo first command"),
|
|
43
46
|
]
|
|
44
|
-
assert state.ops[anotherhost][first_op_hash]
|
|
47
|
+
assert state.ops[anotherhost][first_op_hash].operation_meta._commands == [
|
|
45
48
|
StringCommand("echo first command"),
|
|
46
49
|
]
|
|
47
50
|
|
|
48
51
|
second_op_hash = op_order[1]
|
|
49
|
-
assert state.op_meta[second_op_hash]
|
|
50
|
-
assert state.ops[somehost][second_op_hash]
|
|
52
|
+
assert state.op_meta[second_op_hash].names == {"test_deploy | server.shell"}
|
|
53
|
+
assert state.ops[somehost][second_op_hash].operation_meta._commands == [
|
|
51
54
|
StringCommand("echo second command"),
|
|
52
55
|
]
|
|
53
|
-
assert state.ops[anotherhost][second_op_hash]
|
|
56
|
+
assert state.ops[anotherhost][second_op_hash].operation_meta._commands == [
|
|
54
57
|
StringCommand("echo second command"),
|
|
55
58
|
]
|
|
56
59
|
|
|
57
|
-
# Ensure run ops works
|
|
58
|
-
run_ops(state)
|
|
59
|
-
|
|
60
60
|
# Ensure ops completed OK
|
|
61
|
-
assert state.results[somehost]
|
|
62
|
-
assert state.results[somehost]
|
|
63
|
-
assert state.results[anotherhost]
|
|
64
|
-
assert state.results[anotherhost]
|
|
61
|
+
assert state.results[somehost].success_ops == 2
|
|
62
|
+
assert state.results[somehost].ops == 2
|
|
63
|
+
assert state.results[anotherhost].success_ops == 2
|
|
64
|
+
assert state.results[anotherhost].ops == 2
|
|
65
65
|
|
|
66
66
|
# And w/o errors
|
|
67
|
-
assert state.results[somehost]
|
|
68
|
-
assert state.results[anotherhost]
|
|
69
|
-
|
|
70
|
-
# And with the different modes
|
|
71
|
-
run_ops(state, serial=True)
|
|
72
|
-
run_ops(state, no_wait=True)
|
|
67
|
+
assert state.results[somehost].error_ops == 0
|
|
68
|
+
assert state.results[anotherhost].error_ops == 0
|
|
73
69
|
|
|
74
70
|
disconnect_all(state)
|
|
75
71
|
|
|
@@ -87,11 +83,11 @@ class TestDeploysApi(PatchSSHTestCase):
|
|
|
87
83
|
|
|
88
84
|
connect_all(state)
|
|
89
85
|
|
|
90
|
-
@deploy
|
|
86
|
+
@deploy()
|
|
91
87
|
def test_nested_deploy():
|
|
92
88
|
server.shell(commands=["echo nested command"])
|
|
93
89
|
|
|
94
|
-
@deploy
|
|
90
|
+
@deploy()
|
|
95
91
|
def test_deploy():
|
|
96
92
|
server.shell(commands=["echo first command"])
|
|
97
93
|
test_nested_deploy()
|
|
@@ -104,22 +100,25 @@ class TestDeploysApi(PatchSSHTestCase):
|
|
|
104
100
|
# Ensure we have an op
|
|
105
101
|
assert len(op_order) == 3
|
|
106
102
|
|
|
103
|
+
# Ensure run ops works
|
|
104
|
+
run_ops(state)
|
|
105
|
+
|
|
107
106
|
first_op_hash = op_order[0]
|
|
108
|
-
assert state.op_meta[first_op_hash]
|
|
109
|
-
assert state.ops[somehost][first_op_hash]
|
|
107
|
+
assert state.op_meta[first_op_hash].names == {"test_deploy | server.shell"}
|
|
108
|
+
assert state.ops[somehost][first_op_hash].operation_meta._commands == [
|
|
110
109
|
StringCommand("echo first command"),
|
|
111
110
|
]
|
|
112
111
|
|
|
113
112
|
second_op_hash = op_order[1]
|
|
114
|
-
assert state.op_meta[second_op_hash]
|
|
115
|
-
"test_deploy | test_nested_deploy |
|
|
113
|
+
assert state.op_meta[second_op_hash].names == {
|
|
114
|
+
"test_deploy | test_nested_deploy | server.shell",
|
|
116
115
|
}
|
|
117
|
-
assert state.ops[somehost][second_op_hash]
|
|
116
|
+
assert state.ops[somehost][second_op_hash].operation_meta._commands == [
|
|
118
117
|
StringCommand("echo nested command"),
|
|
119
118
|
]
|
|
120
119
|
|
|
121
120
|
third_op_hash = op_order[2]
|
|
122
|
-
assert state.op_meta[third_op_hash]
|
|
123
|
-
assert state.ops[somehost][third_op_hash]
|
|
121
|
+
assert state.op_meta[third_op_hash].names == {"test_deploy | server.shell"}
|
|
122
|
+
assert state.ops[somehost][third_op_hash].operation_meta._commands == [
|
|
124
123
|
StringCommand("echo second command"),
|
|
125
124
|
]
|