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,232 @@
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
+ from pyinfra.operations import brew
65
+ # Update package list and install packages
66
+ brew.packages(
67
+ name='Install Vim and vimpager',
68
+ packages=["vimpager", "vim"],
69
+ update=True,
70
+ )
71
+
72
+ # Install the latest versions of packages (always check)
73
+ brew.packages(
74
+ name="Install latest Vim",
75
+ packages=["vim"],
76
+ latest=True,
77
+ )
78
+ """
79
+
80
+ if update:
81
+ yield from _update._inner()
82
+
83
+ if upgrade:
84
+ yield from _upgrade._inner()
85
+
86
+ yield from ensure_packages(
87
+ host,
88
+ packages,
89
+ host.get_fact(BrewPackages),
90
+ present,
91
+ install_command="brew install",
92
+ uninstall_command="brew uninstall",
93
+ upgrade_command="brew upgrade",
94
+ version_join="@",
95
+ latest=latest,
96
+ )
97
+
98
+
99
+ def cask_args():
100
+ return ("", " --cask") if new_cask_cli(host.get_fact(BrewVersion)) else ("cask ", "")
101
+
102
+
103
+ @operation(is_idempotent=False)
104
+ def cask_upgrade():
105
+ """
106
+ Upgrades all brew casks.
107
+ """
108
+
109
+ yield "brew %supgrade%s" % cask_args()
110
+
111
+
112
+ @operation()
113
+ def casks(
114
+ casks: str | list[str] | None = None,
115
+ present=True,
116
+ latest=False,
117
+ upgrade=False,
118
+ ):
119
+ """
120
+ Add/remove/update brew casks.
121
+
122
+ + casks: list of casks to ensure
123
+ + present: whether the casks should be installed
124
+ + latest: whether to upgrade casks without a specified version
125
+ + upgrade: run brew cask upgrade before installing casks
126
+
127
+ Versions:
128
+ Cask versions can be pinned like brew: ``<pkg>@<version>``.
129
+
130
+ **Example:**
131
+
132
+ .. code:: python
133
+
134
+ brew.casks(
135
+ name='Upgrade and install the latest cask',
136
+ casks=["godot"],
137
+ upgrade=True,
138
+ latest=True,
139
+ )
140
+
141
+ """
142
+
143
+ if upgrade:
144
+ yield from cask_upgrade._inner()
145
+
146
+ args = cask_args()
147
+
148
+ yield from ensure_packages(
149
+ host,
150
+ casks,
151
+ host.get_fact(BrewCasks),
152
+ present,
153
+ install_command="brew %sinstall%s" % args,
154
+ uninstall_command="brew %suninstall%s" % args,
155
+ upgrade_command="brew %supgrade%s" % args,
156
+ version_join="@",
157
+ latest=latest,
158
+ )
159
+
160
+
161
+ @operation()
162
+ def tap(src: str | None = None, present=True, url: str | None = None):
163
+ """
164
+ Add/remove brew taps.
165
+
166
+ + src: the name of the tap
167
+ + present: whether this tap should be present or not
168
+ + url: the url of the tap. See https://docs.brew.sh/Taps
169
+
170
+ **Examples:**
171
+
172
+ .. code:: python
173
+
174
+ brew.tap(
175
+ name="Add a brew tap",
176
+ src="includeos/includeos",
177
+ )
178
+
179
+ # Just url is equivalent to
180
+ # `brew tap kptdev/kpt https://github.com/kptdev/kpt`
181
+ brew.tap(
182
+ url="https://github.com/kptdev/kpt",
183
+ )
184
+
185
+ # src and url is equivalent to
186
+ # `brew tap example/project https://github.example.com/project`
187
+ brew.tap(
188
+ src="example/project",
189
+ url="https://github.example.com/project",
190
+ )
191
+
192
+ # Multiple taps
193
+ for tap in ["includeos/includeos", "ktr0731/evans"]:
194
+ brew.tap(
195
+ name={f"Add brew tap {tap}"},
196
+ src=tap,
197
+ )
198
+
199
+ """
200
+
201
+ if not (src or url):
202
+ host.noop("no tap was specified")
203
+ return
204
+
205
+ src = src or str(urllib.parse.urlparse(url).path).strip("/")
206
+
207
+ if len(src.split("/")) != 2:
208
+ host.noop("src '{0}' doesn't have two components.".format(src))
209
+ return
210
+
211
+ taps = host.get_fact(BrewTaps)
212
+ already_tapped = src in taps
213
+
214
+ if present and already_tapped:
215
+ host.noop("tap {0} already exists".format(src))
216
+ return
217
+
218
+ if already_tapped:
219
+ yield "brew untap {0}".format(src)
220
+ return
221
+
222
+ if not present:
223
+ host.noop("tap {0} does not exist".format(src))
224
+ return
225
+
226
+ cmd = "brew tap {0}".format(src)
227
+
228
+ if url is not None:
229
+ cmd = " ".join([cmd, url])
230
+
231
+ yield cmd
232
+ return
@@ -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,194 @@
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
+ from pyinfra.operations import crontab
58
+ # simple example for a crontab
59
+ crontab.crontab(
60
+ name="Backup /etc weekly",
61
+ command="/bin/tar cf /tmp/etc_bup.tar /etc",
62
+ name="backup_etc",
63
+ day_of_week=0,
64
+ hour=1,
65
+ minute=0,
66
+ )
67
+ """
68
+
69
+ def comma_sep(value):
70
+ if isinstance(value, (list, tuple)):
71
+ return ",".join("{0}".format(v) for v in value)
72
+ return value
73
+
74
+ minute = comma_sep(minute)
75
+ hour = comma_sep(hour)
76
+ month = comma_sep(month)
77
+ day_of_week = comma_sep(day_of_week)
78
+ day_of_month = comma_sep(day_of_month)
79
+
80
+ ctb0: CrontabFile | dict = host.get_fact(Crontab, user=user)
81
+ # facts from test are in dict
82
+ if isinstance(ctb0, dict):
83
+ ctb = CrontabFile(ctb0)
84
+ else:
85
+ ctb = ctb0
86
+ name_comment = "# pyinfra-name={0}".format(cron_name)
87
+
88
+ existing_crontab = ctb.get_command(
89
+ command=command if cron_name is None else None, name=cron_name
90
+ )
91
+ existing_crontab_command = existing_crontab["command"] if existing_crontab else command
92
+ existing_crontab_match = existing_crontab["command"] if existing_crontab else command
93
+
94
+ exists = existing_crontab is not None
95
+ exists_name = existing_crontab is not None and name_comment in existing_crontab.get(
96
+ "comments", ""
97
+ )
98
+
99
+ edit_commands: list[str | StringCommand] = []
100
+ temp_filename = host.get_temp_filename()
101
+
102
+ if special_time:
103
+ new_crontab_line = "{0} {1}".format(special_time, command)
104
+ else:
105
+ new_crontab_line = "{minute} {hour} {day_of_month} {month} {day_of_week} {command}".format(
106
+ minute=minute,
107
+ hour=hour,
108
+ day_of_month=day_of_month,
109
+ month=month,
110
+ day_of_week=day_of_week,
111
+ command=command,
112
+ )
113
+
114
+ existing_crontab_match = ".*{0}.*".format(existing_crontab_match)
115
+
116
+ # Don't want the cron and it does exist? Remove the line
117
+ if not present and exists:
118
+ edit_commands.append(
119
+ sed_delete(
120
+ temp_filename,
121
+ existing_crontab_match,
122
+ "",
123
+ interpolate_variables=interpolate_variables,
124
+ ),
125
+ )
126
+
127
+ # Want the cron but it doesn't exist? Append the line
128
+ elif present and not exists:
129
+ logger.debug(f"present: {present}, exists: {exists}")
130
+ if ctb: # append a blank line if cron entries already exist
131
+ edit_commands.append("echo '' >> {0}".format(temp_filename))
132
+ if cron_name:
133
+ edit_commands.append(
134
+ "echo {0} >> {1}".format(
135
+ shlex.quote(name_comment),
136
+ temp_filename,
137
+ ),
138
+ )
139
+
140
+ edit_commands.append(
141
+ "echo {0} >> {1}".format(
142
+ shlex.quote(new_crontab_line),
143
+ temp_filename,
144
+ ),
145
+ )
146
+
147
+ # We have the cron and it exists, do it's details? If not, replace the line
148
+ elif present and exists:
149
+ assert existing_crontab is not None
150
+ if any(
151
+ (
152
+ exists_name != (cron_name is not None),
153
+ special_time != existing_crontab.get("special_time"),
154
+ try_int(minute) != existing_crontab.get("minute"),
155
+ try_int(hour) != existing_crontab.get("hour"),
156
+ try_int(month) != existing_crontab.get("month"),
157
+ try_int(day_of_week) != existing_crontab.get("day_of_week"),
158
+ try_int(day_of_month) != existing_crontab.get("day_of_month"),
159
+ existing_crontab_command != command,
160
+ ),
161
+ ):
162
+ if not exists_name and cron_name:
163
+ new_crontab_line = f"{name_comment}\\n{new_crontab_line}"
164
+ edit_commands.append(
165
+ sed_replace(
166
+ temp_filename,
167
+ existing_crontab_match,
168
+ new_crontab_line,
169
+ interpolate_variables=interpolate_variables,
170
+ ),
171
+ )
172
+
173
+ if edit_commands:
174
+ crontab_args = []
175
+ if user:
176
+ crontab_args.append("-u {0}".format(user))
177
+
178
+ # List the crontab into a temporary file if it exists
179
+ if ctb:
180
+ yield "crontab -l {0} > {1}".format(" ".join(crontab_args), temp_filename)
181
+
182
+ # Now yield any edits
183
+ for edit_command in edit_commands:
184
+ yield edit_command
185
+
186
+ # Finally, use the tempfile to write a new crontab
187
+ yield "crontab {0} {1}".format(" ".join(crontab_args), temp_filename)
188
+ else:
189
+ host.noop(
190
+ "crontab {0} {1}".format(
191
+ command,
192
+ "exists" if present else "does not exist",
193
+ ),
194
+ )