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,59 @@
1
+ """
2
+ Manage BSD init services (``/etc/rc.d``, ``/usr/local/etc/rc.d``).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyinfra import host
8
+ from pyinfra.api import operation
9
+ from pyinfra.facts.bsdinit import RcdStatus
10
+ from pyinfra.facts.server import Os
11
+
12
+ from . import files
13
+ from .util.service import handle_service_control
14
+
15
+
16
+ @operation()
17
+ def service(
18
+ service: str,
19
+ running=True,
20
+ restarted=False,
21
+ reloaded=False,
22
+ command: str | None = None,
23
+ enabled: bool | None = None,
24
+ ):
25
+ """
26
+ Manage the state of BSD init services.
27
+
28
+ + service: name of the service to manage
29
+ + running: whether the service should be running
30
+ + restarted: whether the service should be restarted
31
+ + reloaded: whether the service should be reloaded
32
+ + command: custom command to pass like: ``/etc/rc.d/<service> <command>``
33
+ + enabled: whether this service should be enabled/disabled on boot
34
+ """
35
+
36
+ status_argument = "status"
37
+ if host.get_fact(Os) == "OpenBSD":
38
+ status_argument = "check"
39
+
40
+ yield from handle_service_control(
41
+ host,
42
+ service,
43
+ host.get_fact(RcdStatus),
44
+ "test -e /etc/rc.d/{0} && /etc/rc.d/{0} {1} || /usr/local/etc/rc.d/{0} {1}",
45
+ running,
46
+ restarted,
47
+ reloaded,
48
+ command,
49
+ status_argument=status_argument,
50
+ )
51
+
52
+ # BSD init is simple, just add/remove <service>_enabled="YES"
53
+ if isinstance(enabled, bool):
54
+ yield from files.line._inner(
55
+ path="/etc/rc.conf.local",
56
+ line="^{0}_enable=".format(service),
57
+ replace='{0}_enable="YES"'.format(service),
58
+ present=enabled,
59
+ )
@@ -0,0 +1,45 @@
1
+ """
2
+ Manage cargo (aka Rust) packages.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyinfra import host
8
+ from pyinfra.api import operation
9
+ from pyinfra.facts.cargo import CargoPackages
10
+
11
+ from .util.packaging import ensure_packages
12
+
13
+
14
+ @operation()
15
+ def packages(packages: str | list[str] | None = None, present=True, latest=False):
16
+ """
17
+ Install/remove/update cargo packages.
18
+
19
+ + packages: list of packages to ensure
20
+ + present: whether the packages should be present
21
+ + latest: whether to upgrade packages without a specified version
22
+
23
+ Versions:
24
+ Package versions can be pinned like cargo: ``<pkg>@<version>``.
25
+ """
26
+
27
+ current_packages = host.get_fact(CargoPackages)
28
+
29
+ install_command = "cargo install"
30
+
31
+ uninstall_command = "cargo uninstall"
32
+
33
+ upgrade_command = "cargo install"
34
+
35
+ yield from ensure_packages(
36
+ host,
37
+ packages,
38
+ current_packages,
39
+ present,
40
+ install_command=install_command,
41
+ uninstall_command=uninstall_command,
42
+ upgrade_command=upgrade_command,
43
+ version_join="@",
44
+ latest=latest,
45
+ )
@@ -0,0 +1,61 @@
1
+ """
2
+ Manage ``choco`` (Chocolatey) packages (https://chocolatey.org).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyinfra import host
8
+ from pyinfra.api import operation
9
+ from pyinfra.facts.choco import ChocoPackages
10
+
11
+ from .util.packaging import ensure_packages
12
+
13
+
14
+ @operation()
15
+ def packages(packages: str | list[str] | None = None, present=True, latest=False):
16
+ """
17
+ Add/remove/update ``choco`` packages.
18
+
19
+ + packages: list of packages to ensure
20
+ + present: whether the packages should be installed
21
+ + latest: whether to upgrade packages without a specified version
22
+
23
+ Versions:
24
+ Package versions can be pinned like gem: ``<pkg>:<version>``.
25
+
26
+ **Example:**
27
+
28
+ .. code:: python
29
+
30
+ # Note: Assumes that 'choco' is installed and
31
+ # user has Administrator permission.
32
+ choco.packages(
33
+ name="Install Notepad++",
34
+ packages=["notepadplusplus"],
35
+ )
36
+ """
37
+
38
+ yield from ensure_packages(
39
+ host,
40
+ packages,
41
+ host.get_fact(ChocoPackages),
42
+ present,
43
+ install_command="choco install -y",
44
+ uninstall_command="choco uninstall -y -x",
45
+ upgrade_command="choco update -y",
46
+ version_join=":",
47
+ latest=latest,
48
+ )
49
+
50
+
51
+ @operation(is_idempotent=False)
52
+ def install():
53
+ """
54
+ Install ``choco`` (Chocolatey).
55
+ """
56
+
57
+ yield (
58
+ "Set-ExecutionPolicy Bypass -Scope Process -Force ;"
59
+ "iex ((New-Object System.Net.WebClient).DownloadString"
60
+ '("https://chocolatey.org/install.ps1"))'
61
+ ) # noqa
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+
5
+ from pyinfra import logger
6
+ from pyinfra.api.command import StringCommand
7
+ from pyinfra.api.operation import operation
8
+ from pyinfra.api.util import try_int
9
+ from pyinfra.context import host
10
+ from pyinfra.facts.crontab import Crontab, CrontabFile
11
+ from pyinfra.operations.util.files import sed_delete, sed_replace
12
+
13
+
14
+ @operation()
15
+ def crontab(
16
+ command: str,
17
+ present=True,
18
+ user: str | None = None,
19
+ cron_name: str | None = None,
20
+ minute="*",
21
+ hour="*",
22
+ month="*",
23
+ day_of_week="*",
24
+ day_of_month="*",
25
+ special_time: str | None = None,
26
+ interpolate_variables=False,
27
+ ):
28
+ """
29
+ Add/remove/update crontab entries.
30
+
31
+ + command: the command for the cron
32
+ + present: whether this cron command should exist
33
+ + user: the user whose crontab to manage
34
+ + cron_name: name the cronjob so future changes to the command will overwrite
35
+ + modify_cron_name: modify the cron name
36
+ + minute: which minutes to execute the cron
37
+ + hour: which hours to execute the cron
38
+ + month: which months to execute the cron
39
+ + day_of_week: which day of the week to execute the cron
40
+ + day_of_month: which day of the month to execute the cron
41
+ + special_time: cron "nickname" time (@reboot, @daily, etc), overrides others
42
+ + interpolate_variables: whether to interpolate variables in ``command``
43
+
44
+ Cron commands:
45
+ Unless ``name`` is specified the command is used to identify crontab entries.
46
+ This means commands must be unique within a given users crontab. If you require
47
+ multiple identical commands, provide a different name argument for each.
48
+
49
+ Special times:
50
+ When provided, ``special_time`` will be used instead of any values passed in
51
+ for ``minute``/``hour``/``month``/``day_of_week``/``day_of_month``.
52
+
53
+ **Example:**
54
+
55
+ .. code:: python
56
+
57
+ # simple example for a crontab
58
+ crontab.crontab(
59
+ name="Backup /etc weekly",
60
+ command="/bin/tar cf /tmp/etc_bup.tar /etc",
61
+ name="backup_etc",
62
+ day_of_week=0,
63
+ hour=1,
64
+ minute=0,
65
+ )
66
+ """
67
+
68
+ def comma_sep(value):
69
+ if isinstance(value, (list, tuple)):
70
+ return ",".join("{0}".format(v) for v in value)
71
+ return value
72
+
73
+ minute = comma_sep(minute)
74
+ hour = comma_sep(hour)
75
+ month = comma_sep(month)
76
+ day_of_week = comma_sep(day_of_week)
77
+ day_of_month = comma_sep(day_of_month)
78
+
79
+ ctb0: CrontabFile | dict = host.get_fact(Crontab, user=user)
80
+ # facts from test are in dict
81
+ if isinstance(ctb0, dict):
82
+ ctb = CrontabFile(ctb0)
83
+ else:
84
+ ctb = ctb0
85
+ name_comment = "# pyinfra-name={0}".format(cron_name)
86
+
87
+ existing_crontab = ctb.get_command(command=command, name=cron_name)
88
+ existing_crontab_command = existing_crontab["command"] if existing_crontab else command
89
+ existing_crontab_match = existing_crontab["command"] if existing_crontab else command
90
+
91
+ exists = existing_crontab is not None
92
+ exists_name = existing_crontab is not None and name_comment in existing_crontab.get(
93
+ "comments", ""
94
+ )
95
+
96
+ edit_commands: list[str | StringCommand] = []
97
+ temp_filename = host.get_temp_filename()
98
+
99
+ if special_time:
100
+ new_crontab_line = "{0} {1}".format(special_time, command)
101
+ else:
102
+ new_crontab_line = "{minute} {hour} {day_of_month} {month} {day_of_week} {command}".format(
103
+ minute=minute,
104
+ hour=hour,
105
+ day_of_month=day_of_month,
106
+ month=month,
107
+ day_of_week=day_of_week,
108
+ command=command,
109
+ )
110
+
111
+ existing_crontab_match = ".*{0}.*".format(existing_crontab_match)
112
+
113
+ # Don't want the cron and it does exist? Remove the line
114
+ if not present and exists:
115
+ edit_commands.append(
116
+ sed_delete(
117
+ temp_filename,
118
+ existing_crontab_match,
119
+ "",
120
+ interpolate_variables=interpolate_variables,
121
+ ),
122
+ )
123
+
124
+ # Want the cron but it doesn't exist? Append the line
125
+ elif present and not exists:
126
+ logger.debug(f"present: {present}, exists: {exists}")
127
+ if ctb: # append a blank line if cron entries already exist
128
+ edit_commands.append("echo '' >> {0}".format(temp_filename))
129
+ if cron_name:
130
+ edit_commands.append(
131
+ "echo {0} >> {1}".format(
132
+ shlex.quote(name_comment),
133
+ temp_filename,
134
+ ),
135
+ )
136
+
137
+ edit_commands.append(
138
+ "echo {0} >> {1}".format(
139
+ shlex.quote(new_crontab_line),
140
+ temp_filename,
141
+ ),
142
+ )
143
+
144
+ # We have the cron and it exists, do it's details? If not, replace the line
145
+ elif present and exists:
146
+ assert existing_crontab is not None
147
+ if any(
148
+ (
149
+ exists_name != (cron_name is not None),
150
+ special_time != existing_crontab.get("special_time"),
151
+ try_int(minute) != existing_crontab.get("minute"),
152
+ try_int(hour) != existing_crontab.get("hour"),
153
+ try_int(month) != existing_crontab.get("month"),
154
+ try_int(day_of_week) != existing_crontab.get("day_of_week"),
155
+ try_int(day_of_month) != existing_crontab.get("day_of_month"),
156
+ existing_crontab_command != command,
157
+ ),
158
+ ):
159
+ if not exists_name and cron_name:
160
+ new_crontab_line = f"{name_comment}\\n{new_crontab_line}"
161
+ edit_commands.append(
162
+ sed_replace(
163
+ temp_filename,
164
+ existing_crontab_match,
165
+ new_crontab_line,
166
+ interpolate_variables=interpolate_variables,
167
+ ),
168
+ )
169
+
170
+ if edit_commands:
171
+ crontab_args = []
172
+ if user:
173
+ crontab_args.append("-u {0}".format(user))
174
+
175
+ # List the crontab into a temporary file if it exists
176
+ if ctb:
177
+ yield "crontab -l {0} > {1}".format(" ".join(crontab_args), temp_filename)
178
+
179
+ # Now yield any edits
180
+ for edit_command in edit_commands:
181
+ yield edit_command
182
+
183
+ # Finally, use the tempfile to write a new crontab
184
+ yield "crontab {0} {1}".format(" ".join(crontab_args), temp_filename)
185
+ else:
186
+ host.noop(
187
+ "crontab {0} {1}".format(
188
+ command,
189
+ "exists" if present else "does not exist",
190
+ ),
191
+ )
@@ -0,0 +1,210 @@
1
+ """
2
+ Manage dnf packages and repositories. Note that dnf package names are case-sensitive.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pyinfra import host, state
8
+ from pyinfra.api import operation
9
+ from pyinfra.facts.rpm import RpmPackageProvides, RpmPackages
10
+
11
+ from .util.packaging import ensure_packages, ensure_rpm, ensure_yum_repo
12
+
13
+
14
+ @operation(is_idempotent=False)
15
+ def key(src: str):
16
+ """
17
+ Add dnf gpg keys with ``rpm``.
18
+
19
+ + key: filename or URL
20
+
21
+ Note:
22
+ always returns one command, not idempotent
23
+
24
+ **Example:**
25
+
26
+ .. code:: python
27
+
28
+ linux_id = host.get_fact(LinuxDistribution)["release_meta"].get("ID")
29
+ dnf.key(
30
+ name="Add the Docker CentOS gpg key",
31
+ src=f"https://download.docker.com/linux/{linux_id}/gpg",
32
+ )
33
+
34
+ """
35
+
36
+ yield "rpm --import {0}".format(src)
37
+
38
+
39
+ @operation()
40
+ def repo(
41
+ src: str,
42
+ present=True,
43
+ baseurl: str | None = None,
44
+ description: str | None = None,
45
+ enabled=True,
46
+ gpgcheck=True,
47
+ gpgkey: str | None = None,
48
+ ):
49
+ # NOTE: if updating this docstring also update `yum.repo`
50
+ """
51
+ Add/remove/update dnf repositories.
52
+
53
+ + src: URL or name for the ``.repo`` file
54
+ + present: whether the ``.repo`` file should be present
55
+ + baseurl: the baseurl of the repo (if ``name`` is not a URL)
56
+ + description: optional verbose description
57
+ + enabled: whether this repo is enabled
58
+ + gpgcheck: whether set ``gpgcheck=1``
59
+ + gpgkey: the URL to the gpg key for this repo
60
+
61
+ ``Baseurl``/``description``/``gpgcheck``/``gpgkey``:
62
+ These are only valid when ``name`` is a filename (ie not a URL). This is
63
+ for manual construction of repository files. Use a URL to download and
64
+ install remote repository files.
65
+
66
+ **Examples:**
67
+
68
+ .. code:: python
69
+
70
+ # Download a repository file
71
+ dnf.rpm(
72
+ name="Install Docker-CE repo via URL",
73
+ src="https://download.docker.com/linux/centos/docker-ce.repo",
74
+ )
75
+
76
+ # Create the repository file from baseurl/etc
77
+ dnf.repo(
78
+ name="Add the Docker CentOS repo",
79
+ src="DockerCE",
80
+ baseurl="https://download.docker.com/linux/centos/7/$basearch/stable",
81
+ )
82
+ """
83
+
84
+ yield from ensure_yum_repo(
85
+ host,
86
+ src,
87
+ baseurl,
88
+ present,
89
+ description,
90
+ enabled,
91
+ gpgcheck,
92
+ gpgkey,
93
+ )
94
+
95
+
96
+ @operation()
97
+ def rpm(src: str, present=True):
98
+ # NOTE: if updating this docstring also update `yum.rpm`
99
+ """
100
+ Add/remove ``.rpm`` file packages.
101
+
102
+ + src: filename or URL of the ``.rpm`` package
103
+ + present: whether ore not the package should exist on the system
104
+
105
+ URL sources with ``present=False``:
106
+ If the ``.rpm`` file isn't downloaded, pyinfra can't remove any existing
107
+ package as the file won't exist until mid-deploy.
108
+
109
+ **Example:**
110
+
111
+ .. code:: python
112
+
113
+ major_centos_version = host.get_fact(LinuxDistribution)["major"]
114
+ dnf.rpm(
115
+ name="Install EPEL rpm to enable EPEL repo",
116
+ src=f"https://dl.fedoraproject.org/pub/epel/epel-release-latest-{major_centos_version}.noarch.rpm",
117
+ )
118
+ """
119
+
120
+ yield from ensure_rpm(state, host, src, present, "dnf")
121
+
122
+
123
+ @operation(is_idempotent=False)
124
+ def update():
125
+ """
126
+ Updates all dnf packages.
127
+ """
128
+
129
+ yield "dnf update -y"
130
+
131
+
132
+ _update = update._inner # noqa: E305 (for use below where update is a kwarg)
133
+
134
+
135
+ @operation()
136
+ def packages(
137
+ packages: str | list[str] | None = None,
138
+ present=True,
139
+ latest=False,
140
+ update=False,
141
+ clean=False,
142
+ nobest=False,
143
+ extra_install_args: str | None = None,
144
+ extra_uninstall_args: str | None = None,
145
+ ):
146
+ """
147
+ Install/remove/update dnf packages & updates.
148
+
149
+ + packages: packages to ensure
150
+ + present: whether the packages should be installed
151
+ + latest: whether to upgrade packages without a specified version
152
+ + update: run ``dnf update`` before installing packages
153
+ + clean: run ``dnf clean`` before installing packages
154
+ + nobest: add the no best option to install
155
+ + extra_install_args: additional arguments to the dnf install command
156
+ + extra_uninstall_args: additional arguments to the dnf uninstall command
157
+
158
+ Versions:
159
+ Package versions can be pinned as follows: ``<pkg>=<version>``
160
+
161
+ **Examples:**
162
+
163
+ .. code:: python
164
+
165
+ # Update package list and install packages
166
+ dnf.packages(
167
+ name='Install Vim and Vim enhanced',
168
+ packages=["vim-enhanced", "vim"],
169
+ update=True,
170
+ )
171
+
172
+ # Install the latest versions of packages (always check)
173
+ dnf.packages(
174
+ name="Install latest Vim",
175
+ packages=["vim"],
176
+ latest=True,
177
+ )
178
+ """
179
+
180
+ if clean:
181
+ yield "dnf clean all"
182
+
183
+ if update:
184
+ yield from _update()
185
+
186
+ install_command = ["dnf", "install", "-y"]
187
+
188
+ if nobest:
189
+ install_command.append("--nobest")
190
+
191
+ if extra_install_args:
192
+ install_command.append(extra_install_args)
193
+
194
+ uninstall_command = ["dnf", "remove", "-y"]
195
+
196
+ if extra_uninstall_args:
197
+ uninstall_command.append(extra_uninstall_args)
198
+
199
+ yield from ensure_packages(
200
+ host,
201
+ packages,
202
+ host.get_fact(RpmPackages),
203
+ present,
204
+ install_command=" ".join(install_command),
205
+ uninstall_command=" ".join(uninstall_command),
206
+ upgrade_command="dnf update -y",
207
+ version_join="=",
208
+ latest=latest,
209
+ expand_package_fact=lambda package: host.get_fact(RpmPackageProvides, package=package),
210
+ )