pyinfra 2.9.1__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.
- pyinfra/api/__init__.py +3 -0
- pyinfra/api/arguments.py +265 -253
- pyinfra/api/arguments_typed.py +80 -0
- pyinfra/api/command.py +68 -53
- pyinfra/api/config.py +139 -32
- pyinfra/api/connect.py +1 -1
- pyinfra/api/connectors.py +7 -26
- pyinfra/api/deploy.py +21 -52
- pyinfra/api/exceptions.py +33 -8
- pyinfra/api/facts.py +102 -137
- pyinfra/api/host.py +150 -82
- pyinfra/api/inventory.py +21 -25
- pyinfra/api/operation.py +240 -198
- pyinfra/api/operations.py +102 -148
- pyinfra/api/state.py +137 -79
- pyinfra/api/util.py +79 -86
- pyinfra/connectors/base.py +147 -0
- pyinfra/connectors/chroot.py +160 -169
- pyinfra/connectors/docker.py +220 -237
- pyinfra/connectors/dockerssh.py +231 -253
- pyinfra/connectors/local.py +196 -208
- pyinfra/connectors/ssh.py +530 -613
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +5 -3
- pyinfra/connectors/terraform.py +86 -65
- pyinfra/connectors/util.py +211 -137
- pyinfra/connectors/vagrant.py +60 -53
- pyinfra/context.py +4 -2
- pyinfra/facts/apk.py +2 -0
- pyinfra/facts/apt.py +2 -0
- pyinfra/facts/brew.py +2 -0
- pyinfra/facts/bsdinit.py +2 -0
- pyinfra/facts/cargo.py +2 -0
- pyinfra/facts/choco.py +2 -0
- pyinfra/facts/deb.py +7 -2
- pyinfra/facts/dnf.py +2 -0
- pyinfra/facts/docker.py +19 -0
- pyinfra/facts/files.py +47 -32
- pyinfra/facts/gem.py +2 -0
- pyinfra/facts/git.py +3 -1
- pyinfra/facts/gpg.py +3 -1
- pyinfra/facts/hardware.py +34 -24
- pyinfra/facts/iptables.py +5 -3
- pyinfra/facts/launchd.py +2 -0
- pyinfra/facts/lxd.py +2 -0
- pyinfra/facts/mysql.py +13 -6
- pyinfra/facts/npm.py +1 -0
- pyinfra/facts/openrc.py +2 -0
- pyinfra/facts/pacman.py +6 -2
- pyinfra/facts/pip.py +2 -0
- pyinfra/facts/pkg.py +2 -0
- pyinfra/facts/pkgin.py +2 -0
- pyinfra/facts/postgres.py +168 -0
- pyinfra/facts/postgresql.py +6 -160
- pyinfra/facts/rpm.py +12 -9
- pyinfra/facts/runit.py +68 -0
- pyinfra/facts/selinux.py +3 -1
- pyinfra/facts/server.py +80 -36
- pyinfra/facts/snap.py +2 -0
- pyinfra/facts/systemd.py +31 -12
- pyinfra/facts/sysvinit.py +10 -10
- pyinfra/facts/upstart.py +2 -0
- pyinfra/facts/util/packaging.py +7 -4
- pyinfra/facts/vzctl.py +2 -0
- pyinfra/facts/xbps.py +2 -0
- pyinfra/facts/yum.py +2 -0
- pyinfra/facts/zypper.py +2 -0
- pyinfra/local.py +4 -5
- pyinfra/operations/apk.py +6 -4
- pyinfra/operations/apt.py +46 -65
- pyinfra/operations/brew.py +17 -22
- pyinfra/operations/bsdinit.py +9 -7
- pyinfra/operations/cargo.py +4 -2
- pyinfra/operations/choco.py +4 -2
- pyinfra/operations/dnf.py +19 -23
- pyinfra/operations/docker.py +339 -0
- pyinfra/operations/files.py +188 -386
- pyinfra/operations/gem.py +4 -2
- pyinfra/operations/git.py +24 -53
- pyinfra/operations/iptables.py +29 -35
- pyinfra/operations/launchd.py +6 -7
- pyinfra/operations/lxd.py +8 -13
- pyinfra/operations/mysql.py +62 -81
- pyinfra/operations/npm.py +9 -2
- pyinfra/operations/openrc.py +6 -4
- pyinfra/operations/pacman.py +7 -8
- pyinfra/operations/pip.py +25 -24
- pyinfra/operations/pkg.py +4 -2
- pyinfra/operations/pkgin.py +6 -4
- pyinfra/operations/postgres.py +349 -0
- pyinfra/operations/postgresql.py +18 -379
- pyinfra/operations/puppet.py +3 -1
- pyinfra/operations/python.py +8 -19
- pyinfra/operations/runit.py +182 -0
- pyinfra/operations/selinux.py +47 -44
- pyinfra/operations/server.py +111 -127
- pyinfra/operations/snap.py +4 -4
- pyinfra/operations/ssh.py +20 -33
- pyinfra/operations/systemd.py +19 -15
- pyinfra/operations/sysvinit.py +9 -16
- pyinfra/operations/upstart.py +9 -7
- 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 +55 -57
- pyinfra/operations/util/service.py +39 -51
- pyinfra/operations/vzctl.py +12 -10
- pyinfra/operations/xbps.py +6 -4
- pyinfra/operations/yum.py +18 -22
- pyinfra/operations/zypper.py +12 -13
- pyinfra/version.py +5 -2
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
- pyinfra-3.0.dist-info/RECORD +167 -0
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
- pyinfra-3.0.dist-info/entry_points.txt +11 -0
- pyinfra_cli/__main__.py +4 -3
- pyinfra_cli/commands.py +7 -2
- pyinfra_cli/exceptions.py +78 -42
- pyinfra_cli/inventory.py +40 -6
- pyinfra_cli/log.py +17 -3
- pyinfra_cli/main.py +133 -90
- pyinfra_cli/prints.py +95 -127
- pyinfra_cli/util.py +62 -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 +101 -201
- 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 +105 -93
- tests/test_connectors/test_terraform.py +9 -15
- tests/test_connectors/test_util.py +24 -46
- tests/test_connectors/test_vagrant.py +7 -7
- 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.1.dist-info/RECORD +0 -170
- pyinfra-2.9.1.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.1.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
- {pyinfra-2.9.1.dist-info → pyinfra-3.0.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,111 @@ 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
|
|
205
|
+
text = text[: max_length - 3]
|
|
206
|
+
return f"{text}..."
|
|
212
207
|
|
|
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
208
|
|
|
225
|
-
|
|
226
|
-
|
|
209
|
+
def pretty_op_name(op_meta):
|
|
210
|
+
name = list(op_meta.names)[0]
|
|
211
|
+
|
|
212
|
+
if op_meta.args:
|
|
213
|
+
name = "{0} ({1})".format(name, ", ".join(str(arg) for arg in op_meta.args))
|
|
227
214
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
215
|
+
return name
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def print_meta(state: "State"):
|
|
219
|
+
rows: List[Tuple[Callable, Union[List[str], str]]] = [
|
|
220
|
+
(logger.info, ["Operation", "Change", "Conditional Change"]),
|
|
221
|
+
]
|
|
222
|
+
|
|
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]),
|
|
231
240
|
(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
"-"
|
|
242
|
+
if len(hosts_in_op) == 0
|
|
243
|
+
else "{0} ({1})".format(
|
|
244
|
+
len(hosts_in_op),
|
|
245
|
+
truncate(", ".join(sorted(hosts_in_op)), 48),
|
|
246
|
+
)
|
|
237
247
|
),
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"Change: {0}".format(meta["ops_change"]),
|
|
248
|
-
"No change: {0}".format(meta["ops_no_change"]),
|
|
249
|
-
],
|
|
250
|
-
),
|
|
248
|
+
(
|
|
249
|
+
"-"
|
|
250
|
+
if len(hosts_maybe_in_op) == 0
|
|
251
|
+
else "{0} ({1})".format(
|
|
252
|
+
len(hosts_maybe_in_op),
|
|
253
|
+
truncate(", ".join(sorted(hosts_maybe_in_op)), 48),
|
|
254
|
+
)
|
|
255
|
+
),
|
|
256
|
+
],
|
|
251
257
|
)
|
|
252
|
-
|
|
253
|
-
if i != len(group_combinations):
|
|
254
|
-
rows.append((lambda m: click.echo(m, err=True), []))
|
|
258
|
+
)
|
|
255
259
|
|
|
256
260
|
print_rows(rows)
|
|
257
261
|
|
|
258
262
|
|
|
259
263
|
def print_results(state: "State"):
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
for i, (groups, hosts) in enumerate(group_combinations.items(), 1):
|
|
264
|
-
if not hosts:
|
|
265
|
-
continue
|
|
266
|
-
|
|
267
|
-
if groups:
|
|
268
|
-
rows.append(
|
|
269
|
-
(
|
|
270
|
-
logger.info,
|
|
271
|
-
"Groups: {0}".format(
|
|
272
|
-
click.style(" / ".join(groups), bold=True),
|
|
273
|
-
),
|
|
274
|
-
),
|
|
275
|
-
)
|
|
276
|
-
else:
|
|
277
|
-
rows.append((logger.info, "Ungrouped:"))
|
|
264
|
+
rows: List[Tuple[Callable, Union[List[str], str]]] = [
|
|
265
|
+
(logger.info, ["Operation", "Hosts", "Success", "Error", "No Change"]),
|
|
266
|
+
]
|
|
278
267
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
host.style_print_prefix("red", bold=True),
|
|
287
|
-
click.style("No connection", "red"),
|
|
288
|
-
],
|
|
289
|
-
),
|
|
290
|
-
)
|
|
268
|
+
for op_hash in state.get_op_order():
|
|
269
|
+
hosts_in_op = 0
|
|
270
|
+
hosts_in_op_success: list[str] = []
|
|
271
|
+
hosts_in_op_error: list[str] = []
|
|
272
|
+
hosts_in_op_no_change: list[str] = []
|
|
273
|
+
for host in state.inventory.iter_activated_hosts():
|
|
274
|
+
if op_hash not in state.ops[host]:
|
|
291
275
|
continue
|
|
292
276
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
meta = state.meta[host]
|
|
296
|
-
success_ops = results["success_ops"]
|
|
297
|
-
partial_ops = results["partial_ops"]
|
|
298
|
-
# TODO: type meta object
|
|
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"]
|
|
277
|
+
hosts_in_op += 1
|
|
302
278
|
|
|
303
|
-
|
|
304
|
-
|
|
279
|
+
op_meta = state.ops[host][op_hash].operation_meta
|
|
280
|
+
if op_meta.did_succeed(_raise_if_not_complete=False):
|
|
281
|
+
if op_meta._did_change():
|
|
282
|
+
hosts_in_op_success.append(host.name)
|
|
283
|
+
else:
|
|
284
|
+
hosts_in_op_no_change.append(host.name)
|
|
285
|
+
else:
|
|
286
|
+
hosts_in_op_error.append(host.name)
|
|
305
287
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
host_args = ("yellow",)
|
|
288
|
+
row = [
|
|
289
|
+
pretty_op_name(state.op_meta[op_hash]),
|
|
290
|
+
str(hosts_in_op),
|
|
291
|
+
]
|
|
311
292
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
rows.append(
|
|
326
|
-
(
|
|
327
|
-
logger.info,
|
|
328
|
-
[
|
|
329
|
-
host.style_print_prefix(*host_args, **host_kwargs),
|
|
330
|
-
changed_str,
|
|
331
|
-
"No change: {0}".format(click.style(f"{meta['ops_no_change']}", bold=True)),
|
|
332
|
-
error_str,
|
|
333
|
-
],
|
|
334
|
-
),
|
|
335
|
-
)
|
|
293
|
+
if hosts_in_op_success:
|
|
294
|
+
row.append(f"{len(hosts_in_op_success)}")
|
|
295
|
+
else:
|
|
296
|
+
row.append("-")
|
|
297
|
+
if hosts_in_op_error:
|
|
298
|
+
row.append(f"{len(hosts_in_op_error)}")
|
|
299
|
+
else:
|
|
300
|
+
row.append("-")
|
|
301
|
+
if hosts_in_op_no_change:
|
|
302
|
+
row.append(f"{len(hosts_in_op_no_change)}")
|
|
303
|
+
else:
|
|
304
|
+
row.append("-")
|
|
336
305
|
|
|
337
|
-
|
|
338
|
-
rows.append((lambda m: click.echo(m, err=True), []))
|
|
306
|
+
rows.append((logger.info, row))
|
|
339
307
|
|
|
340
308
|
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,44 @@ 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
|
+
elif "." in path:
|
|
158
|
+
# And also a.module.name.function
|
|
159
|
+
mod_path, attr_name = path.rsplit(".", 1)
|
|
160
|
+
else:
|
|
161
|
+
return None
|
|
139
162
|
|
|
163
|
+
possible_modules = [mod_path]
|
|
140
164
|
if prefix:
|
|
141
|
-
|
|
165
|
+
possible_modules.append(f"{prefix}.{mod_path}")
|
|
166
|
+
|
|
167
|
+
module = None
|
|
168
|
+
|
|
169
|
+
for possible in possible_modules:
|
|
142
170
|
try:
|
|
143
|
-
module
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
171
|
+
# First use find_spec which checks if the possible module exists *without* importing
|
|
172
|
+
# it, thus any import errors it contains still get properly raised to the user.
|
|
173
|
+
spec = find_spec(possible)
|
|
174
|
+
except ModuleNotFoundError:
|
|
175
|
+
continue
|
|
176
|
+
else:
|
|
177
|
+
if spec is not None:
|
|
178
|
+
module = import_module(possible)
|
|
179
|
+
break
|
|
148
180
|
|
|
149
181
|
if module is None:
|
|
150
|
-
|
|
151
|
-
module
|
|
152
|
-
|
|
153
|
-
raise CliError(f"No such module: {full_path}")
|
|
182
|
+
if raise_for_none:
|
|
183
|
+
raise CliError(f"No such module: {possible_modules[-1]}")
|
|
184
|
+
return
|
|
154
185
|
|
|
155
186
|
attr = getattr(module, attr_name, None)
|
|
156
187
|
if attr is None:
|
|
157
|
-
|
|
188
|
+
if raise_for_none:
|
|
189
|
+
raise CliError(f"No such attribute in module {possible_modules[-1]}: {attr_name}")
|
|
190
|
+
return
|
|
158
191
|
|
|
159
192
|
return attr
|
|
160
193
|
|
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
|
]
|