pyinfra 3.4__py2.py3-none-any.whl → 3.5__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 +63 -1
- pyinfra/api/config.py +6 -0
- pyinfra/api/connect.py +19 -2
- pyinfra/api/operation.py +54 -1
- pyinfra/api/operations.py +119 -56
- pyinfra/api/state.py +10 -2
- pyinfra/connectors/scp/__init__.py +1 -0
- pyinfra/connectors/scp/client.py +204 -0
- pyinfra/connectors/ssh.py +39 -7
- pyinfra/connectors/util.py +4 -0
- pyinfra/facts/dnf.py +8 -4
- pyinfra/facts/docker.py +28 -8
- pyinfra/facts/files.py +167 -26
- pyinfra/facts/server.py +55 -4
- pyinfra/facts/util/packaging.py +1 -0
- pyinfra/facts/yum.py +8 -4
- pyinfra/facts/zypper.py +3 -3
- pyinfra/operations/crontab.py +1 -1
- pyinfra/operations/docker.py +130 -29
- pyinfra/operations/files.py +162 -7
- pyinfra/operations/git.py +1 -1
- pyinfra/operations/openrc.py +13 -7
- pyinfra/operations/pip.py +6 -7
- pyinfra/operations/pipx.py +19 -7
- pyinfra/operations/util/docker.py +49 -1
- pyinfra/operations/util/files.py +70 -2
- pyinfra/operations/util/packaging.py +98 -55
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/METADATA +3 -3
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/RECORD +37 -35
- pyinfra_cli/main.py +39 -0
- pyinfra_cli/prints.py +4 -0
- tests/test_api/test_api_operations.py +348 -0
- tests/test_cli/test_cli.py +3 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/LICENSE.md +0 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/WHEEL +0 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.4.dist-info → pyinfra-3.5.dist-info}/top_level.txt +0 -0
pyinfra/connectors/ssh.py
CHANGED
|
@@ -5,7 +5,7 @@ from random import uniform
|
|
|
5
5
|
from shutil import which
|
|
6
6
|
from socket import error as socket_error, gaierror
|
|
7
7
|
from time import sleep
|
|
8
|
-
from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
|
|
8
|
+
from typing import IO, TYPE_CHECKING, Any, Iterable, Optional, Protocol, Tuple
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
from paramiko import AuthenticationException, BadHostKeyException, SFTPClient, SSHException
|
|
@@ -17,6 +17,7 @@ from pyinfra.api.exceptions import ConnectError
|
|
|
17
17
|
from pyinfra.api.util import get_file_io, memoize
|
|
18
18
|
|
|
19
19
|
from .base import BaseConnector, DataMeta
|
|
20
|
+
from .scp import SCPClient
|
|
20
21
|
from .ssh_util import get_private_key, raise_connect_error
|
|
21
22
|
from .sshuserclient import SSHClient
|
|
22
23
|
from .util import (
|
|
@@ -53,6 +54,7 @@ class ConnectorData(TypedDict):
|
|
|
53
54
|
ssh_connect_retries: int
|
|
54
55
|
ssh_connect_retry_min_delay: float
|
|
55
56
|
ssh_connect_retry_max_delay: float
|
|
57
|
+
ssh_file_transfer_protocol: str
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
connector_data_meta: dict[str, DataMeta] = {
|
|
@@ -92,9 +94,27 @@ connector_data_meta: dict[str, DataMeta] = {
|
|
|
92
94
|
"Upper bound for random delay between retries",
|
|
93
95
|
0.5,
|
|
94
96
|
),
|
|
97
|
+
"ssh_file_transfer_protocol": DataMeta(
|
|
98
|
+
"Protocol to use for file transfers. Can be ``sftp`` or ``scp``.",
|
|
99
|
+
"sftp",
|
|
100
|
+
),
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
|
|
104
|
+
class FileTransferClient(Protocol):
|
|
105
|
+
def getfo(self, remote_filename: str, fl: IO) -> Any | None:
|
|
106
|
+
"""
|
|
107
|
+
Get a file from the remote host, writing to the provided file-like object.
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
def putfo(self, fl: IO, remote_filename: str) -> Any | None:
|
|
112
|
+
"""
|
|
113
|
+
Put a file to the remote host, reading from the provided file-like object.
|
|
114
|
+
"""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
|
|
98
118
|
class SSHConnector(BaseConnector):
|
|
99
119
|
"""
|
|
100
120
|
Connect to hosts over SSH. This is the default connector and all targets default
|
|
@@ -268,7 +288,7 @@ class SSHConnector(BaseConnector):
|
|
|
268
288
|
|
|
269
289
|
@override
|
|
270
290
|
def disconnect(self) -> None:
|
|
271
|
-
self.
|
|
291
|
+
self.get_file_transfer_connection.cache.clear()
|
|
272
292
|
|
|
273
293
|
@override
|
|
274
294
|
def run_shell_command(
|
|
@@ -353,13 +373,25 @@ class SSHConnector(BaseConnector):
|
|
|
353
373
|
return status, combined_output
|
|
354
374
|
|
|
355
375
|
@memoize
|
|
356
|
-
def
|
|
376
|
+
def get_file_transfer_connection(self) -> FileTransferClient | None:
|
|
357
377
|
assert self.client is not None
|
|
358
378
|
transport = self.client.get_transport()
|
|
359
379
|
assert transport is not None, "No transport"
|
|
360
380
|
try:
|
|
361
|
-
|
|
381
|
+
if self.data["ssh_file_transfer_protocol"] == "sftp":
|
|
382
|
+
logger.debug("Using SFTP for file transfer")
|
|
383
|
+
return SFTPClient.from_transport(transport)
|
|
384
|
+
elif self.data["ssh_file_transfer_protocol"] == "scp":
|
|
385
|
+
logger.debug("Using SCP for file transfer")
|
|
386
|
+
return SCPClient(transport)
|
|
387
|
+
else:
|
|
388
|
+
raise ConnectError(
|
|
389
|
+
"Unsupported file transfer protocol: {0}".format(
|
|
390
|
+
self.data["ssh_file_transfer_protocol"],
|
|
391
|
+
),
|
|
392
|
+
)
|
|
362
393
|
except SSHException as e:
|
|
394
|
+
|
|
363
395
|
raise ConnectError(
|
|
364
396
|
(
|
|
365
397
|
"Unable to establish SFTP connection. Check that the SFTP subsystem "
|
|
@@ -367,9 +399,9 @@ class SSHConnector(BaseConnector):
|
|
|
367
399
|
).format(self.host),
|
|
368
400
|
) from e
|
|
369
401
|
|
|
370
|
-
def _get_file(self, remote_filename: str, filename_or_io):
|
|
402
|
+
def _get_file(self, remote_filename: str, filename_or_io: str | IO):
|
|
371
403
|
with get_file_io(filename_or_io, "wb") as file_io:
|
|
372
|
-
sftp = self.
|
|
404
|
+
sftp = self.get_file_transfer_connection()
|
|
373
405
|
sftp.getfo(remote_filename, file_io)
|
|
374
406
|
|
|
375
407
|
@override
|
|
@@ -448,7 +480,7 @@ class SSHConnector(BaseConnector):
|
|
|
448
480
|
while attempts < 3:
|
|
449
481
|
try:
|
|
450
482
|
with get_file_io(filename_or_io) as file_io:
|
|
451
|
-
sftp = self.
|
|
483
|
+
sftp = self.get_file_transfer_connection()
|
|
452
484
|
sftp.putfo(file_io, remote_location)
|
|
453
485
|
return
|
|
454
486
|
except OSError as e:
|
pyinfra/connectors/util.py
CHANGED
|
@@ -314,6 +314,10 @@ def make_unix_command(
|
|
|
314
314
|
# Doas config
|
|
315
315
|
_doas=False,
|
|
316
316
|
_doas_user=None,
|
|
317
|
+
# Retry config (ignored in command generation but passed through)
|
|
318
|
+
_retries=0,
|
|
319
|
+
_retry_delay=0,
|
|
320
|
+
_retry_until=None,
|
|
317
321
|
) -> StringCommand:
|
|
318
322
|
"""
|
|
319
323
|
Builds a shell command with various kwargs.
|
pyinfra/facts/dnf.py
CHANGED
|
@@ -16,11 +16,15 @@ class DnfRepositories(FactBase):
|
|
|
16
16
|
|
|
17
17
|
[
|
|
18
18
|
{
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
"repoid": "baseos",
|
|
20
|
+
"name": "AlmaLinux $releasever - BaseOS",
|
|
21
|
+
"mirrorlist": "https://mirrors.almalinux.org/mirrorlist/$releasever/baseos",
|
|
22
22
|
"enabled": "1",
|
|
23
|
-
"
|
|
23
|
+
"gpgcheck": "1",
|
|
24
|
+
"countme": "1",
|
|
25
|
+
"gpgkey": "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9",
|
|
26
|
+
"metadata_expire": "86400",
|
|
27
|
+
"enabled_metadata": "1"
|
|
24
28
|
},
|
|
25
29
|
]
|
|
26
30
|
"""
|
pyinfra/facts/docker.py
CHANGED
|
@@ -76,6 +76,28 @@ class DockerNetworks(DockerFactBase):
|
|
|
76
76
|
return "docker network inspect `docker network ls -q`"
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
class DockerVolumes(DockerFactBase):
|
|
80
|
+
"""
|
|
81
|
+
Returns ``docker inspect`` output for all Docker volumes.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
def command(self) -> str:
|
|
86
|
+
return "docker volume inspect `docker volume ls -q`"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DockerPlugins(DockerFactBase):
|
|
90
|
+
"""
|
|
91
|
+
Returns ``docker plugin inspect`` output for all Docker plugins.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def command(self) -> str:
|
|
96
|
+
return """
|
|
97
|
+
ids=$(docker plugin ls -q) && [ -n "$ids" ] && docker plugin inspect $ids || echo "[]"
|
|
98
|
+
""".strip()
|
|
99
|
+
|
|
100
|
+
|
|
79
101
|
# Single Docker objects
|
|
80
102
|
#
|
|
81
103
|
|
|
@@ -113,19 +135,17 @@ class DockerNetwork(DockerSingleMixin):
|
|
|
113
135
|
docker_type = "network"
|
|
114
136
|
|
|
115
137
|
|
|
116
|
-
class
|
|
138
|
+
class DockerVolume(DockerSingleMixin):
|
|
117
139
|
"""
|
|
118
|
-
Returns ``docker inspect`` output for
|
|
140
|
+
Returns ``docker inspect`` output for a single Docker container.
|
|
119
141
|
"""
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
def command(self) -> str:
|
|
123
|
-
return "docker volume inspect `docker volume ls -q`"
|
|
143
|
+
docker_type = "volume"
|
|
124
144
|
|
|
125
145
|
|
|
126
|
-
class
|
|
146
|
+
class DockerPlugin(DockerSingleMixin):
|
|
127
147
|
"""
|
|
128
|
-
Returns ``docker inspect`` output for a single Docker
|
|
148
|
+
Returns ``docker plugin inspect`` output for a single Docker plugin.
|
|
129
149
|
"""
|
|
130
150
|
|
|
131
|
-
docker_type = "
|
|
151
|
+
docker_type = "plugin"
|
pyinfra/facts/files.py
CHANGED
|
@@ -24,13 +24,21 @@ from pyinfra.facts.util.units import parse_size
|
|
|
24
24
|
|
|
25
25
|
LINUX_STAT_COMMAND = "stat -c 'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"
|
|
26
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"
|
|
27
28
|
|
|
28
29
|
STAT_REGEX = (
|
|
29
30
|
r"user=(.*) group=(.*) mode=(.*) "
|
|
30
|
-
r"atime=([0-9]*) mtime=([0-9]*) ctime=([0-9]*) "
|
|
31
|
+
r"atime=(-?[0-9]*) mtime=(-?[0-9]*) ctime=(-?[0-9]*) "
|
|
31
32
|
r"size=([0-9]*) (.*)"
|
|
32
33
|
)
|
|
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
|
+
|
|
34
42
|
FLAG_TO_TYPE = {
|
|
35
43
|
"b": "block",
|
|
36
44
|
"c": "character",
|
|
@@ -82,6 +90,108 @@ def _parse_datetime(value: str) -> Optional[datetime]:
|
|
|
82
90
|
return None
|
|
83
91
|
|
|
84
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
|
+
|
|
85
195
|
class FileDict(TypedDict):
|
|
86
196
|
mode: int
|
|
87
197
|
size: Union[int, str]
|
|
@@ -127,41 +237,54 @@ class File(FactBase[Union[FileDict, Literal[False], None]]):
|
|
|
127
237
|
(
|
|
128
238
|
# only stat if the path exists (file or symlink)
|
|
129
239
|
"! (test -e {0} || test -L {0} ) || "
|
|
130
|
-
"( {linux_stat_command} {0} 2> /dev/null ||
|
|
240
|
+
"( {linux_stat_command} {0} 2> /dev/null || "
|
|
241
|
+
"{bsd_stat_command} {0} || {ls_command} {0} )"
|
|
131
242
|
),
|
|
132
243
|
path,
|
|
133
244
|
linux_stat_command=LINUX_STAT_COMMAND,
|
|
134
245
|
bsd_stat_command=BSD_STAT_COMMAND,
|
|
246
|
+
ls_command=LS_COMMAND,
|
|
135
247
|
)
|
|
136
248
|
|
|
137
249
|
@override
|
|
138
250
|
def process(self, output) -> Union[FileDict, Literal[False], None]:
|
|
251
|
+
# Try to parse as stat output first
|
|
139
252
|
match = re.match(STAT_REGEX, output[0])
|
|
140
|
-
if
|
|
141
|
-
|
|
253
|
+
if match:
|
|
254
|
+
mode = match.group(3)
|
|
255
|
+
path_type = FLAG_TO_TYPE[mode[0]]
|
|
142
256
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
276
|
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
158
281
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
filename, target = filename.split(" -> ")
|
|
162
|
-
data["link_target"] = target.strip("'").lstrip("`")
|
|
282
|
+
if path_type != self.type:
|
|
283
|
+
return False
|
|
163
284
|
|
|
164
|
-
|
|
285
|
+
return data
|
|
286
|
+
|
|
287
|
+
return None
|
|
165
288
|
|
|
166
289
|
|
|
167
290
|
class Link(File):
|
|
@@ -520,10 +643,13 @@ class Block(FactBase):
|
|
|
520
643
|
QuoteString(f"{EXISTS}{path}"),
|
|
521
644
|
QuoteString(f"{MISSING}{path}"),
|
|
522
645
|
)
|
|
523
|
-
|
|
524
|
-
cmd =
|
|
525
|
-
f"awk
|
|
646
|
+
|
|
647
|
+
cmd = StringCommand(
|
|
648
|
+
f"awk '/{end}/{{ f=0}} f; /{start}/{{ f=1}} ' ",
|
|
526
649
|
QuoteString(path),
|
|
650
|
+
" || ",
|
|
651
|
+
backstop,
|
|
652
|
+
_separator="",
|
|
527
653
|
)
|
|
528
654
|
return cmd
|
|
529
655
|
|
|
@@ -534,3 +660,18 @@ class Block(FactBase):
|
|
|
534
660
|
if output and (output[0] == f"{MISSING}{self.path}"):
|
|
535
661
|
return None
|
|
536
662
|
return output
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class FileContents(FactBase):
|
|
666
|
+
"""
|
|
667
|
+
Returns the contents of a file as a list of lines. Works with both sha1sum and sha1. Returns
|
|
668
|
+
``None`` if the file doest not exist.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
@override
|
|
672
|
+
def command(self, path):
|
|
673
|
+
return make_formatted_string_command("cat {0}", QuoteString(path))
|
|
674
|
+
|
|
675
|
+
@override
|
|
676
|
+
def process(self, output):
|
|
677
|
+
return output
|
pyinfra/facts/server.py
CHANGED
|
@@ -6,7 +6,7 @@ import re
|
|
|
6
6
|
import shutil
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from tempfile import mkdtemp
|
|
9
|
-
from typing import Dict, Iterable, List, Optional, Tuple
|
|
9
|
+
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
|
10
10
|
|
|
11
11
|
from dateutil.parser import parse as parse_date
|
|
12
12
|
from distro import distro
|
|
@@ -280,7 +280,7 @@ class Mounts(FactBase[Dict[str, MountsDict]]):
|
|
|
280
280
|
return devices
|
|
281
281
|
|
|
282
282
|
|
|
283
|
-
class Port(FactBase[Tuple[str, int]
|
|
283
|
+
class Port(FactBase[Union[Tuple[str, int], Tuple[None, None]]]):
|
|
284
284
|
"""
|
|
285
285
|
Returns the process occuping a port and its PID
|
|
286
286
|
"""
|
|
@@ -290,7 +290,7 @@ class Port(FactBase[Tuple[str, int] | Tuple[None, None]]):
|
|
|
290
290
|
return f"ss -lptnH 'src :{port}'"
|
|
291
291
|
|
|
292
292
|
@override
|
|
293
|
-
def process(self, output: Iterable[str]) -> Tuple[str, int]
|
|
293
|
+
def process(self, output: Iterable[str]) -> Union[Tuple[str, int], Tuple[None, None]]:
|
|
294
294
|
for line in output:
|
|
295
295
|
proc, pid = line.split('"')[1], int(line.split("pid=")[1].split(",")[0])
|
|
296
296
|
return (proc, pid)
|
|
@@ -661,7 +661,9 @@ class LinuxDistribution(FactBase[LinuxDistributionDict]):
|
|
|
661
661
|
|
|
662
662
|
for filename, content in parts.items():
|
|
663
663
|
with open(
|
|
664
|
-
os.path.join(temp_etc_dir, os.path.basename(filename)),
|
|
664
|
+
os.path.join(temp_etc_dir, os.path.basename(filename)),
|
|
665
|
+
"w",
|
|
666
|
+
encoding="utf-8",
|
|
665
667
|
) as fp:
|
|
666
668
|
fp.write(content)
|
|
667
669
|
|
|
@@ -901,3 +903,52 @@ class SecurityLimits(FactBase):
|
|
|
901
903
|
)
|
|
902
904
|
|
|
903
905
|
return limits
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
class RebootRequired(FactBase[bool]):
|
|
909
|
+
"""
|
|
910
|
+
Returns a boolean indicating whether the system requires a reboot.
|
|
911
|
+
|
|
912
|
+
On Linux systems:
|
|
913
|
+
- Checks /var/run/reboot-required and /var/run/reboot-required.pkgs
|
|
914
|
+
- On Alpine Linux, compares installed kernel with running kernel
|
|
915
|
+
|
|
916
|
+
On FreeBSD systems:
|
|
917
|
+
- Compares running kernel version with installed kernel version
|
|
918
|
+
"""
|
|
919
|
+
|
|
920
|
+
@override
|
|
921
|
+
def command(self) -> str:
|
|
922
|
+
return """
|
|
923
|
+
# Get OS type
|
|
924
|
+
OS_TYPE=$(uname -s)
|
|
925
|
+
if [ "$OS_TYPE" = "Linux" ]; then
|
|
926
|
+
# Check if it's Alpine Linux
|
|
927
|
+
if [ -f /etc/alpine-release ]; then
|
|
928
|
+
RUNNING_KERNEL=$(uname -r)
|
|
929
|
+
INSTALLED_KERNEL=$(ls -1 /lib/modules | sort -V | tail -n1)
|
|
930
|
+
if [ "$RUNNING_KERNEL" != "$INSTALLED_KERNEL" ]; then
|
|
931
|
+
echo "reboot_required"
|
|
932
|
+
exit 0
|
|
933
|
+
fi
|
|
934
|
+
else
|
|
935
|
+
# Check standard Linux reboot required files
|
|
936
|
+
if [ -f /var/run/reboot-required ] || [ -f /var/run/reboot-required.pkgs ]; then
|
|
937
|
+
echo "reboot_required"
|
|
938
|
+
exit 0
|
|
939
|
+
fi
|
|
940
|
+
fi
|
|
941
|
+
elif [ "$OS_TYPE" = "FreeBSD" ]; then
|
|
942
|
+
RUNNING_VERSION=$(freebsd-version -r)
|
|
943
|
+
INSTALLED_VERSION=$(freebsd-version -k)
|
|
944
|
+
if [ "$RUNNING_VERSION" != "$INSTALLED_VERSION" ]; then
|
|
945
|
+
echo "reboot_required"
|
|
946
|
+
exit 0
|
|
947
|
+
fi
|
|
948
|
+
fi
|
|
949
|
+
echo "no_reboot_required"
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
@override
|
|
953
|
+
def process(self, output) -> bool:
|
|
954
|
+
return list(output)[0].strip() == "reboot_required"
|
pyinfra/facts/util/packaging.py
CHANGED
pyinfra/facts/yum.py
CHANGED
|
@@ -16,11 +16,15 @@ class YumRepositories(FactBase):
|
|
|
16
16
|
|
|
17
17
|
[
|
|
18
18
|
{
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
"repoid": "baseos",
|
|
20
|
+
"name": "AlmaLinux $releasever - BaseOS",
|
|
21
|
+
"mirrorlist": "https://mirrors.almalinux.org/mirrorlist/$releasever/baseos",
|
|
22
22
|
"enabled": "1",
|
|
23
|
-
"
|
|
23
|
+
"gpgcheck": "1",
|
|
24
|
+
"countme": "1",
|
|
25
|
+
"gpgkey": "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9",
|
|
26
|
+
"metadata_expire": "86400",
|
|
27
|
+
"enabled_metadata": "1"
|
|
24
28
|
},
|
|
25
29
|
]
|
|
26
30
|
"""
|
pyinfra/facts/zypper.py
CHANGED
|
@@ -16,11 +16,11 @@ class ZypperRepositories(FactBase):
|
|
|
16
16
|
|
|
17
17
|
[
|
|
18
18
|
{
|
|
19
|
+
"repoid": "repo-oss",
|
|
19
20
|
"name": "Main Repository",
|
|
20
21
|
"enabled": "1",
|
|
21
|
-
"autorefresh": "
|
|
22
|
-
"baseurl": "http://download.opensuse.org/distribution/leap/$releasever/repo/oss/"
|
|
23
|
-
"type": "rpm-md",
|
|
22
|
+
"autorefresh": "1",
|
|
23
|
+
"baseurl": "http://download.opensuse.org/distribution/leap/$releasever/repo/oss/"
|
|
24
24
|
},
|
|
25
25
|
]
|
|
26
26
|
"""
|
pyinfra/operations/crontab.py
CHANGED