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