pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__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 (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +184 -118
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.1.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +10 -12
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
pyinfra/facts/git.py CHANGED
@@ -5,32 +5,29 @@ import re
5
5
  from pyinfra.api.facts import FactBase
6
6
 
7
7
 
8
- class GitBranch(FactBase):
9
- requires_command = "git"
8
+ class GitFactBase(FactBase):
9
+ def requires_command(self, *args, **kwargs) -> str:
10
+ return "git"
10
11
 
11
- @staticmethod
12
- def command(repo):
12
+
13
+ class GitBranch(GitFactBase):
14
+ def command(self, repo) -> str:
13
15
  return "! test -d {0} || (cd {0} && git describe --all)".format(repo)
14
16
 
15
- @staticmethod
16
- def process(output):
17
+ def process(self, output):
17
18
  return re.sub(r"(heads|tags)/", r"", "\n".join(output))
18
19
 
19
20
 
20
- class GitConfig(FactBase):
21
+ class GitConfig(GitFactBase):
21
22
  default = dict
22
23
 
23
- requires_command = "git"
24
-
25
- @staticmethod
26
- def command(repo=None):
24
+ def command(self, repo=None) -> str:
27
25
  if repo is None:
28
26
  return "git config --global -l || true"
29
27
 
30
28
  return "! test -d {0} || (cd {0} && git config --local -l)".format(repo)
31
29
 
32
- @staticmethod
33
- def process(output):
30
+ def process(self, output):
34
31
  items: dict[str, list[str]] = {}
35
32
 
36
33
  for line in output:
@@ -40,19 +37,15 @@ class GitConfig(FactBase):
40
37
  return items
41
38
 
42
39
 
43
- class GitTrackingBranch(FactBase):
44
- requires_command = "git"
45
-
46
- @staticmethod
47
- def command(repo):
40
+ class GitTrackingBranch(GitFactBase):
41
+ def command(self, repo) -> str:
48
42
  return r"! test -d {0} || (cd {0} && git status --branch --porcelain)".format(repo)
49
43
 
50
- @staticmethod
51
- def process(output):
44
+ def process(self, output):
52
45
  if not output:
53
46
  return None
54
47
 
55
- m = re.search(r"\.{3}(\S+)\b", output[0])
48
+ m = re.search(r"\.{3}(\S+)\b", list(output)[0])
56
49
  if m:
57
50
  return m.group(1)
58
51
  return None
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
@@ -6,7 +8,8 @@ from pyinfra.api import FactBase
6
8
  class GpgFactBase(FactBase):
7
9
  abstract = True
8
10
 
9
- requires_command = "gpg"
11
+ def requires_command(self, *args, **kwargs) -> str:
12
+ return "gpg"
10
13
 
11
14
  key_record_type = "pub"
12
15
  subkey_record_type = "sub"
pyinfra/facts/hardware.py CHANGED
@@ -5,17 +5,17 @@ import re
5
5
  from pyinfra.api import FactBase, ShortFactBase
6
6
 
7
7
 
8
- class Cpus(FactBase):
8
+ class Cpus(FactBase[int]):
9
9
  """
10
10
  Returns the number of CPUs on this server.
11
11
  """
12
12
 
13
- command = "getconf NPROCESSORS_ONLN 2> /dev/null || getconf _NPROCESSORS_ONLN"
13
+ def command(self) -> str:
14
+ return "getconf NPROCESSORS_ONLN 2> /dev/null || getconf _NPROCESSORS_ONLN"
14
15
 
15
- @staticmethod
16
- def process(output):
16
+ def process(self, output):
17
17
  try:
18
- return int(output[0])
18
+ return int(list(output)[0])
19
19
  except ValueError:
20
20
  pass
21
21
 
@@ -25,11 +25,13 @@ class Memory(FactBase):
25
25
  Returns the memory installed in this server, in MB.
26
26
  """
27
27
 
28
- command = "vmstat -s"
29
- requires_command = "vmstat"
28
+ def requires_command(self) -> str:
29
+ return "vmstat"
30
+
31
+ def command(self) -> str:
32
+ return "vmstat -s"
30
33
 
31
- @staticmethod
32
- def process(output):
34
+ def process(self, output):
33
35
  data = {}
34
36
 
35
37
  for line in output:
@@ -75,10 +77,12 @@ class BlockDevices(FactBase):
75
77
  }
76
78
  """
77
79
 
78
- command = "df"
79
80
  regex = r"([a-zA-Z0-9\/\-_]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]{1,3})%\s+([a-zA-Z\/0-9\-_]+)" # noqa: E501
80
81
  default = dict
81
82
 
83
+ def command(self) -> str:
84
+ return "df"
85
+
82
86
  def process(self, output):
83
87
  devices = {}
84
88
 
@@ -99,157 +103,201 @@ class BlockDevices(FactBase):
99
103
  return devices
100
104
 
101
105
 
102
- nettools_1_regexes = [
103
- (
104
- r"^inet addr:([0-9\.]+).+Bcast:([0-9\.]+).+Mask:([0-9\.]+)$",
105
- ("ipv4", "address", "broadcast", "netmask"),
106
- ),
107
- (
108
- r"^inet6 addr: ([0-9a-z:]+)\/([0-9]+) Scope:Global",
109
- ("ipv6", "address", "mask_bits"),
110
- ),
111
- ]
112
-
113
- nettools_2_regexes = [
114
- (
115
- r"^inet ([0-9\.]+)\s+netmask ([0-9\.fx]+)(?:\s+broadcast ([0-9\.]+))?$",
116
- ("ipv4", "address", "netmask", "broadcast"),
117
- ),
118
- (
119
- r"^inet6 ([0-9a-z:]+)\s+prefixlen ([0-9]+)",
120
- ("ipv6", "address", "mask_bits"),
121
- ),
122
- ]
123
-
124
- iproute2_regexes = [
125
- (
126
- r"^inet ([0-9\.]+)\/([0-9]{1,2})(?:\s+brd ([0-9\.]+))?",
127
- ("ipv4", "address", "mask_bits", "broadcast"),
128
- ),
129
- (
130
- r"^inet6 ([0-9a-z:]+)\/([0-9]{1,3})",
131
- ("ipv6", "address", "mask_bits"),
132
- ),
133
- ]
134
-
135
-
136
- def _parse_regexes(regexes, lines):
137
- data: dict[str, dict] = {
138
- "ipv4": {},
139
- "ipv6": {},
140
- }
141
-
142
- for line in lines:
143
- for regex, groups in regexes:
144
- matches = re.match(regex, line)
145
- if matches:
146
- ip_data = {}
147
-
148
- for i, group in enumerate(groups[1:]):
149
- ip_data[group] = matches.group(i + 1)
150
-
151
- if "mask_bits" in ip_data:
152
- ip_data["mask_bits"] = int(ip_data["mask_bits"])
153
-
154
- target_group = data[groups[0]]
155
- if target_group.get("address"):
156
- target_group.setdefault("additional_ips", []).append(ip_data)
157
- else:
158
- target_group.update(ip_data)
159
-
160
- break
161
-
162
- return data
163
-
164
-
165
106
  class NetworkDevices(FactBase):
166
107
  """
167
108
  Gets & returns a dict of network devices. See the ``ipv4_addresses`` and
168
109
  ``ipv6_addresses`` facts for easier-to-use shortcuts to get device addresses.
169
110
 
170
111
  .. code:: python
171
-
172
- {
173
- "eth0": {
174
- "ipv4": {
175
- "address": "127.0.0.1",
176
- "broadcast": "127.0.0.13",
177
- # Only one of these will exist:
178
- "netmask": "255.255.255.255",
179
- "mask_bits": 32,
180
- },
181
- "ipv6": {
182
- "address": "fe80::a00:27ff:fec3:36f0",
183
- "mask_bits": 64,
184
- "additional_ips": [{
185
- "address": "fe80::",
186
- "mask_bits": 128,
187
- }],
188
- }
112
+ "enp1s0": {
113
+ "ether": "12:34:56:78:9A:BC",
114
+ "mtu": 1500,
115
+ "state": "UP",
116
+ "ipv4": {
117
+ "address": "192.168.1.100",
118
+ "mask_bits": 24,
119
+ "netmask": "255.255.255.0"
120
+ },
121
+ "ipv6": {
122
+ "address": "2001:db8:85a3::8a2e:370:7334",
123
+ "mask_bits": 64,
124
+ "additional_ips": [
125
+ {
126
+ "address": "fe80::1234:5678:9abc:def0",
127
+ "mask_bits": 64
128
+ }
129
+ ]
130
+ }
131
+ },
132
+ "incusbr0": {
133
+ "ether": "DE:AD:BE:EF:CA:FE",
134
+ "mtu": 1500,
135
+ "state": "UP",
136
+ "ipv4": {
137
+ "address": "10.0.0.1",
138
+ "mask_bits": 24,
139
+ "netmask": "255.255.255.0"
189
140
  },
141
+ "ipv6": {
142
+ "address": "fe80::dead:beef:cafe:babe",
143
+ "mask_bits": 64,
144
+ "additional_ips": [
145
+ {
146
+ "address": "2001:db8:1234:5678::1",
147
+ "mask_bits": 64
148
+ }
149
+ ]
150
+ }
151
+ },
152
+ "lo": {
153
+ "mtu": 65536,
154
+ "state": "UP",
155
+ "ipv6": {
156
+ "address": "::1",
157
+ "mask_bits": 128
158
+ }
159
+ },
160
+ "veth98806fd6": {
161
+ "ether": "AA:BB:CC:DD:EE:FF",
162
+ "mtu": 1500,
163
+ "state": "UP"
164
+ },
165
+ "vethda29df81": {
166
+ "ether": "11:22:33:44:55:66",
167
+ "mtu": 1500,
168
+ "state": "UP"
169
+ },
170
+ "wlo1": {
171
+ "ether": "77:88:99:AA:BB:CC",
172
+ "mtu": 1500,
173
+ "state": "UNKNOWN"
190
174
  }
191
175
  """
192
176
 
193
- command = "ip addr show 2> /dev/null || ifconfig"
194
177
  default = dict
195
178
 
179
+ def command(self) -> str:
180
+ return "ip addr show 2> /dev/null || ifconfig -a"
181
+
196
182
  # Definition of valid interface names for Linux:
197
183
  # https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/net/core/dev.c?h=v5.1.3#n1020
198
- _start_regexes = [
199
- (
200
- r"^([^/: \s]+)\s+Link encap:",
201
- lambda lines: _parse_regexes(nettools_1_regexes, lines),
202
- ),
203
- (
204
- r"^([^/: \s]+): flags=",
205
- lambda lines: _parse_regexes(nettools_2_regexes, lines),
206
- ),
207
- (
208
- r"^[0-9]+: ([^/: \s]+): ",
209
- lambda lines: _parse_regexes(iproute2_regexes, lines),
210
- ),
211
- ]
212
-
213
184
  def process(self, output):
214
- devices = {}
215
-
216
- # Store current matches (start lines), the handler and any lines
217
- matches = None
218
- handler = None
219
- line_buffer = []
185
+ def mask(value):
186
+ try:
187
+ if value.startswith("0x"):
188
+ mask_bits = bin(int(value, 16)).count("1")
189
+ else:
190
+ mask_bits = int(value)
191
+ netmask = ".".join(
192
+ str((0xFFFFFFFF << (32 - b) >> mask_bits) & 0xFF) for b in (24, 16, 8, 0)
193
+ )
194
+ except ValueError:
195
+ mask_bits = sum(bin(int(x)).count("1") for x in value.split("."))
196
+ netmask = value
220
197
 
221
- for line in output:
222
- line = line.strip()
198
+ return mask_bits, netmask
223
199
 
224
- matched = False
200
+ # Strip lines and merge them as a block of text
201
+ output = "\n".join(map(str.strip, output))
225
202
 
226
- # Look for start lines
227
- for regex, new_handler in self._start_regexes:
228
- new_matches = re.match(regex, line)
203
+ # Splitting the output into sections per network device
204
+ device_sections = re.split(r"\n(?=\d+: [^\s/:]|[^\s/:]+:.*mtu )", output)
229
205
 
230
- # If we find a start line
231
- if new_matches:
232
- matched = True
206
+ # Dictionary to hold all device information
207
+ all_devices = {}
233
208
 
234
- # Assign any current matches with current handler, reset buffer
235
- if matches:
236
- devices[matches.group(1)] = handler(line_buffer)
237
- line_buffer = []
209
+ for section in device_sections:
210
+ # Extracting the device name
211
+ device_name_match = re.match(r"^(?:\d+: )?([^\s/:]+):", section)
212
+ if not device_name_match:
213
+ continue
214
+ device_name = device_name_match.group(1)
215
+
216
+ # Regular expressions to match different parts of the output
217
+ ether_re = re.compile(r"ether ([0-9A-Fa-f:]{17})")
218
+ mtu_re = re.compile(r"mtu (\d+)")
219
+ ipv4_re = (
220
+ # ip a
221
+ re.compile(
222
+ r"inet (?P<address>\d+\.\d+\.\d+\.\d+)/(?P<mask>\d+)(?: metric \d+)?(?: brd (?P<broadcast>\d+\.\d+\.\d+\.\d+))?" # noqa: E501
223
+ ),
224
+ # ifconfig -a
225
+ re.compile(
226
+ 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
227
+ ),
228
+ )
229
+ ipv6_re = (
230
+ # ip a
231
+ re.compile(r"inet6\s+(?P<address>[0-9a-fA-F:]+)/(?P<mask>\d+)"),
232
+ # ifconfig -a
233
+ re.compile(r"inet6\s+(?P<address>[0-9a-fA-F:]+)\s+prefixlen\s+(?P<mask>\d+)"),
234
+ )
235
+
236
+ # Parsing the output
237
+ ether = ether_re.search(section)
238
+ mtu = mtu_re.search(section)
239
+
240
+ # Building the result dictionary for the device
241
+ device_info = {}
242
+ if ether:
243
+ device_info["ether"] = ether.group(1)
244
+ if mtu:
245
+ device_info["mtu"] = int(mtu.group(1))
246
+
247
+ device_info["state"] = (
248
+ "UP" if "UP" in section else "DOWN" if "DOWN" in section else "UNKNOWN"
249
+ )
250
+
251
+ # IPv4 Addresses
252
+ ipv4_matches: list[re.Match[str]]
253
+ for ipv4_re_ in ipv4_re:
254
+ ipv4_matches = list(ipv4_re_.finditer(section))
255
+ if len(ipv4_matches):
256
+ break
238
257
 
239
- # Set new matches/handler
240
- matches = new_matches
241
- handler = new_handler
258
+ if len(ipv4_matches):
259
+ ipv4_info = []
260
+ for ipv4 in ipv4_matches:
261
+ address = ipv4.group("address")
262
+ mask_value = ipv4.group("mask")
263
+ mask_bits, netmask = mask(mask_value)
264
+ try:
265
+ broadcast = ipv4.group("broadcast")
266
+ except IndexError:
267
+ broadcast = None
268
+
269
+ ipv4_info.append(
270
+ {
271
+ "address": address,
272
+ "mask_bits": mask_bits,
273
+ "netmask": netmask,
274
+ "broadcast": broadcast,
275
+ },
276
+ )
277
+ device_info["ipv4"] = ipv4_info[0]
278
+ if len(ipv4_matches) > 1:
279
+ device_info["ipv4"]["additional_ips"] = ipv4_info[1:] # type: ignore[index]
280
+
281
+ # IPv6 Addresses
282
+ ipv6_matches: list[re.Match[str]]
283
+ for ipv6_re_ in ipv6_re:
284
+ ipv6_matches = list(ipv6_re_.finditer(section))
285
+ if ipv6_matches:
242
286
  break
243
287
 
244
- if not matched:
245
- line_buffer.append(line)
288
+ if len(ipv6_matches):
289
+ ipv6_info = []
290
+ for ipv6 in ipv6_matches:
291
+ address = ipv6.group("address")
292
+ mask_bits = ipv6.group("mask")
293
+ ipv6_info.append({"address": address, "mask_bits": int(mask_bits)})
294
+ device_info["ipv6"] = ipv6_info[0]
295
+ if len(ipv6_matches) > 1:
296
+ device_info["ipv6"]["additional_ips"] = ipv6_info[1:] # type: ignore[index]
246
297
 
247
- # Handle any left over matches
248
- if matches:
249
- assert handler is not None
250
- devices[matches.group(1)] = handler(line_buffer)
298
+ all_devices[device_name] = device_info
251
299
 
252
- return devices
300
+ return all_devices
253
301
 
254
302
 
255
303
  class Ipv4Addrs(ShortFactBase):
@@ -276,7 +324,7 @@ class Ipv4Addrs(ShortFactBase):
276
324
  ips = []
277
325
 
278
326
  ip_details = details.get(self.ip_type)
279
- if not ip_details:
327
+ if not ip_details or not ip_details.get("address"):
280
328
  continue
281
329
 
282
330
  ips.append(ip_details["address"])
@@ -335,7 +383,7 @@ class Ipv4Addresses(ShortFactBase):
335
383
 
336
384
  for interface, details in data.items():
337
385
  ip_details = details.get(self.ip_type)
338
- if not ip_details:
386
+ if not ip_details or not ip_details.get("address"):
339
387
  continue # pragma: no cover
340
388
 
341
389
  addresses[interface] = ip_details["address"]
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
 
@@ -6,8 +8,11 @@ class LaunchdStatus(FactBase):
6
8
  Returns a dict of name -> status for launchd managed services.
7
9
  """
8
10
 
9
- command = "launchctl list"
10
- requires_command = "launchctl"
11
+ def command(self) -> str:
12
+ return "launchctl list"
13
+
14
+ def requires_command(self) -> str:
15
+ return "launchctl"
11
16
 
12
17
  default = dict
13
18
 
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
@@ -8,11 +10,15 @@ class LxdContainers(FactBase):
8
10
  Returns a list of running LXD containers
9
11
  """
10
12
 
11
- command = "lxc list --format json --fast"
12
- requires_command = "lxc"
13
+ def command(self) -> str:
14
+ return "lxc list --format json --fast"
15
+
16
+ def requires_command(self) -> str:
17
+ return "lxc"
13
18
 
14
19
  default = list
15
20
 
16
21
  def process(self, output):
22
+ output = list(output)
17
23
  assert len(output) == 1
18
24
  return json.loads(output[0])
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",
@@ -54,9 +60,11 @@ class MysqlFactBase(FactBase):
54
60
  abstract = True
55
61
 
56
62
  mysql_command: str
57
- requires_command = "mysql"
58
63
  ignore_errors = False
59
64
 
65
+ def requires_command(self, *args, **kwargs) -> str:
66
+ return "mysql"
67
+
60
68
  def command(
61
69
  self,
62
70
  # Details for speaking to MySQL via `mysql` CLI via `mysql` CLI
@@ -64,7 +72,7 @@ class MysqlFactBase(FactBase):
64
72
  mysql_password=None,
65
73
  mysql_host=None,
66
74
  mysql_port=None,
67
- ):
75
+ ) -> StringCommand:
68
76
  return make_execute_mysql_command(
69
77
  self.mysql_command,
70
78
  ignore_errors=self.ignore_errors,
@@ -128,8 +136,7 @@ class MysqlUsers(MysqlFactBase):
128
136
  default = dict
129
137
  mysql_command = "SELECT * FROM mysql.user"
130
138
 
131
- @staticmethod
132
- def process(output):
139
+ def process(self, output):
133
140
  rows = parse_columns_and_rows(output, "\t")
134
141
 
135
142
  users = {}
@@ -188,7 +195,7 @@ class MysqlUserGrants(MysqlFactBase):
188
195
  # Ignore errors as SHOW GRANTS will error if the user does not exist
189
196
  ignore_errors = True
190
197
 
191
- def command(
198
+ def command( # type: ignore[override]
192
199
  self,
193
200
  user,
194
201
  hostname="localhost",
@@ -197,7 +204,7 @@ class MysqlUserGrants(MysqlFactBase):
197
204
  mysql_password=None,
198
205
  mysql_host=None,
199
206
  mysql_port=None,
200
- ):
207
+ ) -> StringCommand:
201
208
  self.mysql_command = 'SHOW GRANTS FOR "{0}"@"{1}"'.format(user, hostname)
202
209
 
203
210
  return super().command(
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
 
@@ -20,7 +21,8 @@ class NpmPackages(FactBase):
20
21
 
21
22
  default = dict
22
23
 
23
- requires_command = "npm"
24
+ def requires_command(self, directory=None) -> str:
25
+ return "npm"
24
26
 
25
27
  def command(self, directory=None):
26
28
  if directory:
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
@@ -9,7 +11,6 @@ class OpenrcStatus(FactBase):
9
11
  """
10
12
 
11
13
  default = dict
12
- requires_command = "rc-status"
13
14
  regex = (
14
15
  r"\s+([a-zA-Z0-9\-_]+)"
15
16
  r"\s+\[\s+"
@@ -19,6 +20,9 @@ class OpenrcStatus(FactBase):
19
20
  r"\s+\]"
20
21
  )
21
22
 
23
+ def requires_command(self, runlevel="default") -> str:
24
+ return "rc-status"
25
+
22
26
  def command(self, runlevel="default"):
23
27
  return "rc-status {0}".format(runlevel)
24
28
 
@@ -39,7 +43,9 @@ class OpenrcEnabled(FactBase):
39
43
  """
40
44
 
41
45
  default = dict
42
- requires_command = "rc-update"
46
+
47
+ def requires_command(self, runlevel="default") -> str:
48
+ return "rc-update"
43
49
 
44
50
  def command(self, runlevel="default"):
45
51
  self.runlevel = runlevel