pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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/__init__.py +9 -12
- pyinfra/__main__.py +4 -0
- pyinfra/api/__init__.py +19 -3
- pyinfra/api/arguments.py +413 -0
- pyinfra/api/arguments_typed.py +79 -0
- pyinfra/api/command.py +274 -0
- pyinfra/api/config.py +222 -28
- pyinfra/api/connect.py +33 -13
- pyinfra/api/connectors.py +27 -0
- pyinfra/api/deploy.py +65 -66
- pyinfra/api/exceptions.py +73 -18
- pyinfra/api/facts.py +267 -200
- pyinfra/api/host.py +416 -50
- pyinfra/api/inventory.py +121 -160
- pyinfra/api/metadata.py +69 -0
- pyinfra/api/operation.py +432 -262
- pyinfra/api/operations.py +273 -260
- pyinfra/api/state.py +302 -248
- pyinfra/api/util.py +309 -369
- pyinfra/connectors/base.py +173 -0
- pyinfra/connectors/chroot.py +212 -0
- pyinfra/connectors/docker.py +405 -0
- pyinfra/connectors/dockerssh.py +297 -0
- pyinfra/connectors/local.py +238 -0
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +727 -0
- pyinfra/connectors/ssh_util.py +114 -0
- pyinfra/connectors/sshuserclient/client.py +309 -0
- pyinfra/connectors/sshuserclient/config.py +102 -0
- pyinfra/connectors/terraform.py +135 -0
- pyinfra/connectors/util.py +417 -0
- pyinfra/connectors/vagrant.py +183 -0
- pyinfra/context.py +145 -0
- pyinfra/facts/__init__.py +7 -6
- pyinfra/facts/apk.py +22 -7
- pyinfra/facts/apt.py +117 -60
- pyinfra/facts/brew.py +100 -15
- pyinfra/facts/bsdinit.py +23 -0
- pyinfra/facts/cargo.py +37 -0
- pyinfra/facts/choco.py +47 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +94 -0
- pyinfra/facts/dnf.py +48 -0
- pyinfra/facts/docker.py +96 -23
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +629 -58
- pyinfra/facts/flatpak.py +77 -0
- pyinfra/facts/freebsd.py +70 -0
- pyinfra/facts/gem.py +19 -6
- pyinfra/facts/git.py +59 -14
- pyinfra/facts/gpg.py +150 -0
- pyinfra/facts/hardware.py +313 -167
- pyinfra/facts/iptables.py +72 -62
- pyinfra/facts/launchd.py +44 -0
- pyinfra/facts/lxd.py +17 -4
- pyinfra/facts/mysql.py +122 -86
- pyinfra/facts/npm.py +17 -9
- pyinfra/facts/openrc.py +71 -0
- pyinfra/facts/opkg.py +246 -0
- pyinfra/facts/pacman.py +50 -7
- pyinfra/facts/pip.py +24 -7
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +15 -6
- pyinfra/facts/pkgin.py +35 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +178 -0
- pyinfra/facts/postgresql.py +6 -147
- pyinfra/facts/rpm.py +105 -0
- pyinfra/facts/runit.py +77 -0
- pyinfra/facts/selinux.py +161 -0
- pyinfra/facts/server.py +762 -285
- pyinfra/facts/snap.py +88 -0
- pyinfra/facts/systemd.py +139 -0
- pyinfra/facts/sysvinit.py +59 -0
- pyinfra/facts/upstart.py +35 -0
- pyinfra/facts/util/__init__.py +17 -0
- pyinfra/facts/util/databases.py +4 -6
- pyinfra/facts/util/packaging.py +37 -6
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/util/win_files.py +99 -0
- pyinfra/facts/vzctl.py +20 -13
- pyinfra/facts/xbps.py +35 -0
- pyinfra/facts/yum.py +34 -40
- pyinfra/facts/zfs.py +77 -0
- pyinfra/facts/zypper.py +42 -0
- pyinfra/local.py +45 -83
- pyinfra/operations/__init__.py +12 -0
- pyinfra/operations/apk.py +99 -0
- pyinfra/operations/apt.py +496 -0
- pyinfra/operations/brew.py +232 -0
- pyinfra/operations/bsdinit.py +59 -0
- pyinfra/operations/cargo.py +45 -0
- pyinfra/operations/choco.py +61 -0
- pyinfra/operations/crontab.py +194 -0
- pyinfra/operations/dnf.py +213 -0
- pyinfra/operations/docker.py +492 -0
- pyinfra/operations/files.py +2014 -0
- pyinfra/operations/flatpak.py +95 -0
- pyinfra/operations/freebsd/__init__.py +12 -0
- pyinfra/operations/freebsd/freebsd_update.py +70 -0
- pyinfra/operations/freebsd/pkg.py +219 -0
- pyinfra/operations/freebsd/service.py +116 -0
- pyinfra/operations/freebsd/sysrc.py +92 -0
- pyinfra/operations/gem.py +48 -0
- pyinfra/operations/git.py +420 -0
- pyinfra/operations/iptables.py +312 -0
- pyinfra/operations/launchd.py +45 -0
- pyinfra/operations/lxd.py +69 -0
- pyinfra/operations/mysql.py +610 -0
- pyinfra/operations/npm.py +57 -0
- pyinfra/operations/openrc.py +63 -0
- pyinfra/operations/opkg.py +89 -0
- pyinfra/operations/pacman.py +82 -0
- pyinfra/operations/pip.py +206 -0
- pyinfra/operations/pipx.py +103 -0
- pyinfra/operations/pkg.py +71 -0
- pyinfra/operations/pkgin.py +92 -0
- pyinfra/operations/postgres.py +437 -0
- pyinfra/operations/postgresql.py +30 -0
- pyinfra/operations/puppet.py +41 -0
- pyinfra/operations/python.py +73 -0
- pyinfra/operations/runit.py +184 -0
- pyinfra/operations/selinux.py +190 -0
- pyinfra/operations/server.py +1100 -0
- pyinfra/operations/snap.py +118 -0
- pyinfra/operations/ssh.py +217 -0
- pyinfra/operations/systemd.py +150 -0
- pyinfra/operations/sysvinit.py +142 -0
- pyinfra/operations/upstart.py +68 -0
- pyinfra/operations/util/__init__.py +12 -0
- pyinfra/operations/util/docker.py +407 -0
- pyinfra/operations/util/files.py +247 -0
- pyinfra/operations/util/packaging.py +338 -0
- pyinfra/operations/util/service.py +46 -0
- pyinfra/operations/vzctl.py +137 -0
- pyinfra/operations/xbps.py +78 -0
- pyinfra/operations/yum.py +213 -0
- pyinfra/operations/zfs.py +176 -0
- pyinfra/operations/zypper.py +193 -0
- pyinfra/progress.py +44 -32
- pyinfra/py.typed +0 -0
- pyinfra/version.py +9 -1
- pyinfra-3.6.dist-info/METADATA +142 -0
- pyinfra-3.6.dist-info/RECORD +160 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
- pyinfra-3.6.dist-info/entry_points.txt +12 -0
- {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
- pyinfra_cli/__init__.py +1 -0
- pyinfra_cli/cli.py +793 -0
- pyinfra_cli/commands.py +66 -0
- pyinfra_cli/exceptions.py +155 -65
- pyinfra_cli/inventory.py +233 -89
- pyinfra_cli/log.py +39 -43
- pyinfra_cli/main.py +26 -495
- pyinfra_cli/prints.py +215 -156
- pyinfra_cli/util.py +172 -105
- pyinfra_cli/virtualenv.py +25 -20
- pyinfra/api/connectors/__init__.py +0 -21
- pyinfra/api/connectors/ansible.py +0 -99
- pyinfra/api/connectors/docker.py +0 -178
- pyinfra/api/connectors/local.py +0 -169
- pyinfra/api/connectors/ssh.py +0 -402
- pyinfra/api/connectors/sshuserclient/client.py +0 -105
- pyinfra/api/connectors/sshuserclient/config.py +0 -90
- pyinfra/api/connectors/util.py +0 -63
- pyinfra/api/connectors/vagrant.py +0 -155
- pyinfra/facts/init.py +0 -176
- pyinfra/facts/util/files.py +0 -102
- pyinfra/hook.py +0 -41
- pyinfra/modules/__init__.py +0 -11
- pyinfra/modules/apk.py +0 -64
- pyinfra/modules/apt.py +0 -272
- pyinfra/modules/brew.py +0 -122
- pyinfra/modules/files.py +0 -711
- pyinfra/modules/gem.py +0 -30
- pyinfra/modules/git.py +0 -115
- pyinfra/modules/init.py +0 -344
- pyinfra/modules/iptables.py +0 -271
- pyinfra/modules/lxd.py +0 -45
- pyinfra/modules/mysql.py +0 -347
- pyinfra/modules/npm.py +0 -47
- pyinfra/modules/pacman.py +0 -60
- pyinfra/modules/pip.py +0 -99
- pyinfra/modules/pkg.py +0 -43
- pyinfra/modules/postgresql.py +0 -245
- pyinfra/modules/puppet.py +0 -20
- pyinfra/modules/python.py +0 -37
- pyinfra/modules/server.py +0 -524
- pyinfra/modules/ssh.py +0 -150
- pyinfra/modules/util/files.py +0 -52
- pyinfra/modules/util/packaging.py +0 -118
- pyinfra/modules/vzctl.py +0 -133
- pyinfra/modules/yum.py +0 -171
- pyinfra/pseudo_modules.py +0 -64
- pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
- pyinfra-0.11.dev3.dist-info/METADATA +0 -135
- pyinfra-0.11.dev3.dist-info/RECORD +0 -95
- pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
- pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
- pyinfra_cli/__main__.py +0 -40
- pyinfra_cli/config.py +0 -92
- /pyinfra/{modules/util → connectors}/__init__.py +0 -0
- /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/facts/files.py
CHANGED
|
@@ -1,105 +1,676 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The files facts provide information about the filesystem and it's contents on the target host.
|
|
3
|
+
|
|
4
|
+
Facts need to be imported before use, eg
|
|
5
|
+
|
|
6
|
+
from pyinfra.facts.files import File
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
1
11
|
import re
|
|
12
|
+
import shlex
|
|
13
|
+
import stat
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from typing_extensions import Literal, NotRequired, TypedDict, override
|
|
2
18
|
|
|
19
|
+
from pyinfra.api import StringCommand
|
|
20
|
+
from pyinfra.api.command import QuoteString, make_formatted_string_command
|
|
3
21
|
from pyinfra.api.facts import FactBase
|
|
22
|
+
from pyinfra.api.util import try_int
|
|
23
|
+
from pyinfra.facts.util.units import parse_size
|
|
24
|
+
|
|
25
|
+
LINUX_STAT_COMMAND = "stat -c 'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"
|
|
26
|
+
BSD_STAT_COMMAND = "stat -f 'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"
|
|
27
|
+
LS_COMMAND = "ls -ld"
|
|
28
|
+
|
|
29
|
+
STAT_REGEX = (
|
|
30
|
+
r"user=(.*) group=(.*) mode=(.*) "
|
|
31
|
+
r"atime=(-?[0-9]*) mtime=(-?[0-9]*) ctime=(-?[0-9]*) "
|
|
32
|
+
r"size=([0-9]*) (.*)"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ls -ld output: permissions links user group size month day year/time path
|
|
36
|
+
# Supports attribute markers: . (SELinux), @ (extended attrs), + (ACL)
|
|
37
|
+
# Handles both "MMM DD" and "DD MMM" date formats
|
|
38
|
+
LS_REGEX = (
|
|
39
|
+
r"^([dlbcsp-][-rwxstST]{9}[.@+]?)\s+\d+\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
FLAG_TO_TYPE = {
|
|
43
|
+
"b": "block",
|
|
44
|
+
"c": "character",
|
|
45
|
+
"d": "directory",
|
|
46
|
+
"l": "link",
|
|
47
|
+
"s": "socket",
|
|
48
|
+
"p": "fifo",
|
|
49
|
+
"-": "file",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Each item is a map of character to permission octal to be combined, taken from stdlib:
|
|
53
|
+
# https://github.com/python/cpython/blob/c1c3be0f9dc414bfae9a5718451ca217751ac687/Lib/stat.py#L128-L154
|
|
54
|
+
CHAR_TO_PERMISSION = (
|
|
55
|
+
# User
|
|
56
|
+
{"r": stat.S_IRUSR},
|
|
57
|
+
{"w": stat.S_IWUSR},
|
|
58
|
+
{"x": stat.S_IXUSR, "S": stat.S_ISUID, "s": stat.S_IXUSR | stat.S_ISUID},
|
|
59
|
+
# Group
|
|
60
|
+
{"r": stat.S_IRGRP},
|
|
61
|
+
{"w": stat.S_IWGRP},
|
|
62
|
+
{"x": stat.S_IXGRP, "S": stat.S_ISGID, "s": stat.S_IXGRP | stat.S_ISGID},
|
|
63
|
+
# Other
|
|
64
|
+
{"r": stat.S_IROTH},
|
|
65
|
+
{"w": stat.S_IWOTH},
|
|
66
|
+
{"x": stat.S_IXOTH, "T": stat.S_ISVTX, "t": stat.S_IXOTH | stat.S_ISVTX},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_mode(mode: str) -> int:
|
|
71
|
+
"""
|
|
72
|
+
Converts ls mode output (rwxrwxrwx) -> octal permission integer (755).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
out = 0
|
|
76
|
+
|
|
77
|
+
for i, char in enumerate(mode):
|
|
78
|
+
for c, m in CHAR_TO_PERMISSION[i].items():
|
|
79
|
+
if char == c:
|
|
80
|
+
out |= m
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return int(oct(out)[2:])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_datetime(value: str) -> Optional[datetime]:
|
|
87
|
+
value = try_int(value)
|
|
88
|
+
if isinstance(value, int):
|
|
89
|
+
return datetime.fromtimestamp(value, timezone.utc).replace(tzinfo=None)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_ls_timestamp(month: str, day: str, year_or_time: str) -> Optional[datetime]:
|
|
94
|
+
"""
|
|
95
|
+
Parse ls timestamp format.
|
|
96
|
+
Examples: "Jan 1 1970", "Apr 2 2025", "Dec 31 12:34"
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
# Month abbreviation to number mapping
|
|
100
|
+
month_map = {
|
|
101
|
+
"Jan": 1,
|
|
102
|
+
"Feb": 2,
|
|
103
|
+
"Mar": 3,
|
|
104
|
+
"Apr": 4,
|
|
105
|
+
"May": 5,
|
|
106
|
+
"Jun": 6,
|
|
107
|
+
"Jul": 7,
|
|
108
|
+
"Aug": 8,
|
|
109
|
+
"Sep": 9,
|
|
110
|
+
"Oct": 10,
|
|
111
|
+
"Nov": 11,
|
|
112
|
+
"Dec": 12,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
month_num = month_map.get(month)
|
|
116
|
+
if month_num is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
day_num = int(day)
|
|
120
|
+
|
|
121
|
+
# Check if year_or_time is a year (4 digits) or time (HH:MM)
|
|
122
|
+
if ":" in year_or_time:
|
|
123
|
+
# It's a time, assume current year
|
|
124
|
+
import time
|
|
125
|
+
|
|
126
|
+
current_year = time.gmtime().tm_year
|
|
127
|
+
hour, minute = map(int, year_or_time.split(":"))
|
|
128
|
+
return datetime(current_year, month_num, day_num, hour, minute)
|
|
129
|
+
else:
|
|
130
|
+
# It's a year
|
|
131
|
+
year_num = int(year_or_time)
|
|
132
|
+
return datetime(year_num, month_num, day_num)
|
|
133
|
+
|
|
134
|
+
except (ValueError, TypeError):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_ls_output(output: str) -> Optional[tuple[FileDict, str]]:
|
|
139
|
+
"""
|
|
140
|
+
Parse ls -ld output and extract file information.
|
|
141
|
+
Example: drwxr-xr-x 1 root root 416 Jan 1 1970 /
|
|
142
|
+
"""
|
|
143
|
+
match = re.match(LS_REGEX, output.strip())
|
|
144
|
+
if not match:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
permissions = match.group(1)
|
|
148
|
+
user = match.group(2)
|
|
149
|
+
group = match.group(3)
|
|
150
|
+
size = match.group(4)
|
|
151
|
+
date_part1 = match.group(5)
|
|
152
|
+
date_part2 = match.group(6)
|
|
153
|
+
year_or_time = match.group(7)
|
|
154
|
+
path = match.group(8)
|
|
155
|
+
|
|
156
|
+
# Determine if it's "MMM DD" or "DD MMM" format
|
|
157
|
+
if date_part1.isdigit():
|
|
158
|
+
# "DD MMM" format (e.g., "22 Jun")
|
|
159
|
+
day = date_part1
|
|
160
|
+
month = date_part2
|
|
161
|
+
else:
|
|
162
|
+
# "MMM DD" format (e.g., "Jun 22")
|
|
163
|
+
month = date_part1
|
|
164
|
+
day = date_part2
|
|
165
|
+
|
|
166
|
+
# Extract file type from first character of permissions
|
|
167
|
+
path_type = FLAG_TO_TYPE[permissions[0]]
|
|
168
|
+
|
|
169
|
+
# Parse mode (skip first character which is file type, and any trailing attribute markers)
|
|
170
|
+
# Remove trailing attribute markers (.@+) if present
|
|
171
|
+
mode_str = permissions[1:10] # Take exactly 9 characters after file type
|
|
172
|
+
mode = _parse_mode(mode_str)
|
|
173
|
+
|
|
174
|
+
# Parse timestamp - ls shows modification time
|
|
175
|
+
mtime = _parse_ls_timestamp(month, day, year_or_time)
|
|
176
|
+
|
|
177
|
+
data: FileDict = {
|
|
178
|
+
"user": user,
|
|
179
|
+
"group": group,
|
|
180
|
+
"mode": mode,
|
|
181
|
+
"atime": None, # ls doesn't provide atime
|
|
182
|
+
"mtime": mtime,
|
|
183
|
+
"ctime": None, # ls doesn't provide ctime
|
|
184
|
+
"size": try_int(size),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Handle symbolic links
|
|
188
|
+
if path_type == "link" and " -> " in path:
|
|
189
|
+
filename, target = path.split(" -> ", 1)
|
|
190
|
+
data["link_target"] = target.strip("'").lstrip("`")
|
|
191
|
+
|
|
192
|
+
return data, path_type
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class FileDict(TypedDict):
|
|
196
|
+
mode: int
|
|
197
|
+
size: Union[int, str]
|
|
198
|
+
atime: Optional[datetime]
|
|
199
|
+
mtime: Optional[datetime]
|
|
200
|
+
ctime: Optional[datetime]
|
|
201
|
+
user: str
|
|
202
|
+
group: str
|
|
203
|
+
link_target: NotRequired[str]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class File(FactBase[Union[FileDict, Literal[False], None]]):
|
|
207
|
+
"""
|
|
208
|
+
Returns information about a file on the remote system:
|
|
209
|
+
|
|
210
|
+
.. code:: python
|
|
211
|
+
|
|
212
|
+
{
|
|
213
|
+
"user": "pyinfra",
|
|
214
|
+
"group": "pyinfra",
|
|
215
|
+
"mode": 644,
|
|
216
|
+
"size": 3928,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
If the path does not exist:
|
|
220
|
+
returns ``None``
|
|
221
|
+
|
|
222
|
+
If the path exists but is not a file:
|
|
223
|
+
returns ``False``
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
type = "file"
|
|
227
|
+
|
|
228
|
+
@override
|
|
229
|
+
def command(self, path):
|
|
230
|
+
if path.startswith("~/"):
|
|
231
|
+
# Do not quote leading tilde to ensure that it gets properly expanded by the shell
|
|
232
|
+
path = f"~/{shlex.quote(path[2:])}"
|
|
233
|
+
else:
|
|
234
|
+
path = QuoteString(path)
|
|
235
|
+
|
|
236
|
+
return make_formatted_string_command(
|
|
237
|
+
(
|
|
238
|
+
# only stat if the path exists (file or symlink)
|
|
239
|
+
"! (test -e {0} || test -L {0} ) || "
|
|
240
|
+
"( {linux_stat_command} {0} 2> /dev/null || "
|
|
241
|
+
"{bsd_stat_command} {0} || {ls_command} {0} )"
|
|
242
|
+
),
|
|
243
|
+
path,
|
|
244
|
+
linux_stat_command=LINUX_STAT_COMMAND,
|
|
245
|
+
bsd_stat_command=BSD_STAT_COMMAND,
|
|
246
|
+
ls_command=LS_COMMAND,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
@override
|
|
250
|
+
def process(self, output) -> Union[FileDict, Literal[False], None]:
|
|
251
|
+
# Try to parse as stat output first
|
|
252
|
+
match = re.match(STAT_REGEX, output[0])
|
|
253
|
+
if match:
|
|
254
|
+
mode = match.group(3)
|
|
255
|
+
path_type = FLAG_TO_TYPE[mode[0]]
|
|
256
|
+
|
|
257
|
+
data: FileDict = {
|
|
258
|
+
"user": match.group(1),
|
|
259
|
+
"group": match.group(2),
|
|
260
|
+
"mode": _parse_mode(mode[1:]),
|
|
261
|
+
"atime": _parse_datetime(match.group(4)),
|
|
262
|
+
"mtime": _parse_datetime(match.group(5)),
|
|
263
|
+
"ctime": _parse_datetime(match.group(6)),
|
|
264
|
+
"size": try_int(match.group(7)),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if path_type != self.type:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
if path_type == "link":
|
|
271
|
+
filename = match.group(8)
|
|
272
|
+
filename, target = filename.split(" -> ")
|
|
273
|
+
data["link_target"] = target.strip("'").lstrip("`")
|
|
274
|
+
|
|
275
|
+
return data
|
|
276
|
+
|
|
277
|
+
# Try to parse as ls output
|
|
278
|
+
ls_result = _parse_ls_output(output[0])
|
|
279
|
+
if ls_result is not None:
|
|
280
|
+
data, path_type = ls_result
|
|
281
|
+
|
|
282
|
+
if path_type != self.type:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
return data
|
|
286
|
+
|
|
287
|
+
return None
|
|
4
288
|
|
|
5
|
-
from .util.files import parse_ls_output
|
|
6
289
|
|
|
290
|
+
class Link(File):
|
|
291
|
+
"""
|
|
292
|
+
Returns information about a link on the remote system:
|
|
7
293
|
|
|
8
|
-
|
|
9
|
-
# Types must match FLAG_TO_TYPE in .util.files.py
|
|
10
|
-
type = 'file'
|
|
294
|
+
.. code:: python
|
|
11
295
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
296
|
+
{
|
|
297
|
+
"user": "pyinfra",
|
|
298
|
+
"group": "pyinfra",
|
|
299
|
+
"link_target": "/path/to/link/target"
|
|
300
|
+
}
|
|
15
301
|
|
|
16
|
-
|
|
17
|
-
|
|
302
|
+
If the path does not exist:
|
|
303
|
+
returns ``None``
|
|
18
304
|
|
|
305
|
+
If the path exists but is not a link:
|
|
306
|
+
returns ``False``
|
|
307
|
+
"""
|
|
19
308
|
|
|
20
|
-
|
|
21
|
-
type = 'link'
|
|
309
|
+
type = "link"
|
|
22
310
|
|
|
23
311
|
|
|
24
312
|
class Directory(File):
|
|
25
|
-
|
|
313
|
+
"""
|
|
314
|
+
Returns information about a directory on the remote system:
|
|
315
|
+
|
|
316
|
+
.. code:: python
|
|
317
|
+
|
|
318
|
+
{
|
|
319
|
+
"user": "pyinfra",
|
|
320
|
+
"group": "pyinfra",
|
|
321
|
+
"mode": 644,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
If the path does not exist:
|
|
325
|
+
returns ``None``
|
|
326
|
+
|
|
327
|
+
If the path exists but is not a directory:
|
|
328
|
+
returns ``False``
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
type = "directory"
|
|
26
332
|
|
|
27
333
|
|
|
28
334
|
class Socket(File):
|
|
29
|
-
|
|
335
|
+
"""
|
|
336
|
+
Returns information about a socket on the remote system:
|
|
30
337
|
|
|
338
|
+
.. code:: python
|
|
31
339
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
340
|
+
{
|
|
341
|
+
"user": "pyinfra",
|
|
342
|
+
"group": "pyinfra",
|
|
343
|
+
}
|
|
36
344
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
r'^SHA1\s+\(%s\)\s+=\s+([a-zA-Z0-9]{40})$',
|
|
40
|
-
]
|
|
345
|
+
If the path does not exist:
|
|
346
|
+
returns ``None``
|
|
41
347
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
348
|
+
If the path exists but is not a socket:
|
|
349
|
+
returns ``False``
|
|
350
|
+
"""
|
|
45
351
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
352
|
+
type = "socket"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if TYPE_CHECKING:
|
|
356
|
+
FactBaseOptionalStr = FactBase[Optional[str]]
|
|
357
|
+
else:
|
|
358
|
+
FactBaseOptionalStr = FactBase
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class HashFileFactBase(FactBaseOptionalStr):
|
|
362
|
+
_raw_cmd: str
|
|
363
|
+
_regexes: Tuple[str, str]
|
|
364
|
+
|
|
365
|
+
@override
|
|
366
|
+
def __init_subclass__(cls, digits: int, cmds: List[str], **kwargs) -> None:
|
|
367
|
+
super().__init_subclass__(**kwargs)
|
|
368
|
+
|
|
369
|
+
raw_hash_cmds = ["%s {0} 2> /dev/null" % cmd for cmd in cmds]
|
|
370
|
+
raw_hash_cmd = " || ".join(raw_hash_cmds)
|
|
371
|
+
cls._raw_cmd = "test -e {0} && ( %s ) || true" % raw_hash_cmd
|
|
50
372
|
|
|
373
|
+
assert cls.__name__.endswith("File")
|
|
374
|
+
hash_name = cls.__name__[:-4].upper()
|
|
375
|
+
cls._regexes = (
|
|
376
|
+
# GNU coreutils style:
|
|
377
|
+
r"^([a-fA-F0-9]{%d})\s+%%s$" % digits,
|
|
378
|
+
# BSD style:
|
|
379
|
+
r"^%s\s+\(%%s\)\s+=\s+([a-fA-F0-9]{%d})$" % (hash_name, digits),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
@override
|
|
383
|
+
def command(self, path):
|
|
384
|
+
self.path = path
|
|
385
|
+
return make_formatted_string_command(self._raw_cmd, QuoteString(path))
|
|
386
|
+
|
|
387
|
+
@override
|
|
388
|
+
def process(self, output) -> Optional[str]:
|
|
389
|
+
output = output[0]
|
|
390
|
+
escaped_path = re.escape(self.path)
|
|
391
|
+
for regex in self._regexes:
|
|
392
|
+
matches = re.match(regex % escaped_path, output)
|
|
51
393
|
if matches:
|
|
52
394
|
return matches.group(1)
|
|
395
|
+
return None
|
|
53
396
|
|
|
54
397
|
|
|
55
|
-
class
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
398
|
+
class Sha1File(HashFileFactBase, digits=40, cmds=["sha1sum", "shasum", "sha1"]):
|
|
399
|
+
"""
|
|
400
|
+
Returns a SHA1 hash of a file. Works with both sha1sum and sha1. Returns
|
|
401
|
+
``None`` if the file doest not exist.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class Sha256File(HashFileFactBase, digits=64, cmds=["sha256sum", "shasum -a 256", "sha256"]):
|
|
406
|
+
"""
|
|
407
|
+
Returns a SHA256 hash of a file, or ``None`` if the file does not exist.
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class Sha384File(HashFileFactBase, digits=96, cmds=["sha384sum", "shasum -a 384", "sha384"]):
|
|
412
|
+
"""
|
|
413
|
+
Returns a SHA384 hash of a file, or ``None`` if the file does not exist.
|
|
414
|
+
"""
|
|
415
|
+
|
|
60
416
|
|
|
61
|
-
|
|
62
|
-
|
|
417
|
+
class Md5File(HashFileFactBase, digits=32, cmds=["md5sum", "md5"]):
|
|
418
|
+
"""
|
|
419
|
+
Returns an MD5 hash of a file, or ``None`` if the file does not exist.
|
|
420
|
+
"""
|
|
63
421
|
|
|
64
|
-
return '''
|
|
65
|
-
grep "{0}" {1} || (find {1} -type f > /dev/null && echo "__pyinfra_exists_{1}")
|
|
66
|
-
'''.format(pattern, name).strip()
|
|
67
422
|
|
|
423
|
+
class FindInFile(FactBase):
|
|
424
|
+
"""
|
|
425
|
+
Checks for the existence of text in a file using grep. Returns a list of matching
|
|
426
|
+
lines if the file exists, and ``None`` if the file does not.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
@override
|
|
430
|
+
def command(self, path, pattern, interpolate_variables=False):
|
|
431
|
+
self.exists_flag = "__pyinfra_exists_{0}".format(path)
|
|
432
|
+
|
|
433
|
+
if interpolate_variables:
|
|
434
|
+
pattern = '"{0}"'.format(pattern.replace('"', '\\"'))
|
|
435
|
+
else:
|
|
436
|
+
pattern = QuoteString(pattern)
|
|
437
|
+
|
|
438
|
+
return make_formatted_string_command(
|
|
439
|
+
(
|
|
440
|
+
"grep -e {0} {1} 2> /dev/null || "
|
|
441
|
+
"( find {1} -type f > /dev/null && echo {2} || true )"
|
|
442
|
+
),
|
|
443
|
+
pattern,
|
|
444
|
+
QuoteString(path),
|
|
445
|
+
QuoteString(self.exists_flag),
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
@override
|
|
68
449
|
def process(self, output):
|
|
69
450
|
# If output is the special string: no matches, so return an empty list;
|
|
70
451
|
# this allows us to differentiate between no matches in an existing file
|
|
71
452
|
# or a file not existing.
|
|
72
|
-
if output and output[0] ==
|
|
453
|
+
if output and output[0] == self.exists_flag:
|
|
73
454
|
return []
|
|
74
455
|
|
|
75
456
|
return output
|
|
76
457
|
|
|
77
458
|
|
|
78
|
-
class
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def command(self, name):
|
|
84
|
-
return 'find {0} -type f'.format(name)
|
|
459
|
+
class FindFilesBase(FactBase):
|
|
460
|
+
abstract = True
|
|
461
|
+
default = list
|
|
462
|
+
type_flag: str
|
|
85
463
|
|
|
464
|
+
@override
|
|
86
465
|
def process(self, output):
|
|
87
466
|
return output
|
|
88
467
|
|
|
468
|
+
@override
|
|
469
|
+
def command(
|
|
470
|
+
self,
|
|
471
|
+
path: str,
|
|
472
|
+
size: Optional[str | int] = None,
|
|
473
|
+
min_size: Optional[str | int] = None,
|
|
474
|
+
max_size: Optional[str | int] = None,
|
|
475
|
+
maxdepth: Optional[int] = None,
|
|
476
|
+
fname: Optional[str] = None,
|
|
477
|
+
iname: Optional[str] = None,
|
|
478
|
+
regex: Optional[str] = None,
|
|
479
|
+
args: Optional[List[str]] = None,
|
|
480
|
+
quote_path=True,
|
|
481
|
+
):
|
|
482
|
+
"""
|
|
483
|
+
@param path: the path to start the search from
|
|
484
|
+
@param size: exact size in bytes or human-readable format.
|
|
485
|
+
GB means 1e9 bytes, GiB means 2^30 bytes
|
|
486
|
+
@param min_size: minimum size in bytes or human-readable format
|
|
487
|
+
@param max_size: maximum size in bytes or human-readable format
|
|
488
|
+
@param maxdepth: maximum depth to descend to
|
|
489
|
+
@param name: True if the last component of the pathname being examined matches pattern.
|
|
490
|
+
Special shell pattern matching characters (“[”, “]”, “*”, and “?”)
|
|
491
|
+
may be used as part of pattern.
|
|
492
|
+
These characters may be matched explicitly
|
|
493
|
+
by escaping them with a backslash (“\\”).
|
|
494
|
+
|
|
495
|
+
@param iname: Like -name, but the match is case insensitive.
|
|
496
|
+
@param regex: True if the whole path of the file matches pattern using regular expression.
|
|
497
|
+
@param args: additional arguments to pass to find
|
|
498
|
+
@param quote_path: if the path should be quoted
|
|
499
|
+
@return:
|
|
500
|
+
"""
|
|
501
|
+
if args is None:
|
|
502
|
+
args = []
|
|
503
|
+
|
|
504
|
+
def maybe_quote(value):
|
|
505
|
+
return QuoteString(value) if quote_path else value
|
|
506
|
+
|
|
507
|
+
command = [
|
|
508
|
+
"find",
|
|
509
|
+
maybe_quote(path),
|
|
510
|
+
"-type",
|
|
511
|
+
self.type_flag,
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
"""
|
|
515
|
+
Why we need special handling for size:
|
|
516
|
+
https://unix.stackexchange.com/questions/275925/why-does-find-size-1g-not-find-any-files
|
|
517
|
+
In short, 'c' means bytes, without it, it means 512-byte blocks.
|
|
518
|
+
If we use any units other than 'c', it has a weird rounding behavior,
|
|
519
|
+
and is implementation-specific. So, we always use 'c'
|
|
520
|
+
"""
|
|
521
|
+
if "-size" not in args:
|
|
522
|
+
if min_size is not None:
|
|
523
|
+
command.append("-size")
|
|
524
|
+
command.append("+{0}c".format(parse_size(min_size)))
|
|
525
|
+
|
|
526
|
+
if max_size is not None:
|
|
527
|
+
command.append("-size")
|
|
528
|
+
command.append("-{0}c".format(parse_size(max_size)))
|
|
529
|
+
|
|
530
|
+
if size is not None:
|
|
531
|
+
command.append("-size")
|
|
532
|
+
command.append("{0}c".format(size))
|
|
533
|
+
|
|
534
|
+
if maxdepth is not None and "-maxdepth" not in args:
|
|
535
|
+
command.append("-maxdepth")
|
|
536
|
+
command.append("{0}".format(maxdepth))
|
|
537
|
+
|
|
538
|
+
if fname is not None and "-fname" not in args:
|
|
539
|
+
command.append("-name")
|
|
540
|
+
command.append(maybe_quote(fname))
|
|
541
|
+
|
|
542
|
+
if iname is not None and "-iname" not in args:
|
|
543
|
+
command.append("-iname")
|
|
544
|
+
command.append(maybe_quote(iname))
|
|
545
|
+
|
|
546
|
+
if regex is not None and "-regex" not in args:
|
|
547
|
+
command.append("-regex")
|
|
548
|
+
command.append(maybe_quote(regex))
|
|
549
|
+
|
|
550
|
+
command.append("||")
|
|
551
|
+
command.append("true")
|
|
552
|
+
|
|
553
|
+
return StringCommand(*command)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class FindFiles(FindFilesBase):
|
|
557
|
+
"""
|
|
558
|
+
Returns a list of files from a start path, recursively using ``find``.
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
type_flag = "f"
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class FindLinks(FindFilesBase):
|
|
565
|
+
"""
|
|
566
|
+
Returns a list of links from a start path, recursively using ``find``.
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
type_flag = "l"
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class FindDirectories(FindFilesBase):
|
|
573
|
+
"""
|
|
574
|
+
Returns a list of directories from a start path, recursively using ``find``.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
type_flag = "d"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class Flags(FactBase):
|
|
581
|
+
"""
|
|
582
|
+
Returns a list of the file flags set for the specified file or directory.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
@override
|
|
586
|
+
def requires_command(self, path) -> str:
|
|
587
|
+
return "chflags" # don't try to retrieve them if we can't set them
|
|
588
|
+
|
|
589
|
+
@override
|
|
590
|
+
def command(self, path):
|
|
591
|
+
return make_formatted_string_command(
|
|
592
|
+
"! test -e {0} || stat -f %Sf {0}",
|
|
593
|
+
QuoteString(path),
|
|
594
|
+
)
|
|
89
595
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
596
|
+
@override
|
|
597
|
+
def process(self, output):
|
|
598
|
+
return [flag for flag in output[0].split(",") if len(flag) > 0] if len(output) == 1 else []
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
MARKER_DEFAULT = "# {mark} PYINFRA BLOCK"
|
|
602
|
+
MARKER_BEGIN_DEFAULT = "BEGIN"
|
|
603
|
+
MARKER_END_DEFAULT = "END"
|
|
604
|
+
EXISTS = "__pyinfra_exists_"
|
|
605
|
+
MISSING = "__pyinfra_missing_"
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class Block(FactBase):
|
|
609
|
+
"""
|
|
610
|
+
Returns a (possibly empty) list of the lines found between the markers.
|
|
611
|
+
|
|
612
|
+
.. code:: python
|
|
613
|
+
|
|
614
|
+
[
|
|
615
|
+
"xray: one",
|
|
616
|
+
"alpha: two"
|
|
617
|
+
]
|
|
618
|
+
|
|
619
|
+
If the ``path`` doesn't exist
|
|
620
|
+
returns ``None``
|
|
621
|
+
|
|
622
|
+
If the ``path`` exists but the markers are not found
|
|
623
|
+
returns ``[]``
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
# if markers aren't found, awk will return 0 and produce no output but we need to
|
|
627
|
+
# distinguish between "markers not found" and "markers found but nothing between them"
|
|
628
|
+
# for the former we use the empty list (created the call to default) and for the latter
|
|
629
|
+
# the list with a single empty string.
|
|
630
|
+
default = list
|
|
631
|
+
|
|
632
|
+
@override
|
|
633
|
+
def command(self, path, marker=None, begin=None, end=None):
|
|
634
|
+
self.path = path
|
|
635
|
+
start = (marker or MARKER_DEFAULT).format(mark=begin or MARKER_BEGIN_DEFAULT)
|
|
636
|
+
end = (marker or MARKER_DEFAULT).format(mark=end or MARKER_END_DEFAULT)
|
|
637
|
+
if start == end:
|
|
638
|
+
raise ValueError(f"delimiters for block must be different but found only '{start}'")
|
|
639
|
+
|
|
640
|
+
backstop = make_formatted_string_command(
|
|
641
|
+
"(find {0} -type f > /dev/null && echo {1} || echo {2} )",
|
|
642
|
+
QuoteString(path),
|
|
643
|
+
QuoteString(f"{EXISTS}{path}"),
|
|
644
|
+
QuoteString(f"{MISSING}{path}"),
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
cmd = StringCommand(
|
|
648
|
+
f"awk '/{end}/{{ f=0}} f; /{start}/{{ f=1}} ' ",
|
|
649
|
+
QuoteString(path),
|
|
650
|
+
" || ",
|
|
651
|
+
backstop,
|
|
652
|
+
_separator="",
|
|
653
|
+
)
|
|
654
|
+
return cmd
|
|
655
|
+
|
|
656
|
+
@override
|
|
657
|
+
def process(self, output):
|
|
658
|
+
if output and (output[0] == f"{EXISTS}{self.path}"):
|
|
659
|
+
return []
|
|
660
|
+
if output and (output[0] == f"{MISSING}{self.path}"):
|
|
661
|
+
return None
|
|
662
|
+
return output
|
|
94
663
|
|
|
95
|
-
def command(self, name):
|
|
96
|
-
return 'find {0} -type l'.format(name)
|
|
97
664
|
|
|
665
|
+
class FileContents(FactBase):
|
|
666
|
+
"""
|
|
667
|
+
Returns the contents of a file as a list of lines. Returns ``None`` if the file does not exist.
|
|
668
|
+
"""
|
|
98
669
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'''
|
|
670
|
+
@override
|
|
671
|
+
def command(self, path):
|
|
672
|
+
return make_formatted_string_command("cat {0}", QuoteString(path))
|
|
103
673
|
|
|
104
|
-
|
|
105
|
-
|
|
674
|
+
@override
|
|
675
|
+
def process(self, output):
|
|
676
|
+
return output
|