pyinfra 3.0b0__py2.py3-none-any.whl → 3.0b2__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 (115) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +12 -5
  3. pyinfra/api/arguments_typed.py +19 -6
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +115 -13
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/exceptions.py +19 -0
  8. pyinfra/api/facts.py +34 -33
  9. pyinfra/api/host.py +51 -12
  10. pyinfra/api/inventory.py +4 -0
  11. pyinfra/api/operation.py +88 -42
  12. pyinfra/api/operations.py +10 -11
  13. pyinfra/api/state.py +11 -2
  14. pyinfra/api/util.py +24 -16
  15. pyinfra/connectors/base.py +4 -7
  16. pyinfra/connectors/chroot.py +5 -6
  17. pyinfra/connectors/docker.py +13 -19
  18. pyinfra/connectors/dockerssh.py +5 -4
  19. pyinfra/connectors/local.py +7 -7
  20. pyinfra/connectors/ssh.py +46 -25
  21. pyinfra/connectors/terraform.py +9 -6
  22. pyinfra/connectors/util.py +7 -8
  23. pyinfra/connectors/vagrant.py +11 -10
  24. pyinfra/context.py +1 -0
  25. pyinfra/facts/apk.py +2 -0
  26. pyinfra/facts/apt.py +2 -0
  27. pyinfra/facts/brew.py +2 -0
  28. pyinfra/facts/bsdinit.py +2 -0
  29. pyinfra/facts/cargo.py +2 -0
  30. pyinfra/facts/choco.py +3 -1
  31. pyinfra/facts/deb.py +9 -4
  32. pyinfra/facts/dnf.py +2 -0
  33. pyinfra/facts/docker.py +2 -0
  34. pyinfra/facts/files.py +2 -0
  35. pyinfra/facts/gem.py +2 -0
  36. pyinfra/facts/gpg.py +2 -0
  37. pyinfra/facts/hardware.py +30 -22
  38. pyinfra/facts/launchd.py +2 -0
  39. pyinfra/facts/lxd.py +2 -0
  40. pyinfra/facts/mysql.py +12 -6
  41. pyinfra/facts/npm.py +1 -0
  42. pyinfra/facts/openrc.py +2 -0
  43. pyinfra/facts/pacman.py +6 -2
  44. pyinfra/facts/pip.py +2 -0
  45. pyinfra/facts/pkg.py +2 -0
  46. pyinfra/facts/pkgin.py +2 -0
  47. pyinfra/facts/postgres.py +168 -0
  48. pyinfra/facts/postgresql.py +5 -162
  49. pyinfra/facts/rpm.py +12 -9
  50. pyinfra/facts/server.py +10 -13
  51. pyinfra/facts/snap.py +2 -0
  52. pyinfra/facts/systemd.py +28 -10
  53. pyinfra/facts/upstart.py +2 -0
  54. pyinfra/facts/util/packaging.py +3 -2
  55. pyinfra/facts/vzctl.py +2 -0
  56. pyinfra/facts/xbps.py +2 -0
  57. pyinfra/facts/yum.py +2 -0
  58. pyinfra/facts/zypper.py +2 -0
  59. pyinfra/operations/apk.py +3 -1
  60. pyinfra/operations/apt.py +16 -18
  61. pyinfra/operations/brew.py +10 -8
  62. pyinfra/operations/bsdinit.py +5 -3
  63. pyinfra/operations/cargo.py +3 -1
  64. pyinfra/operations/choco.py +3 -1
  65. pyinfra/operations/dnf.py +15 -19
  66. pyinfra/operations/files.py +86 -69
  67. pyinfra/operations/gem.py +3 -1
  68. pyinfra/operations/git.py +18 -16
  69. pyinfra/operations/iptables.py +33 -25
  70. pyinfra/operations/launchd.py +5 -6
  71. pyinfra/operations/lxd.py +7 -4
  72. pyinfra/operations/mysql.py +57 -53
  73. pyinfra/operations/npm.py +8 -1
  74. pyinfra/operations/openrc.py +5 -3
  75. pyinfra/operations/pacman.py +4 -5
  76. pyinfra/operations/pip.py +16 -9
  77. pyinfra/operations/pkg.py +3 -1
  78. pyinfra/operations/pkgin.py +3 -1
  79. pyinfra/operations/postgres.py +349 -0
  80. pyinfra/operations/postgresql.py +18 -335
  81. pyinfra/operations/puppet.py +3 -1
  82. pyinfra/operations/python.py +7 -3
  83. pyinfra/operations/selinux.py +42 -16
  84. pyinfra/operations/server.py +48 -43
  85. pyinfra/operations/snap.py +3 -1
  86. pyinfra/operations/ssh.py +12 -10
  87. pyinfra/operations/systemd.py +13 -9
  88. pyinfra/operations/sysvinit.py +6 -4
  89. pyinfra/operations/upstart.py +5 -3
  90. pyinfra/operations/util/files.py +24 -16
  91. pyinfra/operations/util/packaging.py +53 -37
  92. pyinfra/operations/util/service.py +18 -13
  93. pyinfra/operations/vzctl.py +12 -10
  94. pyinfra/operations/xbps.py +3 -1
  95. pyinfra/operations/yum.py +14 -18
  96. pyinfra/operations/zypper.py +8 -9
  97. pyinfra/version.py +5 -2
  98. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/METADATA +31 -29
  99. pyinfra-3.0b2.dist-info/RECORD +163 -0
  100. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/WHEEL +1 -1
  101. pyinfra_cli/commands.py +3 -2
  102. pyinfra_cli/inventory.py +38 -19
  103. pyinfra_cli/main.py +2 -0
  104. pyinfra_cli/prints.py +27 -105
  105. pyinfra_cli/util.py +3 -1
  106. tests/test_api/test_api_deploys.py +5 -5
  107. tests/test_api/test_api_operations.py +5 -5
  108. tests/test_connectors/test_ssh.py +105 -0
  109. tests/test_connectors/test_terraform.py +11 -8
  110. tests/test_connectors/test_vagrant.py +6 -6
  111. pyinfra-3.0b0.dist-info/RECORD +0 -162
  112. pyinfra_cli/inventory_dsl.py +0 -23
  113. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/LICENSE.md +0 -0
  114. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/entry_points.txt +0 -0
  115. {pyinfra-3.0b0.dist-info → pyinfra-3.0b2.dist-info}/top_level.txt +0 -0
pyinfra/connectors/ssh.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import shlex
4
- from distutils.spawn import find_executable
4
+ from random import uniform
5
+ from shutil import which
5
6
  from socket import error as socket_error, gaierror
7
+ from time import sleep
6
8
  from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple
7
9
 
8
10
  import click
@@ -48,6 +50,10 @@ class ConnectorData(TypedDict):
48
50
 
49
51
  ssh_paramiko_connect_kwargs: dict
50
52
 
53
+ ssh_connect_retries: int
54
+ ssh_connect_retry_min_delay: float
55
+ ssh_connect_retry_max_delay: float
56
+
51
57
 
52
58
  connector_data_meta: dict[str, DataMeta] = {
53
59
  "ssh_hostname": DataMeta("SSH hostname"),
@@ -77,6 +83,15 @@ connector_data_meta: dict[str, DataMeta] = {
77
83
  "ssh_paramiko_connect_kwargs": DataMeta(
78
84
  "Override keyword arguments passed into Paramiko's ``SSHClient.connect``"
79
85
  ),
86
+ "ssh_connect_retries": DataMeta("Number of tries to connect via ssh", 0),
87
+ "ssh_connect_retry_min_delay": DataMeta(
88
+ "Lower bound for random delay between retries",
89
+ 0.1,
90
+ ),
91
+ "ssh_connect_retry_max_delay": DataMeta(
92
+ "Upper bound for random delay between retries",
93
+ 0.5,
94
+ ),
80
95
  }
81
96
 
82
97
 
@@ -125,8 +140,9 @@ class SSHConnector(BaseConnector):
125
140
 
126
141
  client: Optional[SSHClient] = None
127
142
 
128
- def make_names_data(hostname):
129
- yield "@ssh/{0}".format(hostname), {"ssh_hostname": hostname}, []
143
+ @staticmethod
144
+ def make_names_data(name):
145
+ yield "@ssh/{0}".format(name), {"ssh_hostname": name}, []
130
146
 
131
147
  def make_paramiko_kwargs(self) -> dict[str, Any]:
132
148
  kwargs = {
@@ -172,6 +188,29 @@ class SSHConnector(BaseConnector):
172
188
  return kwargs
173
189
 
174
190
  def connect(self) -> None:
191
+ retries = self.data["ssh_connect_retries"]
192
+
193
+ try:
194
+ while True:
195
+ try:
196
+ return self._connect()
197
+ except (SSHException, gaierror, socket_error, EOFError):
198
+ if retries == 0:
199
+ raise
200
+ retries -= 1
201
+ min_delay = self.data["ssh_connect_retry_min_delay"]
202
+ max_delay = self.data["ssh_connect_retry_max_delay"]
203
+ sleep(uniform(min_delay, max_delay))
204
+ except SSHException as e:
205
+ raise_connect_error(self.host, "SSH error", e)
206
+ except gaierror as e:
207
+ raise_connect_error(self.host, "Could not resolve hostname", e)
208
+ except socket_error as e:
209
+ raise_connect_error(self.host, "Could not connect", e)
210
+ except EOFError as e:
211
+ raise_connect_error(self.host, "EOF error", e)
212
+
213
+ def _connect(self) -> None:
175
214
  """
176
215
  Connect to a single host. Returns the SSH client if successful. Stateless by
177
216
  design so can be run in parallel.
@@ -221,18 +260,6 @@ class SSHConnector(BaseConnector):
221
260
  f"Host key for {e.hostname} does not match.",
222
261
  )
223
262
 
224
- except SSHException as e:
225
- raise_connect_error(self.host, "SSH error", e)
226
-
227
- except gaierror:
228
- raise_connect_error(self.host, "Could not resolve hostname", hostname)
229
-
230
- except socket_error as e:
231
- raise_connect_error(self.host, "Could not connect", e)
232
-
233
- except EOFError as e:
234
- raise_connect_error(self.host, "EOF error", e)
235
-
236
263
  def run_shell_command(
237
264
  self,
238
265
  command: StringCommand,
@@ -450,16 +477,10 @@ class SSHConnector(BaseConnector):
450
477
  self._put_file(filename_or_io, temp_file)
451
478
 
452
479
  # Make sure our sudo/su user can access the file
453
- if _su_user:
454
- command = StringCommand("setfacl", "-m", "u:{0}:r".format(_su_user), temp_file)
455
- elif _sudo_user:
456
- command = StringCommand("setfacl", "-m", "u:{0}:r".format(_sudo_user), temp_file)
457
- elif _doas_user:
458
- command = StringCommand("setfacl", "-m", "u:{0}:r".format(_doas_user), temp_file)
459
-
460
- if _su_user or _sudo_user or _doas_user:
480
+ other_user = _su_user or _sudo_user or _doas_user
481
+ if other_user:
461
482
  status, output = self.run_shell_command(
462
- command,
483
+ StringCommand("setfacl", "-m", f"u:{other_user}:r", temp_file),
463
484
  print_output=print_output,
464
485
  print_input=print_input,
465
486
  **arguments,
@@ -518,7 +539,7 @@ class SSHConnector(BaseConnector):
518
539
  if self.data["ssh_password"]:
519
540
  raise NotImplementedError("Rsync does not currently work with SSH passwords.")
520
541
 
521
- if not find_executable("rsync"):
542
+ if not which("rsync"):
522
543
  raise NotImplementedError("The `rsync` binary is not available on this system.")
523
544
 
524
545
  def rsync(
@@ -28,7 +28,8 @@ def _flatten_dict(d: dict, parent_key: str = "", sep: str = "."):
28
28
 
29
29
  class TerraformInventoryConnector(BaseConnector):
30
30
  """
31
- Generate one or more SSH hosts from a Terraform output variable. The variable must be a list of hostnames or dictionaries.
31
+ Generate one or more SSH hosts from a Terraform output variable. The variable
32
+ must be a list of hostnames or dictionaries.
32
33
 
33
34
  Output is fetched from a flattened JSON dictionary output from ``terraform output
34
35
  -json``. For example the following object:
@@ -77,21 +78,23 @@ class TerraformInventoryConnector(BaseConnector):
77
78
  """
78
79
 
79
80
  @staticmethod
80
- def make_names_data(output_key=None):
81
+ def make_names_data(name=None):
81
82
  show_warning()
82
83
 
83
- if not output_key:
84
- raise InventoryError("No Terraform output key!")
84
+ if not name:
85
+ name = ""
85
86
 
86
87
  with progress_spinner({"fetch terraform output"}):
87
88
  tf_output_raw = local.shell("terraform output -json")
88
89
 
90
+ assert isinstance(tf_output_raw, str)
89
91
  tf_output = json.loads(tf_output_raw)
90
92
  tf_output = _flatten_dict(tf_output)
91
93
 
92
- tf_output_value = tf_output.get(output_key)
94
+ tf_output_value = tf_output.get(name)
93
95
  if tf_output_value is None:
94
- raise InventoryError(f"No Terraform output with key: `{output_key}`")
96
+ keys = "\n".join(f" - {k}" for k in tf_output.keys())
97
+ raise InventoryError(f"No Terraform output with key: `{name}`, valid keys:\n{keys}")
95
98
 
96
99
  if not isinstance(tf_output_value, list):
97
100
  raise InventoryError(
@@ -40,7 +40,7 @@ def run_local_process(
40
40
  stdin=None,
41
41
  timeout: Optional[int] = None,
42
42
  print_output: bool = False,
43
- print_prefix=None,
43
+ print_prefix: str = "",
44
44
  ) -> tuple[int, "CommandOutput"]:
45
45
  process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, stdin=PIPE)
46
46
 
@@ -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", {}):
@@ -132,8 +132,8 @@ class VagrantInventoryConnector(BaseConnector):
132
132
  """
133
133
 
134
134
  @staticmethod
135
- def make_names_data(limit=None):
136
- vagrant_ssh_info = get_vagrant_config(limit)
135
+ def make_names_data(name=None):
136
+ vagrant_ssh_info = get_vagrant_config(name)
137
137
 
138
138
  logger.debug("Got Vagrant SSH info: \n%s", vagrant_ssh_info)
139
139
 
@@ -170,10 +170,11 @@ class VagrantInventoryConnector(BaseConnector):
170
170
  hosts.append(_make_name_data(current_host))
171
171
 
172
172
  if not hosts:
173
- if limit:
173
+ if name:
174
174
  raise InventoryError(
175
- "No running Vagrant instances matching `{0}` found!".format(limit)
175
+ "No running Vagrant instances matching `{0}` found!".format(name)
176
176
  )
177
177
  raise InventoryError("No running Vagrant instances found!")
178
178
 
179
- return hosts
179
+ for host in hosts:
180
+ yield host
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
@@ -16,7 +18,7 @@ class ChocoPackages(FactBase):
16
18
  }
17
19
  """
18
20
 
19
- command = "choco list --local-only"
21
+ command = "choco list"
20
22
  shell_executable = "ps"
21
23
 
22
24
  default = dict
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
 
@@ -48,14 +51,16 @@ class DebPackage(FactBase):
48
51
  """
49
52
 
50
53
  _regexes = {
51
- "name": r"^Package: ({0})$".format(DEB_PACKAGE_NAME_REGEX),
52
- "version": r"^Version: ({0})$".format(DEB_PACKAGE_VERSION_REGEX),
54
+ "name": r"^Package:\s+({0})$".format(DEB_PACKAGE_NAME_REGEX),
55
+ "version": r"^Version:\s+({0})$".format(DEB_PACKAGE_VERSION_REGEX),
53
56
  }
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
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