pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__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 +184 -118
- 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.1.dist-info}/METADATA +51 -58
- pyinfra-3.0.1.dist-info/RECORD +168 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.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 +10 -12
- 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.1.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
pyinfra/operations/files.py
CHANGED
|
@@ -11,17 +11,18 @@ import traceback
|
|
|
11
11
|
from datetime import timedelta
|
|
12
12
|
from fnmatch import fnmatch
|
|
13
13
|
from io import StringIO
|
|
14
|
-
from
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import IO, Any, Union
|
|
15
16
|
|
|
16
17
|
from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError
|
|
17
18
|
|
|
18
|
-
import pyinfra
|
|
19
19
|
from pyinfra import host, logger, state
|
|
20
20
|
from pyinfra.api import (
|
|
21
21
|
FileDownloadCommand,
|
|
22
22
|
FileUploadCommand,
|
|
23
23
|
OperationError,
|
|
24
24
|
OperationTypeError,
|
|
25
|
+
OperationValueError,
|
|
25
26
|
QuoteString,
|
|
26
27
|
RsyncCommand,
|
|
27
28
|
StringCommand,
|
|
@@ -30,6 +31,7 @@ from pyinfra.api import (
|
|
|
30
31
|
from pyinfra.api.command import make_formatted_string_command
|
|
31
32
|
from pyinfra.api.util import (
|
|
32
33
|
get_call_location,
|
|
34
|
+
get_file_io,
|
|
33
35
|
get_file_sha1,
|
|
34
36
|
get_path_permissions_mode,
|
|
35
37
|
get_template,
|
|
@@ -56,22 +58,21 @@ from .util import files as file_utils
|
|
|
56
58
|
from .util.files import adjust_regex, ensure_mode_int, get_timestamp, sed_replace, unix_path_join
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
@operation(
|
|
60
|
-
pipeline_facts={"file": "dest"},
|
|
61
|
-
)
|
|
61
|
+
@operation()
|
|
62
62
|
def download(
|
|
63
|
-
src,
|
|
64
|
-
dest,
|
|
65
|
-
user=None,
|
|
66
|
-
group=None,
|
|
67
|
-
mode=None,
|
|
68
|
-
cache_time=None,
|
|
63
|
+
src: str,
|
|
64
|
+
dest: str,
|
|
65
|
+
user: str | None = None,
|
|
66
|
+
group: str | None = None,
|
|
67
|
+
mode: str | None = None,
|
|
68
|
+
cache_time: int | None = None,
|
|
69
69
|
force=False,
|
|
70
|
-
sha256sum=None,
|
|
71
|
-
sha1sum=None,
|
|
72
|
-
md5sum=None,
|
|
73
|
-
headers=None,
|
|
70
|
+
sha256sum: str | None = None,
|
|
71
|
+
sha1sum: str | None = None,
|
|
72
|
+
md5sum: str | None = None,
|
|
73
|
+
headers: dict[str, str] | None = None,
|
|
74
74
|
insecure=False,
|
|
75
|
+
proxy: str | None = None,
|
|
75
76
|
):
|
|
76
77
|
"""
|
|
77
78
|
Download files from remote locations using ``curl`` or ``wget``.
|
|
@@ -88,6 +89,7 @@ def download(
|
|
|
88
89
|
+ md5sum: md5 hash to checksum the downloaded file against
|
|
89
90
|
+ headers: optional dictionary of headers to set for the HTTP request
|
|
90
91
|
+ insecure: disable SSL verification for the HTTP request
|
|
92
|
+
+ proxy: simple HTTP proxy through which we can download files, form `http://<yourproxy>:<port>`
|
|
91
93
|
|
|
92
94
|
**Example:**
|
|
93
95
|
|
|
@@ -121,8 +123,8 @@ def download(
|
|
|
121
123
|
if cache_time:
|
|
122
124
|
# Time on files is not tz-aware, and will be the same tz as the server's time,
|
|
123
125
|
# so we can safely remove the tzinfo from the Date fact before comparison.
|
|
124
|
-
|
|
125
|
-
if info["mtime"] and info["mtime"] <
|
|
126
|
+
ctime = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
|
|
127
|
+
if info["mtime"] and info["mtime"] < ctime:
|
|
126
128
|
download = True
|
|
127
129
|
|
|
128
130
|
if sha1sum:
|
|
@@ -139,10 +141,16 @@ def download(
|
|
|
139
141
|
|
|
140
142
|
# If we download, always do user/group/mode as SSH user may be different
|
|
141
143
|
if download:
|
|
142
|
-
temp_file =
|
|
144
|
+
temp_file = host.get_temp_filename(dest)
|
|
143
145
|
|
|
144
146
|
curl_args: list[Union[str, StringCommand]] = ["-sSLf"]
|
|
145
147
|
wget_args: list[Union[str, StringCommand]] = ["-q"]
|
|
148
|
+
|
|
149
|
+
if proxy:
|
|
150
|
+
curl_args.append(f"--proxy {proxy}")
|
|
151
|
+
wget_args.append("-e use_proxy=yes")
|
|
152
|
+
wget_args.append(f"-e http_proxy={proxy}")
|
|
153
|
+
|
|
146
154
|
if insecure:
|
|
147
155
|
curl_args.append("--insecure")
|
|
148
156
|
wget_args.append("--no-check-certificate")
|
|
@@ -219,11 +227,11 @@ def download(
|
|
|
219
227
|
|
|
220
228
|
@operation()
|
|
221
229
|
def line(
|
|
222
|
-
path,
|
|
223
|
-
line,
|
|
230
|
+
path: str,
|
|
231
|
+
line: str,
|
|
224
232
|
present=True,
|
|
225
|
-
replace=None,
|
|
226
|
-
flags=None,
|
|
233
|
+
replace: str | None = None,
|
|
234
|
+
flags: list[str] | None = None,
|
|
227
235
|
backup=False,
|
|
228
236
|
interpolate_variables=False,
|
|
229
237
|
escape_regex_characters=False,
|
|
@@ -261,7 +269,7 @@ def line(
|
|
|
261
269
|
it will be append to the end of the file.
|
|
262
270
|
|
|
263
271
|
Ensure new line:
|
|
264
|
-
This will ensure that the ``line`` being appended is always on a
|
|
272
|
+
This will ensure that the ``line`` being appended is always on a separate new
|
|
265
273
|
line in case the file doesn't end with a newline character.
|
|
266
274
|
|
|
267
275
|
|
|
@@ -375,48 +383,27 @@ def line(
|
|
|
375
383
|
|
|
376
384
|
# No line and we want it, append it
|
|
377
385
|
if not present_lines and present:
|
|
378
|
-
# If
|
|
379
|
-
#
|
|
380
|
-
if
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
echo_command=echo_command,
|
|
394
|
-
sed_replace_command=sed_replace_command,
|
|
386
|
+
# If we're doing replacement, only append if the *replacement* line
|
|
387
|
+
# does not exist (as we are appending the replacement).
|
|
388
|
+
if replace:
|
|
389
|
+
# Ensure replace explicitly matches a whole line
|
|
390
|
+
replace_line = replace
|
|
391
|
+
if not replace_line.startswith("^"):
|
|
392
|
+
replace_line = f"^{replace_line}"
|
|
393
|
+
if not replace_line.endswith("$"):
|
|
394
|
+
replace_line = f"{replace_line}$"
|
|
395
|
+
|
|
396
|
+
present_lines = host.get_fact(
|
|
397
|
+
FindInFile,
|
|
398
|
+
path=path,
|
|
399
|
+
pattern=replace_line,
|
|
400
|
+
interpolate_variables=interpolate_variables,
|
|
395
401
|
)
|
|
396
402
|
|
|
397
|
-
|
|
403
|
+
if not present_lines:
|
|
404
|
+
yield echo_command
|
|
398
405
|
else:
|
|
399
|
-
|
|
400
|
-
# does not exist (as we are appending the replacement).
|
|
401
|
-
if replace:
|
|
402
|
-
# Ensure replace explicitly matches a whole line
|
|
403
|
-
replace_line = replace
|
|
404
|
-
if not replace_line.startswith("^"):
|
|
405
|
-
replace_line = f"^{replace_line}"
|
|
406
|
-
if not replace_line.endswith("$"):
|
|
407
|
-
replace_line = f"{replace_line}$"
|
|
408
|
-
|
|
409
|
-
present_lines = host.get_fact(
|
|
410
|
-
FindInFile,
|
|
411
|
-
path=path,
|
|
412
|
-
pattern=replace_line,
|
|
413
|
-
interpolate_variables=interpolate_variables,
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
if not present_lines:
|
|
417
|
-
yield echo_command
|
|
418
|
-
else:
|
|
419
|
-
host.noop('line "{0}" exists in {1}'.format(replace or line, path))
|
|
406
|
+
host.noop('line "{0}" exists in {1}'.format(replace or line, path))
|
|
420
407
|
|
|
421
408
|
# Line(s) exists and we want to remove them, replace with nothing
|
|
422
409
|
elif present_lines and not present:
|
|
@@ -440,10 +427,10 @@ def line(
|
|
|
440
427
|
|
|
441
428
|
@operation()
|
|
442
429
|
def replace(
|
|
443
|
-
path,
|
|
444
|
-
text=None,
|
|
445
|
-
replace=None,
|
|
446
|
-
flags=None,
|
|
430
|
+
path: str,
|
|
431
|
+
text: str | None = None,
|
|
432
|
+
replace: str | None = None,
|
|
433
|
+
flags: list[str] | None = None,
|
|
447
434
|
backup=False,
|
|
448
435
|
interpolate_variables=False,
|
|
449
436
|
match=None, # deprecated
|
|
@@ -512,19 +499,17 @@ def replace(
|
|
|
512
499
|
host.noop('string "{0}" does not exist in {1}'.format(text, path))
|
|
513
500
|
|
|
514
501
|
|
|
515
|
-
@operation(
|
|
516
|
-
pipeline_facts={"find_files": "destination"},
|
|
517
|
-
)
|
|
502
|
+
@operation()
|
|
518
503
|
def sync(
|
|
519
|
-
src,
|
|
520
|
-
dest,
|
|
521
|
-
user=None,
|
|
522
|
-
group=None,
|
|
523
|
-
mode=None,
|
|
524
|
-
dir_mode=None,
|
|
504
|
+
src: str,
|
|
505
|
+
dest: str,
|
|
506
|
+
user: str | None = None,
|
|
507
|
+
group: str | None = None,
|
|
508
|
+
mode: str | None = None,
|
|
509
|
+
dir_mode: str | None = None,
|
|
525
510
|
delete=False,
|
|
526
|
-
exclude=None,
|
|
527
|
-
exclude_dir=None,
|
|
511
|
+
exclude: str | list[str] | tuple[str] | None = None,
|
|
512
|
+
exclude_dir: str | list[str] | tuple[str] | None = None,
|
|
528
513
|
add_deploy_dir=True,
|
|
529
514
|
):
|
|
530
515
|
"""
|
|
@@ -586,7 +571,7 @@ def sync(
|
|
|
586
571
|
put_files = []
|
|
587
572
|
ensure_dirnames = []
|
|
588
573
|
for dirpath, dirnames, filenames in os.walk(src, topdown=True):
|
|
589
|
-
remote_dirpath = os.path.normpath(os.path.relpath(dirpath, src))
|
|
574
|
+
remote_dirpath = Path(os.path.normpath(os.path.relpath(dirpath, src))).as_posix()
|
|
590
575
|
|
|
591
576
|
# Filter excluded dirs
|
|
592
577
|
for child_dir in dirnames[:]:
|
|
@@ -620,7 +605,7 @@ def sync(
|
|
|
620
605
|
if dest_link_info:
|
|
621
606
|
dest_to_ensure = dest_link_info["link_target"]
|
|
622
607
|
|
|
623
|
-
yield from directory(
|
|
608
|
+
yield from directory._inner(
|
|
624
609
|
path=dest_to_ensure,
|
|
625
610
|
user=user,
|
|
626
611
|
group=group,
|
|
@@ -629,7 +614,7 @@ def sync(
|
|
|
629
614
|
|
|
630
615
|
# Ensure any remote dirnames
|
|
631
616
|
for dir_path_curr, dir_mode_curr in ensure_dirnames:
|
|
632
|
-
yield from directory(
|
|
617
|
+
yield from directory._inner(
|
|
633
618
|
path=unix_path_join(dest, dir_path_curr),
|
|
634
619
|
user=user,
|
|
635
620
|
group=group,
|
|
@@ -638,7 +623,7 @@ def sync(
|
|
|
638
623
|
|
|
639
624
|
# Put each file combination
|
|
640
625
|
for local_filename, remote_filename in put_files:
|
|
641
|
-
yield from put(
|
|
626
|
+
yield from put._inner(
|
|
642
627
|
src=local_filename,
|
|
643
628
|
dest=remote_filename,
|
|
644
629
|
user=user,
|
|
@@ -658,7 +643,7 @@ def sync(
|
|
|
658
643
|
if exclude and any(fnmatch(filename, match) for match in exclude):
|
|
659
644
|
continue
|
|
660
645
|
|
|
661
|
-
yield from file(path=filename, present=False)
|
|
646
|
+
yield from file._inner(path=filename, present=False)
|
|
662
647
|
|
|
663
648
|
|
|
664
649
|
@memoize
|
|
@@ -667,7 +652,7 @@ def show_rsync_warning():
|
|
|
667
652
|
|
|
668
653
|
|
|
669
654
|
@operation(is_idempotent=False)
|
|
670
|
-
def rsync(src, dest, flags
|
|
655
|
+
def rsync(src: str, dest: str, flags: list[str] | None = None):
|
|
671
656
|
"""
|
|
672
657
|
Use ``rsync`` to sync a local directory to the remote system. This operation will actually call
|
|
673
658
|
the ``rsync`` binary on your system.
|
|
@@ -682,6 +667,8 @@ def rsync(src, dest, flags=["-ax", "--delete"]):
|
|
|
682
667
|
global arguments.
|
|
683
668
|
"""
|
|
684
669
|
|
|
670
|
+
if flags is None:
|
|
671
|
+
flags = ["-ax", "--delete"]
|
|
685
672
|
show_rsync_warning()
|
|
686
673
|
|
|
687
674
|
try:
|
|
@@ -692,11 +679,11 @@ def rsync(src, dest, flags=["-ax", "--delete"]):
|
|
|
692
679
|
yield RsyncCommand(src, dest, flags)
|
|
693
680
|
|
|
694
681
|
|
|
695
|
-
def _create_remote_dir(
|
|
682
|
+
def _create_remote_dir(remote_filename, user, group):
|
|
696
683
|
# Always use POSIX style path as local might be Windows, remote always *nix
|
|
697
684
|
remote_dirname = posixpath.dirname(remote_filename)
|
|
698
685
|
if remote_dirname:
|
|
699
|
-
yield from directory(
|
|
686
|
+
yield from directory._inner(
|
|
700
687
|
path=remote_dirname,
|
|
701
688
|
user=user,
|
|
702
689
|
group=group,
|
|
@@ -709,14 +696,10 @@ def _create_remote_dir(state, host, remote_filename, user, group):
|
|
|
709
696
|
# We don't (currently) cache the local state, so there's nothing we can
|
|
710
697
|
# update to flag the local file as present.
|
|
711
698
|
is_idempotent=False,
|
|
712
|
-
pipeline_facts={
|
|
713
|
-
"file": "src",
|
|
714
|
-
"sha1_file": "src",
|
|
715
|
-
},
|
|
716
699
|
)
|
|
717
700
|
def get(
|
|
718
|
-
src,
|
|
719
|
-
dest,
|
|
701
|
+
src: str,
|
|
702
|
+
dest: str,
|
|
720
703
|
add_deploy_dir=True,
|
|
721
704
|
create_local_dir=False,
|
|
722
705
|
force=False,
|
|
@@ -757,11 +740,11 @@ def get(
|
|
|
757
740
|
|
|
758
741
|
# No remote file, so assume exists and download it "blind"
|
|
759
742
|
if not remote_file or force:
|
|
760
|
-
yield FileDownloadCommand(src, dest, remote_temp_filename=
|
|
743
|
+
yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
|
|
761
744
|
|
|
762
745
|
# No local file, so always download
|
|
763
746
|
elif not os.path.exists(dest):
|
|
764
|
-
yield FileDownloadCommand(src, dest, remote_temp_filename=
|
|
747
|
+
yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
|
|
765
748
|
|
|
766
749
|
# Remote file exists - check if it matches our local
|
|
767
750
|
else:
|
|
@@ -770,21 +753,16 @@ def get(
|
|
|
770
753
|
|
|
771
754
|
# Check sha1sum, upload if needed
|
|
772
755
|
if local_sum != remote_sum:
|
|
773
|
-
yield FileDownloadCommand(src, dest, remote_temp_filename=
|
|
756
|
+
yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
|
|
774
757
|
|
|
775
758
|
|
|
776
|
-
@operation(
|
|
777
|
-
pipeline_facts={
|
|
778
|
-
"file": "dest",
|
|
779
|
-
"sha1_file": "dest",
|
|
780
|
-
},
|
|
781
|
-
)
|
|
759
|
+
@operation()
|
|
782
760
|
def put(
|
|
783
|
-
src,
|
|
784
|
-
dest,
|
|
785
|
-
user=None,
|
|
786
|
-
group=None,
|
|
787
|
-
mode=None,
|
|
761
|
+
src: str | IO[Any],
|
|
762
|
+
dest: str,
|
|
763
|
+
user: str | None = None,
|
|
764
|
+
group: str | None = None,
|
|
765
|
+
mode: int | str | bool | None = None,
|
|
788
766
|
add_deploy_dir=True,
|
|
789
767
|
create_remote_dir=True,
|
|
790
768
|
force=False,
|
|
@@ -845,6 +823,7 @@ def put(
|
|
|
845
823
|
|
|
846
824
|
# Assume string filename
|
|
847
825
|
else:
|
|
826
|
+
assert isinstance(src, (str, Path))
|
|
848
827
|
# Add deploy directory?
|
|
849
828
|
if add_deploy_dir and state.cwd:
|
|
850
829
|
src = os.path.join(state.cwd, src)
|
|
@@ -859,7 +838,7 @@ def put(
|
|
|
859
838
|
raise IOError("No such file: {0}".format(local_file))
|
|
860
839
|
|
|
861
840
|
if mode is True:
|
|
862
|
-
if os.path.isfile(local_file):
|
|
841
|
+
if isinstance(local_file, str) and os.path.isfile(local_file):
|
|
863
842
|
mode = get_path_permissions_mode(local_file)
|
|
864
843
|
else:
|
|
865
844
|
logger.warning(
|
|
@@ -872,19 +851,20 @@ def put(
|
|
|
872
851
|
|
|
873
852
|
remote_file = host.get_fact(File, path=dest)
|
|
874
853
|
|
|
875
|
-
if not remote_file and host.get_fact(Directory, path=dest):
|
|
854
|
+
if not remote_file and bool(host.get_fact(Directory, path=dest)):
|
|
855
|
+
assert isinstance(src, str)
|
|
876
856
|
dest = unix_path_join(dest, os.path.basename(src))
|
|
877
857
|
remote_file = host.get_fact(File, path=dest)
|
|
878
858
|
|
|
879
859
|
if create_remote_dir:
|
|
880
|
-
yield from _create_remote_dir(
|
|
860
|
+
yield from _create_remote_dir(dest, user, group)
|
|
881
861
|
|
|
882
862
|
# No remote file, always upload and user/group/mode if supplied
|
|
883
863
|
if not remote_file or force:
|
|
884
864
|
yield FileUploadCommand(
|
|
885
865
|
local_file,
|
|
886
866
|
dest,
|
|
887
|
-
remote_temp_filename=
|
|
867
|
+
remote_temp_filename=host.get_temp_filename(dest),
|
|
888
868
|
)
|
|
889
869
|
|
|
890
870
|
if user or group:
|
|
@@ -902,7 +882,7 @@ def put(
|
|
|
902
882
|
yield FileUploadCommand(
|
|
903
883
|
local_file,
|
|
904
884
|
dest,
|
|
905
|
-
remote_temp_filename=
|
|
885
|
+
remote_temp_filename=host.get_temp_filename(dest),
|
|
906
886
|
)
|
|
907
887
|
|
|
908
888
|
if user or group:
|
|
@@ -929,7 +909,15 @@ def put(
|
|
|
929
909
|
|
|
930
910
|
|
|
931
911
|
@operation()
|
|
932
|
-
def template(
|
|
912
|
+
def template(
|
|
913
|
+
src: str | IO[Any],
|
|
914
|
+
dest: str,
|
|
915
|
+
user: str | None = None,
|
|
916
|
+
group: str | None = None,
|
|
917
|
+
mode: str | None = None,
|
|
918
|
+
create_remote_dir=True,
|
|
919
|
+
**data,
|
|
920
|
+
):
|
|
933
921
|
'''
|
|
934
922
|
Generate a template using jinja2 and write it to the remote system.
|
|
935
923
|
|
|
@@ -1011,7 +999,6 @@ def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True
|
|
|
1011
999
|
data.setdefault("host", host)
|
|
1012
1000
|
data.setdefault("state", state)
|
|
1013
1001
|
data.setdefault("inventory", state.inventory)
|
|
1014
|
-
data.setdefault("facts", pyinfra.facts)
|
|
1015
1002
|
|
|
1016
1003
|
# Render and make file-like it's output
|
|
1017
1004
|
try:
|
|
@@ -1026,7 +1013,7 @@ def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True
|
|
|
1026
1013
|
line_number = trace_frames[-1][1]
|
|
1027
1014
|
|
|
1028
1015
|
# Quickly read the line in question and one above/below for nicer debugging
|
|
1029
|
-
with
|
|
1016
|
+
with get_file_io(src, "r") as f:
|
|
1030
1017
|
template_lines = f.readlines()
|
|
1031
1018
|
|
|
1032
1019
|
template_lines = [line.strip() for line in template_lines]
|
|
@@ -1039,14 +1026,14 @@ def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True
|
|
|
1039
1026
|
e,
|
|
1040
1027
|
"\n".join(relevant_lines),
|
|
1041
1028
|
),
|
|
1042
|
-
)
|
|
1029
|
+
) from None
|
|
1043
1030
|
|
|
1044
1031
|
output_file = StringIO(output)
|
|
1045
1032
|
# Set the template attribute for nicer debugging
|
|
1046
1033
|
output_file.template = src # type: ignore[attr-defined]
|
|
1047
1034
|
|
|
1048
1035
|
# Pass to the put function
|
|
1049
|
-
yield from put(
|
|
1036
|
+
yield from put._inner(
|
|
1050
1037
|
src=output_file,
|
|
1051
1038
|
dest=dest,
|
|
1052
1039
|
user=user,
|
|
@@ -1078,20 +1065,18 @@ def _raise_or_remove_invalid_path(fs_type, path, force, force_backup, force_back
|
|
|
1078
1065
|
raise OperationError("{0} exists and is not a {1}".format(path, fs_type))
|
|
1079
1066
|
|
|
1080
1067
|
|
|
1081
|
-
@operation(
|
|
1082
|
-
pipeline_facts={"link": "path"},
|
|
1083
|
-
)
|
|
1068
|
+
@operation()
|
|
1084
1069
|
def link(
|
|
1085
|
-
path,
|
|
1086
|
-
target=None,
|
|
1070
|
+
path: str,
|
|
1071
|
+
target: str | None = None,
|
|
1087
1072
|
present=True,
|
|
1088
|
-
user=None,
|
|
1089
|
-
group=None,
|
|
1073
|
+
user: str | None = None,
|
|
1074
|
+
group: str | None = None,
|
|
1090
1075
|
symbolic=True,
|
|
1091
1076
|
create_remote_dir=True,
|
|
1092
1077
|
force=False,
|
|
1093
1078
|
force_backup=True,
|
|
1094
|
-
force_backup_dir=None,
|
|
1079
|
+
force_backup_dir: str | None = None,
|
|
1095
1080
|
):
|
|
1096
1081
|
"""
|
|
1097
1082
|
Add/remove/update links.
|
|
@@ -1148,7 +1133,6 @@ def link(
|
|
|
1148
1133
|
if symbolic:
|
|
1149
1134
|
add_args.append("-s")
|
|
1150
1135
|
|
|
1151
|
-
add_cmd = StringCommand(" ".join(add_args), QuoteString(target), QuoteString(path))
|
|
1152
1136
|
remove_cmd = StringCommand("rm", "-f", QuoteString(path))
|
|
1153
1137
|
|
|
1154
1138
|
if not present:
|
|
@@ -1158,9 +1142,12 @@ def link(
|
|
|
1158
1142
|
host.noop("link {link} does not exist")
|
|
1159
1143
|
return
|
|
1160
1144
|
|
|
1145
|
+
assert target is not None # appease typing QuoteString below
|
|
1146
|
+
add_cmd = StringCommand(" ".join(add_args), QuoteString(target), QuoteString(path))
|
|
1147
|
+
|
|
1161
1148
|
if info is None: # create
|
|
1162
1149
|
if create_remote_dir:
|
|
1163
|
-
yield from _create_remote_dir(
|
|
1150
|
+
yield from _create_remote_dir(path, user, group)
|
|
1164
1151
|
|
|
1165
1152
|
yield add_cmd
|
|
1166
1153
|
|
|
@@ -1185,20 +1172,18 @@ def link(
|
|
|
1185
1172
|
host.noop("link {0} already exists".format(path))
|
|
1186
1173
|
|
|
1187
1174
|
|
|
1188
|
-
@operation(
|
|
1189
|
-
pipeline_facts={"file": "path"},
|
|
1190
|
-
)
|
|
1175
|
+
@operation()
|
|
1191
1176
|
def file(
|
|
1192
|
-
path,
|
|
1177
|
+
path: str,
|
|
1193
1178
|
present=True,
|
|
1194
|
-
user=None,
|
|
1195
|
-
group=None,
|
|
1196
|
-
mode=None,
|
|
1179
|
+
user: str | None = None,
|
|
1180
|
+
group: str | None = None,
|
|
1181
|
+
mode: int | str | None = None,
|
|
1197
1182
|
touch=False,
|
|
1198
1183
|
create_remote_dir=True,
|
|
1199
1184
|
force=False,
|
|
1200
1185
|
force_backup=True,
|
|
1201
|
-
force_backup_dir=None,
|
|
1186
|
+
force_backup_dir: str | None = None,
|
|
1202
1187
|
):
|
|
1203
1188
|
"""
|
|
1204
1189
|
Add/remove/update files.
|
|
@@ -1259,7 +1244,7 @@ def file(
|
|
|
1259
1244
|
|
|
1260
1245
|
if info is None: # create
|
|
1261
1246
|
if create_remote_dir:
|
|
1262
|
-
yield from _create_remote_dir(
|
|
1247
|
+
yield from _create_remote_dir(path, user, group)
|
|
1263
1248
|
|
|
1264
1249
|
yield StringCommand("touch", QuoteString(path))
|
|
1265
1250
|
|
|
@@ -1289,19 +1274,17 @@ def file(
|
|
|
1289
1274
|
host.noop("file {0} already exists".format(path))
|
|
1290
1275
|
|
|
1291
1276
|
|
|
1292
|
-
@operation(
|
|
1293
|
-
pipeline_facts={"directory": "path"},
|
|
1294
|
-
)
|
|
1277
|
+
@operation()
|
|
1295
1278
|
def directory(
|
|
1296
|
-
path,
|
|
1279
|
+
path: str,
|
|
1297
1280
|
present=True,
|
|
1298
|
-
user=None,
|
|
1299
|
-
group=None,
|
|
1300
|
-
mode=None,
|
|
1281
|
+
user: str | None = None,
|
|
1282
|
+
group: str | None = None,
|
|
1283
|
+
mode: int | str | None = None,
|
|
1301
1284
|
recursive=False,
|
|
1302
1285
|
force=False,
|
|
1303
1286
|
force_backup=True,
|
|
1304
|
-
force_backup_dir=None,
|
|
1287
|
+
force_backup_dir: str | None = None,
|
|
1305
1288
|
_no_check_owner_mode=False,
|
|
1306
1289
|
_no_fail_on_link=False,
|
|
1307
1290
|
):
|
|
@@ -1393,8 +1376,8 @@ def directory(
|
|
|
1393
1376
|
host.noop("directory {0} already exists".format(path))
|
|
1394
1377
|
|
|
1395
1378
|
|
|
1396
|
-
@operation(
|
|
1397
|
-
def flags(path, flags=None, present=True):
|
|
1379
|
+
@operation()
|
|
1380
|
+
def flags(path: str, flags: list[str] | None = None, present=True):
|
|
1398
1381
|
"""
|
|
1399
1382
|
Set/clear file flags.
|
|
1400
1383
|
|
|
@@ -1441,21 +1424,20 @@ def flags(path, flags=None, present=True):
|
|
|
1441
1424
|
)
|
|
1442
1425
|
|
|
1443
1426
|
|
|
1444
|
-
@operation(
|
|
1445
|
-
pipeline_facts={"file": "path"},
|
|
1446
|
-
)
|
|
1427
|
+
@operation()
|
|
1447
1428
|
def block(
|
|
1448
|
-
path,
|
|
1449
|
-
content=None,
|
|
1429
|
+
path: str,
|
|
1430
|
+
content: str | list[str] | None = None,
|
|
1450
1431
|
present=True,
|
|
1451
|
-
line=None,
|
|
1432
|
+
line: str | None = None,
|
|
1452
1433
|
backup=False,
|
|
1453
1434
|
escape_regex_characters=False,
|
|
1435
|
+
try_prevent_shell_expansion=False,
|
|
1454
1436
|
before=False,
|
|
1455
1437
|
after=False,
|
|
1456
|
-
marker=None,
|
|
1457
|
-
begin=None,
|
|
1458
|
-
end=None,
|
|
1438
|
+
marker: str | None = None,
|
|
1439
|
+
begin: str | None = None,
|
|
1440
|
+
end: str | None = None,
|
|
1459
1441
|
):
|
|
1460
1442
|
"""
|
|
1461
1443
|
Ensure content, surrounded by the appropriate markers, is present (or not) in the file.
|
|
@@ -1468,6 +1450,7 @@ def block(
|
|
|
1468
1450
|
+ line: regex before or after which the content should be added if it doesn't exist.
|
|
1469
1451
|
+ backup: whether to backup the file (see ``files.line``). Default False.
|
|
1470
1452
|
+ escape_regex_characters: whether to escape regex characters from the matching line
|
|
1453
|
+
+ try_prevent_shell_expansion: tries to prevent shell expanding by values like `$`
|
|
1471
1454
|
+ marker: the base string used to mark the text. Default is ``# {mark} PYINFRA BLOCK``
|
|
1472
1455
|
+ begin: the value for ``{mark}`` in the marker before the content. Default is ``BEGIN``
|
|
1473
1456
|
+ end: the value for ``{mark}`` in the marker after the content. Default is ``END``
|
|
@@ -1484,12 +1467,15 @@ def block(
|
|
|
1484
1467
|
|
|
1485
1468
|
Removal ignores ``content`` and ``line``
|
|
1486
1469
|
|
|
1470
|
+
Preventing shell expansion works by wrapping the content in '`' before passing to `awk`.
|
|
1471
|
+
WARNING: This will break if the content contains raw single quotes.
|
|
1472
|
+
|
|
1487
1473
|
**Examples:**
|
|
1488
1474
|
|
|
1489
1475
|
.. code:: python
|
|
1490
1476
|
|
|
1491
1477
|
# add entry to /etc/host
|
|
1492
|
-
files.
|
|
1478
|
+
files.block(
|
|
1493
1479
|
name="add IP address for red server",
|
|
1494
1480
|
path="/etc/hosts",
|
|
1495
1481
|
content="10.0.0.1 mars-one",
|
|
@@ -1498,7 +1484,7 @@ def block(
|
|
|
1498
1484
|
)
|
|
1499
1485
|
|
|
1500
1486
|
# have two entries in /etc/host
|
|
1501
|
-
files.
|
|
1487
|
+
files.block(
|
|
1502
1488
|
name="add IP address for red server",
|
|
1503
1489
|
path="/etc/hosts",
|
|
1504
1490
|
content="10.0.0.1 mars-one\\n10.0.0.2 mars-two",
|
|
@@ -1507,14 +1493,14 @@ def block(
|
|
|
1507
1493
|
)
|
|
1508
1494
|
|
|
1509
1495
|
# remove marked entry from /etc/hosts
|
|
1510
|
-
files.
|
|
1496
|
+
files.block(
|
|
1511
1497
|
name="remove all 10.* addresses from /etc/hosts",
|
|
1512
1498
|
path="/etc/hosts",
|
|
1513
1499
|
present=False
|
|
1514
1500
|
)
|
|
1515
1501
|
|
|
1516
1502
|
# add out of date warning to web page
|
|
1517
|
-
files.
|
|
1503
|
+
files.block(
|
|
1518
1504
|
name="add out of date warning to web page",
|
|
1519
1505
|
path="/var/www/html/something.html",
|
|
1520
1506
|
content= "<p>Warning: this page is out of date.</p>",
|
|
@@ -1522,6 +1508,14 @@ def block(
|
|
|
1522
1508
|
after=True
|
|
1523
1509
|
marker="<!-- {mark} PYINFRA BLOCK -->",
|
|
1524
1510
|
)
|
|
1511
|
+
|
|
1512
|
+
# put complex alias into .zshrc
|
|
1513
|
+
files.block(
|
|
1514
|
+
path="/home/user/.zshrc",
|
|
1515
|
+
content="eval $(thefuck -a)",
|
|
1516
|
+
try_prevent_shell_expansion=True,
|
|
1517
|
+
marker="## {mark} ALIASES ##"
|
|
1518
|
+
)
|
|
1525
1519
|
"""
|
|
1526
1520
|
|
|
1527
1521
|
logger.warning("The `files.block` operation is currently in beta!")
|
|
@@ -1560,14 +1554,23 @@ def block(
|
|
|
1560
1554
|
cmd = None
|
|
1561
1555
|
if present:
|
|
1562
1556
|
if not content:
|
|
1563
|
-
raise
|
|
1557
|
+
raise OperationValueError("'content' must be supplied when 'present' == True")
|
|
1564
1558
|
if line:
|
|
1565
1559
|
if before == after:
|
|
1566
|
-
raise
|
|
1560
|
+
raise OperationValueError(
|
|
1561
|
+
"only one of 'before' or 'after' used when 'line` is specified"
|
|
1562
|
+
)
|
|
1567
1563
|
elif before != after:
|
|
1568
|
-
raise
|
|
1564
|
+
raise OperationValueError(
|
|
1565
|
+
"'line' must be supplied or 'before' and 'after' must be equal"
|
|
1566
|
+
)
|
|
1567
|
+
if isinstance(content, str):
|
|
1568
|
+
# convert string to list of lines
|
|
1569
|
+
content = content.split("\n")
|
|
1570
|
+
if try_prevent_shell_expansion and any("'" in line for line in content):
|
|
1571
|
+
logger.warning("content contains single quotes, shell expansion prevention may fail")
|
|
1569
1572
|
|
|
1570
|
-
the_block = "\n".join([mark_1, content, mark_2])
|
|
1573
|
+
the_block = "\n".join([mark_1, *content, mark_2])
|
|
1571
1574
|
|
|
1572
1575
|
if (current is None) or ((current == []) and (before == after)):
|
|
1573
1576
|
# a) no file or b) file but no markers and we're adding at start or end. Both use 'cat'
|
|
@@ -1575,8 +1578,14 @@ def block(
|
|
|
1575
1578
|
stdin = "- " if ((current == []) and before) else ""
|
|
1576
1579
|
# here = hex(random.randint(0, 2147483647))
|
|
1577
1580
|
here = "PYINFRAHERE"
|
|
1578
|
-
cmd = StringCommand(
|
|
1581
|
+
cmd = StringCommand(
|
|
1582
|
+
f"cat {stdin}{redirect}",
|
|
1583
|
+
q_path,
|
|
1584
|
+
f"<<{here}" if not try_prevent_shell_expansion else f"<<'{here}'",
|
|
1585
|
+
f"\n{the_block}\n{here}",
|
|
1586
|
+
)
|
|
1579
1587
|
elif current == []: # markers not found and have a pattern to match (not start or end)
|
|
1588
|
+
assert isinstance(line, str)
|
|
1580
1589
|
regex = adjust_regex(line, escape_regex_characters)
|
|
1581
1590
|
print_before = "{ print }" if before else ""
|
|
1582
1591
|
print_after = "{ print }" if after else ""
|
|
@@ -1585,21 +1594,31 @@ def block(
|
|
|
1585
1594
|
f"{print_after} f!=1 && /{regex}/ {{ print x; f=1}} "
|
|
1586
1595
|
f"END {{if (f==0) print ARGV[2] }} {print_before}'"
|
|
1587
1596
|
)
|
|
1588
|
-
cmd = StringCommand(
|
|
1597
|
+
cmd = StringCommand(
|
|
1598
|
+
out_prep,
|
|
1599
|
+
prog,
|
|
1600
|
+
q_path,
|
|
1601
|
+
f'"{the_block}"' if not try_prevent_shell_expansion else f"'{the_block}'",
|
|
1602
|
+
"> $OUT &&",
|
|
1603
|
+
real_out,
|
|
1604
|
+
)
|
|
1589
1605
|
else:
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
not all(lines[0] == lines[1] for lines in zip(pieces, current))
|
|
1606
|
+
if (len(current) != len(content)) or (
|
|
1607
|
+
not all(lines[0] == lines[1] for lines in zip(content, current))
|
|
1593
1608
|
): # marked_block found but text is different
|
|
1594
1609
|
prog = (
|
|
1595
1610
|
'awk \'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=""}}'
|
|
1596
|
-
f"/{mark_1}/ {{print; print x; f=0}} /{mark_2}/ {{print; f=1}} f'"
|
|
1611
|
+
f"/{mark_1}/ {{print; print x; f=0}} /{mark_2}/ {{print; f=1; next}} f'"
|
|
1597
1612
|
)
|
|
1598
1613
|
cmd = StringCommand(
|
|
1599
1614
|
out_prep,
|
|
1600
1615
|
prog,
|
|
1601
1616
|
q_path,
|
|
1602
|
-
|
|
1617
|
+
(
|
|
1618
|
+
'"' + "\n".join(content) + '"'
|
|
1619
|
+
if not try_prevent_shell_expansion
|
|
1620
|
+
else "'" + "\n".join(content) + "'"
|
|
1621
|
+
),
|
|
1603
1622
|
"> $OUT &&",
|
|
1604
1623
|
real_out,
|
|
1605
1624
|
)
|