pyinfra 3.1.1__py2.py3-none-any.whl → 3.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyinfra/api/arguments.py +9 -2
- pyinfra/api/deploy.py +4 -2
- pyinfra/api/host.py +5 -3
- pyinfra/connectors/docker.py +17 -6
- pyinfra/connectors/sshuserclient/client.py +26 -14
- pyinfra/facts/apk.py +3 -1
- pyinfra/facts/apt.py +60 -0
- pyinfra/facts/crontab.py +190 -0
- pyinfra/facts/docker.py +6 -0
- pyinfra/facts/efibootmgr.py +108 -0
- pyinfra/facts/files.py +93 -6
- pyinfra/facts/git.py +3 -2
- pyinfra/facts/mysql.py +1 -2
- pyinfra/facts/opkg.py +233 -0
- pyinfra/facts/pipx.py +74 -0
- pyinfra/facts/podman.py +47 -0
- pyinfra/facts/postgres.py +2 -0
- pyinfra/facts/server.py +39 -77
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/zfs.py +22 -19
- pyinfra/local.py +3 -2
- pyinfra/operations/apt.py +27 -20
- pyinfra/operations/crontab.py +189 -0
- pyinfra/operations/docker.py +13 -12
- pyinfra/operations/files.py +18 -0
- pyinfra/operations/git.py +23 -7
- pyinfra/operations/opkg.py +88 -0
- pyinfra/operations/pip.py +3 -2
- pyinfra/operations/pipx.py +90 -0
- pyinfra/operations/postgres.py +15 -11
- pyinfra/operations/runit.py +2 -0
- pyinfra/operations/server.py +3 -177
- pyinfra/operations/zfs.py +3 -3
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/METADATA +11 -12
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/RECORD +45 -36
- pyinfra_cli/inventory.py +26 -9
- pyinfra_cli/prints.py +18 -3
- pyinfra_cli/util.py +3 -0
- tests/test_cli/test_cli_deploy.py +15 -13
- tests/test_cli/test_cli_inventory.py +53 -0
- tests/test_connectors/test_sshuserclient.py +68 -1
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/WHEEL +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/top_level.txt +0 -0
pyinfra/facts/files.py
CHANGED
|
@@ -12,9 +12,11 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
|
12
12
|
|
|
13
13
|
from typing_extensions import Literal, NotRequired, TypedDict
|
|
14
14
|
|
|
15
|
+
from pyinfra.api import StringCommand
|
|
15
16
|
from pyinfra.api.command import QuoteString, make_formatted_string_command
|
|
16
17
|
from pyinfra.api.facts import FactBase
|
|
17
18
|
from pyinfra.api.util import try_int
|
|
19
|
+
from pyinfra.facts.util.units import parse_size
|
|
18
20
|
|
|
19
21
|
LINUX_STAT_COMMAND = "stat -c 'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"
|
|
20
22
|
BSD_STAT_COMMAND = "stat -f 'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"
|
|
@@ -274,6 +276,12 @@ class Sha256File(HashFileFactBase, digits=64, cmds=["sha256sum", "shasum -a 256"
|
|
|
274
276
|
"""
|
|
275
277
|
|
|
276
278
|
|
|
279
|
+
class Sha384File(HashFileFactBase, digits=96, cmds=["sha384sum", "shasum -a 384", "sha384"]):
|
|
280
|
+
"""
|
|
281
|
+
Returns a SHA384 hash of a file, or ``None`` if the file does not exist.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
|
|
277
285
|
class Md5File(HashFileFactBase, digits=32, cmds=["md5sum", "md5"]):
|
|
278
286
|
"""
|
|
279
287
|
Returns an MD5 hash of a file, or ``None`` if the file does not exist.
|
|
@@ -322,12 +330,91 @@ class FindFilesBase(FactBase):
|
|
|
322
330
|
def process(self, output):
|
|
323
331
|
return output
|
|
324
332
|
|
|
325
|
-
def command(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
333
|
+
def command(
|
|
334
|
+
self,
|
|
335
|
+
path: str,
|
|
336
|
+
size: Optional[str | int] = None,
|
|
337
|
+
min_size: Optional[str | int] = None,
|
|
338
|
+
max_size: Optional[str | int] = None,
|
|
339
|
+
maxdepth: Optional[int] = None,
|
|
340
|
+
fname: Optional[str] = None,
|
|
341
|
+
iname: Optional[str] = None,
|
|
342
|
+
regex: Optional[str] = None,
|
|
343
|
+
args: Optional[List[str]] = None,
|
|
344
|
+
quote_path=True,
|
|
345
|
+
):
|
|
346
|
+
"""
|
|
347
|
+
@param path: the path to start the search from
|
|
348
|
+
@param size: exact size in bytes or human-readable format.
|
|
349
|
+
GB means 1e9 bytes, GiB means 2^30 bytes
|
|
350
|
+
@param min_size: minimum size in bytes or human-readable format
|
|
351
|
+
@param max_size: maximum size in bytes or human-readable format
|
|
352
|
+
@param maxdepth: maximum depth to descend to
|
|
353
|
+
@param name: True if the last component of the pathname being examined matches pattern.
|
|
354
|
+
Special shell pattern matching characters (“[”, “]”, “*”, and “?”)
|
|
355
|
+
may be used as part of pattern.
|
|
356
|
+
These characters may be matched explicitly
|
|
357
|
+
by escaping them with a backslash (“\\”).
|
|
358
|
+
|
|
359
|
+
@param iname: Like -name, but the match is case insensitive.
|
|
360
|
+
@param regex: True if the whole path of the file matches pattern using regular expression.
|
|
361
|
+
@param args: additional arguments to pass to find
|
|
362
|
+
@param quote_path: if the path should be quoted
|
|
363
|
+
@return:
|
|
364
|
+
"""
|
|
365
|
+
if args is None:
|
|
366
|
+
args = []
|
|
367
|
+
|
|
368
|
+
def maybe_quote(value):
|
|
369
|
+
return QuoteString(value) if quote_path else value
|
|
370
|
+
|
|
371
|
+
command = [
|
|
372
|
+
"find",
|
|
373
|
+
maybe_quote(path),
|
|
374
|
+
"-type",
|
|
375
|
+
self.type_flag,
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
Why we need special handling for size:
|
|
380
|
+
https://unix.stackexchange.com/questions/275925/why-does-find-size-1g-not-find-any-files
|
|
381
|
+
In short, 'c' means bytes, without it, it means 512-byte blocks.
|
|
382
|
+
If we use any units other than 'c', it has a weird rounding behavior,
|
|
383
|
+
and is implementation-specific. So, we always use 'c'
|
|
384
|
+
"""
|
|
385
|
+
if "-size" not in args:
|
|
386
|
+
if min_size is not None:
|
|
387
|
+
command.append("-size")
|
|
388
|
+
command.append("+{0}c".format(parse_size(min_size)))
|
|
389
|
+
|
|
390
|
+
if max_size is not None:
|
|
391
|
+
command.append("-size")
|
|
392
|
+
command.append("-{0}c".format(parse_size(max_size)))
|
|
393
|
+
|
|
394
|
+
if size is not None:
|
|
395
|
+
command.append("-size")
|
|
396
|
+
command.append("{0}c".format(size))
|
|
397
|
+
|
|
398
|
+
if maxdepth is not None and "-maxdepth" not in args:
|
|
399
|
+
command.append("-maxdepth")
|
|
400
|
+
command.append("{0}".format(maxdepth))
|
|
401
|
+
|
|
402
|
+
if fname is not None and "-fname" not in args:
|
|
403
|
+
command.append("-name")
|
|
404
|
+
command.append(maybe_quote(fname))
|
|
405
|
+
|
|
406
|
+
if iname is not None and "-iname" not in args:
|
|
407
|
+
command.append("-iname")
|
|
408
|
+
command.append(maybe_quote(iname))
|
|
409
|
+
|
|
410
|
+
if regex is not None and "-regex" not in args:
|
|
411
|
+
command.append("-regex")
|
|
412
|
+
command.append(maybe_quote(regex))
|
|
413
|
+
|
|
414
|
+
command.append("||")
|
|
415
|
+
command.append("true")
|
|
416
|
+
|
|
417
|
+
return StringCommand(*command)
|
|
331
418
|
|
|
332
419
|
|
|
333
420
|
class FindFiles(FindFilesBase):
|
pyinfra/facts/git.py
CHANGED
|
@@ -21,9 +21,10 @@ class GitBranch(GitFactBase):
|
|
|
21
21
|
class GitConfig(GitFactBase):
|
|
22
22
|
default = dict
|
|
23
23
|
|
|
24
|
-
def command(self, repo=None) -> str:
|
|
24
|
+
def command(self, repo=None, system=False) -> str:
|
|
25
25
|
if repo is None:
|
|
26
|
-
|
|
26
|
+
level = "--system" if system else "--global"
|
|
27
|
+
return f"git config {level} -l || true"
|
|
27
28
|
|
|
28
29
|
return "! test -d {0} || (cd {0} && git config --local -l)".format(repo)
|
|
29
30
|
|
pyinfra/facts/mysql.py
CHANGED
pyinfra/facts/opkg.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gather the information provided by ``opkg`` on OpenWrt systems:
|
|
3
|
+
+ ``opkg`` configuration
|
|
4
|
+
+ feeds configuration
|
|
5
|
+
+ list of installed packages
|
|
6
|
+
+ list of packages with available upgrades
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
see https://openwrt.org/docs/guide-user/additional-software/opkg
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Dict, NamedTuple, Union
|
|
14
|
+
|
|
15
|
+
from pyinfra import logger
|
|
16
|
+
from pyinfra.api import FactBase
|
|
17
|
+
from pyinfra.facts.util.packaging import parse_packages
|
|
18
|
+
|
|
19
|
+
# TODO - change NamedTuple to dataclass Opkgbut need to figure out how to get json serialization
|
|
20
|
+
# to work without changing core code
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OpkgPkgUpgradeInfo(NamedTuple):
|
|
24
|
+
installed: str
|
|
25
|
+
available: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OpkgConfInfo(NamedTuple):
|
|
29
|
+
paths: Dict[str, str] # list of paths, e.g. {'root':'/', 'ram':'/tmp}
|
|
30
|
+
list_dir: str # where package lists are stored, e.g. /var/opkg-lists
|
|
31
|
+
options: Dict[
|
|
32
|
+
str, Union[str, bool]
|
|
33
|
+
] # mapping from option to value, e.g. {'check_signature': True}
|
|
34
|
+
arch_cfg: Dict[str, int] # priorities for architectures
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OpkgFeedInfo(NamedTuple):
|
|
38
|
+
url: str # url for the feed
|
|
39
|
+
fmt: str # format of the feed, e.g. "src/gz"
|
|
40
|
+
kind: str # whether it comes from the 'distribution' or is 'custom'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OpkgConf(FactBase):
|
|
44
|
+
"""
|
|
45
|
+
Returns a NamedTuple with the current configuration:
|
|
46
|
+
|
|
47
|
+
.. code:: python
|
|
48
|
+
|
|
49
|
+
ConfInfo(
|
|
50
|
+
paths = {
|
|
51
|
+
"root": "/",
|
|
52
|
+
"ram": "/tmp",
|
|
53
|
+
},
|
|
54
|
+
list_dir = "/opt/opkg-lists",
|
|
55
|
+
options = {
|
|
56
|
+
"overlay_root": "/overlay"
|
|
57
|
+
},
|
|
58
|
+
arch_cfg = {
|
|
59
|
+
"all": 1,
|
|
60
|
+
"noarch": 1,
|
|
61
|
+
"i386_pentium": 10
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
regex = re.compile(
|
|
68
|
+
r"""
|
|
69
|
+
^(?:\s*)
|
|
70
|
+
(?:
|
|
71
|
+
(?:arch\s+(?P<arch>\w+)\s+(?P<priority>\d+))|
|
|
72
|
+
(?:dest\s+(?P<dest>\w+)\s+(?P<dest_path>[\w/\-]+))|
|
|
73
|
+
(?:lists_dir\s+(?P<lists_dir>ext)\s+(?P<list_path>[\w/\-]+))|
|
|
74
|
+
(?:option\s+(?P<option>\w+)(?:\s+(?P<value>[^#]+))?)
|
|
75
|
+
)?
|
|
76
|
+
(?:\s*\#.*)?
|
|
77
|
+
$
|
|
78
|
+
""",
|
|
79
|
+
re.X,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def default():
|
|
84
|
+
return OpkgConfInfo({}, "", {}, {})
|
|
85
|
+
|
|
86
|
+
def command(self) -> str:
|
|
87
|
+
return "cat /etc/opkg.conf"
|
|
88
|
+
|
|
89
|
+
def process(self, output):
|
|
90
|
+
dest, lists_dir, options, arch_cfg = {}, "", {}, {}
|
|
91
|
+
for line in output:
|
|
92
|
+
match = self.regex.match(line)
|
|
93
|
+
|
|
94
|
+
if match is None:
|
|
95
|
+
logger.warning(f"Opkg: could not parse opkg.conf line '{line}'")
|
|
96
|
+
elif match.group("arch") is not None:
|
|
97
|
+
arch_cfg[match.group("arch")] = int(match.group("priority"))
|
|
98
|
+
elif match.group("dest") is not None:
|
|
99
|
+
dest[match.group("dest")] = match.group("dest_path")
|
|
100
|
+
elif match.group("lists_dir") is not None:
|
|
101
|
+
lists_dir = match.group("list_path")
|
|
102
|
+
elif match.group("option") is not None:
|
|
103
|
+
options[match.group("option")] = match.group("value") or True
|
|
104
|
+
|
|
105
|
+
return OpkgConfInfo(dest, lists_dir, options, arch_cfg)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class OpkgFeeds(FactBase):
|
|
109
|
+
"""
|
|
110
|
+
Returns a dictionary containing the information for the distribution-provided and
|
|
111
|
+
custom opkg feeds:
|
|
112
|
+
|
|
113
|
+
.. code:: python
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
'openwrt_base': FeedInfo(url='http://downloads ... /i386_pentium/base', fmt='src/gz', kind='distribution'), # noqa: E501
|
|
117
|
+
'openwrt_core': FeedInfo(url='http://downloads ... /x86/geode/packages', fmt='src/gz', kind='distribution'), # noqa: E501
|
|
118
|
+
'openwrt_luci': FeedInfo(url='http://downloads ... /i386_pentium/luci', fmt='src/gz', kind='distribution'),# noqa: E501
|
|
119
|
+
'openwrt_packages': FeedInfo(url='http://downloads ... /i386_pentium/packages', fmt='src/gz', kind='distribution'),# noqa: E501
|
|
120
|
+
'openwrt_routing': FeedInfo(url='http://downloads ... /i386_pentium/routing', fmt='src/gz', kind='distribution'),# noqa: E501
|
|
121
|
+
'openwrt_telephony': FeedInfo(url='http://downloads ... /i386_pentium/telephony', fmt='src/gz', kind='distribution') # noqa: E501
|
|
122
|
+
}
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
regex = re.compile(
|
|
126
|
+
r"^(CUSTOM)|(?:\s*(?P<fmt>[\w/]+)\s+(?P<name>[\w]+)\s+(?P<url>[\w./:]+))?(?:\s*#.*)?$"
|
|
127
|
+
)
|
|
128
|
+
default = dict
|
|
129
|
+
|
|
130
|
+
def command(self) -> str:
|
|
131
|
+
return "cat /etc/opkg/distfeeds.conf; echo CUSTOM; cat /etc/opkg/customfeeds.conf"
|
|
132
|
+
|
|
133
|
+
def process(self, output):
|
|
134
|
+
feeds, kind = {}, "distribution"
|
|
135
|
+
for line in output:
|
|
136
|
+
match = self.regex.match(line)
|
|
137
|
+
|
|
138
|
+
if match is None:
|
|
139
|
+
logger.warning(f"Opkg: could not parse /etc/opkg/*feeds.conf line '{line}'")
|
|
140
|
+
elif match.group(0) == "CUSTOM":
|
|
141
|
+
kind = "custom"
|
|
142
|
+
elif match.group("name") is not None:
|
|
143
|
+
feeds[match.group("name")] = OpkgFeedInfo(
|
|
144
|
+
match.group("url"), match.group("fmt"), kind
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return feeds
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class OpkgInstallableArchitectures(FactBase):
|
|
151
|
+
"""
|
|
152
|
+
Returns a dictionary containing the currently installable architectures for this system along
|
|
153
|
+
with their priority:
|
|
154
|
+
|
|
155
|
+
.. code:: python
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
'all': 1,
|
|
159
|
+
'i386_pentium': 10,
|
|
160
|
+
'noarch': 1
|
|
161
|
+
}
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
regex = re.compile(r"^(?:\s*arch\s+(?P<arch>[\w]+)\s+(?P<prio>\d+))?(\s*#.*)?$")
|
|
165
|
+
default = dict
|
|
166
|
+
|
|
167
|
+
def command(self) -> str:
|
|
168
|
+
return "/bin/opkg print-architecture"
|
|
169
|
+
|
|
170
|
+
def process(self, output):
|
|
171
|
+
arch_list = {}
|
|
172
|
+
for line in output:
|
|
173
|
+
match = self.regex.match(line)
|
|
174
|
+
|
|
175
|
+
if match is None:
|
|
176
|
+
logger.warning(f"could not parse arch line '{line}'")
|
|
177
|
+
elif match.group("arch") is not None:
|
|
178
|
+
arch_list[match.group("arch")] = int(match.group("prio"))
|
|
179
|
+
|
|
180
|
+
return arch_list
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class OpkgPackages(FactBase):
|
|
184
|
+
"""
|
|
185
|
+
Returns a dict of installed opkg packages:
|
|
186
|
+
|
|
187
|
+
.. code:: python
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
'package_name': ['version'],
|
|
191
|
+
...
|
|
192
|
+
}
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
regex = r"^([a-zA-Z0-9][\w\-\.]*)\s-\s([\w\-\.]+)"
|
|
196
|
+
default = dict
|
|
197
|
+
|
|
198
|
+
def command(self) -> str:
|
|
199
|
+
return "/bin/opkg list-installed"
|
|
200
|
+
|
|
201
|
+
def process(self, output):
|
|
202
|
+
return parse_packages(self.regex, sorted(output))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class OpkgUpgradeablePackages(FactBase):
|
|
206
|
+
"""
|
|
207
|
+
Returns a dict of installed and upgradable opkg packages:
|
|
208
|
+
|
|
209
|
+
.. code:: python
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
'package_name': (installed='1.2.3', available='1.2.8')
|
|
213
|
+
...
|
|
214
|
+
}
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
regex = re.compile(r"^([a-zA-Z0-9][\w\-.]*)\s-\s([\w\-.]+)\s-\s([\w\-.]+)")
|
|
218
|
+
default = dict
|
|
219
|
+
use_default_on_error = True
|
|
220
|
+
|
|
221
|
+
def command(self) -> str:
|
|
222
|
+
return "/bin/opkg list-upgradable" # yes, really spelled that way
|
|
223
|
+
|
|
224
|
+
def process(self, output):
|
|
225
|
+
result = {}
|
|
226
|
+
for line in output:
|
|
227
|
+
match = self.regex.match(line)
|
|
228
|
+
if match and len(match.groups()) == 3:
|
|
229
|
+
result[match.group(1)] = OpkgPkgUpgradeInfo(match.group(2), match.group(3))
|
|
230
|
+
else:
|
|
231
|
+
logger.warning(f"Opkg: could not list-upgradable line '{line}'")
|
|
232
|
+
|
|
233
|
+
return result
|
pyinfra/facts/pipx.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from pyinfra.api import FactBase
|
|
4
|
+
|
|
5
|
+
from .util.packaging import parse_packages
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# TODO: move to an utils file
|
|
9
|
+
def parse_environment(output):
|
|
10
|
+
environment_REGEX = r"^(?P<key>[A-Z_]+)=(?P<value>.*)$"
|
|
11
|
+
environment_variables = {}
|
|
12
|
+
|
|
13
|
+
for line in output:
|
|
14
|
+
matches = re.match(environment_REGEX, line)
|
|
15
|
+
|
|
16
|
+
if matches:
|
|
17
|
+
environment_variables[matches.group("key")] = matches.group("value")
|
|
18
|
+
|
|
19
|
+
return environment_variables
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PIPX_REGEX = r"^([a-zA-Z0-9_\-\+\.]+)\s+([0-9\.]+[a-z0-9\-]*)$"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PipxPackages(FactBase):
|
|
26
|
+
"""
|
|
27
|
+
Returns a dict of installed pipx packages:
|
|
28
|
+
|
|
29
|
+
.. code:: python
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
"package_name": ["version"],
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
default = dict
|
|
37
|
+
|
|
38
|
+
def requires_command(self) -> str:
|
|
39
|
+
return "pipx"
|
|
40
|
+
|
|
41
|
+
def command(self) -> str:
|
|
42
|
+
return "pipx list --short"
|
|
43
|
+
|
|
44
|
+
def process(self, output):
|
|
45
|
+
return parse_packages(PIPX_REGEX, output)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PipxEnvironment(FactBase):
|
|
49
|
+
"""
|
|
50
|
+
Returns a dict of pipx environment variables:
|
|
51
|
+
|
|
52
|
+
.. code:: python
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
"PIPX_HOME": "/home/doodba/.local/pipx",
|
|
56
|
+
"PIPX_BIN_DIR": "/home/doodba/.local/bin",
|
|
57
|
+
"PIPX_SHARED_LIBS": "/home/doodba/.local/pipx/shared",
|
|
58
|
+
"PIPX_LOCAL_VENVS": "/home/doodba/.local/pipx/venvs",
|
|
59
|
+
"PIPX_LOG_DIR": "/home/doodba/.local/pipx/logs",
|
|
60
|
+
"PIPX_TRASH_DIR": "/home/doodba/.local/pipx/.trash",
|
|
61
|
+
"PIPX_VENV_CACHEDIR": "/home/doodba/.local/pipx/.cache",
|
|
62
|
+
}
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
default = dict
|
|
66
|
+
|
|
67
|
+
def requires_command(self) -> str:
|
|
68
|
+
return "pipx"
|
|
69
|
+
|
|
70
|
+
def command(self) -> str:
|
|
71
|
+
return "pipx environment"
|
|
72
|
+
|
|
73
|
+
def process(self, output):
|
|
74
|
+
return parse_environment(output)
|
pyinfra/facts/podman.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, Iterable, List, TypeVar
|
|
5
|
+
|
|
6
|
+
from pyinfra.api import FactBase
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PodmanFactBase(FactBase[T]):
|
|
12
|
+
"""
|
|
13
|
+
Base for facts using `podman` to retrieve
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
abstract = True
|
|
17
|
+
|
|
18
|
+
def requires_command(self, *args, **kwargs) -> str:
|
|
19
|
+
return "podman"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PodmanSystemInfo(PodmanFactBase[Dict[str, Any]]):
|
|
23
|
+
"""
|
|
24
|
+
Output of 'podman system info'
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def command(self) -> str:
|
|
28
|
+
return "podman system info --format=json"
|
|
29
|
+
|
|
30
|
+
def process(self, output: Iterable[str]) -> Dict[str, Any]:
|
|
31
|
+
output = json.loads(("").join(output))
|
|
32
|
+
assert isinstance(output, dict)
|
|
33
|
+
return output
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PodmanPs(PodmanFactBase[List[Dict[str, Any]]]):
|
|
37
|
+
"""
|
|
38
|
+
Output of 'podman ps'
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def command(self) -> str:
|
|
42
|
+
return "podman ps --format=json --all"
|
|
43
|
+
|
|
44
|
+
def process(self, output: Iterable[str]) -> List[Dict[str, Any]]:
|
|
45
|
+
output = json.loads(("").join(output))
|
|
46
|
+
assert isinstance(output, list)
|
|
47
|
+
return output # type: ignore
|
pyinfra/facts/postgres.py
CHANGED
|
@@ -58,6 +58,7 @@ class PostgresFactBase(FactBase):
|
|
|
58
58
|
psql_password=None,
|
|
59
59
|
psql_host=None,
|
|
60
60
|
psql_port=None,
|
|
61
|
+
psql_database=None,
|
|
61
62
|
):
|
|
62
63
|
return make_execute_psql_command(
|
|
63
64
|
self.psql_command,
|
|
@@ -65,6 +66,7 @@ class PostgresFactBase(FactBase):
|
|
|
65
66
|
password=psql_password,
|
|
66
67
|
host=psql_host,
|
|
67
68
|
port=psql_port,
|
|
69
|
+
database=psql_database,
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
|
pyinfra/facts/server.py
CHANGED
|
@@ -5,14 +5,15 @@ import re
|
|
|
5
5
|
import shutil
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from tempfile import mkdtemp
|
|
8
|
-
from typing import Dict, List, Optional
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
9
|
|
|
10
10
|
from dateutil.parser import parse as parse_date
|
|
11
11
|
from distro import distro
|
|
12
|
-
from typing_extensions import
|
|
12
|
+
from typing_extensions import TypedDict
|
|
13
13
|
|
|
14
14
|
from pyinfra.api import FactBase, ShortFactBase
|
|
15
15
|
from pyinfra.api.util import try_int
|
|
16
|
+
from pyinfra.facts import crontab
|
|
16
17
|
|
|
17
18
|
ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
|
18
19
|
|
|
@@ -31,8 +32,7 @@ class Home(FactBase[Optional[str]]):
|
|
|
31
32
|
Returns the home directory of the given user, or the current user if no user is given.
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
def command(user=""):
|
|
35
|
+
def command(self, user=""):
|
|
36
36
|
return f"echo ~{user}"
|
|
37
37
|
|
|
38
38
|
|
|
@@ -123,8 +123,7 @@ class Command(FactBase[str]):
|
|
|
123
123
|
Returns the raw output lines of a given command.
|
|
124
124
|
"""
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
def command(command):
|
|
126
|
+
def command(self, command):
|
|
128
127
|
return command
|
|
129
128
|
|
|
130
129
|
|
|
@@ -133,8 +132,7 @@ class Which(FactBase[Optional[str]]):
|
|
|
133
132
|
Returns the path of a given command according to `command -v`, if available.
|
|
134
133
|
"""
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
def command(command):
|
|
135
|
+
def command(self, command):
|
|
138
136
|
return "command -v {0} || true".format(command)
|
|
139
137
|
|
|
140
138
|
|
|
@@ -307,6 +305,36 @@ class LsbRelease(FactBase):
|
|
|
307
305
|
return items
|
|
308
306
|
|
|
309
307
|
|
|
308
|
+
class OsRelease(FactBase):
|
|
309
|
+
"""
|
|
310
|
+
Returns a dictionary of release information stored in ``/etc/os-release``.
|
|
311
|
+
|
|
312
|
+
.. code:: python
|
|
313
|
+
|
|
314
|
+
{
|
|
315
|
+
"name": "EndeavourOS",
|
|
316
|
+
"pretty_name": "EndeavourOS",
|
|
317
|
+
"id": "endeavouros",
|
|
318
|
+
"id_like": "arch",
|
|
319
|
+
"build_id": "2024.06.25",
|
|
320
|
+
...
|
|
321
|
+
}
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def command(self):
|
|
325
|
+
return "cat /etc/os-release"
|
|
326
|
+
|
|
327
|
+
def process(self, output):
|
|
328
|
+
items = {}
|
|
329
|
+
|
|
330
|
+
for line in output:
|
|
331
|
+
if "=" in line:
|
|
332
|
+
key, value = line.split("=", 1)
|
|
333
|
+
items[key.strip().lower()] = value.strip().strip('"')
|
|
334
|
+
|
|
335
|
+
return items
|
|
336
|
+
|
|
337
|
+
|
|
310
338
|
class Sysctl(FactBase):
|
|
311
339
|
"""
|
|
312
340
|
Returns a dictionary of sysctl settings and values.
|
|
@@ -377,75 +405,9 @@ class Groups(FactBase[List[str]]):
|
|
|
377
405
|
return groups
|
|
378
406
|
|
|
379
407
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
month: NotRequired[Union[int, str]]
|
|
384
|
-
day_of_month: NotRequired[Union[int, str]]
|
|
385
|
-
day_of_week: NotRequired[Union[int, str]]
|
|
386
|
-
comments: Optional[list[str]]
|
|
387
|
-
special_time: NotRequired[str]
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
class Crontab(FactBase[Dict[str, CrontabDict]]):
|
|
391
|
-
"""
|
|
392
|
-
Returns a dictionary of cron command -> execution time.
|
|
393
|
-
|
|
394
|
-
.. code:: python
|
|
395
|
-
|
|
396
|
-
{
|
|
397
|
-
"/path/to/command": {
|
|
398
|
-
"minute": "*",
|
|
399
|
-
"hour": "*",
|
|
400
|
-
"month": "*",
|
|
401
|
-
"day_of_month": "*",
|
|
402
|
-
"day_of_week": "*",
|
|
403
|
-
},
|
|
404
|
-
"echo another command": {
|
|
405
|
-
"special_time": "@daily",
|
|
406
|
-
},
|
|
407
|
-
}
|
|
408
|
-
"""
|
|
409
|
-
|
|
410
|
-
default = dict
|
|
411
|
-
|
|
412
|
-
def requires_command(self, user=None) -> str:
|
|
413
|
-
return "crontab"
|
|
414
|
-
|
|
415
|
-
def command(self, user=None):
|
|
416
|
-
if user:
|
|
417
|
-
return "crontab -l -u {0} || true".format(user)
|
|
418
|
-
return "crontab -l || true"
|
|
419
|
-
|
|
420
|
-
def process(self, output):
|
|
421
|
-
crons: dict[str, CrontabDict] = {}
|
|
422
|
-
current_comments = []
|
|
423
|
-
|
|
424
|
-
for line in output:
|
|
425
|
-
line = line.strip()
|
|
426
|
-
if not line or line.startswith("#"):
|
|
427
|
-
current_comments.append(line)
|
|
428
|
-
continue
|
|
429
|
-
|
|
430
|
-
if line.startswith("@"):
|
|
431
|
-
special_time, command = line.split(None, 1)
|
|
432
|
-
crons[command] = {
|
|
433
|
-
"special_time": special_time,
|
|
434
|
-
"comments": current_comments,
|
|
435
|
-
}
|
|
436
|
-
else:
|
|
437
|
-
minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
|
|
438
|
-
crons[command] = {
|
|
439
|
-
"minute": try_int(minute),
|
|
440
|
-
"hour": try_int(hour),
|
|
441
|
-
"month": try_int(month),
|
|
442
|
-
"day_of_month": try_int(day_of_month),
|
|
443
|
-
"day_of_week": try_int(day_of_week),
|
|
444
|
-
"comments": current_comments,
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
current_comments = []
|
|
448
|
-
return crons
|
|
408
|
+
# for compatibility
|
|
409
|
+
CrontabDict = crontab.CrontabDict
|
|
410
|
+
Crontab = crontab.Crontab
|
|
449
411
|
|
|
450
412
|
|
|
451
413
|
class Users(FactBase):
|