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
@@ -0,0 +1,488 @@
1
+ """
2
+ Manage apt packages and repositories.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from datetime import timedelta
8
+ from urllib.parse import urlparse
9
+
10
+ from pyinfra import host
11
+ from pyinfra.api import OperationError, operation
12
+ from pyinfra.facts.apt import (
13
+ AptKeys,
14
+ AptSources,
15
+ SimulateOperationWillChange,
16
+ noninteractive_apt,
17
+ parse_apt_repo,
18
+ )
19
+ from pyinfra.facts.deb import DebPackage, DebPackages
20
+ from pyinfra.facts.files import File
21
+ from pyinfra.facts.gpg import GpgKey
22
+ from pyinfra.facts.server import Date
23
+
24
+ from . import files
25
+ from .util.packaging import ensure_packages
26
+
27
+ APT_UPDATE_FILENAME = "/var/lib/apt/periodic/update-success-stamp"
28
+
29
+
30
+ def _simulate_then_perform(command: str):
31
+ changes = host.get_fact(SimulateOperationWillChange, command)
32
+
33
+ if not changes:
34
+ # Simulating apt-get command failed, so the actual
35
+ # operation will probably fail too:
36
+ yield noninteractive_apt(command)
37
+ elif (
38
+ changes["upgraded"] == 0
39
+ and changes["newly_installed"] == 0
40
+ and changes["removed"] == 0
41
+ and changes["not_upgraded"] == 0
42
+ ):
43
+ host.noop(f"{command} skipped, no changes would be performed")
44
+ else:
45
+ yield noninteractive_apt(command)
46
+
47
+
48
+ @operation()
49
+ def key(src: str | None = None, keyserver: str | None = None, keyid: str | list[str] | None = None):
50
+ """
51
+ Add apt gpg keys with ``apt-key``.
52
+
53
+ + src: filename or URL
54
+ + keyserver: URL of keyserver to fetch key from
55
+ + keyid: key ID or list of key IDs when using keyserver
56
+
57
+ keyserver/id:
58
+ These must be provided together.
59
+
60
+ .. warning::
61
+ ``apt-key`` is deprecated in Debian, it is recommended NOT to use this
62
+ operation and instead follow the instructions here:
63
+
64
+ https://wiki.debian.org/DebianRepository/UseThirdParty
65
+
66
+ **Examples:**
67
+
68
+ .. code:: python
69
+
70
+ # Note: If using URL, wget is assumed to be installed.
71
+ apt.key(
72
+ name="Add the Docker apt gpg key",
73
+ src="https://download.docker.com/linux/ubuntu/gpg",
74
+ )
75
+
76
+ apt.key(
77
+ name="Install VirtualBox key",
78
+ src="https://www.virtualbox.org/download/oracle_vbox_2016.asc",
79
+ )
80
+ """
81
+
82
+ existing_keys = host.get_fact(AptKeys)
83
+
84
+ if src:
85
+ key_data = host.get_fact(GpgKey, src=src)
86
+ if key_data:
87
+ keyid = list(key_data.keys())
88
+
89
+ if not keyid or not all(kid in existing_keys for kid in keyid):
90
+ # If URL, wget the key to stdout and pipe into apt-key, because the "adv"
91
+ # apt-key passes to gpg which doesn't always support https!
92
+ if urlparse(src).scheme:
93
+ yield "(wget -O - {0} || curl -sSLf {0}) | apt-key add -".format(src)
94
+ else:
95
+ yield "apt-key add {0}".format(src)
96
+ else:
97
+ host.noop("All keys from {0} are already available in the apt keychain".format(src))
98
+
99
+ if keyserver:
100
+ if not keyid:
101
+ raise OperationError("`keyid` must be provided with `keyserver`")
102
+
103
+ if isinstance(keyid, str):
104
+ keyid = [keyid]
105
+
106
+ needed_keys = sorted(set(keyid) - set(existing_keys.keys()))
107
+ if needed_keys:
108
+ yield "apt-key adv --keyserver {0} --recv-keys {1}".format(
109
+ keyserver,
110
+ " ".join(needed_keys),
111
+ )
112
+ else:
113
+ host.noop(
114
+ "Keys {0} are already available in the apt keychain".format(
115
+ ", ".join(keyid),
116
+ ),
117
+ )
118
+
119
+
120
+ @operation()
121
+ def repo(src: str, present=True, filename: str | None = None):
122
+ """
123
+ Add/remove apt repositories.
124
+
125
+ + src: apt source string eg ``deb http://X hardy main``
126
+ + present: whether the repo should exist on the system
127
+ + filename: optional filename to use ``/etc/apt/sources.list.d/<filename>.list``. By
128
+ default uses ``/etc/apt/sources.list``.
129
+
130
+ **Example:**
131
+
132
+ .. code:: python
133
+
134
+ apt.repo(
135
+ name="Install VirtualBox repo",
136
+ src="deb https://download.virtualbox.org/virtualbox/debian bionic contrib",
137
+ )
138
+ """
139
+
140
+ # Get the target .list file to manage
141
+ if filename:
142
+ filename = "/etc/apt/sources.list.d/{0}.list".format(filename)
143
+ else:
144
+ filename = "/etc/apt/sources.list"
145
+
146
+ # Work out if the repo exists already
147
+ apt_sources = host.get_fact(AptSources)
148
+
149
+ is_present = False
150
+ repo = parse_apt_repo(src)
151
+ if repo and repo in apt_sources:
152
+ is_present = True
153
+
154
+ # Doesn't exist and we want it
155
+ if not is_present and present:
156
+ yield from files.line._inner(
157
+ path=filename,
158
+ line=src,
159
+ escape_regex_characters=True,
160
+ )
161
+
162
+ # Exists and we don't want it
163
+ elif is_present and not present:
164
+ yield from files.line._inner(
165
+ path=filename,
166
+ line=src,
167
+ present=False,
168
+ escape_regex_characters=True,
169
+ )
170
+ else:
171
+ host.noop(
172
+ 'apt repo "{0}" {1}'.format(
173
+ src,
174
+ "exists" if present else "does not exist",
175
+ ),
176
+ )
177
+
178
+
179
+ @operation(is_idempotent=False)
180
+ def ppa(src: str, present=True):
181
+ """
182
+ Add/remove Ubuntu ppa repositories.
183
+
184
+ + src: the PPA name (full ppa:user/repo format)
185
+ + present: whether it should exist
186
+
187
+ Note:
188
+ requires ``apt-add-repository`` on the remote host
189
+
190
+ **Example:**
191
+
192
+ .. code:: python
193
+
194
+ # Note: Assumes software-properties-common is installed.
195
+ apt.ppa(
196
+ name="Add the Bitcoin ppa",
197
+ src="ppa:bitcoin/bitcoin",
198
+ )
199
+
200
+ """
201
+
202
+ if present:
203
+ yield 'apt-add-repository -y "{0}"'.format(src)
204
+
205
+ if not present:
206
+ yield 'apt-add-repository -y --remove "{0}"'.format(src)
207
+
208
+
209
+ @operation()
210
+ def deb(src: str, present=True, force=False):
211
+ """
212
+ Add/remove ``.deb`` file packages.
213
+
214
+ + src: filename or URL of the ``.deb`` file
215
+ + present: whether or not the package should exist on the system
216
+ + force: whether to force the package install by passing `--force-yes` to apt
217
+
218
+ Note:
219
+ When installing, ``apt-get install -f`` will be run to install any unmet
220
+ dependencies.
221
+
222
+ URL sources with ``present=False``:
223
+ If the ``.deb`` file isn't downloaded, pyinfra can't remove any existing
224
+ package as the file won't exist until mid-deploy.
225
+
226
+ **Example:**
227
+
228
+ .. code:: python
229
+
230
+ # Note: Assumes wget is installed.
231
+ apt.deb(
232
+ name="Install Chrome via deb",
233
+ src="https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb",
234
+ )
235
+ """
236
+
237
+ original_src = src
238
+
239
+ # If source is a url
240
+ if urlparse(src).scheme:
241
+ # Generate a temp filename
242
+ temp_filename = host.get_temp_filename(src)
243
+
244
+ # Ensure it's downloaded
245
+ yield from files.download._inner(src=src, dest=temp_filename)
246
+
247
+ # Override the source with the downloaded file
248
+ src = temp_filename
249
+
250
+ # Check for file .deb information (if file is present)
251
+ info = host.get_fact(DebPackage, package=src)
252
+ current_packages = host.get_fact(DebPackages)
253
+
254
+ exists = False
255
+
256
+ # We have deb info! Check against installed packages
257
+ if info and info.get("version") in current_packages.get(info.get("name"), {}):
258
+ exists = True
259
+
260
+ # Package does not exist and we want?
261
+ if present:
262
+ if not exists:
263
+ # Install .deb file - ignoring failure (on unmet dependencies)
264
+ yield "dpkg --force-confdef --force-confold -i {0} 2> /dev/null || true".format(src)
265
+ # Attempt to install any missing dependencies
266
+ yield "{0} -f".format(noninteractive_apt("install", force=force))
267
+ # Now reinstall, and critically configure, the package - if there are still
268
+ # missing deps, now we error
269
+ yield "dpkg --force-confdef --force-confold -i {0}".format(src)
270
+ else:
271
+ host.noop("deb {0} is installed".format(original_src))
272
+
273
+ # Package exists but we don't want?
274
+ if not present:
275
+ if exists:
276
+ yield "{0} {1}".format(
277
+ noninteractive_apt("remove", force=force),
278
+ info["name"],
279
+ )
280
+ else:
281
+ host.noop("deb {0} is not installed".format(original_src))
282
+
283
+
284
+ @operation(
285
+ is_idempotent=False,
286
+ idempotent_notice=(
287
+ "This operation will always execute commands "
288
+ "unless the ``cache_time`` argument is provided."
289
+ ),
290
+ )
291
+ def update(cache_time: int | None = None):
292
+ """
293
+ Updates apt repositories.
294
+
295
+ + cache_time: cache updates for this many seconds
296
+
297
+ **Example:**
298
+
299
+ .. code:: python
300
+
301
+ apt.update(
302
+ name="Update apt repositories",
303
+ cache_time=3600,
304
+ )
305
+ """
306
+
307
+ # If cache_time check when apt was last updated, prevent updates if within time
308
+ if cache_time:
309
+ # Ubuntu provides this handy file
310
+ cache_info = host.get_fact(File, path=APT_UPDATE_FILENAME)
311
+
312
+ # Time on files is not tz-aware, and will be the same tz as the server's time,
313
+ # so we can safely remove the tzinfo from the Date fact before comparison.
314
+ host_cache_time = host.get_fact(Date).replace(tzinfo=None) - timedelta(seconds=cache_time)
315
+ if cache_info and cache_info["mtime"] and cache_info["mtime"] > host_cache_time:
316
+ host.noop("apt is already up to date")
317
+ return
318
+
319
+ yield "apt-get update"
320
+
321
+ # Some apt systems (Debian) have the /var/lib/apt/periodic directory, but
322
+ # don't bother touching anything in there - so pyinfra does it, enabling
323
+ # cache_time to work.
324
+ if cache_time:
325
+ yield "touch {0}".format(APT_UPDATE_FILENAME)
326
+
327
+
328
+ _update = update # noqa: E305
329
+
330
+
331
+ @operation()
332
+ def upgrade(auto_remove: bool = False):
333
+ """
334
+ Upgrades all apt packages.
335
+
336
+ + auto_remove: removes transitive dependencies that are no longer needed.
337
+
338
+ **Example:**
339
+
340
+ .. code:: python
341
+
342
+ # Upgrade all packages
343
+ apt.upgrade(
344
+ name="Upgrade apt packages",
345
+ )
346
+
347
+ # Upgrade all packages and remove unneeded transitive dependencies
348
+ apt.upgrade(
349
+ name="Upgrade apt packages and remove unneeded dependencies",
350
+ auto_remove=True
351
+ )
352
+ """
353
+
354
+ command = ["upgrade"]
355
+
356
+ if auto_remove:
357
+ command.append("--autoremove")
358
+
359
+ yield from _simulate_then_perform(" ".join(command))
360
+
361
+
362
+ _upgrade = upgrade # noqa: E305 (for use below where update is a kwarg)
363
+
364
+
365
+ @operation()
366
+ def dist_upgrade(auto_remove: bool = False):
367
+ """
368
+ Updates all apt packages, employing dist-upgrade.
369
+
370
+ + auto_remove: removes transitive dependencies that are no longer needed.
371
+
372
+ **Example:**
373
+
374
+ .. code:: python
375
+
376
+ apt.dist_upgrade(
377
+ name="Upgrade apt packages using dist-upgrade",
378
+ )
379
+ """
380
+
381
+ command = ["dist-upgrade"]
382
+
383
+ if auto_remove:
384
+ command.append("--autoremove")
385
+
386
+ yield from _simulate_then_perform(" ".join(command))
387
+
388
+
389
+ @operation()
390
+ def packages(
391
+ packages: str | list[str] | None = None,
392
+ present=True,
393
+ latest=False,
394
+ update=False,
395
+ cache_time: int | None = None,
396
+ upgrade=False,
397
+ force=False,
398
+ no_recommends=False,
399
+ allow_downgrades=False,
400
+ extra_install_args: str | None = None,
401
+ extra_uninstall_args: str | None = None,
402
+ ):
403
+ """
404
+ Install/remove/update packages & update apt.
405
+
406
+ + packages: list of packages to ensure
407
+ + present: whether the packages should be installed
408
+ + latest: whether to upgrade packages without a specified version
409
+ + update: run ``apt update`` before installing packages
410
+ + cache_time: when used with ``update``, cache for this many seconds
411
+ + upgrade: run ``apt upgrade`` before installing packages
412
+ + force: whether to force package installs by passing `--force-yes` to apt
413
+ + no_recommends: don't install recommended packages
414
+ + allow_downgrades: allow downgrading packages with version (--allow-downgrades)
415
+ + extra_install_args: additional arguments to the apt install command
416
+ + extra_uninstall_args: additional arguments to the apt uninstall command
417
+
418
+ Versions:
419
+ Package versions can be pinned like apt: ``<pkg>=<version>``
420
+
421
+ Cache time:
422
+ When ``cache_time`` is set the ``/var/lib/apt/periodic/update-success-stamp`` file
423
+ is touched upon successful update. Some distros already do this (Ubuntu), but others
424
+ simply leave the periodic directory empty (Debian).
425
+
426
+ **Examples:**
427
+
428
+ .. code:: python
429
+
430
+ # Update package list and install packages
431
+ apt.packages(
432
+ name="Install Asterisk and Vim",
433
+ packages=["asterisk", "vim"],
434
+ update=True,
435
+ )
436
+
437
+ # Install the latest versions of packages (always check)
438
+ apt.packages(
439
+ name="Install latest Vim",
440
+ packages=["vim"],
441
+ latest=True,
442
+ )
443
+
444
+ # Note: host.get_fact(OsVersion) is the same as `uname -r` (ex: '4.15.0-72-generic')
445
+ apt.packages(
446
+ name="Install kernel headers",
447
+ packages=[f"linux-headers-{host.get_fact(OsVersion)}"],
448
+ update=True,
449
+ )
450
+ """
451
+
452
+ if update:
453
+ yield from _update._inner(cache_time=cache_time)
454
+
455
+ if upgrade:
456
+ yield from _upgrade._inner()
457
+
458
+ install_command_args = ["install"]
459
+ if no_recommends is True:
460
+ install_command_args.append("--no-install-recommends")
461
+ if allow_downgrades:
462
+ install_command_args.append("--allow-downgrades")
463
+
464
+ upgrade_command = " ".join(install_command_args)
465
+
466
+ if extra_install_args:
467
+ install_command_args.append(extra_install_args)
468
+
469
+ install_command = " ".join(install_command_args)
470
+
471
+ uninstall_command_args = ["remove"]
472
+ if extra_uninstall_args:
473
+ uninstall_command_args.append(extra_uninstall_args)
474
+
475
+ uninstall_command = " ".join(uninstall_command_args)
476
+
477
+ # Compare/ensure packages are present/not
478
+ yield from ensure_packages(
479
+ host,
480
+ packages,
481
+ host.get_fact(DebPackages),
482
+ present,
483
+ install_command=noninteractive_apt(install_command, force=force),
484
+ uninstall_command=noninteractive_apt(uninstall_command, force=force),
485
+ upgrade_command=noninteractive_apt(upgrade_command, force=force),
486
+ version_join="=",
487
+ latest=latest,
488
+ )
@@ -0,0 +1,231 @@
1
+ """
2
+ Manage brew packages on mac/OSX. See https://brew.sh/
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import urllib.parse
8
+
9
+ from pyinfra import host
10
+ from pyinfra.api import operation
11
+ from pyinfra.facts.brew import BrewCasks, BrewPackages, BrewTaps, BrewVersion, new_cask_cli
12
+
13
+ from .util.packaging import ensure_packages
14
+
15
+
16
+ @operation(is_idempotent=False)
17
+ def update():
18
+ """
19
+ Updates brew repositories.
20
+ """
21
+
22
+ yield "brew update"
23
+
24
+
25
+ _update = update # noqa: E305
26
+
27
+
28
+ @operation(is_idempotent=False)
29
+ def upgrade():
30
+ """
31
+ Upgrades all brew packages.
32
+ """
33
+
34
+ yield "brew upgrade"
35
+
36
+
37
+ _upgrade = upgrade # noqa: E305
38
+
39
+
40
+ @operation()
41
+ def packages(
42
+ packages: str | list[str] | None = None,
43
+ present=True,
44
+ latest=False,
45
+ update=False,
46
+ upgrade=False,
47
+ ):
48
+ """
49
+ Add/remove/update brew packages.
50
+
51
+ + packages: list of packages to ensure
52
+ + present: whether the packages should be installed
53
+ + latest: whether to upgrade packages without a specified version
54
+ + update: run ``brew update`` before installing packages
55
+ + upgrade: run ``brew upgrade`` before installing packages
56
+
57
+ Versions:
58
+ Package versions can be pinned like brew: ``<pkg>@<version>``.
59
+
60
+ **Examples:**
61
+
62
+ .. code:: python
63
+
64
+ # Update package list and install packages
65
+ brew.packages(
66
+ name='Install Vim and vimpager',
67
+ packages=["vimpager", "vim"],
68
+ update=True,
69
+ )
70
+
71
+ # Install the latest versions of packages (always check)
72
+ brew.packages(
73
+ name="Install latest Vim",
74
+ packages=["vim"],
75
+ latest=True,
76
+ )
77
+ """
78
+
79
+ if update:
80
+ yield from _update._inner()
81
+
82
+ if upgrade:
83
+ yield from _upgrade._inner()
84
+
85
+ yield from ensure_packages(
86
+ host,
87
+ packages,
88
+ host.get_fact(BrewPackages),
89
+ present,
90
+ install_command="brew install",
91
+ uninstall_command="brew uninstall",
92
+ upgrade_command="brew upgrade",
93
+ version_join="@",
94
+ latest=latest,
95
+ )
96
+
97
+
98
+ def cask_args():
99
+ return ("", " --cask") if new_cask_cli(host.get_fact(BrewVersion)) else ("cask ", "")
100
+
101
+
102
+ @operation(is_idempotent=False)
103
+ def cask_upgrade():
104
+ """
105
+ Upgrades all brew casks.
106
+ """
107
+
108
+ yield "brew %supgrade%s" % cask_args()
109
+
110
+
111
+ @operation()
112
+ def casks(
113
+ casks: str | list[str] | None = None,
114
+ present=True,
115
+ latest=False,
116
+ upgrade=False,
117
+ ):
118
+ """
119
+ Add/remove/update brew casks.
120
+
121
+ + casks: list of casks to ensure
122
+ + present: whether the casks should be installed
123
+ + latest: whether to upgrade casks without a specified version
124
+ + upgrade: run brew cask upgrade before installing casks
125
+
126
+ Versions:
127
+ Cask versions can be pinned like brew: ``<pkg>@<version>``.
128
+
129
+ **Example:**
130
+
131
+ .. code:: python
132
+
133
+ brew.casks(
134
+ name='Upgrade and install the latest cask',
135
+ casks=["godot"],
136
+ upgrade=True,
137
+ latest=True,
138
+ )
139
+
140
+ """
141
+
142
+ if upgrade:
143
+ yield from cask_upgrade._inner()
144
+
145
+ args = cask_args()
146
+
147
+ yield from ensure_packages(
148
+ host,
149
+ casks,
150
+ host.get_fact(BrewCasks),
151
+ present,
152
+ install_command="brew %sinstall%s" % args,
153
+ uninstall_command="brew %suninstall%s" % args,
154
+ upgrade_command="brew %supgrade%s" % args,
155
+ version_join="@",
156
+ latest=latest,
157
+ )
158
+
159
+
160
+ @operation()
161
+ def tap(src: str | None = None, present=True, url: str | None = None):
162
+ """
163
+ Add/remove brew taps.
164
+
165
+ + src: the name of the tap
166
+ + present: whether this tap should be present or not
167
+ + url: the url of the tap. See https://docs.brew.sh/Taps
168
+
169
+ **Examples:**
170
+
171
+ .. code:: python
172
+
173
+ brew.tap(
174
+ name="Add a brew tap",
175
+ src="includeos/includeos",
176
+ )
177
+
178
+ # Just url is equivalent to
179
+ # `brew tap kptdev/kpt https://github.com/kptdev/kpt`
180
+ brew.tap(
181
+ url="https://github.com/kptdev/kpt",
182
+ )
183
+
184
+ # src and url is equivalent to
185
+ # `brew tap example/project https://github.example.com/project`
186
+ brew.tap(
187
+ src="example/project",
188
+ url="https://github.example.com/project",
189
+ )
190
+
191
+ # Multiple taps
192
+ for tap in ["includeos/includeos", "ktr0731/evans"]:
193
+ brew.tap(
194
+ name={f"Add brew tap {tap}"},
195
+ src=tap,
196
+ )
197
+
198
+ """
199
+
200
+ if not (src or url):
201
+ host.noop("no tap was specified")
202
+ return
203
+
204
+ src = src or str(urllib.parse.urlparse(url).path).strip("/")
205
+
206
+ if len(src.split("/")) != 2:
207
+ host.noop("src '{0}' doesn't have two components.".format(src))
208
+ return
209
+
210
+ taps = host.get_fact(BrewTaps)
211
+ already_tapped = src in taps
212
+
213
+ if present and already_tapped:
214
+ host.noop("tap {0} already exists".format(src))
215
+ return
216
+
217
+ if already_tapped:
218
+ yield "brew untap {0}".format(src)
219
+ return
220
+
221
+ if not present:
222
+ host.noop("tap {0} does not exist".format(src))
223
+ return
224
+
225
+ cmd = "brew tap {0}".format(src)
226
+
227
+ if url is not None:
228
+ cmd = " ".join([cmd, url])
229
+
230
+ yield cmd
231
+ return