pyinfra 3.1.1__py2.py3-none-any.whl → 3.3__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/arguments_typed.py +4 -5
- pyinfra/api/command.py +22 -3
- pyinfra/api/config.py +5 -2
- pyinfra/api/deploy.py +4 -2
- pyinfra/api/facts.py +3 -0
- pyinfra/api/host.py +15 -7
- pyinfra/api/operation.py +2 -1
- pyinfra/api/state.py +1 -1
- pyinfra/connectors/base.py +34 -8
- pyinfra/connectors/chroot.py +7 -2
- pyinfra/connectors/docker.py +24 -8
- pyinfra/connectors/dockerssh.py +7 -2
- pyinfra/connectors/local.py +7 -2
- pyinfra/connectors/ssh.py +9 -2
- pyinfra/connectors/sshuserclient/client.py +42 -14
- pyinfra/connectors/sshuserclient/config.py +2 -0
- pyinfra/connectors/terraform.py +1 -1
- pyinfra/connectors/util.py +13 -9
- pyinfra/context.py +9 -2
- pyinfra/facts/apk.py +8 -1
- pyinfra/facts/apt.py +68 -0
- pyinfra/facts/brew.py +13 -0
- pyinfra/facts/bsdinit.py +3 -0
- pyinfra/facts/cargo.py +5 -0
- pyinfra/facts/choco.py +6 -0
- pyinfra/facts/crontab.py +195 -0
- pyinfra/facts/deb.py +10 -0
- pyinfra/facts/dnf.py +5 -0
- pyinfra/facts/docker.py +16 -0
- pyinfra/facts/efibootmgr.py +113 -0
- pyinfra/facts/files.py +112 -7
- pyinfra/facts/flatpak.py +7 -0
- pyinfra/facts/freebsd.py +75 -0
- pyinfra/facts/gem.py +5 -0
- pyinfra/facts/git.py +12 -2
- pyinfra/facts/gpg.py +7 -0
- pyinfra/facts/hardware.py +13 -0
- pyinfra/facts/iptables.py +9 -1
- pyinfra/facts/launchd.py +5 -0
- pyinfra/facts/lxd.py +5 -0
- pyinfra/facts/mysql.py +9 -2
- pyinfra/facts/npm.py +5 -0
- pyinfra/facts/openrc.py +8 -0
- pyinfra/facts/opkg.py +245 -0
- pyinfra/facts/pacman.py +9 -1
- pyinfra/facts/pip.py +5 -0
- pyinfra/facts/pipx.py +82 -0
- pyinfra/facts/pkg.py +4 -0
- pyinfra/facts/pkgin.py +5 -0
- pyinfra/facts/podman.py +54 -0
- pyinfra/facts/postgres.py +10 -2
- pyinfra/facts/rpm.py +11 -0
- pyinfra/facts/runit.py +7 -0
- pyinfra/facts/selinux.py +16 -0
- pyinfra/facts/server.py +87 -79
- pyinfra/facts/snap.py +7 -0
- pyinfra/facts/systemd.py +5 -0
- pyinfra/facts/sysvinit.py +4 -0
- pyinfra/facts/upstart.py +5 -0
- pyinfra/facts/util/__init__.py +4 -1
- pyinfra/facts/util/units.py +30 -0
- pyinfra/facts/vzctl.py +5 -0
- pyinfra/facts/xbps.py +6 -1
- pyinfra/facts/yum.py +5 -0
- pyinfra/facts/zfs.py +41 -21
- pyinfra/facts/zypper.py +5 -0
- pyinfra/local.py +3 -2
- pyinfra/operations/apt.py +36 -22
- pyinfra/operations/crontab.py +189 -0
- pyinfra/operations/docker.py +61 -56
- pyinfra/operations/files.py +65 -1
- 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/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 +114 -27
- pyinfra/operations/runit.py +2 -0
- pyinfra/operations/server.py +9 -181
- pyinfra/operations/util/docker.py +44 -22
- pyinfra/operations/zfs.py +3 -3
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/LICENSE.md +1 -1
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/METADATA +25 -25
- pyinfra-3.3.dist-info/RECORD +187 -0
- pyinfra_cli/exceptions.py +5 -0
- pyinfra_cli/inventory.py +26 -9
- pyinfra_cli/log.py +3 -0
- pyinfra_cli/main.py +9 -8
- pyinfra_cli/prints.py +19 -4
- pyinfra_cli/util.py +3 -0
- pyinfra_cli/virtualenv.py +1 -1
- tests/test_cli/test_cli_deploy.py +15 -13
- tests/test_cli/test_cli_inventory.py +53 -0
- tests/test_connectors/test_ssh.py +302 -182
- tests/test_connectors/test_sshuserclient.py +68 -1
- pyinfra-3.1.1.dist-info/RECORD +0 -172
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/WHEEL +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/entry_points.txt +0 -0
- {pyinfra-3.1.1.dist-info → pyinfra-3.3.dist-info}/top_level.txt +0 -0
pyinfra/connectors/ssh.py
CHANGED
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
|
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
from paramiko import AuthenticationException, BadHostKeyException, SFTPClient, SSHException
|
|
12
|
-
from typing_extensions import TypedDict, Unpack
|
|
12
|
+
from typing_extensions import TypedDict, Unpack, override
|
|
13
13
|
|
|
14
14
|
from pyinfra import logger
|
|
15
15
|
from pyinfra.api.command import QuoteString, StringCommand
|
|
@@ -191,6 +191,7 @@ class SSHConnector(BaseConnector):
|
|
|
191
191
|
|
|
192
192
|
return kwargs
|
|
193
193
|
|
|
194
|
+
@override
|
|
194
195
|
def connect(self) -> None:
|
|
195
196
|
retries = self.data["ssh_connect_retries"]
|
|
196
197
|
|
|
@@ -264,9 +265,11 @@ class SSHConnector(BaseConnector):
|
|
|
264
265
|
f"Host key for {e.hostname} does not match.",
|
|
265
266
|
)
|
|
266
267
|
|
|
268
|
+
@override
|
|
267
269
|
def disconnect(self) -> None:
|
|
268
270
|
self.get_sftp_connection.cache.clear()
|
|
269
271
|
|
|
272
|
+
@override
|
|
270
273
|
def run_shell_command(
|
|
271
274
|
self,
|
|
272
275
|
command: StringCommand,
|
|
@@ -368,6 +371,7 @@ class SSHConnector(BaseConnector):
|
|
|
368
371
|
sftp = self.get_sftp_connection()
|
|
369
372
|
sftp.getfo(remote_filename, file_io)
|
|
370
373
|
|
|
374
|
+
@override
|
|
371
375
|
def get_file(
|
|
372
376
|
self,
|
|
373
377
|
remote_filename: str,
|
|
@@ -454,6 +458,7 @@ class SSHConnector(BaseConnector):
|
|
|
454
458
|
if last_e is not None:
|
|
455
459
|
raise last_e
|
|
456
460
|
|
|
461
|
+
@override
|
|
457
462
|
def put_file(
|
|
458
463
|
self,
|
|
459
464
|
filename_or_io,
|
|
@@ -537,7 +542,8 @@ class SSHConnector(BaseConnector):
|
|
|
537
542
|
|
|
538
543
|
return True
|
|
539
544
|
|
|
540
|
-
|
|
545
|
+
@override
|
|
546
|
+
def check_can_rsync(self) -> None:
|
|
541
547
|
if self.data["ssh_key_password"]:
|
|
542
548
|
raise NotImplementedError(
|
|
543
549
|
"Rsync does not currently work with SSH keys needing passwords."
|
|
@@ -549,6 +555,7 @@ class SSHConnector(BaseConnector):
|
|
|
549
555
|
if not which("rsync"):
|
|
550
556
|
raise NotImplementedError("The `rsync` binary is not available on this system.")
|
|
551
557
|
|
|
558
|
+
@override
|
|
552
559
|
def rsync(
|
|
553
560
|
self,
|
|
554
561
|
src: str,
|
|
@@ -14,6 +14,8 @@ from paramiko import (
|
|
|
14
14
|
SSHException,
|
|
15
15
|
)
|
|
16
16
|
from paramiko.agent import AgentRequestHandler
|
|
17
|
+
from paramiko.hostkeys import HostKeyEntry
|
|
18
|
+
from typing_extensions import override
|
|
17
19
|
|
|
18
20
|
from pyinfra import logger
|
|
19
21
|
from pyinfra.api.util import memoize
|
|
@@ -24,6 +26,7 @@ HOST_KEYS_LOCK = BoundedSemaphore()
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class StrictPolicy(MissingHostKeyPolicy):
|
|
29
|
+
@override
|
|
27
30
|
def missing_host_key(self, client, hostname, key):
|
|
28
31
|
logger.error("No host key for {0} found in known_hosts".format(hostname))
|
|
29
32
|
raise SSHException(
|
|
@@ -31,7 +34,30 @@ class StrictPolicy(MissingHostKeyPolicy):
|
|
|
31
34
|
)
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
def append_hostkey(client, hostname, key):
|
|
38
|
+
"""Append hostname to the clients host_keys_file"""
|
|
39
|
+
|
|
40
|
+
with HOST_KEYS_LOCK:
|
|
41
|
+
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
42
|
+
# this correctly, so use that with the client filename variable.
|
|
43
|
+
# See: https://github.com/paramiko/paramiko/pull/1989
|
|
44
|
+
host_key_entry = HostKeyEntry([hostname], key)
|
|
45
|
+
if host_key_entry is None:
|
|
46
|
+
raise SSHException(
|
|
47
|
+
"Append Hostkey: Failed to parse host {0}, could not append to hostfile".format(
|
|
48
|
+
hostname
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
with open(client._host_keys_filename, "a") as host_keys_file:
|
|
52
|
+
hk_entry = host_key_entry.to_line()
|
|
53
|
+
if hk_entry is None:
|
|
54
|
+
raise SSHException(f"Append Hostkey: Failed to append hostkey ({host_key_entry})")
|
|
55
|
+
|
|
56
|
+
host_keys_file.write(hk_entry)
|
|
57
|
+
|
|
58
|
+
|
|
34
59
|
class AcceptNewPolicy(MissingHostKeyPolicy):
|
|
60
|
+
@override
|
|
35
61
|
def missing_host_key(self, client, hostname, key):
|
|
36
62
|
logger.warning(
|
|
37
63
|
(
|
|
@@ -40,16 +66,12 @@ class AcceptNewPolicy(MissingHostKeyPolicy):
|
|
|
40
66
|
),
|
|
41
67
|
)
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
host_keys.add(hostname, key.get_name(), key)
|
|
46
|
-
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
47
|
-
# this correctly, so use that with the client filename variable.
|
|
48
|
-
# See: https://github.com/paramiko/paramiko/pull/1989
|
|
49
|
-
host_keys.save(client._host_keys_filename)
|
|
69
|
+
append_hostkey(client, hostname, key)
|
|
70
|
+
logger.warning("Added host key for {0} to known_hosts".format(hostname))
|
|
50
71
|
|
|
51
72
|
|
|
52
73
|
class AskPolicy(MissingHostKeyPolicy):
|
|
74
|
+
@override
|
|
53
75
|
def missing_host_key(self, client, hostname, key):
|
|
54
76
|
should_continue = input(
|
|
55
77
|
"No host key for {0} found in known_hosts, do you want to continue [y/n] ".format(
|
|
@@ -60,18 +82,13 @@ class AskPolicy(MissingHostKeyPolicy):
|
|
|
60
82
|
raise SSHException(
|
|
61
83
|
"AskPolicy: No host key for {0} found in known_hosts".format(hostname),
|
|
62
84
|
)
|
|
63
|
-
|
|
64
|
-
host_keys = client.get_host_keys()
|
|
65
|
-
host_keys.add(hostname, key.get_name(), key)
|
|
66
|
-
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
67
|
-
# this correctly, so use that with the client filename variable.
|
|
68
|
-
# See: https://github.com/paramiko/paramiko/pull/1989
|
|
69
|
-
host_keys.save(client._host_keys_filename)
|
|
85
|
+
append_hostkey(client, hostname, key)
|
|
70
86
|
logger.warning("Added host key for {0} to known_hosts".format(hostname))
|
|
71
87
|
return
|
|
72
88
|
|
|
73
89
|
|
|
74
90
|
class WarningPolicy(MissingHostKeyPolicy):
|
|
91
|
+
@override
|
|
75
92
|
def missing_host_key(self, client, hostname, key):
|
|
76
93
|
logger.warning("No host key for {0} found in known_hosts".format(hostname))
|
|
77
94
|
|
|
@@ -124,6 +141,7 @@ class SSHClient(ParamikoClient):
|
|
|
124
141
|
original idea at http://bitprophet.org/blog/2012/11/05/gateway-solutions/.
|
|
125
142
|
"""
|
|
126
143
|
|
|
144
|
+
@override
|
|
127
145
|
def connect( # type: ignore[override]
|
|
128
146
|
self,
|
|
129
147
|
hostname,
|
|
@@ -165,6 +183,13 @@ class SSHClient(ParamikoClient):
|
|
|
165
183
|
if _pyinfra_ssh_forward_agent is not None:
|
|
166
184
|
forward_agent = _pyinfra_ssh_forward_agent
|
|
167
185
|
|
|
186
|
+
keep_alive = config.get("keep_alive")
|
|
187
|
+
|
|
188
|
+
if keep_alive:
|
|
189
|
+
transport = self.get_transport()
|
|
190
|
+
assert transport is not None, "No transport"
|
|
191
|
+
transport.set_keepalive(keep_alive)
|
|
192
|
+
|
|
168
193
|
if forward_agent:
|
|
169
194
|
transport = self.get_transport()
|
|
170
195
|
assert transport is not None, "No transport"
|
|
@@ -222,6 +247,9 @@ class SSHClient(ParamikoClient):
|
|
|
222
247
|
if "port" in host_config:
|
|
223
248
|
cfg["port"] = int(host_config["port"])
|
|
224
249
|
|
|
250
|
+
if "serveraliveinterval" in host_config:
|
|
251
|
+
cfg["keep_alive"] = int(host_config["serveraliveinterval"])
|
|
252
|
+
|
|
225
253
|
if "proxycommand" in host_config:
|
|
226
254
|
cfg["sock"] = ProxyCommand(host_config["proxycommand"])
|
|
227
255
|
|
|
@@ -10,6 +10,7 @@ from os import environ, path
|
|
|
10
10
|
import paramiko.config
|
|
11
11
|
from gevent.subprocess import CalledProcessError, check_call
|
|
12
12
|
from paramiko import SSHConfig as ParamikoSSHConfig
|
|
13
|
+
from typing_extensions import override
|
|
13
14
|
|
|
14
15
|
from pyinfra import logger
|
|
15
16
|
|
|
@@ -95,6 +96,7 @@ class SSHConfig(ParamikoSSHConfig):
|
|
|
95
96
|
https://github.com/paramiko/paramiko/pull/1194
|
|
96
97
|
"""
|
|
97
98
|
|
|
99
|
+
@override
|
|
98
100
|
def parse(self, file_obj):
|
|
99
101
|
file_obj = _expand_include_statements(file_obj)
|
|
100
102
|
return super().parse(file_obj)
|
pyinfra/connectors/terraform.py
CHANGED
pyinfra/connectors/util.py
CHANGED
|
@@ -198,14 +198,18 @@ def execute_command_with_sudo_retry(
|
|
|
198
198
|
) -> tuple[int, CommandOutput]:
|
|
199
199
|
return_code, output = execute_command()
|
|
200
200
|
|
|
201
|
+
# If we failed look for a sudo password prompt line and re-submit using the sudo password. Look
|
|
202
|
+
# at all lines here in case anything else gets printed, eg in:
|
|
203
|
+
# https://github.com/pyinfra-dev/pyinfra/issues/1292
|
|
201
204
|
if return_code != 0 and output and output.combined_lines:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
for line in reversed(output.combined_lines):
|
|
206
|
+
if line.line.strip() == "sudo: a password is required":
|
|
207
|
+
# If we need a password, ask the user for it and attach to the host
|
|
208
|
+
# internal connector data for use when executing future commands.
|
|
209
|
+
sudo_password = getpass("{0}sudo password: ".format(host.print_prefix))
|
|
210
|
+
host.connector_data["prompted_sudo_password"] = sudo_password
|
|
211
|
+
return_code, output = execute_command()
|
|
212
|
+
break
|
|
209
213
|
|
|
210
214
|
return return_code, output
|
|
211
215
|
|
|
@@ -232,7 +236,7 @@ def remove_any_sudo_askpass_file(host) -> None:
|
|
|
232
236
|
|
|
233
237
|
|
|
234
238
|
@memoize
|
|
235
|
-
def _show_use_su_login_warning():
|
|
239
|
+
def _show_use_su_login_warning() -> None:
|
|
236
240
|
logger.warning(
|
|
237
241
|
(
|
|
238
242
|
"Using `use_su_login` may not work: "
|
|
@@ -304,7 +308,7 @@ def make_unix_command(
|
|
|
304
308
|
_sudo=False,
|
|
305
309
|
_sudo_user=None,
|
|
306
310
|
_use_sudo_login=False,
|
|
307
|
-
_sudo_password=
|
|
311
|
+
_sudo_password="",
|
|
308
312
|
_sudo_askpass_path=None,
|
|
309
313
|
_preserve_sudo_env=False,
|
|
310
314
|
# Doas config
|
pyinfra/context.py
CHANGED
|
@@ -10,6 +10,7 @@ from types import ModuleType
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
from gevent.local import local
|
|
13
|
+
from typing_extensions import override
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from pyinfra.api.config import Config
|
|
@@ -26,22 +27,25 @@ class ContextObject:
|
|
|
26
27
|
_container_cls = container
|
|
27
28
|
_base_cls: ModuleType
|
|
28
29
|
|
|
29
|
-
def __init__(self):
|
|
30
|
+
def __init__(self) -> None:
|
|
30
31
|
self._container = self._container_cls()
|
|
31
32
|
self._container.module = None
|
|
32
33
|
|
|
33
34
|
def _get_module(self):
|
|
34
35
|
return self._container.module
|
|
35
36
|
|
|
37
|
+
@override
|
|
36
38
|
def __repr__(self):
|
|
37
39
|
return "ContextObject({0}):{1}".format(
|
|
38
40
|
self._base_cls.__name__,
|
|
39
41
|
repr(self._get_module()),
|
|
40
42
|
)
|
|
41
43
|
|
|
44
|
+
@override
|
|
42
45
|
def __str__(self):
|
|
43
46
|
return str(self._get_module())
|
|
44
47
|
|
|
48
|
+
@override
|
|
45
49
|
def __dir__(self):
|
|
46
50
|
return dir(self._base_cls)
|
|
47
51
|
|
|
@@ -50,6 +54,7 @@ class ContextObject:
|
|
|
50
54
|
return getattr(self._base_cls, key)
|
|
51
55
|
return getattr(self._get_module(), key)
|
|
52
56
|
|
|
57
|
+
@override
|
|
53
58
|
def __setattr__(self, key, value):
|
|
54
59
|
if key in ("_container", "_base_cls"):
|
|
55
60
|
return super().__setattr__(key, value)
|
|
@@ -65,9 +70,11 @@ class ContextObject:
|
|
|
65
70
|
def __len__(self):
|
|
66
71
|
return len(self._get_module())
|
|
67
72
|
|
|
73
|
+
@override
|
|
68
74
|
def __eq__(self, other):
|
|
69
75
|
return self._get_module() == other
|
|
70
76
|
|
|
77
|
+
@override
|
|
71
78
|
def __hash__(self):
|
|
72
79
|
return hash(self._get_module())
|
|
73
80
|
|
|
@@ -89,7 +96,7 @@ class ContextManager:
|
|
|
89
96
|
def set_base(self, module):
|
|
90
97
|
self.context._base_cls = module
|
|
91
98
|
|
|
92
|
-
def reset(self):
|
|
99
|
+
def reset(self) -> None:
|
|
93
100
|
self.context._container.module = None
|
|
94
101
|
|
|
95
102
|
def isset(self):
|
pyinfra/facts/apk.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
3
5
|
from pyinfra.api import FactBase
|
|
4
6
|
|
|
5
7
|
from .util.packaging import parse_packages
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
# Source: https://superuser.com/a/1472405
|
|
10
|
+
# Modified to return version and release inside a single group and removed extra capturing groups
|
|
11
|
+
APK_REGEX = r"(.+)-([^-]+-r[^-]+) \S+ \{\S+\} \(.+?\)"
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
class ApkPackages(FactBase):
|
|
@@ -18,13 +22,16 @@ class ApkPackages(FactBase):
|
|
|
18
22
|
}
|
|
19
23
|
"""
|
|
20
24
|
|
|
25
|
+
@override
|
|
21
26
|
def command(self) -> str:
|
|
22
27
|
return "apk list --installed"
|
|
23
28
|
|
|
29
|
+
@override
|
|
24
30
|
def requires_command(self) -> str:
|
|
25
31
|
return "apk"
|
|
26
32
|
|
|
27
33
|
default = dict
|
|
28
34
|
|
|
35
|
+
@override
|
|
29
36
|
def process(self, output):
|
|
30
37
|
return parse_packages(APK_REGEX, output)
|
pyinfra/facts/apt.py
CHANGED
|
@@ -2,12 +2,36 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from typing_extensions import TypedDict, override
|
|
6
|
+
|
|
5
7
|
from pyinfra.api import FactBase
|
|
6
8
|
|
|
7
9
|
from .gpg import GpgFactBase
|
|
8
10
|
from .util import make_cat_files_command
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def noninteractive_apt(command: str, force=False):
|
|
14
|
+
args = ["DEBIAN_FRONTEND=noninteractive apt-get -y"]
|
|
15
|
+
|
|
16
|
+
if force:
|
|
17
|
+
args.append("--force-yes")
|
|
18
|
+
|
|
19
|
+
args.extend(
|
|
20
|
+
(
|
|
21
|
+
'-o Dpkg::Options::="--force-confdef"',
|
|
22
|
+
'-o Dpkg::Options::="--force-confold"',
|
|
23
|
+
command,
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return " ".join(args)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
APT_CHANGES_RE = re.compile(
|
|
31
|
+
r"^(\d+) upgraded, (\d+) newly installed, (\d+) to remove and (\d+) not upgraded.$"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
11
35
|
def parse_apt_repo(name):
|
|
12
36
|
regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([^\s]+)\s+([a-z-\s\d]*)$"
|
|
13
37
|
|
|
@@ -52,17 +76,20 @@ class AptSources(FactBase):
|
|
|
52
76
|
]
|
|
53
77
|
"""
|
|
54
78
|
|
|
79
|
+
@override
|
|
55
80
|
def command(self) -> str:
|
|
56
81
|
return make_cat_files_command(
|
|
57
82
|
"/etc/apt/sources.list",
|
|
58
83
|
"/etc/apt/sources.list.d/*.list",
|
|
59
84
|
)
|
|
60
85
|
|
|
86
|
+
@override
|
|
61
87
|
def requires_command(self) -> str:
|
|
62
88
|
return "apt" # if apt installed, above should exist
|
|
63
89
|
|
|
64
90
|
default = list
|
|
65
91
|
|
|
92
|
+
@override
|
|
66
93
|
def process(self, output):
|
|
67
94
|
repos = []
|
|
68
95
|
|
|
@@ -89,8 +116,49 @@ class AptKeys(GpgFactBase):
|
|
|
89
116
|
"""
|
|
90
117
|
|
|
91
118
|
# This requires both apt-key *and* apt-key itself requires gpg
|
|
119
|
+
@override
|
|
92
120
|
def command(self) -> str:
|
|
93
121
|
return "! command -v gpg || apt-key list --with-colons"
|
|
94
122
|
|
|
123
|
+
@override
|
|
95
124
|
def requires_command(self) -> str:
|
|
96
125
|
return "apt-key"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AptSimulationDict(TypedDict):
|
|
129
|
+
upgraded: int
|
|
130
|
+
newly_installed: int
|
|
131
|
+
removed: int
|
|
132
|
+
not_upgraded: int
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SimulateOperationWillChange(FactBase[AptSimulationDict]):
|
|
136
|
+
"""
|
|
137
|
+
Simulate an 'apt-get' operation and try to detect if any changes would be performed.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
def command(self, command: str) -> str:
|
|
142
|
+
# LC_ALL=C: Ensure the output is in english, as we want to parse it
|
|
143
|
+
return "LC_ALL=C " + noninteractive_apt(f"{command} --dry-run")
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
def requires_command(self, command: str) -> str:
|
|
147
|
+
return "apt-get"
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def process(self, output) -> AptSimulationDict:
|
|
151
|
+
# We are looking for a line similar to
|
|
152
|
+
# "3 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
|
|
153
|
+
for line in output:
|
|
154
|
+
result = APT_CHANGES_RE.match(line)
|
|
155
|
+
if result is not None:
|
|
156
|
+
return {
|
|
157
|
+
"upgraded": int(result[1]),
|
|
158
|
+
"newly_installed": int(result[2]),
|
|
159
|
+
"removed": int(result[3]),
|
|
160
|
+
"not_upgraded": int(result[4]),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# We did not find the line we expected:
|
|
164
|
+
raise Exception("Did not find proposed changes in output")
|
pyinfra/facts/brew.py
CHANGED
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
5
7
|
from pyinfra import logger
|
|
6
8
|
from pyinfra.api import FactBase
|
|
7
9
|
|
|
@@ -37,9 +39,11 @@ class BrewVersion(FactBase):
|
|
|
37
39
|
|
|
38
40
|
"""
|
|
39
41
|
|
|
42
|
+
@override
|
|
40
43
|
def command(self) -> str:
|
|
41
44
|
return "brew --version"
|
|
42
45
|
|
|
46
|
+
@override
|
|
43
47
|
def requires_command(self) -> str:
|
|
44
48
|
return "brew"
|
|
45
49
|
|
|
@@ -47,6 +51,7 @@ class BrewVersion(FactBase):
|
|
|
47
51
|
def default():
|
|
48
52
|
return [0, 0, 0]
|
|
49
53
|
|
|
54
|
+
@override
|
|
50
55
|
def process(self, output):
|
|
51
56
|
out = list(output)[0]
|
|
52
57
|
m = VERSION_MATCHER.match(out)
|
|
@@ -67,14 +72,17 @@ class BrewPackages(FactBase):
|
|
|
67
72
|
}
|
|
68
73
|
"""
|
|
69
74
|
|
|
75
|
+
@override
|
|
70
76
|
def command(self) -> str:
|
|
71
77
|
return "brew list --versions"
|
|
72
78
|
|
|
79
|
+
@override
|
|
73
80
|
def requires_command(self) -> str:
|
|
74
81
|
return "brew"
|
|
75
82
|
|
|
76
83
|
default = dict
|
|
77
84
|
|
|
85
|
+
@override
|
|
78
86
|
def process(self, output):
|
|
79
87
|
return parse_packages(BREW_REGEX, output)
|
|
80
88
|
|
|
@@ -90,12 +98,14 @@ class BrewCasks(BrewPackages):
|
|
|
90
98
|
}
|
|
91
99
|
"""
|
|
92
100
|
|
|
101
|
+
@override
|
|
93
102
|
def command(self) -> str:
|
|
94
103
|
return (
|
|
95
104
|
r'if brew --version | grep -q -e "Homebrew\ +(1\.|2\.[0-5]).*" 1>/dev/null;'
|
|
96
105
|
r"then brew cask list --versions; else brew list --cask --versions; fi"
|
|
97
106
|
)
|
|
98
107
|
|
|
108
|
+
@override
|
|
99
109
|
def requires_command(self) -> str:
|
|
100
110
|
return "brew"
|
|
101
111
|
|
|
@@ -105,13 +115,16 @@ class BrewTaps(FactBase):
|
|
|
105
115
|
Returns a list of brew taps.
|
|
106
116
|
"""
|
|
107
117
|
|
|
118
|
+
@override
|
|
108
119
|
def command(self) -> str:
|
|
109
120
|
return "brew tap"
|
|
110
121
|
|
|
122
|
+
@override
|
|
111
123
|
def requires_command(self) -> str:
|
|
112
124
|
return "brew"
|
|
113
125
|
|
|
114
126
|
default = list
|
|
115
127
|
|
|
128
|
+
@override
|
|
116
129
|
def process(self, output):
|
|
117
130
|
return output
|
pyinfra/facts/bsdinit.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
3
5
|
from .sysvinit import InitdStatus
|
|
4
6
|
|
|
5
7
|
|
|
@@ -9,6 +11,7 @@ class RcdStatus(InitdStatus):
|
|
|
9
11
|
BSD init scripts are well behaved and as such their output can be trusted.
|
|
10
12
|
"""
|
|
11
13
|
|
|
14
|
+
@override
|
|
12
15
|
def command(self) -> str:
|
|
13
16
|
return """
|
|
14
17
|
for SERVICE in `find /etc/rc.d /usr/local/etc/rc.d -type f`; do
|
pyinfra/facts/cargo.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
|
|
5
7
|
from pyinfra.api import FactBase
|
|
6
8
|
|
|
7
9
|
from .util.packaging import parse_packages
|
|
@@ -22,11 +24,14 @@ class CargoPackages(FactBase):
|
|
|
22
24
|
|
|
23
25
|
default = dict
|
|
24
26
|
|
|
27
|
+
@override
|
|
25
28
|
def command(self) -> str:
|
|
26
29
|
return "cargo install --list"
|
|
27
30
|
|
|
31
|
+
@override
|
|
28
32
|
def requires_command(self) -> str:
|
|
29
33
|
return "cargo"
|
|
30
34
|
|
|
35
|
+
@override
|
|
31
36
|
def process(self, output):
|
|
32
37
|
return parse_packages(CARGO_REGEX, output)
|
pyinfra/facts/choco.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
3
5
|
from pyinfra.api import FactBase
|
|
4
6
|
|
|
5
7
|
from .util.packaging import parse_packages
|
|
@@ -18,6 +20,7 @@ class ChocoPackages(FactBase):
|
|
|
18
20
|
}
|
|
19
21
|
"""
|
|
20
22
|
|
|
23
|
+
@override
|
|
21
24
|
def command(self) -> str:
|
|
22
25
|
return "choco list"
|
|
23
26
|
|
|
@@ -25,6 +28,7 @@ class ChocoPackages(FactBase):
|
|
|
25
28
|
|
|
26
29
|
default = dict
|
|
27
30
|
|
|
31
|
+
@override
|
|
28
32
|
def process(self, output):
|
|
29
33
|
return parse_packages(CHOCO_REGEX, output)
|
|
30
34
|
|
|
@@ -34,8 +38,10 @@ class ChocoVersion(FactBase):
|
|
|
34
38
|
Returns the choco (Chocolatey) version.
|
|
35
39
|
"""
|
|
36
40
|
|
|
41
|
+
@override
|
|
37
42
|
def command(self) -> str:
|
|
38
43
|
return "choco --version"
|
|
39
44
|
|
|
45
|
+
@override
|
|
40
46
|
def process(self, output):
|
|
41
47
|
return "".join(output).replace("\n", "")
|