pyinfra 3.0b1__py2.py3-none-any.whl → 3.0b3__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.
Files changed (108) hide show
  1. pyinfra/api/arguments.py +9 -3
  2. pyinfra/api/arguments_typed.py +8 -5
  3. pyinfra/api/command.py +5 -3
  4. pyinfra/api/config.py +115 -13
  5. pyinfra/api/connectors.py +5 -2
  6. pyinfra/api/facts.py +33 -32
  7. pyinfra/api/host.py +5 -5
  8. pyinfra/api/inventory.py +4 -0
  9. pyinfra/api/operation.py +22 -14
  10. pyinfra/api/util.py +24 -16
  11. pyinfra/connectors/base.py +3 -6
  12. pyinfra/connectors/docker.py +2 -9
  13. pyinfra/connectors/local.py +2 -2
  14. pyinfra/connectors/ssh.py +2 -2
  15. pyinfra/connectors/util.py +6 -7
  16. pyinfra/connectors/vagrant.py +5 -5
  17. pyinfra/context.py +1 -0
  18. pyinfra/facts/apk.py +2 -0
  19. pyinfra/facts/apt.py +2 -0
  20. pyinfra/facts/brew.py +2 -0
  21. pyinfra/facts/bsdinit.py +2 -0
  22. pyinfra/facts/cargo.py +2 -0
  23. pyinfra/facts/choco.py +2 -0
  24. pyinfra/facts/deb.py +7 -2
  25. pyinfra/facts/dnf.py +2 -0
  26. pyinfra/facts/docker.py +18 -0
  27. pyinfra/facts/files.py +2 -0
  28. pyinfra/facts/gem.py +2 -0
  29. pyinfra/facts/gpg.py +2 -0
  30. pyinfra/facts/hardware.py +30 -22
  31. pyinfra/facts/launchd.py +2 -0
  32. pyinfra/facts/lxd.py +2 -0
  33. pyinfra/facts/mysql.py +12 -6
  34. pyinfra/facts/npm.py +1 -0
  35. pyinfra/facts/openrc.py +2 -0
  36. pyinfra/facts/pacman.py +6 -2
  37. pyinfra/facts/pip.py +2 -0
  38. pyinfra/facts/pkg.py +2 -0
  39. pyinfra/facts/pkgin.py +2 -0
  40. pyinfra/facts/postgres.py +6 -6
  41. pyinfra/facts/postgresql.py +2 -0
  42. pyinfra/facts/rpm.py +12 -9
  43. pyinfra/facts/runit.py +68 -0
  44. pyinfra/facts/server.py +10 -13
  45. pyinfra/facts/snap.py +2 -0
  46. pyinfra/facts/systemd.py +2 -0
  47. pyinfra/facts/upstart.py +2 -0
  48. pyinfra/facts/util/packaging.py +3 -2
  49. pyinfra/facts/vzctl.py +2 -0
  50. pyinfra/facts/xbps.py +2 -0
  51. pyinfra/facts/yum.py +2 -0
  52. pyinfra/facts/zypper.py +2 -0
  53. pyinfra/operations/apk.py +3 -1
  54. pyinfra/operations/apt.py +16 -18
  55. pyinfra/operations/brew.py +10 -8
  56. pyinfra/operations/bsdinit.py +5 -3
  57. pyinfra/operations/cargo.py +3 -1
  58. pyinfra/operations/choco.py +3 -1
  59. pyinfra/operations/dnf.py +15 -19
  60. pyinfra/operations/docker.py +339 -0
  61. pyinfra/operations/files.py +81 -66
  62. pyinfra/operations/gem.py +3 -1
  63. pyinfra/operations/git.py +18 -16
  64. pyinfra/operations/iptables.py +27 -25
  65. pyinfra/operations/launchd.py +5 -6
  66. pyinfra/operations/lxd.py +7 -4
  67. pyinfra/operations/mysql.py +57 -53
  68. pyinfra/operations/npm.py +8 -1
  69. pyinfra/operations/openrc.py +5 -3
  70. pyinfra/operations/pacman.py +4 -5
  71. pyinfra/operations/pip.py +11 -9
  72. pyinfra/operations/pkg.py +3 -1
  73. pyinfra/operations/pkgin.py +3 -1
  74. pyinfra/operations/postgres.py +39 -37
  75. pyinfra/operations/postgresql.py +2 -0
  76. pyinfra/operations/puppet.py +3 -1
  77. pyinfra/operations/python.py +7 -3
  78. pyinfra/operations/runit.py +182 -0
  79. pyinfra/operations/selinux.py +42 -16
  80. pyinfra/operations/server.py +52 -43
  81. pyinfra/operations/snap.py +3 -1
  82. pyinfra/operations/ssh.py +12 -10
  83. pyinfra/operations/systemd.py +12 -8
  84. pyinfra/operations/sysvinit.py +6 -4
  85. pyinfra/operations/upstart.py +5 -3
  86. pyinfra/operations/util/docker.py +177 -0
  87. pyinfra/operations/util/files.py +24 -16
  88. pyinfra/operations/util/packaging.py +53 -37
  89. pyinfra/operations/util/service.py +25 -18
  90. pyinfra/operations/vzctl.py +12 -10
  91. pyinfra/operations/xbps.py +3 -1
  92. pyinfra/operations/yum.py +14 -18
  93. pyinfra/operations/zypper.py +8 -9
  94. pyinfra/version.py +5 -2
  95. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/METADATA +30 -28
  96. pyinfra-3.0b3.dist-info/RECORD +167 -0
  97. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/WHEEL +1 -1
  98. pyinfra_cli/exceptions.py +0 -5
  99. pyinfra_cli/inventory.py +38 -19
  100. pyinfra_cli/prints.py +15 -11
  101. pyinfra_cli/util.py +3 -1
  102. tests/test_api/test_api_operations.py +1 -1
  103. tests/test_connectors/test_ssh.py +66 -13
  104. tests/test_connectors/test_vagrant.py +3 -3
  105. pyinfra-3.0b1.dist-info/RECORD +0 -163
  106. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/LICENSE.md +0 -0
  107. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/entry_points.txt +0 -0
  108. {pyinfra-3.0b1.dist-info → pyinfra-3.0b3.dist-info}/top_level.txt +0 -0
pyinfra/connectors/ssh.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import shlex
4
- from distutils.spawn import find_executable
5
4
  from random import uniform
5
+ from shutil import which
6
6
  from socket import error as socket_error, gaierror
7
7
  from time import sleep
8
8
  from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
@@ -539,7 +539,7 @@ class SSHConnector(BaseConnector):
539
539
  if self.data["ssh_password"]:
540
540
  raise NotImplementedError("Rsync does not currently work with SSH passwords.")
541
541
 
542
- if not find_executable("rsync"):
542
+ if not which("rsync"):
543
543
  raise NotImplementedError("The `rsync` binary is not available on this system.")
544
544
 
545
545
  def rsync(
@@ -274,12 +274,11 @@ def make_unix_command_for_host(
274
274
  # If no sudo, we've nothing to do here
275
275
  return make_unix_command(command, **command_arguments)
276
276
 
277
- # Ensure the sudo password is set from either the direct arguments or any
278
- # connector data value.
279
- command_arguments["_sudo_password"] = command_arguments.get(
280
- "_sudo_password",
281
- host.connector_data.get("prompted_sudo_password"),
282
- )
277
+ # If the sudo password is not set in the direct arguments,
278
+ # set it from the connector data value.
279
+ if "_sudo_password" not in command_arguments or not command_arguments["_sudo_password"]:
280
+ command_arguments["_sudo_password"] = host.connector_data.get("prompted_sudo_password")
281
+
283
282
  if command_arguments["_sudo_password"]:
284
283
  # Ensure the askpass path is correctly set and passed through
285
284
  _ensure_sudo_askpass_set_for_host(host)
@@ -339,7 +338,7 @@ def make_unix_command(
339
338
  [
340
339
  "env",
341
340
  "SUDO_ASKPASS={0}".format(_sudo_askpass_path),
342
- MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, _sudo_password)),
341
+ MaskString("{0}={1}".format(SUDO_ASKPASS_ENV_VAR, shlex.quote(_sudo_password))),
343
342
  ],
344
343
  )
345
344
 
@@ -93,13 +93,13 @@ def _make_name_data(host):
93
93
  "ssh_hostname": host["HostName"],
94
94
  }
95
95
 
96
- for config_key, data_key in (
97
- ("Port", "ssh_port"),
98
- ("User", "ssh_user"),
99
- ("IdentityFile", "ssh_key"),
96
+ for config_key, data_key, data_cast in (
97
+ ("Port", "ssh_port", int),
98
+ ("User", "ssh_user", str),
99
+ ("IdentityFile", "ssh_key", str),
100
100
  ):
101
101
  if config_key in host:
102
- data[data_key] = host[config_key]
102
+ data[data_key] = data_cast(host[config_key])
103
103
 
104
104
  # Update any configured JSON data
105
105
  if vagrant_host in vagrant_options.get("data", {}):
pyinfra/context.py CHANGED
@@ -4,6 +4,7 @@ are imported and used throughout pyinfra and end user deploy code (CLI mode).
4
4
 
5
5
  These variables always represent the current executing pyinfra context.
6
6
  """
7
+
7
8
  from contextlib import contextmanager
8
9
  from types import ModuleType
9
10
  from typing import TYPE_CHECKING
pyinfra/facts/apk.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/apt.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api import FactBase
pyinfra/facts/brew.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra import logger
pyinfra/facts/bsdinit.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from .sysvinit import InitdStatus
2
4
 
3
5
 
pyinfra/facts/cargo.py CHANGED
@@ -1,5 +1,7 @@
1
1
  # encoding: utf8
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  from pyinfra.api import FactBase
4
6
 
5
7
  from .util.packaging import parse_packages
pyinfra/facts/choco.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/deb.py CHANGED
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ import shlex
2
5
 
3
6
  from pyinfra.api import FactBase
4
7
 
@@ -54,8 +57,10 @@ class DebPackage(FactBase):
54
57
 
55
58
  requires_command = "dpkg"
56
59
 
57
- def command(self, name):
58
- return "! test -e {0} && (dpkg -s {0} 2>/dev/null || true) || dpkg -I {0}".format(name)
60
+ def command(self, package):
61
+ return "! test -e {0} && (dpkg -s {0} 2>/dev/null || true) || dpkg -I {0}".format(
62
+ shlex.quote(package)
63
+ )
59
64
 
60
65
  def process(self, output):
61
66
  data = {}
pyinfra/facts/dnf.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util import make_cat_files_command
pyinfra/facts/docker.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from pyinfra.api import FactBase
@@ -84,3 +86,19 @@ class DockerNetwork(DockerSingleMixin):
84
86
  """
85
87
 
86
88
  docker_type = "network"
89
+
90
+
91
+ class DockerVolumes(DockerFactBase):
92
+ """
93
+ Returns ``docker inspect`` output for all Docker volumes.
94
+ """
95
+
96
+ command = "docker volume inspect `docker volume ls -q`"
97
+
98
+
99
+ class DockerVolume(DockerSingleMixin):
100
+ """
101
+ Returns ``docker inspect`` output for a single Docker container.
102
+ """
103
+
104
+ docker_type = "volume"
pyinfra/facts/files.py CHANGED
@@ -2,6 +2,8 @@
2
2
  The files facts provide information about the filesystem and it's contents on the target host.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import re
6
8
  import stat
7
9
  from datetime import datetime
pyinfra/facts/gem.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/gpg.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from urllib.parse import urlparse
2
4
 
3
5
  from pyinfra.api import FactBase
pyinfra/facts/hardware.py CHANGED
@@ -195,28 +195,36 @@ class NetworkDevices(FactBase):
195
195
  output = "\n".join(map(str.strip, output))
196
196
 
197
197
  # Splitting the output into sections per network device
198
- device_sections = re.split(r"\n(?=\d+: \w|\w+:.*mtu.*)", output)
198
+ device_sections = re.split(r"\n(?=\d+: [^\s/:]|[^\s/:]+:.*mtu )", output)
199
199
 
200
200
  # Dictionary to hold all device information
201
201
  all_devices = {}
202
202
 
203
203
  for section in device_sections:
204
204
  # Extracting the device name
205
- device_name_match = re.match(r"^(?:\d+: )?([\w@]+):", section)
205
+ device_name_match = re.match(r"^(?:\d+: )?([^\s/:]+):", section)
206
206
  if not device_name_match:
207
207
  continue
208
208
  device_name = device_name_match.group(1)
209
209
 
210
210
  # Regular expressions to match different parts of the output
211
- ether_re = re.compile(r"([0-9A-Fa-f:]{17})")
211
+ ether_re = re.compile(r"ether ([0-9A-Fa-f:]{17})")
212
212
  mtu_re = re.compile(r"mtu (\d+)")
213
213
  ipv4_re = (
214
+ # ip a
214
215
  re.compile(
215
- r"inet (\d+\.\d+\.\d+\.\d+)/(\d+)(?: brd (\d+\.\d+\.\d+\.\d+))"
216
- ), # ip a output,
216
+ r"inet (?P<address>\d+\.\d+\.\d+\.\d+)/(?P<mask>\d+)(?: metric \d+)?(?: brd (?P<broadcast>\d+\.\d+\.\d+\.\d+))?" # noqa: E501
217
+ ),
218
+ # ifconfig -a
217
219
  re.compile(
218
- r"inet (\d+\.\d+\.\d+\.\d+)\s+netmask\s+((?:\d+\.\d+\.\d+\.\d+)|(?:[0-9a-fA-FxX]+))(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))" # noqa: E501
219
- ), # ifconfig -a output
220
+ r"inet (?P<address>\d+\.\d+\.\d+\.\d+)\s+netmask\s+(?P<mask>(?:\d+\.\d+\.\d+\.\d+)|(?:[0-9a-fA-FxX]+))(?:\s+broadcast\s+(?P<broadcast>\d+\.\d+\.\d+\.\d+))?" # noqa: E501
221
+ ),
222
+ )
223
+ ipv6_re = (
224
+ # ip a
225
+ re.compile(r"inet6\s+(?P<address>[0-9a-fA-F:]+)/(?P<mask>\d+)"),
226
+ # ifconfig -a
227
+ re.compile(r"inet6\s+(?P<address>[0-9a-fA-F:]+)\s+prefixlen\s+(?P<mask>\d+)"),
220
228
  )
221
229
 
222
230
  # Parsing the output
@@ -235,18 +243,22 @@ class NetworkDevices(FactBase):
235
243
  )
236
244
 
237
245
  # IPv4 Addresses
246
+ ipv4_matches: list[re.Match[str]]
238
247
  for ipv4_re_ in ipv4_re:
239
- ipv4_matches = ipv4_re_.findall(section)
240
- if ipv4_matches:
248
+ ipv4_matches = list(ipv4_re_.finditer(section))
249
+ if len(ipv4_matches):
241
250
  break
242
251
 
243
- if ipv4_matches:
252
+ if len(ipv4_matches):
244
253
  ipv4_info = []
245
254
  for ipv4 in ipv4_matches:
246
- address = ipv4[0]
247
- mask_value = ipv4[1]
255
+ address = ipv4.group("address")
256
+ mask_value = ipv4.group("mask")
248
257
  mask_bits, netmask = mask(mask_value)
249
- broadcast = ipv4[2] if len(ipv4) == 3 else None
258
+ try:
259
+ broadcast = ipv4.group("broadcast")
260
+ except IndexError:
261
+ broadcast = None
250
262
 
251
263
  ipv4_info.append(
252
264
  {
@@ -261,21 +273,17 @@ class NetworkDevices(FactBase):
261
273
  device_info["ipv4"]["additional_ips"] = ipv4_info[1:] # type: ignore[index]
262
274
 
263
275
  # IPv6 Addresses
264
- ipv6_re = (
265
- re.compile(r"inet6\s+([0-9a-fA-F:]+)/(\d+)"),
266
- re.compile(r"inet6\s+([0-9a-fA-F:]+)\s+prefixlen\s+(\d+)"),
267
- )
268
-
276
+ ipv6_matches: list[re.Match[str]]
269
277
  for ipv6_re_ in ipv6_re:
270
- ipv6_matches = ipv6_re_.findall(section)
278
+ ipv6_matches = list(ipv6_re_.finditer(section))
271
279
  if ipv6_matches:
272
280
  break
273
281
 
274
- if ipv6_matches:
282
+ if len(ipv6_matches):
275
283
  ipv6_info = []
276
284
  for ipv6 in ipv6_matches:
277
- address = ipv6[0]
278
- mask_bits = ipv6[1] or ipv6[2]
285
+ address = ipv6.group("address")
286
+ mask_bits = ipv6.group("mask")
279
287
  ipv6_info.append({"address": address, "mask_bits": int(mask_bits)})
280
288
  device_info["ipv6"] = ipv6_info[0]
281
289
  if len(ipv6_matches) > 1:
pyinfra/facts/launchd.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
 
pyinfra/facts/lxd.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from pyinfra.api import FactBase
pyinfra/facts/mysql.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
  from collections import defaultdict
3
5
 
@@ -8,11 +10,11 @@ from .util.databases import parse_columns_and_rows
8
10
 
9
11
 
10
12
  def make_mysql_command(
11
- database=None,
12
- user=None,
13
- password=None,
14
- host=None,
15
- port=None,
13
+ database: str | None = None,
14
+ user: str | None = None,
15
+ password: str | None = None,
16
+ host: str | None = None,
17
+ port: int | None = None,
16
18
  executable="mysql",
17
19
  ):
18
20
  target_bits = [executable]
@@ -37,7 +39,11 @@ def make_mysql_command(
37
39
  return StringCommand(*target_bits)
38
40
 
39
41
 
40
- def make_execute_mysql_command(command, ignore_errors=False, **mysql_kwargs):
42
+ def make_execute_mysql_command(
43
+ command: str | StringCommand,
44
+ ignore_errors=False,
45
+ **mysql_kwargs,
46
+ ):
41
47
  commands_bits = [
42
48
  make_mysql_command(**mysql_kwargs),
43
49
  "-Be",
pyinfra/facts/npm.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # encoding: utf8
2
+ from __future__ import annotations
2
3
 
3
4
  from pyinfra.api import FactBase
4
5
 
pyinfra/facts/openrc.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api import FactBase
pyinfra/facts/pacman.py CHANGED
@@ -1,3 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
1
5
  from pyinfra.api import FactBase
2
6
 
3
7
  from .util.packaging import parse_packages
@@ -21,9 +25,9 @@ class PacmanUnpackGroup(FactBase):
21
25
 
22
26
  default = list
23
27
 
24
- def command(self, name):
28
+ def command(self, package):
25
29
  # Accept failure here (|| true) for invalid/unknown packages
26
- return 'pacman -S --print-format "%n" {0} || true'.format(name)
30
+ return 'pacman -S --print-format "%n" {0} || true'.format(shlex.quote(package))
27
31
 
28
32
  def process(self, output):
29
33
  return output
pyinfra/facts/pip.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/pkg.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/pkgin.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  from .util.packaging import parse_packages
pyinfra/facts/postgres.py CHANGED
@@ -7,13 +7,13 @@ from .util.databases import parse_columns_and_rows
7
7
 
8
8
 
9
9
  def make_psql_command(
10
- database=None,
11
- user=None,
12
- password=None,
13
- host=None,
14
- port=None,
10
+ database: str | None = None,
11
+ user: str | None = None,
12
+ password: str | None = None,
13
+ host: str | None = None,
14
+ port: str | int | None = None,
15
15
  executable="psql",
16
- ):
16
+ ) -> StringCommand:
17
17
  target_bits: list[str] = []
18
18
 
19
19
  if password:
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from .postgres import PostgresDatabases, PostgresRoles
2
4
 
3
5
 
pyinfra/facts/rpm.py CHANGED
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ import shlex
2
5
 
3
6
  from pyinfra.api import FactBase
4
7
 
@@ -19,7 +22,7 @@ class RpmPackages(FactBase):
19
22
  }
20
23
  """
21
24
 
22
- command = 'rpm --queryformat "{0}" -qa'.format(rpm_query_format)
25
+ command = "rpm --queryformat {0} -qa".format(shlex.quote(rpm_query_format))
23
26
  requires_command = "rpm"
24
27
 
25
28
  default = dict
@@ -42,12 +45,12 @@ class RpmPackage(FactBase):
42
45
 
43
46
  requires_command = "rpm"
44
47
 
45
- def command(self, name):
48
+ def command(self, package):
46
49
  return (
47
- 'rpm --queryformat "{0}" -q {1} || '
50
+ "rpm --queryformat {0} -q {1} || "
48
51
  "! test -e {1} || "
49
- 'rpm --queryformat "{0}" -qp {1} 2> /dev/null'
50
- ).format(rpm_query_format, name)
52
+ "rpm --queryformat {0} -qp {1} 2> /dev/null"
53
+ ).format(shlex.quote(rpm_query_format), shlex.quote(package))
51
54
 
52
55
  def process(self, output):
53
56
  for line in output:
@@ -69,11 +72,11 @@ class RpmPackageProvides(FactBase):
69
72
  requires_command = "repoquery"
70
73
 
71
74
  @staticmethod
72
- def command(name):
75
+ def command(package):
73
76
  # Accept failure here (|| true) for invalid/unknown packages
74
- return 'repoquery --queryformat "{0}" --whatprovides {1} || true'.format(
75
- rpm_query_format,
76
- name,
77
+ return "repoquery --queryformat {0} --whatprovides {1} || true".format(
78
+ shlex.quote(rpm_query_format),
79
+ shlex.quote(package),
77
80
  )
78
81
 
79
82
  @staticmethod
pyinfra/facts/runit.py ADDED
@@ -0,0 +1,68 @@
1
+ from pyinfra.api import FactBase
2
+
3
+
4
+ class RunitStatus(FactBase):
5
+ """
6
+ Returns a dict of name -> status for runit services.
7
+
8
+ + service: optionally check only for a single service
9
+ + svdir: alternative ``SVDIR``
10
+
11
+ .. code:: python
12
+
13
+ {
14
+ 'agetty-tty1': True, # service is running
15
+ 'dhcpcd': False, # service is down
16
+ 'wpa_supplicant': None, # service is managed, but not running or down,
17
+ # possibly in a fail state
18
+ }
19
+ """
20
+
21
+ requires_command = "sv"
22
+ default = dict
23
+
24
+ def command(self, service=None, svdir="/var/service"):
25
+ if service is None:
26
+ return (
27
+ 'export SVDIR="{0}" && '
28
+ 'cd "$SVDIR" && find * -maxdepth 0 -exec sv status {{}} + 2>/dev/null'
29
+ ).format(svdir)
30
+ else:
31
+ return 'SVDIR="{0}" sv status "{1}"'.format(svdir, service)
32
+
33
+ def process(self, output):
34
+ services = {}
35
+ for line in output:
36
+ statusstr, service, _ = line.split(sep=": ", maxsplit=2)
37
+ status = None
38
+
39
+ if statusstr == "run":
40
+ status = True
41
+ elif statusstr == "down":
42
+ status = False
43
+ # another observable state is "fail"
44
+ # report as ``None`` for now
45
+
46
+ services[service] = status
47
+
48
+ return services
49
+
50
+
51
+ class RunitManaged(FactBase):
52
+ """
53
+ Returns a set of all services managed by runit
54
+
55
+ + service: optionally check only for a single service
56
+ + svdir: alternative ``SVDIR``
57
+ """
58
+
59
+ default = set
60
+
61
+ def command(self, service=None, svdir="/var/service"):
62
+ if service is None:
63
+ return 'cd "{0}" && find -mindepth 1 -maxdepth 1 -type l -printf "%f\n"'.format(svdir)
64
+ else:
65
+ return 'cd "{0}" && test -h "{1}" && echo "{1}" || true'.format(svdir, service)
66
+
67
+ def process(self, output):
68
+ return set(output)
pyinfra/facts/server.py CHANGED
@@ -5,7 +5,7 @@ import re
5
5
  import shutil
6
6
  from datetime import datetime
7
7
  from tempfile import mkdtemp
8
- from typing import Dict, List, NewType, Optional, Union
8
+ from typing import Dict, List, Optional, Union
9
9
 
10
10
  from dateutil.parser import parse as parse_date
11
11
  from distro import distro
@@ -25,12 +25,14 @@ class User(FactBase):
25
25
  command = "echo $USER"
26
26
 
27
27
 
28
- class Home(FactBase):
28
+ class Home(FactBase[Optional[str]]):
29
29
  """
30
- Returns the home directory of the current user.
30
+ Returns the home directory of the given user, or the current user if no user is given.
31
31
  """
32
32
 
33
- command = "echo $HOME"
33
+ @staticmethod
34
+ def command(user=""):
35
+ return f"echo ~{user}"
34
36
 
35
37
 
36
38
  class Path(FactBase):
@@ -348,8 +350,7 @@ class Groups(FactBase[List[str]]):
348
350
  command = "cat /etc/group"
349
351
  default = list
350
352
 
351
- @staticmethod
352
- def process(output) -> list[str]:
353
+ def process(self, output) -> list[str]:
353
354
  groups: list[str] = []
354
355
 
355
356
  for line in output:
@@ -359,9 +360,6 @@ class Groups(FactBase[List[str]]):
359
360
  return groups
360
361
 
361
362
 
362
- CrontabCommand = NewType("CrontabCommand", int)
363
-
364
-
365
363
  class CrontabDict(TypedDict):
366
364
  minute: NotRequired[Union[int, str]]
367
365
  hour: NotRequired[Union[int, str]]
@@ -372,7 +370,7 @@ class CrontabDict(TypedDict):
372
370
  special_time: NotRequired[str]
373
371
 
374
372
 
375
- class Crontab(FactBase[Dict[CrontabCommand, CrontabDict]]):
373
+ class Crontab(FactBase[Dict[str, CrontabDict]]):
376
374
  """
377
375
  Returns a dictionary of cron command -> execution time.
378
376
 
@@ -402,9 +400,8 @@ class Crontab(FactBase[Dict[CrontabCommand, CrontabDict]]):
402
400
  return "crontab -l -u {0} || true".format(user)
403
401
  return "crontab -l || true"
404
402
 
405
- @staticmethod
406
- def process(output):
407
- crons: dict[Command, CrontabDict] = {}
403
+ def process(self, output):
404
+ crons: dict[str, CrontabDict] = {}
408
405
  current_comments = []
409
406
 
410
407
  for line in output:
pyinfra/facts/snap.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api import FactBase
pyinfra/facts/systemd.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
  from typing import Dict, Iterable
3
5
 
pyinfra/facts/upstart.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api import FactBase
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ from typing import Iterable
4
5
 
5
6
 
6
- def parse_packages(regex, output):
7
+ def parse_packages(regex: str, output: Iterable[str]) -> dict[str, set[str]]:
7
8
  packages: dict[str, set[str]] = {}
8
9
 
9
10
  for line in output:
@@ -34,7 +35,7 @@ def _parse_yum_or_zypper_repositories(output):
34
35
  current_repo["name"] = line[1:-1]
35
36
 
36
37
  if current_repo and "=" in line:
37
- key, value = line.split("=", 1)
38
+ key, value = re.split(r"\s*=\s*", line, maxsplit=1)
38
39
  current_repo[key] = value
39
40
 
40
41
  if current_repo: