pyinfra 2.9.1__py2.py3-none-any.whl → 3.0__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 (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.1.dist-info/RECORD +0 -170
  151. pyinfra-2.9.1.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
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
@@ -6,6 +8,7 @@ from pyinfra.api import FactBase
6
8
  class DockerFactBase(FactBase):
7
9
  abstract = True
8
10
 
11
+ docker_type: str
9
12
  requires_command = "docker"
10
13
 
11
14
  def process(self, output):
@@ -83,3 +86,19 @@ class DockerNetwork(DockerSingleMixin):
83
86
  """
84
87
 
85
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,10 +2,14 @@
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
8
- from typing import List, Tuple
10
+ from typing import TYPE_CHECKING, List, Optional, Tuple, Union
11
+
12
+ from typing_extensions import Literal, NotRequired, TypedDict
9
13
 
10
14
  from pyinfra.api.command import QuoteString, make_formatted_string_command
11
15
  from pyinfra.api.facts import FactBase
@@ -64,7 +68,25 @@ def _parse_mode(mode: str) -> int:
64
68
  return int(oct(out)[2:])
65
69
 
66
70
 
67
- class File(FactBase):
71
+ def _parse_datetime(value: str) -> Optional[datetime]:
72
+ value = try_int(value)
73
+ if isinstance(value, int):
74
+ return datetime.utcfromtimestamp(value)
75
+ return None
76
+
77
+
78
+ class FileDict(TypedDict):
79
+ mode: int
80
+ size: Union[int, str]
81
+ atime: Optional[datetime]
82
+ mtime: Optional[datetime]
83
+ ctime: Optional[datetime]
84
+ user: str
85
+ group: str
86
+ link_target: NotRequired[str]
87
+
88
+
89
+ class File(FactBase[Union[FileDict, Literal[False], None]]):
68
90
  """
69
91
  Returns information about a file on the remote system:
70
92
 
@@ -98,36 +120,23 @@ class File(FactBase):
98
120
  bsd_stat_command=BSD_STAT_COMMAND,
99
121
  )
100
122
 
101
- def process(self, output):
123
+ def process(self, output) -> Union[FileDict, Literal[False], None]:
102
124
  match = re.match(STAT_REGEX, output[0])
103
125
  if not match:
104
126
  return None
105
127
 
106
- data = {}
107
- path_type = None
108
-
109
- for key, value in (
110
- ("user", match.group(1)),
111
- ("group", match.group(2)),
112
- ("mode", match.group(3)),
113
- ("atime", match.group(4)),
114
- ("mtime", match.group(5)),
115
- ("ctime", match.group(6)),
116
- ("size", match.group(7)),
117
- ):
118
- if key == "mode":
119
- path_type = FLAG_TO_TYPE[value[0]]
120
- value = _parse_mode(value[1:])
121
-
122
- elif key == "size":
123
- value = try_int(value)
124
-
125
- elif key in ("atime", "mtime", "ctime"):
126
- value = try_int(value)
127
- if isinstance(value, int):
128
- value = datetime.utcfromtimestamp(value)
129
-
130
- data[key] = value
128
+ mode = match.group(3)
129
+ path_type = FLAG_TO_TYPE[mode[0]]
130
+
131
+ data: FileDict = {
132
+ "user": match.group(1),
133
+ "group": match.group(2),
134
+ "mode": _parse_mode(mode[1:]),
135
+ "atime": _parse_datetime(match.group(4)),
136
+ "mtime": _parse_datetime(match.group(5)),
137
+ "ctime": _parse_datetime(match.group(6)),
138
+ "size": try_int(match.group(7)),
139
+ }
131
140
 
132
141
  if path_type != self.type:
133
142
  return False
@@ -205,7 +214,13 @@ class Socket(File):
205
214
  type = "socket"
206
215
 
207
216
 
208
- class HashFileFactBase(FactBase):
217
+ if TYPE_CHECKING:
218
+ FactBaseOptionalStr = FactBase[Optional[str]]
219
+ else:
220
+ FactBaseOptionalStr = FactBase
221
+
222
+
223
+ class HashFileFactBase(FactBaseOptionalStr):
209
224
  _raw_cmd: str
210
225
  _regexes: Tuple[str, str]
211
226
 
@@ -229,13 +244,14 @@ class HashFileFactBase(FactBase):
229
244
  self.path = path
230
245
  return make_formatted_string_command(self._raw_cmd, QuoteString(path))
231
246
 
232
- def process(self, output):
247
+ def process(self, output) -> Optional[str]:
233
248
  output = output[0]
234
249
  escaped_path = re.escape(self.path)
235
250
  for regex in self._regexes:
236
251
  matches = re.match(regex % escaped_path, output)
237
252
  if matches:
238
253
  return matches.group(1)
254
+ return None
239
255
 
240
256
 
241
257
  class Sha1File(HashFileFactBase, digits=40, cmds=["sha1sum", "shasum", "sha1"]):
@@ -294,6 +310,7 @@ class FindInFile(FactBase):
294
310
  class FindFilesBase(FactBase):
295
311
  abstract = True
296
312
  default = list
313
+ type_flag: str
297
314
 
298
315
  @staticmethod
299
316
  def process(output):
@@ -345,7 +362,6 @@ class Flags(FactBase):
345
362
  )
346
363
 
347
364
  def process(self, output):
348
-
349
365
  return [flag for flag in output[0].split(",") if len(flag) > 0] if len(output) == 1 else []
350
366
 
351
367
 
@@ -381,7 +397,6 @@ class Block(FactBase):
381
397
  default = list
382
398
 
383
399
  def command(self, path, marker=None, begin=None, end=None):
384
-
385
400
  self.path = path
386
401
  start = (marker or MARKER_DEFAULT).format(mark=begin or MARKER_BEGIN_DEFAULT)
387
402
  end = (marker or MARKER_DEFAULT).format(mark=end or MARKER_END_DEFAULT)
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/git.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
 
3
5
  from pyinfra.api.facts import FactBase
@@ -29,7 +31,7 @@ class GitConfig(FactBase):
29
31
 
30
32
  @staticmethod
31
33
  def process(output):
32
- items = {}
34
+ items: dict[str, list[str]] = {}
33
35
 
34
36
  for line in output:
35
37
  key, value = line.split("=", 1)
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
@@ -48,7 +50,7 @@ class GpgFactBase(FactBase):
48
50
 
49
51
  elif current_subkey or current_key:
50
52
  target = current_subkey or current_key
51
-
53
+ assert target is not None
52
54
  if bits[0] == "fpr":
53
55
  target["fingerprint"] = bits[9] # fingerprint = field 10
54
56
  elif bits[0] == "uid":
pyinfra/facts/hardware.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, ShortFactBase
@@ -193,28 +195,36 @@ class NetworkDevices(FactBase):
193
195
  output = "\n".join(map(str.strip, output))
194
196
 
195
197
  # Splitting the output into sections per network device
196
- device_sections = re.split(r"\n(?=\d+: \w|\w+:.*mtu.*)", output)
198
+ device_sections = re.split(r"\n(?=\d+: [^\s/:]|[^\s/:]+:.*mtu )", output)
197
199
 
198
200
  # Dictionary to hold all device information
199
201
  all_devices = {}
200
202
 
201
203
  for section in device_sections:
202
204
  # Extracting the device name
203
- device_name_match = re.match(r"^(?:\d+: )?([\w@]+):", section)
205
+ device_name_match = re.match(r"^(?:\d+: )?([^\s/:]+):", section)
204
206
  if not device_name_match:
205
207
  continue
206
208
  device_name = device_name_match.group(1)
207
209
 
208
210
  # Regular expressions to match different parts of the output
209
- ether_re = re.compile(r"([0-9A-Fa-f:]{17})")
211
+ ether_re = re.compile(r"ether ([0-9A-Fa-f:]{17})")
210
212
  mtu_re = re.compile(r"mtu (\d+)")
211
213
  ipv4_re = (
214
+ # ip a
212
215
  re.compile(
213
- r"inet (\d+\.\d+\.\d+\.\d+)/(\d+)(?: brd (\d+\.\d+\.\d+\.\d+))"
214
- ), # 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
215
219
  re.compile(
216
- 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
217
- ), # 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+)"),
218
228
  )
219
229
 
220
230
  # Parsing the output
@@ -233,18 +243,22 @@ class NetworkDevices(FactBase):
233
243
  )
234
244
 
235
245
  # IPv4 Addresses
246
+ ipv4_matches: list[re.Match[str]]
236
247
  for ipv4_re_ in ipv4_re:
237
- ipv4_matches = ipv4_re_.findall(section)
238
- if ipv4_matches:
248
+ ipv4_matches = list(ipv4_re_.finditer(section))
249
+ if len(ipv4_matches):
239
250
  break
240
251
 
241
- if ipv4_matches:
252
+ if len(ipv4_matches):
242
253
  ipv4_info = []
243
254
  for ipv4 in ipv4_matches:
244
- address = ipv4[0]
245
- mask_value = ipv4[1]
255
+ address = ipv4.group("address")
256
+ mask_value = ipv4.group("mask")
246
257
  mask_bits, netmask = mask(mask_value)
247
- broadcast = ipv4[2] if len(ipv4) == 3 else None
258
+ try:
259
+ broadcast = ipv4.group("broadcast")
260
+ except IndexError:
261
+ broadcast = None
248
262
 
249
263
  ipv4_info.append(
250
264
  {
@@ -256,28 +270,24 @@ class NetworkDevices(FactBase):
256
270
  )
257
271
  device_info["ipv4"] = ipv4_info[0]
258
272
  if len(ipv4_matches) > 1:
259
- device_info["ipv4"]["additional_ips"] = ipv4_info[1:]
273
+ device_info["ipv4"]["additional_ips"] = ipv4_info[1:] # type: ignore[index]
260
274
 
261
275
  # IPv6 Addresses
262
- ipv6_re = (
263
- re.compile(r"inet6\s+([0-9a-fA-F:]+)/(\d+)"),
264
- re.compile(r"inet6\s+([0-9a-fA-F:]+)\s+prefixlen\s+(\d+)"),
265
- )
266
-
276
+ ipv6_matches: list[re.Match[str]]
267
277
  for ipv6_re_ in ipv6_re:
268
- ipv6_matches = ipv6_re_.findall(section)
278
+ ipv6_matches = list(ipv6_re_.finditer(section))
269
279
  if ipv6_matches:
270
280
  break
271
281
 
272
- if ipv6_matches:
282
+ if len(ipv6_matches):
273
283
  ipv6_info = []
274
284
  for ipv6 in ipv6_matches:
275
- address = ipv6[0]
276
- mask_bits = ipv6[1] or ipv6[2]
285
+ address = ipv6.group("address")
286
+ mask_bits = ipv6.group("mask")
277
287
  ipv6_info.append({"address": address, "mask_bits": int(mask_bits)})
278
288
  device_info["ipv6"] = ipv6_info[0]
279
289
  if len(ipv6_matches) > 1:
280
- device_info["ipv6"]["additional_ips"] = ipv6_info[1:]
290
+ device_info["ipv6"]["additional_ips"] = ipv6_info[1:] # type: ignore[index]
281
291
 
282
292
  all_devices[device_name] = device_info
283
293
 
pyinfra/facts/iptables.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pyinfra.api import FactBase
2
4
 
3
5
  # Mapping for iptables code arguments to variable names
@@ -27,16 +29,16 @@ def parse_iptables_rule(line):
27
29
 
28
30
  bits = line.split()
29
31
 
30
- definition = {}
32
+ definition: dict = {}
31
33
 
32
34
  key = None
33
- args = []
35
+ args: list[str] = []
34
36
  not_arg = False
35
37
 
36
38
  def add_args():
37
39
  arg_string = " ".join(args)
38
40
 
39
- if key in IPTABLES_ARGS:
41
+ if key and key in IPTABLES_ARGS:
40
42
  definition_key = "not_{0}".format(IPTABLES_ARGS[key]) if not_arg else IPTABLES_ARGS[key]
41
43
  definition[definition_key] = arg_string
42
44
  else:
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",
@@ -53,6 +59,7 @@ def make_execute_mysql_command(command, ignore_errors=False, **mysql_kwargs):
53
59
  class MysqlFactBase(FactBase):
54
60
  abstract = True
55
61
 
62
+ mysql_command: str
56
63
  requires_command = "mysql"
57
64
  ignore_errors = False
58
65
 
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
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from pyinfra.api import FactBase, MaskString, QuoteString, StringCommand
4
+ from pyinfra.api.util import try_int
5
+
6
+ from .util.databases import parse_columns_and_rows
7
+
8
+
9
+ def make_psql_command(
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
+ executable="psql",
16
+ ) -> StringCommand:
17
+ target_bits: list[str] = []
18
+
19
+ if password:
20
+ target_bits.append(MaskString('PGPASSWORD="{0}"'.format(password)))
21
+
22
+ target_bits.append(executable)
23
+
24
+ if database:
25
+ target_bits.append("-d {0}".format(database))
26
+
27
+ if user:
28
+ target_bits.append("-U {0}".format(user))
29
+
30
+ if host:
31
+ target_bits.append("-h {0}".format(host))
32
+
33
+ if port:
34
+ target_bits.append("-p {0}".format(port))
35
+
36
+ return StringCommand(*target_bits)
37
+
38
+
39
+ def make_execute_psql_command(command, **psql_kwargs):
40
+ return StringCommand(
41
+ make_psql_command(**psql_kwargs),
42
+ "-Ac",
43
+ QuoteString(command), # quote this whole item as a single shell argument
44
+ )
45
+
46
+
47
+ class PostgresFactBase(FactBase):
48
+ abstract = True
49
+
50
+ psql_command: str
51
+ requires_command = "psql"
52
+
53
+ def command(
54
+ self,
55
+ psql_user=None,
56
+ psql_password=None,
57
+ psql_host=None,
58
+ psql_port=None,
59
+ ):
60
+ return make_execute_psql_command(
61
+ self.psql_command,
62
+ user=psql_user,
63
+ password=psql_password,
64
+ host=psql_host,
65
+ port=psql_port,
66
+ )
67
+
68
+
69
+ class PostgresRoles(PostgresFactBase):
70
+ """
71
+ Returns a dict of PostgreSQL roles and data:
72
+
73
+ .. code:: python
74
+
75
+ {
76
+ "pyinfra": {
77
+ "super": true,
78
+ "createrole": false,
79
+ "createdb": false,
80
+ ...
81
+ },
82
+ }
83
+ """
84
+
85
+ default = dict
86
+ psql_command = "SELECT * FROM pg_catalog.pg_roles"
87
+
88
+ def process(self, output):
89
+ # Remove the last line of the output (row count)
90
+ output = output[:-1]
91
+ rows = parse_columns_and_rows(
92
+ output,
93
+ "|",
94
+ # Remove the "rol" prefix on column names
95
+ remove_column_prefix="rol",
96
+ )
97
+
98
+ users = {}
99
+
100
+ for details in rows:
101
+ for key, value in list(details.items()):
102
+ if key in ("oid", "connlimit"):
103
+ details[key] = try_int(value)
104
+
105
+ if key in (
106
+ "super",
107
+ "inherit",
108
+ "createrole",
109
+ "createdb",
110
+ "canlogin",
111
+ "replication",
112
+ "bypassrls",
113
+ ):
114
+ details[key] = value == "t"
115
+
116
+ users[details.pop("name")] = details
117
+
118
+ return users
119
+
120
+
121
+ class PostgresDatabases(PostgresFactBase):
122
+ """
123
+ Returns a dict of PostgreSQL databases and metadata:
124
+
125
+ .. code:: python
126
+
127
+ {
128
+ "pyinfra_stuff": {
129
+ "encoding": "UTF8",
130
+ "collate": "en_US.UTF-8",
131
+ "ctype": "en_US.UTF-8",
132
+ ...
133
+ },
134
+ }
135
+ """
136
+
137
+ default = dict
138
+ psql_command = "SELECT pg_catalog.pg_encoding_to_char(encoding), * FROM pg_catalog.pg_database"
139
+
140
+ def process(self, output):
141
+ # Remove the last line of the output (row count)
142
+ output = output[:-1]
143
+ rows = parse_columns_and_rows(
144
+ output,
145
+ "|",
146
+ # Remove the "dat" prefix on column names
147
+ remove_column_prefix="dat",
148
+ )
149
+
150
+ databases = {}
151
+
152
+ for details in rows:
153
+ details["encoding"] = details.pop("pg_encoding_to_char")
154
+
155
+ for key, value in list(details.items()):
156
+ if key.endswith("id") or key in (
157
+ "dba",
158
+ "tablespace",
159
+ "connlimit",
160
+ ):
161
+ details[key] = try_int(value)
162
+
163
+ if key in ("istemplate", "allowconn"):
164
+ details[key] = value == "t"
165
+
166
+ databases[details.pop("name")] = details
167
+
168
+ return databases