pyinfra 0.11.dev3__py3-none-any.whl → 3.6__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 (204) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +19 -3
  4. pyinfra/api/arguments.py +413 -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 +73 -18
  12. pyinfra/api/facts.py +267 -200
  13. pyinfra/api/host.py +416 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/metadata.py +69 -0
  16. pyinfra/api/operation.py +432 -262
  17. pyinfra/api/operations.py +273 -260
  18. pyinfra/api/state.py +302 -248
  19. pyinfra/api/util.py +309 -369
  20. pyinfra/connectors/base.py +173 -0
  21. pyinfra/connectors/chroot.py +212 -0
  22. pyinfra/connectors/docker.py +405 -0
  23. pyinfra/connectors/dockerssh.py +297 -0
  24. pyinfra/connectors/local.py +238 -0
  25. pyinfra/connectors/scp/__init__.py +1 -0
  26. pyinfra/connectors/scp/client.py +204 -0
  27. pyinfra/connectors/ssh.py +727 -0
  28. pyinfra/connectors/ssh_util.py +114 -0
  29. pyinfra/connectors/sshuserclient/client.py +309 -0
  30. pyinfra/connectors/sshuserclient/config.py +102 -0
  31. pyinfra/connectors/terraform.py +135 -0
  32. pyinfra/connectors/util.py +417 -0
  33. pyinfra/connectors/vagrant.py +183 -0
  34. pyinfra/context.py +145 -0
  35. pyinfra/facts/__init__.py +7 -6
  36. pyinfra/facts/apk.py +22 -7
  37. pyinfra/facts/apt.py +117 -60
  38. pyinfra/facts/brew.py +100 -15
  39. pyinfra/facts/bsdinit.py +23 -0
  40. pyinfra/facts/cargo.py +37 -0
  41. pyinfra/facts/choco.py +47 -0
  42. pyinfra/facts/crontab.py +195 -0
  43. pyinfra/facts/deb.py +94 -0
  44. pyinfra/facts/dnf.py +48 -0
  45. pyinfra/facts/docker.py +96 -23
  46. pyinfra/facts/efibootmgr.py +113 -0
  47. pyinfra/facts/files.py +629 -58
  48. pyinfra/facts/flatpak.py +77 -0
  49. pyinfra/facts/freebsd.py +70 -0
  50. pyinfra/facts/gem.py +19 -6
  51. pyinfra/facts/git.py +59 -14
  52. pyinfra/facts/gpg.py +150 -0
  53. pyinfra/facts/hardware.py +313 -167
  54. pyinfra/facts/iptables.py +72 -62
  55. pyinfra/facts/launchd.py +44 -0
  56. pyinfra/facts/lxd.py +17 -4
  57. pyinfra/facts/mysql.py +122 -86
  58. pyinfra/facts/npm.py +17 -9
  59. pyinfra/facts/openrc.py +71 -0
  60. pyinfra/facts/opkg.py +246 -0
  61. pyinfra/facts/pacman.py +50 -7
  62. pyinfra/facts/pip.py +24 -7
  63. pyinfra/facts/pipx.py +82 -0
  64. pyinfra/facts/pkg.py +15 -6
  65. pyinfra/facts/pkgin.py +35 -0
  66. pyinfra/facts/podman.py +54 -0
  67. pyinfra/facts/postgres.py +178 -0
  68. pyinfra/facts/postgresql.py +6 -147
  69. pyinfra/facts/rpm.py +105 -0
  70. pyinfra/facts/runit.py +77 -0
  71. pyinfra/facts/selinux.py +161 -0
  72. pyinfra/facts/server.py +762 -285
  73. pyinfra/facts/snap.py +88 -0
  74. pyinfra/facts/systemd.py +139 -0
  75. pyinfra/facts/sysvinit.py +59 -0
  76. pyinfra/facts/upstart.py +35 -0
  77. pyinfra/facts/util/__init__.py +17 -0
  78. pyinfra/facts/util/databases.py +4 -6
  79. pyinfra/facts/util/packaging.py +37 -6
  80. pyinfra/facts/util/units.py +30 -0
  81. pyinfra/facts/util/win_files.py +99 -0
  82. pyinfra/facts/vzctl.py +20 -13
  83. pyinfra/facts/xbps.py +35 -0
  84. pyinfra/facts/yum.py +34 -40
  85. pyinfra/facts/zfs.py +77 -0
  86. pyinfra/facts/zypper.py +42 -0
  87. pyinfra/local.py +45 -83
  88. pyinfra/operations/__init__.py +12 -0
  89. pyinfra/operations/apk.py +99 -0
  90. pyinfra/operations/apt.py +496 -0
  91. pyinfra/operations/brew.py +232 -0
  92. pyinfra/operations/bsdinit.py +59 -0
  93. pyinfra/operations/cargo.py +45 -0
  94. pyinfra/operations/choco.py +61 -0
  95. pyinfra/operations/crontab.py +194 -0
  96. pyinfra/operations/dnf.py +213 -0
  97. pyinfra/operations/docker.py +492 -0
  98. pyinfra/operations/files.py +2014 -0
  99. pyinfra/operations/flatpak.py +95 -0
  100. pyinfra/operations/freebsd/__init__.py +12 -0
  101. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  102. pyinfra/operations/freebsd/pkg.py +219 -0
  103. pyinfra/operations/freebsd/service.py +116 -0
  104. pyinfra/operations/freebsd/sysrc.py +92 -0
  105. pyinfra/operations/gem.py +48 -0
  106. pyinfra/operations/git.py +420 -0
  107. pyinfra/operations/iptables.py +312 -0
  108. pyinfra/operations/launchd.py +45 -0
  109. pyinfra/operations/lxd.py +69 -0
  110. pyinfra/operations/mysql.py +610 -0
  111. pyinfra/operations/npm.py +57 -0
  112. pyinfra/operations/openrc.py +63 -0
  113. pyinfra/operations/opkg.py +89 -0
  114. pyinfra/operations/pacman.py +82 -0
  115. pyinfra/operations/pip.py +206 -0
  116. pyinfra/operations/pipx.py +103 -0
  117. pyinfra/operations/pkg.py +71 -0
  118. pyinfra/operations/pkgin.py +92 -0
  119. pyinfra/operations/postgres.py +437 -0
  120. pyinfra/operations/postgresql.py +30 -0
  121. pyinfra/operations/puppet.py +41 -0
  122. pyinfra/operations/python.py +73 -0
  123. pyinfra/operations/runit.py +184 -0
  124. pyinfra/operations/selinux.py +190 -0
  125. pyinfra/operations/server.py +1100 -0
  126. pyinfra/operations/snap.py +118 -0
  127. pyinfra/operations/ssh.py +217 -0
  128. pyinfra/operations/systemd.py +150 -0
  129. pyinfra/operations/sysvinit.py +142 -0
  130. pyinfra/operations/upstart.py +68 -0
  131. pyinfra/operations/util/__init__.py +12 -0
  132. pyinfra/operations/util/docker.py +407 -0
  133. pyinfra/operations/util/files.py +247 -0
  134. pyinfra/operations/util/packaging.py +338 -0
  135. pyinfra/operations/util/service.py +46 -0
  136. pyinfra/operations/vzctl.py +137 -0
  137. pyinfra/operations/xbps.py +78 -0
  138. pyinfra/operations/yum.py +213 -0
  139. pyinfra/operations/zfs.py +176 -0
  140. pyinfra/operations/zypper.py +193 -0
  141. pyinfra/progress.py +44 -32
  142. pyinfra/py.typed +0 -0
  143. pyinfra/version.py +9 -1
  144. pyinfra-3.6.dist-info/METADATA +142 -0
  145. pyinfra-3.6.dist-info/RECORD +160 -0
  146. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info}/WHEEL +1 -2
  147. pyinfra-3.6.dist-info/entry_points.txt +12 -0
  148. {pyinfra-0.11.dev3.dist-info → pyinfra-3.6.dist-info/licenses}/LICENSE.md +1 -1
  149. pyinfra_cli/__init__.py +1 -0
  150. pyinfra_cli/cli.py +793 -0
  151. pyinfra_cli/commands.py +66 -0
  152. pyinfra_cli/exceptions.py +155 -65
  153. pyinfra_cli/inventory.py +233 -89
  154. pyinfra_cli/log.py +39 -43
  155. pyinfra_cli/main.py +26 -495
  156. pyinfra_cli/prints.py +215 -156
  157. pyinfra_cli/util.py +172 -105
  158. pyinfra_cli/virtualenv.py +25 -20
  159. pyinfra/api/connectors/__init__.py +0 -21
  160. pyinfra/api/connectors/ansible.py +0 -99
  161. pyinfra/api/connectors/docker.py +0 -178
  162. pyinfra/api/connectors/local.py +0 -169
  163. pyinfra/api/connectors/ssh.py +0 -402
  164. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  165. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  166. pyinfra/api/connectors/util.py +0 -63
  167. pyinfra/api/connectors/vagrant.py +0 -155
  168. pyinfra/facts/init.py +0 -176
  169. pyinfra/facts/util/files.py +0 -102
  170. pyinfra/hook.py +0 -41
  171. pyinfra/modules/__init__.py +0 -11
  172. pyinfra/modules/apk.py +0 -64
  173. pyinfra/modules/apt.py +0 -272
  174. pyinfra/modules/brew.py +0 -122
  175. pyinfra/modules/files.py +0 -711
  176. pyinfra/modules/gem.py +0 -30
  177. pyinfra/modules/git.py +0 -115
  178. pyinfra/modules/init.py +0 -344
  179. pyinfra/modules/iptables.py +0 -271
  180. pyinfra/modules/lxd.py +0 -45
  181. pyinfra/modules/mysql.py +0 -347
  182. pyinfra/modules/npm.py +0 -47
  183. pyinfra/modules/pacman.py +0 -60
  184. pyinfra/modules/pip.py +0 -99
  185. pyinfra/modules/pkg.py +0 -43
  186. pyinfra/modules/postgresql.py +0 -245
  187. pyinfra/modules/puppet.py +0 -20
  188. pyinfra/modules/python.py +0 -37
  189. pyinfra/modules/server.py +0 -524
  190. pyinfra/modules/ssh.py +0 -150
  191. pyinfra/modules/util/files.py +0 -52
  192. pyinfra/modules/util/packaging.py +0 -118
  193. pyinfra/modules/vzctl.py +0 -133
  194. pyinfra/modules/yum.py +0 -171
  195. pyinfra/pseudo_modules.py +0 -64
  196. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  197. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  198. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  199. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  200. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  201. pyinfra_cli/__main__.py +0 -40
  202. pyinfra_cli/config.py +0 -92
  203. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  204. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
@@ -0,0 +1,195 @@
1
+ import re
2
+ from typing import Dict, List, Optional, TypedDict, Union
3
+
4
+ from typing_extensions import NotRequired, override
5
+
6
+ from pyinfra.api import FactBase
7
+ from pyinfra.api.util import try_int
8
+
9
+
10
+ class CrontabDict(TypedDict):
11
+ command: NotRequired[str]
12
+ # handles cases like CRON_TZ=UTC
13
+ env: NotRequired[str]
14
+ minute: NotRequired[Union[int, str]]
15
+ hour: NotRequired[Union[int, str]]
16
+ month: NotRequired[Union[int, str]]
17
+ day_of_month: NotRequired[Union[int, str]]
18
+ day_of_week: NotRequired[Union[int, str]]
19
+ comments: NotRequired[List[str]]
20
+ special_time: NotRequired[str]
21
+
22
+
23
+ # for compatibility, also keeps a dict of command -> crontab dict
24
+ class CrontabFile:
25
+ commands: List[CrontabDict]
26
+
27
+ def __init__(self, input_dict: Optional[Dict[str, CrontabDict]] = None):
28
+ super().__init__()
29
+ self.commands = []
30
+ if input_dict:
31
+ for command, others in input_dict.items():
32
+ val = others.copy()
33
+ val["command"] = command
34
+ self.add_item(val)
35
+
36
+ def add_item(self, item: CrontabDict):
37
+ self.commands.append(item)
38
+
39
+ def __len__(self):
40
+ return len(self.commands)
41
+
42
+ def __bool__(self):
43
+ return len(self) > 0
44
+
45
+ def items(self):
46
+ return {item.get("command") or item.get("env"): item for item in self.commands}
47
+
48
+ def get_command(
49
+ self, command: Optional[str] = None, name: Optional[str] = None
50
+ ) -> Optional[CrontabDict]:
51
+ assert command or name, "Either command or name must be provided"
52
+
53
+ name_comment = "# pyinfra-name={0}".format(name)
54
+ for cmd in self.commands:
55
+ if cmd.get("command") == command:
56
+ return cmd
57
+ if cmd.get("comments") and name_comment in cmd["comments"]:
58
+ return cmd
59
+ return None
60
+
61
+ def get_env(self, env: str) -> Optional[CrontabDict]:
62
+ for cmd in self.commands:
63
+ if cmd.get("env") == env:
64
+ return cmd
65
+ return None
66
+
67
+ def get(self, item: str) -> Optional[CrontabDict]:
68
+ return self.get_command(command=item, name=item) or self.get_env(item)
69
+
70
+ def __getitem__(self, item) -> Optional[CrontabDict]:
71
+ return self.get(item)
72
+
73
+ @override
74
+ def __repr__(self):
75
+ return f"CrontabResult({self.commands})"
76
+
77
+ # noinspection PyMethodMayBeStatic
78
+ def format_item(self, item: CrontabDict):
79
+ lines = []
80
+ for comment in item.get("comments", []):
81
+ lines.append(comment)
82
+
83
+ if "env" in item:
84
+ lines.append(item["env"])
85
+ elif "special_time" in item:
86
+ lines.append(f"{item['special_time']} {item['command']}")
87
+ else:
88
+ lines.append(
89
+ f"{item['minute']} {item['hour']} "
90
+ f"{item['day_of_month']} {item['month']} {item['day_of_week']} "
91
+ f"{item['command']}"
92
+ )
93
+ return "\n".join(lines)
94
+
95
+ @override
96
+ def __str__(self):
97
+ return "\n".join(self.format_item(item) for item in self.commands)
98
+
99
+ def to_json(self):
100
+ return self.commands
101
+
102
+
103
+ _crontab_env_re = re.compile(r"^\s*([A-Z_]+)=(.*)$")
104
+
105
+
106
+ class Crontab(FactBase[CrontabFile]):
107
+ """
108
+ Returns a dictionary of CrontabFile.
109
+
110
+ .. code:: python
111
+
112
+ # CrontabFile.items()
113
+ {
114
+ "/path/to/command": {
115
+ "minute": "*",
116
+ "hour": "*",
117
+ "month": "*",
118
+ "day_of_month": "*",
119
+ "day_of_week": "*",
120
+ },
121
+ "echo another command": {
122
+ "special_time": "@daily",
123
+ },
124
+ }
125
+ # or CrontabFile.to_json()
126
+ [
127
+ {
128
+ "command": "/path/to/command",
129
+ "minute": "*",
130
+ "hour": "*",
131
+ "month": "*",
132
+ "day_of_month": "*",
133
+ "day_of_week": "*",
134
+ },
135
+ {
136
+ "command": "echo another command",
137
+ "special_time": "@daily",
138
+ }
139
+ ]
140
+ """
141
+
142
+ default = CrontabFile
143
+
144
+ @override
145
+ def requires_command(self, user=None) -> str:
146
+ return "crontab"
147
+
148
+ @override
149
+ def command(self, user=None):
150
+ if user:
151
+ return "crontab -l -u {0} || true".format(user)
152
+ return "crontab -l || true"
153
+
154
+ @override
155
+ def process(self, output):
156
+ crons = CrontabFile()
157
+ current_comments = []
158
+
159
+ for line in output:
160
+ line = line.strip()
161
+ if not line or line.startswith("#"):
162
+ current_comments.append(line)
163
+ continue
164
+
165
+ if line.startswith("@"):
166
+ special_time, command = line.split(None, 1)
167
+ item = CrontabDict(
168
+ command=command,
169
+ special_time=special_time,
170
+ comments=current_comments,
171
+ )
172
+ crons.add_item(item)
173
+
174
+ elif _crontab_env_re.match(line):
175
+ # handle environment variables
176
+ item = CrontabDict(
177
+ env=line,
178
+ comments=current_comments,
179
+ )
180
+ crons.add_item(item)
181
+ else:
182
+ minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
183
+ item = CrontabDict(
184
+ command=command,
185
+ minute=try_int(minute),
186
+ hour=try_int(hour),
187
+ month=try_int(month),
188
+ day_of_month=try_int(day_of_month),
189
+ day_of_week=try_int(day_of_week),
190
+ comments=current_comments,
191
+ )
192
+ crons.add_item(item)
193
+
194
+ current_comments = []
195
+ return crons
pyinfra/facts/deb.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shlex
5
+
6
+ from typing_extensions import override
7
+
8
+ from pyinfra.api import FactBase
9
+
10
+ from .util.packaging import parse_packages
11
+
12
+ DEB_PACKAGE_NAME_REGEX = r"[a-zA-Z0-9\+\-\.]+"
13
+ DEB_PACKAGE_VERSION_REGEX = r"[a-zA-Z0-9:~\.\-\+]+"
14
+
15
+
16
+ class DebArch(FactBase):
17
+ """
18
+ Returns the architecture string used in apt repository sources, eg ``amd64``.
19
+ """
20
+
21
+ @override
22
+ def command(self) -> str:
23
+ return "dpkg --print-architecture"
24
+
25
+ @override
26
+ def requires_command(self) -> str:
27
+ return "dpkg"
28
+
29
+
30
+ class DebPackages(FactBase):
31
+ """
32
+ Returns a dict of installed dpkg packages:
33
+
34
+ .. code:: python
35
+
36
+ {
37
+ "package_name": ["version"],
38
+ }
39
+ """
40
+
41
+ @override
42
+ def command(self) -> str:
43
+ return "dpkg -l"
44
+
45
+ @override
46
+ def requires_command(self) -> str:
47
+ return "dpkg"
48
+
49
+ default = dict
50
+
51
+ regex = r"^[i|h]i\s+({0}):?[a-zA-Z0-9]*\s+({1}).+$".format(
52
+ DEB_PACKAGE_NAME_REGEX,
53
+ DEB_PACKAGE_VERSION_REGEX,
54
+ )
55
+
56
+ @override
57
+ def process(self, output):
58
+ return parse_packages(self.regex, output)
59
+
60
+
61
+ class DebPackage(FactBase):
62
+ """
63
+ Returns information on a .deb archive or installed package.
64
+ """
65
+
66
+ _regexes = {
67
+ "name": r"^Package:\s+({0})$".format(DEB_PACKAGE_NAME_REGEX),
68
+ "version": r"^Version:\s+({0})$".format(DEB_PACKAGE_VERSION_REGEX),
69
+ }
70
+
71
+ @override
72
+ def requires_command(self, package) -> str:
73
+ return "dpkg"
74
+
75
+ @override
76
+ def command(self, package):
77
+ return "! test -e {0} && (dpkg -s {0} 2>/dev/null || true) || dpkg -I {0}".format(
78
+ shlex.quote(package)
79
+ )
80
+
81
+ @override
82
+ def process(self, output):
83
+ data = {}
84
+
85
+ for line in output:
86
+ line = line.strip()
87
+ for key, regex in self._regexes.items():
88
+ matches = re.match(regex, line)
89
+ if matches:
90
+ value = matches.group(1)
91
+ data[key] = value
92
+ break
93
+
94
+ return data
pyinfra/facts/dnf.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
5
+ from pyinfra.api import FactBase
6
+
7
+ from .util import make_cat_files_command
8
+ from .util.packaging import parse_yum_repositories
9
+
10
+
11
+ class DnfRepositories(FactBase):
12
+ """
13
+ Returns a list of installed dnf repositories:
14
+
15
+ .. code:: python
16
+
17
+ [
18
+ {
19
+ "repoid": "baseos",
20
+ "name": "AlmaLinux $releasever - BaseOS",
21
+ "mirrorlist": "https://mirrors.almalinux.org/mirrorlist/$releasever/baseos",
22
+ "enabled": "1",
23
+ "gpgcheck": "1",
24
+ "countme": "1",
25
+ "gpgkey": "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux-9",
26
+ "metadata_expire": "86400",
27
+ "enabled_metadata": "1"
28
+ },
29
+ ]
30
+ """
31
+
32
+ @override
33
+ def command(self) -> str:
34
+ return make_cat_files_command(
35
+ "/etc/dnf.conf",
36
+ "/etc/dnf.repos.d/*.repo",
37
+ "/etc/yum.repos.d/*.repo",
38
+ )
39
+
40
+ @override
41
+ def requires_command(self) -> str:
42
+ return "dnf"
43
+
44
+ default = list
45
+
46
+ @override
47
+ def process(self, output):
48
+ return parse_yum_repositories(output)
pyinfra/facts/docker.py CHANGED
@@ -1,78 +1,151 @@
1
+ """
2
+ Facts about Docker containers, volumes and networks. These facts give you information from the view
3
+ of the current inventory host. See the :doc:`../connectors/docker` to use Docker containers as
4
+ inventory directly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
1
9
  import json
2
10
 
11
+ from typing_extensions import override
12
+
3
13
  from pyinfra.api import FactBase
4
14
 
5
15
 
6
16
  class DockerFactBase(FactBase):
7
17
  abstract = True
8
18
 
19
+ docker_type: str
20
+
21
+ @override
22
+ def requires_command(self, *args, **kwargs) -> str:
23
+ return "docker"
24
+
25
+ @override
9
26
  def process(self, output):
10
- output = ''.join(output)
27
+ output = "".join(output)
11
28
  return json.loads(output)
12
29
 
13
30
 
14
31
  class DockerSystemInfo(DockerFactBase):
15
- '''
32
+ """
16
33
  Returns ``docker system info`` output in JSON format.
17
- '''
34
+ """
18
35
 
19
- command = 'docker system info --format="{{json .}}"'
36
+ @override
37
+ def command(self) -> str:
38
+ return 'docker system info --format="{{json .}}"'
20
39
 
21
40
 
22
41
  # All Docker objects
23
42
  #
24
43
 
44
+
25
45
  class DockerContainers(DockerFactBase):
26
- '''
46
+ """
27
47
  Returns ``docker inspect`` output for all Docker containers.
28
- '''
48
+ """
29
49
 
30
- command = 'docker container inspect `docker ps -qa`'
50
+ @override
51
+ def command(self) -> str:
52
+ return """
53
+ ids=$(docker ps -qa) && [ -n "$ids" ] && docker container inspect $ids || echo "[]"
54
+ """.strip()
31
55
 
32
56
 
33
57
  class DockerImages(DockerFactBase):
34
- '''
58
+ """
35
59
  Returns ``docker inspect`` output for all Docker images.
36
- '''
60
+ """
37
61
 
38
- command = 'docker image inspect `docker images -q`'
62
+ @override
63
+ def command(self) -> str:
64
+ return """
65
+ ids=$(docker images -q) && [ -n "$ids" ] && docker image inspect $ids || echo "[]"
66
+ """.strip()
39
67
 
40
68
 
41
69
  class DockerNetworks(DockerFactBase):
42
- '''
70
+ """
43
71
  Returns ``docker inspect`` output for all Docker networks.
44
- '''
72
+ """
73
+
74
+ @override
75
+ def command(self) -> str:
76
+ return "docker network inspect `docker network ls -q`"
77
+
78
+
79
+ class DockerVolumes(DockerFactBase):
80
+ """
81
+ Returns ``docker inspect`` output for all Docker volumes.
82
+ """
83
+
84
+ @override
85
+ def command(self) -> str:
86
+ return "docker volume inspect `docker volume ls -q`"
45
87
 
46
- command = 'docker network inspect `docker network ls -q`'
88
+
89
+ class DockerPlugins(DockerFactBase):
90
+ """
91
+ Returns ``docker plugin inspect`` output for all Docker plugins.
92
+ """
93
+
94
+ @override
95
+ def command(self) -> str:
96
+ return """
97
+ ids=$(docker plugin ls -q) && [ -n "$ids" ] && docker plugin inspect $ids || echo "[]"
98
+ """.strip()
47
99
 
48
100
 
49
101
  # Single Docker objects
50
102
  #
51
103
 
104
+
52
105
  class DockerSingleMixin(DockerFactBase):
106
+ @override
53
107
  def command(self, object_id):
54
- return 'docker {0} inspect {1}'.format(self.docker_type, object_id)
108
+ return "docker {0} inspect {1} 2>&- || true".format(
109
+ self.docker_type,
110
+ object_id,
111
+ )
55
112
 
56
113
 
57
114
  class DockerContainer(DockerSingleMixin):
58
- '''
115
+ """
59
116
  Returns ``docker inspect`` output for a single Docker container.
60
- '''
117
+ """
61
118
 
62
- docker_type = 'container'
119
+ docker_type = "container"
63
120
 
64
121
 
65
122
  class DockerImage(DockerSingleMixin):
66
- '''
123
+ """
67
124
  Returns ``docker inspect`` output for a single Docker image.
68
- '''
125
+ """
69
126
 
70
- docker_type = 'image'
127
+ docker_type = "image"
71
128
 
72
129
 
73
130
  class DockerNetwork(DockerSingleMixin):
74
- '''
131
+ """
75
132
  Returns ``docker inspect`` output for a single Docker network.
76
- '''
133
+ """
134
+
135
+ docker_type = "network"
136
+
137
+
138
+ class DockerVolume(DockerSingleMixin):
139
+ """
140
+ Returns ``docker inspect`` output for a single Docker container.
141
+ """
142
+
143
+ docker_type = "volume"
144
+
145
+
146
+ class DockerPlugin(DockerSingleMixin):
147
+ """
148
+ Returns ``docker plugin inspect`` output for a single Docker plugin.
149
+ """
77
150
 
78
- docker_type = 'network'
151
+ docker_type = "plugin"
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, TypedDict
4
+
5
+ from typing_extensions import override
6
+
7
+ from pyinfra.api import FactBase
8
+
9
+ BootEntry = Tuple[bool, str]
10
+ EFIBootMgrInfoDict = TypedDict(
11
+ "EFIBootMgrInfoDict",
12
+ {
13
+ "BootNext": Optional[int],
14
+ "BootCurrent": Optional[int],
15
+ "Timeout": Optional[int],
16
+ "BootOrder": Optional[List[int]],
17
+ "Entries": Dict[int, BootEntry],
18
+ },
19
+ )
20
+
21
+
22
+ class EFIBootMgr(FactBase[Optional[EFIBootMgrInfoDict]]):
23
+ """
24
+ Returns information about the UEFI boot variables:
25
+
26
+ .. code:: python
27
+
28
+ {
29
+ "BootNext": 6,
30
+ "BootCurrent": 6,
31
+ "Timeout": 0,
32
+ "BootOrder": [1,4,3],
33
+ "Entries": {
34
+ 1: (True, "myefi1"),
35
+ 2: (False, "myefi2.efi"),
36
+ 3: (True, "myefi3.efi"),
37
+ 4: (True, "grub2.efi"),
38
+ },
39
+ }
40
+ """
41
+
42
+ @override
43
+ def requires_command(self, *args: Any, **kwargs: Any) -> str:
44
+ return "efibootmgr"
45
+
46
+ @override
47
+ def command(self) -> str:
48
+ # FIXME: Use '|| true' to properly handle the case where
49
+ # 'efibootmgr' is run on a non-UEFI system
50
+ return "efibootmgr || true"
51
+
52
+ @override
53
+ def process(self, output: Iterable[str]) -> Optional[EFIBootMgrInfoDict]:
54
+ # This parsing code closely follows the printing code of efibootmgr
55
+ # at <https://github.com/rhboot/efibootmgr/blob/main/src/efibootmgr.c#L2020-L2048>
56
+
57
+ info: EFIBootMgrInfoDict = {
58
+ "BootNext": None,
59
+ "BootCurrent": None,
60
+ "Timeout": None,
61
+ "BootOrder": [],
62
+ "Entries": {},
63
+ }
64
+
65
+ output = iter(output)
66
+
67
+ line: Optional[str] = next(output, None)
68
+
69
+ if line is None:
70
+ # efibootmgr run on a non-UEFI system, likely printed
71
+ # "EFI variables are not supported on this system."
72
+ # to stderr
73
+ return None
74
+
75
+ # 1. Maybe have BootNext
76
+ if line and line.startswith("BootNext: "):
77
+ info["BootNext"] = int(line[len("BootNext: ") :], 16)
78
+ line = next(output, None)
79
+
80
+ # 2. Maybe have BootCurrent
81
+ if line and line.startswith("BootCurrent: "):
82
+ info["BootCurrent"] = int(line[len("BootCurrent: ") :], 16)
83
+ line = next(output, None)
84
+
85
+ # 3. Maybe have Timeout
86
+ if line and line.startswith("Timeout: "):
87
+ info["Timeout"] = int(line[len("Timeout: ") : -len(" seconds")])
88
+ line = next(output, None)
89
+
90
+ # 4. `show_order`
91
+ if line and line.startswith("BootOrder: "):
92
+ entries = line[len("BootOrder: ") :]
93
+ info["BootOrder"] = list(map(lambda x: int(x, 16), entries.split(",")))
94
+ line = next(output, None)
95
+
96
+ # 5. `show_vars`: The actual boot entries
97
+ while line is not None and line.startswith("Boot"):
98
+ number = int(line[4:8], 16)
99
+
100
+ # Entries marked with a * are active
101
+ active = line[8:9] == "*"
102
+
103
+ # TODO: Maybe split and parse (name vs. arguments ?), might require --verbose ?
104
+ entry = line[10:]
105
+ info["Entries"][number] = (active, entry)
106
+ line = next(output, None)
107
+
108
+ # 6. `show_mirror`
109
+ # Currently not implemented, since I haven't actually encountered this in the wild.
110
+ if line is not None:
111
+ raise ValueError(f"Unexpected line '{line}' while parsing")
112
+
113
+ return info