pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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 (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
pyinfra/facts/snap.py ADDED
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from typing_extensions import override
6
+
7
+ from pyinfra.api import FactBase
8
+
9
+
10
+ class SnapBaseFact(FactBase):
11
+ abstract = True
12
+
13
+ @override
14
+ def requires_command(self, *args, **kwargs) -> str:
15
+ return "snap"
16
+
17
+
18
+ class SnapPackage(SnapBaseFact):
19
+ """
20
+ Returns information for an installed snap package
21
+
22
+ .. code:: python
23
+
24
+ {
25
+ "name": "lxd",
26
+ "publisher": "Canonical✓",
27
+ "snap-id": "J60k4JY0HppjwOjW8dZdYc8obXKxujRu",
28
+ "channel": "4.0/stable",
29
+ "version": "4.0.9"
30
+ }
31
+ """
32
+
33
+ default = dict
34
+ _regexes = {
35
+ "name": "^name:[ ]+(.*)",
36
+ "publisher": r"^publisher:[ ]+(.*)",
37
+ "snap-id": r"^snap-id:[ ]+(.*)",
38
+ "channel": r"^tracking:[ ]+([\w\d.-]+/[\w\d.-]+)[/]?.*$",
39
+ "version": r"^installed:[ ]+([\w\d.-]+).*$",
40
+ }
41
+
42
+ @override
43
+ def command(self, package):
44
+ return f"snap info {package}"
45
+
46
+ @override
47
+ def process(self, output):
48
+ data = {}
49
+ for line in output:
50
+ for regex_name, regex in self._regexes.items():
51
+ matches = re.match(regex, line)
52
+ if matches:
53
+ data[regex_name] = matches.group(1)
54
+
55
+ return data
56
+
57
+
58
+ class SnapPackages(SnapBaseFact):
59
+ """
60
+ Returns a list of installed snap packages:
61
+
62
+ .. code:: python
63
+
64
+ [
65
+ "core",
66
+ "core18",
67
+ "core20",
68
+ "lxd",
69
+ "snapd"
70
+ ]
71
+ """
72
+
73
+ default = list
74
+
75
+ @override
76
+ def command(self) -> str:
77
+ return "snap list"
78
+
79
+ @override
80
+ def process(self, output):
81
+ # Discard header output line from snap list command
82
+ # 'snap list' command example output lines:
83
+ # $ snap list
84
+ # Name Version Rev Tracking Publisher Notes
85
+ # core 16-2.57.2 13886 latest/stable canonical✓ core
86
+ # lxd 4.0.9 22753 4.0/stable/… canonical✓ -
87
+ #
88
+ return [snap.split()[0] for snap in output[1:]]
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Dict, Iterable
5
+
6
+ from typing_extensions import override
7
+
8
+ from pyinfra.api import FactBase, QuoteString, StringCommand
9
+
10
+ # Valid unit names consist of a "name prefix" and a dot and a suffix specifying the unit type.
11
+ # The "unit prefix" must consist of one or more valid characters
12
+ # (ASCII letters, digits, ":", "-", "_", ".", and "\").
13
+ # The total length of the unit name including the suffix must not exceed 256 characters.
14
+ # The type suffix must be one of
15
+ # ".service", ".socket", ".device", ".mount", ".automount",
16
+ # ".swap", ".target", ".path", ".timer", ".slice", or ".scope".
17
+ # Units names can be parameterized by a single argument called the "instance name".
18
+ # A template unit must have a single "@" at the end of the name (right before the type suffix).
19
+ # The name of the full unit is formed by inserting the instance name
20
+ # between "@" and the unit type suffix.
21
+ SYSTEMD_UNIT_NAME_REGEX = (
22
+ r"[a-zA-Z0-9\:\-\_\.\\\@]+\."
23
+ r"(?:service|socket|device|mount|automount|swap|target|path|timer|slice|scope)"
24
+ )
25
+
26
+
27
+ def _make_systemctl_cmd(user_mode=False, machine=None, user_name=None):
28
+ # base command for normal and user mode
29
+ systemctl_cmd = ["systemctl --user"] if user_mode else ["systemctl"]
30
+
31
+ # add user and machine flag if given in args
32
+ if machine is not None:
33
+ if user_name is not None:
34
+ systemctl_cmd.append("--machine={1}@{0}".format(machine, user_name))
35
+ else:
36
+ systemctl_cmd.append("--machine={0}".format(machine))
37
+ elif user_name is not None:
38
+ # If only the user is given, assume that the connection should be made to the local machine
39
+ systemctl_cmd.append("--machine={0}@.host".format(user_name))
40
+
41
+ return StringCommand(*systemctl_cmd)
42
+
43
+
44
+ class SystemdStatus(FactBase[Dict[str, bool]]):
45
+ """
46
+ Returns a dictionary map of systemd units to booleans indicating whether they are active.
47
+
48
+ + user_mode: whether to use user mode
49
+ + machine: machine name
50
+
51
+ .. code:: python
52
+
53
+ {
54
+ "ssh.service": True,
55
+ "containerd.service": True,
56
+ "apt-daily.timer": False,
57
+ }
58
+ """
59
+
60
+ @override
61
+ def requires_command(self, *args, **kwargs) -> str:
62
+ return "systemctl"
63
+
64
+ default = dict
65
+
66
+ state_key = "SubState"
67
+ state_values = ["running", "waiting", "exited", "listening", "mounted"]
68
+
69
+ @override
70
+ def command(
71
+ self,
72
+ user_mode: bool = False,
73
+ machine: str | None = None,
74
+ user_name: str | None = None,
75
+ services: str | list[str] | None = None,
76
+ ) -> StringCommand:
77
+ fact_cmd = _make_systemctl_cmd(
78
+ user_mode=user_mode,
79
+ machine=machine,
80
+ user_name=user_name,
81
+ )
82
+
83
+ if services is None:
84
+ service_strs = [QuoteString("*")]
85
+ elif isinstance(services, str):
86
+ service_strs = [QuoteString(services)]
87
+ elif isinstance(services, Iterable):
88
+ service_strs = [QuoteString(s) for s in services]
89
+
90
+ return StringCommand(
91
+ fact_cmd,
92
+ "show",
93
+ "--all",
94
+ "--property",
95
+ "Id",
96
+ "--property",
97
+ self.state_key,
98
+ *service_strs,
99
+ )
100
+
101
+ @override
102
+ def process(self, output) -> Dict[str, bool]:
103
+ services: Dict[str, bool] = {}
104
+
105
+ current_unit = None
106
+ for line in output:
107
+ line = line.strip()
108
+
109
+ try:
110
+ key, value = line.split("=", 1)
111
+ except ValueError:
112
+ current_unit = None # reset current_unit just in case
113
+ continue
114
+
115
+ if key == "Id" and re.match(SYSTEMD_UNIT_NAME_REGEX, value):
116
+ current_unit = value
117
+ continue
118
+
119
+ if key == self.state_key and current_unit:
120
+ services[current_unit] = value in self.state_values
121
+
122
+ return services
123
+
124
+
125
+ class SystemdEnabled(SystemdStatus):
126
+ """
127
+ Returns a dictionary map of systemd units to booleans indicating whether they are enabled.
128
+
129
+ .. code:: python
130
+
131
+ {
132
+ "ssh.service": True,
133
+ "containerd.service": True,
134
+ "apt-daily.timer": False,
135
+ }
136
+ """
137
+
138
+ state_key = "UnitFileState"
139
+ state_values = ["enabled", "static"]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ from typing_extensions import override
7
+
8
+ from pyinfra.api import FactBase
9
+
10
+
11
+ class InitdStatus(FactBase):
12
+ """
13
+ Low level check for every /etc/init.d/* script. Unfortunately many of these
14
+ misbehave and return exit status 0 while also displaying the help info/not
15
+ offering status support.
16
+
17
+ Returns a dict of name -> status.
18
+
19
+ Expected codes found at:
20
+ http://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
21
+ """
22
+
23
+ @override
24
+ def command(self) -> str:
25
+ return """
26
+ for SERVICE in `ls /etc/init.d/`; do
27
+ _=`cat /etc/init.d/$SERVICE | grep "### BEGIN INIT INFO"`
28
+
29
+ if [ "$?" = "0" ]; then
30
+ STATUS=`/etc/init.d/$SERVICE status`
31
+ echo "$SERVICE=$?"
32
+ fi
33
+ done
34
+ """
35
+
36
+ regex = r"([a-zA-Z0-9\-]+)=([0-9]+)"
37
+ default = dict
38
+
39
+ @override
40
+ def process(self, output) -> dict[str, Optional[bool]]:
41
+ services: dict[str, Optional[bool]] = {}
42
+
43
+ for line in output:
44
+ matches = re.match(self.regex, line)
45
+ if matches:
46
+ intstatus = int(matches.group(2))
47
+ status: Optional[bool] = None
48
+
49
+ # Exit code 0 = OK/running
50
+ if intstatus == 0:
51
+ status = True
52
+
53
+ # Exit codes 1-3 = DOWN/not running
54
+ elif intstatus < 4:
55
+ status = False
56
+
57
+ services[matches.group(1)] = status
58
+
59
+ return services
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from typing_extensions import override
6
+
7
+ from pyinfra.api import FactBase
8
+
9
+
10
+ class UpstartStatus(FactBase):
11
+ """
12
+ Returns a dict of name -> status for upstart managed services.
13
+ """
14
+
15
+ @override
16
+ def requires_command(self) -> str:
17
+ return "initctl"
18
+
19
+ regex = r"^([a-z\-]+) [a-z]+\/([a-z]+)"
20
+ default = dict
21
+
22
+ @override
23
+ def command(self):
24
+ return "initctl list"
25
+
26
+ @override
27
+ def process(self, output):
28
+ services = {}
29
+
30
+ for line in output:
31
+ matches = re.match(self.regex, line)
32
+ if matches:
33
+ services[matches.group(1)] = matches.group(2) == "running"
34
+
35
+ return services
@@ -0,0 +1,17 @@
1
+ from typing import Iterable
2
+
3
+
4
+ def make_cat_files_command(*filenames: Iterable[str]) -> str:
5
+ commands = []
6
+
7
+ for filename in filenames:
8
+ if "*" in filename:
9
+ # There's no way to test against a glob expression, so accept anything here
10
+ commands.append("cat {0} || true".format(filename))
11
+ else:
12
+ commands.append("! test -f {0} || cat {0}".format(filename))
13
+
14
+ if len(commands) > 1: # if we have multiple, wrap them
15
+ commands = ["({0})".format(command) for command in commands]
16
+
17
+ return " && ".join(commands)
@@ -1,12 +1,13 @@
1
1
  def _remove_prefix(title, remove_prefix):
2
2
  if title.startswith(remove_prefix):
3
- return title[len(remove_prefix):]
3
+ return title[len(remove_prefix) :]
4
4
 
5
5
  return title
6
6
 
7
7
 
8
8
  def parse_columns_and_rows(
9
- lines, delimiter,
9
+ lines,
10
+ delimiter,
10
11
  remove_column_prefix=None,
11
12
  title_parser=None,
12
13
  ):
@@ -17,10 +18,7 @@ def parse_columns_and_rows(
17
18
  titles = [title_parser(title) for title in titles]
18
19
 
19
20
  if remove_column_prefix:
20
- titles = [
21
- _remove_prefix(title, remove_column_prefix)
22
- for title in titles
23
- ]
21
+ titles = [_remove_prefix(title, remove_column_prefix) for title in titles]
24
22
 
25
23
  rows = []
26
24
 
@@ -1,19 +1,50 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ from typing import Iterable
2
5
 
3
6
 
4
- def parse_packages(regex, output, lower=True):
5
- packages = {}
7
+ def parse_packages(regex: str, output: Iterable[str]) -> dict[str, set[str]]:
8
+ packages: dict[str, set[str]] = {}
6
9
 
7
10
  for line in output:
8
11
  matches = re.match(regex, line)
9
12
 
10
13
  if matches:
11
- # Sort out name
12
14
  name = matches.group(1)
13
- if lower:
14
- name = name.lower()
15
-
16
15
  packages.setdefault(name, set())
17
16
  packages[name].add(matches.group(2))
18
17
 
19
18
  return packages
19
+
20
+
21
+ def _parse_yum_or_zypper_repositories(output):
22
+ repos = []
23
+
24
+ current_repo: dict[str, str] = {}
25
+ for line in output:
26
+ line = line.strip()
27
+ if not line or line.startswith("#"):
28
+ continue
29
+
30
+ if line.startswith("["):
31
+ if current_repo:
32
+ repos.append(current_repo)
33
+ current_repo = {}
34
+
35
+ current_repo["repoid"] = line[1:-1]
36
+ current_repo["name"] = line[1:-1]
37
+
38
+ if current_repo and "=" in line:
39
+ key, value = re.split(r"\s*=\s*", line, maxsplit=1)
40
+ current_repo[key] = value
41
+
42
+ if current_repo:
43
+ repos.append(current_repo)
44
+
45
+ return repos
46
+
47
+
48
+ parse_yum_repositories = _parse_yum_or_zypper_repositories
49
+
50
+ parse_zypper_repositories = _parse_yum_or_zypper_repositories
@@ -0,0 +1,30 @@
1
+ # from https://stackoverflow.com/a/60708339, but with a few modifications
2
+ from __future__ import annotations # for | in type hints
3
+
4
+ import re
5
+
6
+ units = {
7
+ "B": 1,
8
+ "KB": 10**3,
9
+ "MB": 10**6,
10
+ "GB": 10**9,
11
+ "TB": 10**12,
12
+ "KIB": 2**10,
13
+ "MIB": 2**20,
14
+ "GIB": 2**30,
15
+ "TIB": 2**40,
16
+ }
17
+
18
+
19
+ def parse_human_readable_size(size: str) -> int:
20
+ size = size.upper()
21
+ if not re.match(r" ", size):
22
+ size = re.sub(r"([KMGT]?I?[B])", r" \1", size)
23
+ number, unit = [string.strip() for string in size.split()]
24
+ return int(float(number) * units[unit])
25
+
26
+
27
+ def parse_size(size: str | int) -> int:
28
+ if isinstance(size, int):
29
+ return size
30
+ return parse_human_readable_size(size)
@@ -0,0 +1,99 @@
1
+ import re
2
+ from datetime import datetime
3
+
4
+ WIN_LS_REGEX = re.compile(
5
+ (
6
+ # filetype and mode
7
+ r"^([darhsl\-]{6})\s+"
8
+ # Windows date
9
+ r"([0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4})\s+([0-9]{1,2}:[0-9]{1,2}\s[AP][M])\s+"
10
+ # Size (Note: no size on directories)
11
+ r"([0-9]+)?\s+"
12
+ # Size and Filename
13
+ r"([\w\/\.@-]+)"
14
+ ),
15
+ )
16
+
17
+ WIN_FLAG_TO_TYPE = {
18
+ "d": "directory",
19
+ "-": "file",
20
+ }
21
+
22
+ WIN_FLAG_TO_ATTR = {
23
+ "a": "archive",
24
+ "r": "readonly",
25
+ "h": "hidden",
26
+ "s": "system",
27
+ "l": "link",
28
+ "-": "none",
29
+ }
30
+
31
+
32
+ def _parse_time(time):
33
+ # Try matching windows format
34
+ try:
35
+ tmp = datetime.strptime(time, "%m/%d/%Y %H:%M %p")
36
+ return tmp
37
+ except ValueError:
38
+ pass
39
+
40
+
41
+ def parse_win_ls_output(output, wanted_type):
42
+ if output:
43
+ matches = re.match(WIN_LS_REGEX, output)
44
+ if matches:
45
+ if output[5] == "l":
46
+ type = "link"
47
+ else:
48
+ type = WIN_FLAG_TO_TYPE[output[0]]
49
+
50
+ if type != wanted_type:
51
+ return False
52
+
53
+ hidden = False
54
+ system = False
55
+ readonly = False
56
+ archive = False
57
+ link = False
58
+
59
+ mode_without_first = matches.group(1)[1:]
60
+
61
+ for c in mode_without_first:
62
+ tmp = WIN_FLAG_TO_ATTR[c]
63
+ if tmp != "none":
64
+ if tmp == "hidden":
65
+ hidden = True
66
+ if tmp == "readonly":
67
+ readonly = True
68
+ if tmp == "archive":
69
+ archive = True
70
+ if tmp == "system":
71
+ system = True
72
+ if tmp == "link":
73
+ link = True
74
+
75
+ mode = {
76
+ "archive": archive,
77
+ "hidden": hidden,
78
+ "readonly": readonly,
79
+ "system": system,
80
+ "link": link,
81
+ }
82
+
83
+ date_and_time = "{} {}".format(matches.group(2), matches.group(3))
84
+
85
+ size = "0"
86
+ if type == "file":
87
+ size = matches.group(4)
88
+
89
+ out = {
90
+ "type": type,
91
+ "mode": mode,
92
+ "mtime": _parse_time(date_and_time),
93
+ "size": size,
94
+ "name": matches.group(5),
95
+ # TODO: You will need to run another powershell command to
96
+ # get the link target, so bailing on that for now.
97
+ }
98
+
99
+ return out
pyinfra/facts/vzctl.py CHANGED
@@ -1,33 +1,40 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
5
+ from typing_extensions import override
6
+
3
7
  from pyinfra.api import FactBase
4
8
 
5
9
 
6
10
  class OpenvzContainers(FactBase):
7
- '''
11
+ """
8
12
  Returns a dict of running OpenVZ containers by CTID:
9
13
 
10
14
  .. code:: python
11
15
 
12
16
  {
13
17
  666: {
14
- 'ip': [],
15
- 'ostemplate': 'ubuntu...',
18
+ "ip": [],
19
+ "ostemplate": "ubuntu...",
16
20
  ...
17
21
  },
18
- ...
19
22
  }
20
- '''
23
+ """
24
+
25
+ @override
26
+ def command(self) -> str:
27
+ return "vzlist -a -j"
28
+
29
+ @override
30
+ def requires_command(self) -> str:
31
+ return "vzlist"
21
32
 
22
- command = 'vzlist -a -j'
23
33
  default = dict
24
34
 
25
- @staticmethod
26
- def process(output):
27
- combined_json = ''.join(output)
35
+ @override
36
+ def process(self, output):
37
+ combined_json = "".join(output)
28
38
  vz_data = json.loads(combined_json)
29
39
 
30
- return {
31
- int(vz.pop('ctid')): vz
32
- for vz in vz_data
33
- }
40
+ return {int(vz.pop("ctid")): vz for vz in vz_data}
pyinfra/facts/xbps.py ADDED
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
5
+ from pyinfra.api import FactBase
6
+
7
+ from .util.packaging import parse_packages
8
+
9
+
10
+ class XbpsPackages(FactBase):
11
+ """
12
+ Returns a dict of installed XBPS packages:
13
+
14
+ .. code:: python
15
+
16
+ {
17
+ "package_name": ["version"],
18
+ }
19
+ """
20
+
21
+ @override
22
+ def requires_command(self) -> str:
23
+ return "xbps-query"
24
+
25
+ default = dict
26
+
27
+ regex = r"^.. ([a-zA-Z0-9_\-\+\.]+)\-([0-9a-z\.]+_[0-9]+)"
28
+
29
+ @override
30
+ def command(self):
31
+ return "xbps-query -l"
32
+
33
+ @override
34
+ def process(self, output):
35
+ return parse_packages(self.regex, output)