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,1100 @@
1
+ """
2
+ The server module takes care of os-level state. Targets POSIX compatibility, tested on
3
+ Linux/BSD.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from io import StringIO
9
+ from itertools import filterfalse, tee
10
+ from os import path
11
+ from time import sleep
12
+ from typing import TYPE_CHECKING
13
+
14
+ from pyinfra import host, logger, state
15
+ from pyinfra.api import FunctionCommand, OperationError, StringCommand, operation
16
+ from pyinfra.api.util import try_int
17
+ from pyinfra.connectors.util import remove_any_sudo_askpass_file
18
+ from pyinfra.facts.files import Directory, FindInFile, Link
19
+ from pyinfra.facts.server import (
20
+ Groups,
21
+ Home,
22
+ Hostname,
23
+ Kernel,
24
+ KernelModules,
25
+ Locales,
26
+ Mounts,
27
+ Os,
28
+ Sysctl,
29
+ Users,
30
+ Which,
31
+ )
32
+ from pyinfra.operations import crontab as crontab_
33
+
34
+ from . import (
35
+ apk,
36
+ apt,
37
+ brew,
38
+ bsdinit,
39
+ dnf,
40
+ files,
41
+ openrc,
42
+ pacman,
43
+ pkg,
44
+ runit,
45
+ systemd,
46
+ sysvinit,
47
+ upstart,
48
+ xbps,
49
+ yum,
50
+ zypper,
51
+ )
52
+ from .util.files import chmod
53
+
54
+ if TYPE_CHECKING:
55
+ from pyinfra.api.arguments_typed import PyinfraOperation
56
+
57
+
58
+ @operation(is_idempotent=False)
59
+ def reboot(delay=10, interval=1, reboot_timeout=300):
60
+ """
61
+ Reboot the server and wait for reconnection.
62
+
63
+ + delay: number of seconds to wait before attempting reconnect
64
+ + interval: interval (s) between reconnect attempts
65
+ + reboot_timeout: total time before giving up reconnecting
66
+
67
+ **Example:**
68
+
69
+ .. code:: python
70
+
71
+ from pyinfra.operations import server
72
+ server.reboot(
73
+ name="Reboot the server and wait to reconnect",
74
+ delay=60,
75
+ reboot_timeout=600,
76
+ )
77
+ """
78
+
79
+ # Remove this now, before we reboot the server - if the reboot fails (expected or
80
+ # not) we'll error if we don't clean this up now. Will simply be re-uploaded if
81
+ # needed later.
82
+ def remove_any_askpass_file(state, host):
83
+ remove_any_sudo_askpass_file(host)
84
+
85
+ yield FunctionCommand(remove_any_askpass_file, (), {})
86
+
87
+ yield StringCommand("reboot", _success_exit_codes=[0, -1]) # -1 being error/disconnected
88
+
89
+ def wait_and_reconnect(state, host): # pragma: no cover
90
+ sleep(delay)
91
+ max_retries = round(reboot_timeout / interval)
92
+
93
+ host.disconnect() # make sure we are properly disconnected
94
+ retries = 0
95
+
96
+ while True:
97
+ host.connect(show_errors=False)
98
+ if host.connected:
99
+ break
100
+
101
+ if retries > max_retries:
102
+ raise Exception(
103
+ ("Server did not reboot in time (reboot_timeout={0}s)").format(reboot_timeout),
104
+ )
105
+
106
+ sleep(interval)
107
+ retries += 1
108
+
109
+ yield FunctionCommand(wait_and_reconnect, (), {})
110
+
111
+ # On certain systems sudo files are lost on reboot
112
+ def clean_sudo_info(state, host):
113
+ host.connector_data["sudo_askpass_path"] = None
114
+
115
+ yield FunctionCommand(clean_sudo_info, (), {})
116
+
117
+
118
+ @operation(is_idempotent=False)
119
+ def wait(port: int):
120
+ """
121
+ Waits for a port to come active on the target machine. Requires netstat, checks every
122
+ second.
123
+
124
+ + port: port number to wait for
125
+
126
+ **Example:**
127
+
128
+ .. code:: python
129
+
130
+ server.wait(
131
+ name="Wait for webserver to start",
132
+ port=80,
133
+ )
134
+ """
135
+
136
+ yield r"""
137
+ while ! (netstat -an | grep LISTEN | grep -e "\.{0}" -e ":{0}"); do
138
+ echo "waiting for port {0}..."
139
+ sleep 1
140
+ done
141
+ """.format(
142
+ port,
143
+ )
144
+
145
+
146
+ @operation(is_idempotent=False)
147
+ def shell(commands: str | list[str]):
148
+ """
149
+ Run raw shell code on server during a deploy. If the command would
150
+ modify data that would be in a fact, the fact would not be updated
151
+ since facts are only run at the start of a deploy.
152
+
153
+ + commands: command or list of commands to execute on the remote server
154
+
155
+ **Example:**
156
+
157
+ .. code:: python
158
+
159
+ server.shell(
160
+ name="Run lxd auto init",
161
+ commands=["lxd init --auto"],
162
+ )
163
+ """
164
+
165
+ # Ensure we have a list
166
+ if isinstance(commands, str):
167
+ commands = [commands]
168
+
169
+ for command in commands:
170
+ yield command
171
+
172
+
173
+ @operation(is_idempotent=False)
174
+ def script(src: str, args=()):
175
+ """
176
+ Upload and execute a local script on the remote host.
177
+
178
+ + src: local script filename to upload & execute
179
+ + args: iterable to pass as arguments to the script
180
+
181
+ **Example:**
182
+
183
+ .. code:: python
184
+
185
+ # Note: This assumes there is a file in files/hello.bash locally.
186
+ server.script(
187
+ name="Hello",
188
+ src="files/hello.bash",
189
+ )
190
+
191
+ # Example passing arguments to the script
192
+ server.script(
193
+ name="Hello",
194
+ src="files/hello.bash",
195
+ args=("do-something", "with-this"),
196
+ )
197
+ """
198
+
199
+ temp_file = host.get_temp_filename()
200
+ yield from files.put._inner(src=src, dest=temp_file)
201
+
202
+ yield chmod(temp_file, "+x")
203
+ yield StringCommand(temp_file, *args)
204
+
205
+
206
+ @operation(is_idempotent=False)
207
+ def script_template(src: str, args=(), **data):
208
+ """
209
+ Generate, upload and execute a local script template on the remote host.
210
+
211
+ + src: local script template filename
212
+
213
+ **Example:**
214
+
215
+ .. code:: python
216
+
217
+ # Example showing how to pass python variable to a script template file.
218
+ # The .j2 file can use `{{ some_var }}` to be interpolated.
219
+ # To see output need to run pyinfra with '-v'
220
+ # Note: This assumes there is a file in templates/hello2.bash.j2 locally.
221
+ some_var = 'blah blah blah '
222
+ server.script_template(
223
+ name="Hello from script",
224
+ src="templates/hello2.bash.j2",
225
+ some_var=some_var,
226
+ )
227
+ """
228
+
229
+ temp_file = host.get_temp_filename("{0}{1}".format(src, data))
230
+ yield from files.template._inner(src, temp_file, **data)
231
+
232
+ yield chmod(temp_file, "+x")
233
+ yield StringCommand(temp_file, *args)
234
+
235
+
236
+ @operation()
237
+ def modprobe(module: str, present=True, force=False):
238
+ """
239
+ Load/unload kernel modules.
240
+
241
+ + module: name of the module to manage
242
+ + present: whether the module should be loaded or not
243
+ + force: whether to force any add/remove modules
244
+
245
+ **Example:**
246
+
247
+ .. code:: python
248
+
249
+ server.modprobe(
250
+ name="Silly example for modprobe",
251
+ module="floppy",
252
+ )
253
+ """
254
+ list_value = [module] if isinstance(module, str) else module
255
+
256
+ # NOTE: https://docs.python.org/3/library/itertools.html#itertools-recipes
257
+ def partition(predicate, iterable):
258
+ t1, t2 = tee(iterable)
259
+ return list(filter(predicate, t2)), list(filterfalse(predicate, t1))
260
+
261
+ modules = host.get_fact(KernelModules)
262
+ present_mods, missing_mods = partition(lambda mod: mod in modules, list_value)
263
+
264
+ args = ""
265
+ if force:
266
+ args = " -f"
267
+
268
+ # Module is loaded and we don't want it?
269
+ if not present and present_mods:
270
+ yield "modprobe{0} -r -a {1}".format(args, " ".join(present_mods))
271
+
272
+ # Module isn't loaded and we want it?
273
+ elif present and missing_mods:
274
+ yield "modprobe{0} -a {1}".format(args, " ".join(missing_mods))
275
+
276
+ else:
277
+ host.noop(
278
+ "{0} {1} {2} {3}".format(
279
+ "modules" if len(list_value) > 1 else "module",
280
+ "/".join(list_value),
281
+ "are" if len(list_value) > 1 else "is",
282
+ "loaded" if present else "not loaded",
283
+ ),
284
+ )
285
+
286
+
287
+ @operation()
288
+ def mount(
289
+ path: str,
290
+ mounted=True,
291
+ options: list[str] | None = None,
292
+ device: str | None = None,
293
+ fs_type: str | None = None,
294
+ # TODO: do we want to manage fstab here?
295
+ # update_fstab=False,
296
+ ):
297
+ """
298
+ Manage mounted filesystems.
299
+
300
+ + path: the path of the mounted filesystem
301
+ + mounted: whether the filesystem should be mounted
302
+ + options: the mount options
303
+
304
+ Options:
305
+ If the currently mounted filesystem does not have all of the provided
306
+ options it will be remounted with the options provided.
307
+
308
+ ``/etc/fstab``:
309
+ This operation does not attempt to modify the on disk fstab file - for
310
+ that you should use the `files.line operation <./files.html#files-line>`_.
311
+ """
312
+ options = options or []
313
+ options_string = ",".join(options)
314
+
315
+ mounts = host.get_fact(Mounts)
316
+ is_mounted = path in mounts
317
+
318
+ # Want mount but don't have?
319
+ if mounted and not is_mounted:
320
+ args = []
321
+ if fs_type:
322
+ args.extend(["-t", fs_type])
323
+ if options_string:
324
+ args.extend(["-o", options_string])
325
+ if device:
326
+ args.append(device)
327
+ args.append(path)
328
+
329
+ yield StringCommand("mount", *args)
330
+
331
+ # Want no mount but mounted?
332
+ elif mounted is False and is_mounted:
333
+ yield "umount {0}".format(path)
334
+
335
+ # Want mount and is mounted! Check the options
336
+ elif is_mounted and mounted and options:
337
+ mounted_options = mounts[path]["options"]
338
+ needed_options = set(options) - set(mounted_options)
339
+ if needed_options:
340
+ if host.get_fact(Kernel).strip() == "FreeBSD":
341
+ fs_type = mounts[path]["type"]
342
+ device = mounts[path]["device"]
343
+
344
+ yield "mount -o update,{options} -t {fs_type} {device} {path}".format(
345
+ options=options_string, fs_type=fs_type, device=device, path=path
346
+ )
347
+ else:
348
+ yield "mount -o remount,{0} {1}".format(options_string, path)
349
+
350
+ else:
351
+ host.noop(
352
+ "filesystem {0} is {1}".format(
353
+ path,
354
+ "mounted" if mounted else "not mounted",
355
+ ),
356
+ )
357
+
358
+
359
+ @operation()
360
+ def hostname(hostname: str, hostname_file: str | None = None):
361
+ """
362
+ Set the system hostname using ``hostnamectl`` or ``hostname`` on older systems.
363
+
364
+ + hostname: the hostname that should be set
365
+ + hostname_file: the file that permanently sets the hostname
366
+
367
+ Hostname file:
368
+ The hostname file only matters no systems that do not have ``hostnamectl``,
369
+ which is part of ``systemd``.
370
+
371
+ By default pyinfra will auto detect this by targeting ``/etc/hostname``
372
+ on Linux and ``/etc/myname`` on OpenBSD.
373
+
374
+ To completely disable writing the hostname file, set ``hostname_file=False``.
375
+
376
+ **Example:**
377
+
378
+ .. code:: python
379
+
380
+ server.hostname(
381
+ name="Set the hostname",
382
+ hostname="server1.example.com",
383
+ )
384
+ """
385
+
386
+ current_hostname = host.get_fact(Hostname)
387
+
388
+ if host.get_fact(Which, command="hostnamectl"):
389
+ if current_hostname != hostname:
390
+ yield "hostnamectl set-hostname {0}".format(hostname)
391
+ else:
392
+ host.noop("hostname is set")
393
+ return
394
+
395
+ if hostname_file is None:
396
+ os = host.get_fact(Os)
397
+
398
+ if os == "Linux":
399
+ hostname_file = "/etc/hostname"
400
+ elif os == "OpenBSD":
401
+ hostname_file = "/etc/myname"
402
+
403
+ if current_hostname != hostname:
404
+ yield "hostname {0}".format(hostname)
405
+ else:
406
+ host.noop("hostname is set")
407
+
408
+ if hostname_file:
409
+ # Create a whole new hostname file
410
+ file = StringIO("{0}\n".format(hostname))
411
+
412
+ # And ensure it exists
413
+ yield from files.put._inner(src=file, dest=hostname_file)
414
+
415
+
416
+ @operation()
417
+ def sysctl(
418
+ key: str,
419
+ value: str | int | list[str | int],
420
+ persist=False,
421
+ persist_file="/etc/sysctl.conf",
422
+ ):
423
+ """
424
+ Edit sysctl configuration.
425
+
426
+ + key: name of the sysctl setting to ensure
427
+ + value: the value or list of values the sysctl should be
428
+ + persist: whether to write this sysctl to the config
429
+ + persist_file: file to write the sysctl to persist on reboot
430
+
431
+ **Example:**
432
+
433
+ .. code:: python
434
+
435
+ server.sysctl(
436
+ name="Change the fs.file-max value",
437
+ key="fs.file-max",
438
+ value=100000,
439
+ persist=True,
440
+ )
441
+ """
442
+
443
+ string_value = " ".join(["{0}".format(v) for v in value]) if isinstance(value, list) else value
444
+
445
+ value = [try_int(v) for v in value] if isinstance(value, list) else try_int(value)
446
+
447
+ existing_sysctls = host.get_fact(Sysctl, keys=[key])
448
+ existing_value = existing_sysctls.get(key)
449
+
450
+ if existing_value != value:
451
+ yield "sysctl {0}='{1}'".format(key, string_value)
452
+ else:
453
+ host.noop("sysctl {0} is set to {1}".format(key, string_value))
454
+
455
+ if persist:
456
+ yield from files.line._inner(
457
+ path=persist_file,
458
+ line="{0}[[:space:]]*=[[:space:]]*{1}".format(key, string_value),
459
+ replace="{0} = {1}".format(key, string_value),
460
+ )
461
+
462
+
463
+ @operation()
464
+ def service(
465
+ service: str,
466
+ running=True,
467
+ restarted=False,
468
+ reloaded=False,
469
+ command: str | None = None,
470
+ enabled: bool | None = None,
471
+ ):
472
+ """
473
+ Manage the state of services. This command checks for the presence of all the
474
+ Linux init systems pyinfra can handle and executes the relevant operation.
475
+
476
+ + service: name of the service to manage
477
+ + running: whether the service should be running
478
+ + restarted: whether the service should be restarted
479
+ + reloaded: whether the service should be reloaded
480
+ + command: custom command execute
481
+ + enabled: whether this service should be enabled/disabled on boot
482
+
483
+ **Example:**
484
+
485
+ .. code:: python
486
+
487
+ server.service(
488
+ name="Enable open-vm-tools service",
489
+ service="open-vm-tools",
490
+ enabled=True,
491
+ )
492
+ """
493
+
494
+ service_operation: "PyinfraOperation"
495
+
496
+ if host.get_fact(Which, command="systemctl"):
497
+ service_operation = systemd.service
498
+
499
+ elif host.get_fact(Which, command="rc-service"):
500
+ service_operation = openrc.service
501
+
502
+ elif host.get_fact(Which, command="initctl"):
503
+ service_operation = upstart.service
504
+
505
+ elif host.get_fact(Which, command="sv"):
506
+ service_operation = runit.service
507
+
508
+ elif (
509
+ host.get_fact(Which, command="service")
510
+ or host.get_fact(Link, path="/etc/init.d")
511
+ or host.get_fact(Directory, path="/etc/init.d")
512
+ ):
513
+ service_operation = sysvinit.service
514
+
515
+ # NOTE: important that we are not Linux here because /etc/rc.d will exist but checking it's
516
+ # contents may trigger things (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
517
+ elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
518
+ service_operation = bsdinit.service
519
+
520
+ else:
521
+ raise OperationError(
522
+ ("No init system found (no systemctl, initctl, /etc/init.d or /etc/rc.d found)"),
523
+ )
524
+
525
+ yield from service_operation._inner(
526
+ service=service,
527
+ running=running,
528
+ restarted=restarted,
529
+ reloaded=reloaded,
530
+ command=command,
531
+ enabled=enabled,
532
+ )
533
+
534
+
535
+ @operation()
536
+ def packages(
537
+ packages: str | list[str],
538
+ present=True,
539
+ ):
540
+ """
541
+ Add or remove system packages. This command checks for the presence of all the
542
+ system package managers pyinfra can handle and executes the relevant operation.
543
+
544
+ + packages: list of packages to ensure
545
+ + present: whether the packages should be installed
546
+
547
+ **Example:**
548
+
549
+ .. code:: python
550
+
551
+ server.packages(
552
+ name="Install Vim and vimpager",
553
+ packages=["vimpager", "vim"],
554
+ )
555
+ """
556
+
557
+ package_operation: "PyinfraOperation"
558
+
559
+ # TODO: improve this - use LinuxDistribution fact + mapping with fallback below?
560
+ # Here to be preferred on openSUSE which also provides aptitude
561
+ # See: https://github.com/Fizzadar/pyinfra/issues/799
562
+ if host.get_fact(Which, command="zypper"):
563
+ package_operation = zypper.packages
564
+
565
+ elif host.get_fact(Which, command="apk"):
566
+ package_operation = apk.packages
567
+
568
+ elif host.get_fact(Which, command="apt"):
569
+ package_operation = apt.packages
570
+
571
+ elif host.get_fact(Which, command="brew"):
572
+ package_operation = brew.packages
573
+
574
+ elif host.get_fact(Which, command="dnf"):
575
+ package_operation = dnf.packages
576
+
577
+ elif host.get_fact(Which, command="pacman"):
578
+ package_operation = pacman.packages
579
+
580
+ elif host.get_fact(Which, command="xbps-install") or host.get_fact(Which, command="xbps"):
581
+ package_operation = xbps.packages
582
+
583
+ elif host.get_fact(Which, command="yum"):
584
+ package_operation = yum.packages
585
+
586
+ elif host.get_fact(Which, command="pkg") or host.get_fact(Which, command="pkg_add"):
587
+ package_operation = pkg.packages
588
+
589
+ else:
590
+ raise OperationError(
591
+ (
592
+ "No system package manager found "
593
+ "(no apk, apt, brew, dnf, pacman, pkg, xbps, yum or zypper found)"
594
+ ),
595
+ )
596
+
597
+ yield from package_operation._inner(packages=packages, present=present)
598
+
599
+
600
+ crontab = crontab_.crontab
601
+
602
+
603
+ @operation()
604
+ def group(group: str, present=True, system=False, gid: int | str | None = None):
605
+ """
606
+ Add/remove system groups.
607
+
608
+ + group: name of the group to ensure
609
+ + present: whether the group should be present or not
610
+ + system: whether to create a system group
611
+ + gid: use a specific groupid number
612
+
613
+ System users:
614
+ System users don't exist on BSD, so the argument is ignored for BSD targets.
615
+
616
+ **Examples:**
617
+
618
+ .. code:: python
619
+
620
+ server.group(
621
+ name="Create docker group",
622
+ group="docker",
623
+ )
624
+
625
+ # multiple groups
626
+ for group in ["wheel", "lusers"]:
627
+ server.group(
628
+ name=f"Create the group {group}",
629
+ group=group,
630
+ )
631
+ """
632
+
633
+ groups = host.get_fact(Groups)
634
+ os_type = host.get_fact(Os)
635
+ is_present = group in groups
636
+
637
+ # Group exists but we don't want them?
638
+ if not present and is_present:
639
+ if os_type == "FreeBSD":
640
+ yield "pw groupdel -n {0}".format(group)
641
+ else:
642
+ yield "groupdel {0}".format(group)
643
+
644
+ # Group doesn't exist and we want it?
645
+ elif present and not is_present:
646
+ args = []
647
+
648
+ # BSD doesn't do system users
649
+ if system and "BSD" not in host.get_fact(Os):
650
+ args.append("-r")
651
+
652
+ if os_type == "FreeBSD":
653
+ args.append("-n {0}".format(group))
654
+ else:
655
+ args.append(group)
656
+
657
+ if gid:
658
+ if os_type == "FreeBSD":
659
+ args.append("-g {0}".format(gid))
660
+ else:
661
+ args.append("--gid {0}".format(gid))
662
+
663
+ # Groups are often added by other operations (package installs), so check
664
+ # for the group at runtime before adding.
665
+ group_add_command = "groupadd"
666
+ if os_type == "FreeBSD":
667
+ group_add_command = "pw groupadd"
668
+ yield "{0} {1}".format(group_add_command, " ".join(args))
669
+
670
+
671
+ @operation()
672
+ def user_authorized_keys(
673
+ user: str,
674
+ public_keys: str | list[str],
675
+ group: str | None = None,
676
+ delete_keys=False,
677
+ authorized_key_directory: str | None = None,
678
+ authorized_key_filename: str | None = None,
679
+ ):
680
+ """
681
+ Manage `authorized_keys` of system users.
682
+
683
+ + user: name of the user to ensure
684
+ + public_keys: list of public keys to attach to this user, ``home`` must be specified
685
+ + group: the users primary group
686
+ + delete_keys: whether to remove any keys not specified in ``public_keys``
687
+
688
+ Public keys:
689
+ These can be provided as strings containing the public key or as a path to
690
+ a public key file which pyinfra will read.
691
+
692
+ **Examples:**
693
+
694
+ .. code:: python
695
+
696
+ server.user_authorized_keys(
697
+ name="Ensure user has a public key",
698
+ user="kevin",
699
+ public_keys=["ed25519..."],
700
+ )
701
+ """
702
+
703
+ if not authorized_key_directory:
704
+ home = host.get_fact(Home, user=user)
705
+ authorized_key_directory = f"{home}/.ssh"
706
+
707
+ if not authorized_key_filename:
708
+ authorized_key_filename = "authorized_keys"
709
+
710
+ if isinstance(public_keys, str):
711
+ public_keys = [public_keys]
712
+
713
+ def read_any_pub_key_file(key):
714
+ try_path = key
715
+ if state.cwd:
716
+ try_path = path.join(state.cwd, key)
717
+
718
+ if path.exists(try_path):
719
+ with open(try_path, "r") as f:
720
+ return [key.strip() for key in f.readlines()]
721
+
722
+ return [key.strip()]
723
+
724
+ public_keys = [key for key_or_file in public_keys for key in read_any_pub_key_file(key_or_file)]
725
+
726
+ # Ensure .ssh directory
727
+ # note that this always outputs commands unless the SSH user has access to the
728
+ # authorized_keys file, ie the SSH user is the user defined in this function
729
+ yield from files.directory._inner(
730
+ path=authorized_key_directory,
731
+ user=user,
732
+ group=group or user,
733
+ mode=700,
734
+ )
735
+
736
+ authorized_key_file = f"{authorized_key_directory}/{authorized_key_filename}"
737
+
738
+ if delete_keys:
739
+ # Create a whole new authorized_keys file
740
+ keys_file = StringIO(
741
+ "{0}\n".format(
742
+ "\n".join(public_keys),
743
+ ),
744
+ )
745
+
746
+ # And ensure it exists
747
+ yield from files.put._inner(
748
+ src=keys_file,
749
+ dest=authorized_key_file,
750
+ user=user,
751
+ group=group or user,
752
+ mode=600,
753
+ )
754
+
755
+ else:
756
+ # Ensure authorized_keys exists
757
+ yield from files.file._inner(
758
+ path=authorized_key_file,
759
+ user=user,
760
+ group=group or user,
761
+ mode=600,
762
+ )
763
+
764
+ # And every public key is present
765
+ for key in public_keys:
766
+ yield from files.line._inner(path=authorized_key_file, line=key, ensure_newline=True)
767
+
768
+
769
+ @operation()
770
+ def user(
771
+ user: str,
772
+ present=True,
773
+ home: str | None = None,
774
+ shell: str | None = None,
775
+ group: str | None = None,
776
+ groups: list[str] | None = None,
777
+ append=False,
778
+ public_keys: str | list[str] | None = None,
779
+ delete_keys=False,
780
+ ensure_home=True,
781
+ create_home=False,
782
+ system=False,
783
+ uid: int | None = None,
784
+ comment: str | None = None,
785
+ unique=True,
786
+ password: str | None = None,
787
+ ):
788
+ """
789
+ Add/remove/update system users & their ssh `authorized_keys`.
790
+
791
+ + user: name of the user to ensure
792
+ + present: whether this user should exist
793
+ + home: the users home directory
794
+ + shell: the users shell
795
+ + group: the users primary group
796
+ + groups: the users secondary groups
797
+ + append: whether to add `user` to `groups`, w/o losing membership of other groups
798
+ + public_keys: list of public keys to attach to this user, ``home`` must be specified
799
+ + delete_keys: whether to remove any keys not specified in ``public_keys``
800
+ + ensure_home: whether to ensure the ``home`` directory exists
801
+ + create_home: whether user create new user home directories from the system skeleton
802
+ + system: whether to create a system account
803
+ + uid: use a specific userid number
804
+ + comment: the user GECOS comment
805
+ + unique: prevent creating users with duplicate UID
806
+ + password: set the encrypted password for the user
807
+
808
+ Home directory:
809
+ When ``ensure_home`` or ``public_keys`` are provided, ``home`` defaults to
810
+ ``/home/{name}``. When ``create_home`` is ``True`` any newly created users
811
+ will be created with the ``-m`` flag to build a new home directory from the
812
+ systems skeleton directory.
813
+
814
+ Public keys:
815
+ These can be provided as strings containing the public key or as a path to
816
+ a public key file which pyinfra will read.
817
+
818
+ **Examples:**
819
+
820
+ .. code:: python
821
+
822
+ server.user(
823
+ name="Ensure user is removed",
824
+ user="kevin",
825
+ present=False,
826
+ )
827
+
828
+ server.user(
829
+ name="Ensure myweb user exists",
830
+ user="myweb",
831
+ shell="/bin/bash",
832
+ )
833
+
834
+ # multiple users
835
+ for user in ["kevin", "bob"]:
836
+ server.user(
837
+ name=f"Ensure user {user} is removed",
838
+ user=user,
839
+ present=False,
840
+ )
841
+ """
842
+
843
+ users = host.get_fact(Users)
844
+ existing_groups = host.get_fact(Groups)
845
+ existing_user = users.get(user)
846
+ os_type = host.get_fact(Os)
847
+ if groups is None:
848
+ groups = []
849
+
850
+ if home is None:
851
+ home = "/home/{0}".format(user)
852
+ if existing_user:
853
+ home = existing_user.get("home", home)
854
+
855
+ # User not wanted?
856
+ if not present:
857
+ if existing_user:
858
+ if os_type == "FreeBSD":
859
+ yield "pw userdel -n {0}".format(user)
860
+ else:
861
+ yield "userdel {0}".format(user)
862
+ return
863
+
864
+ # User doesn't exist but we want them?
865
+ if present and existing_user is None:
866
+ # Fix the case where a group of the same name already exists, tell useradd to use this
867
+ # group rather than failing trying to create it.
868
+ if not group and user in existing_groups:
869
+ group = user
870
+
871
+ # Create the user w/home/shell
872
+ args = []
873
+
874
+ if home:
875
+ args.append("-d {0}".format(home))
876
+
877
+ if shell:
878
+ args.append("-s {0}".format(shell))
879
+
880
+ if group:
881
+ args.append("-g {0}".format(group))
882
+
883
+ if groups:
884
+ args.append("-G {0}".format(",".join(groups)))
885
+
886
+ if system and "BSD" not in host.get_fact(Os):
887
+ args.append("-r")
888
+
889
+ if uid:
890
+ if os_type == "FreeBSD":
891
+ args.append("-u {0}".format(uid))
892
+ else:
893
+ args.append("--uid {0}".format(uid))
894
+
895
+ if comment:
896
+ args.append("-c '{0}'".format(comment))
897
+
898
+ if not unique:
899
+ args.append("-o")
900
+
901
+ if create_home:
902
+ args.append("-m")
903
+ elif os_type != "FreeBSD":
904
+ args.append("-M")
905
+
906
+ if password and os_type != "FreeBSD":
907
+ args.append("-p '{0}'".format(password))
908
+
909
+ # Users are often added by other operations (package installs), so check
910
+ # for the user at runtime before adding.
911
+ add_user_command = "useradd"
912
+
913
+ if os_type == "FreeBSD":
914
+ add_user_command = "pw useradd"
915
+
916
+ if password:
917
+ yield "echo '{3}' | {0} -n {2} -H 0 {1}".format(
918
+ add_user_command, " ".join(args), user, password
919
+ )
920
+ else:
921
+ yield "{0} -n {2} {1}".format(
922
+ add_user_command,
923
+ " ".join(args),
924
+ user,
925
+ )
926
+ else:
927
+ yield "{0} {1} {2}".format(
928
+ add_user_command,
929
+ " ".join(args),
930
+ user,
931
+ )
932
+
933
+ # User exists and we want them, check home/shell/keys/password
934
+ else:
935
+ args = []
936
+
937
+ # Check homedir
938
+ if home and existing_user["home"] != home:
939
+ args.append("-d {0}".format(home))
940
+
941
+ # Check shell
942
+ if shell and existing_user["shell"] != shell:
943
+ args.append("-s {0}".format(shell))
944
+
945
+ # Check primary group
946
+ if group and existing_user["group"] != group:
947
+ args.append("-g {0}".format(group))
948
+
949
+ # Check secondary groups, if defined
950
+ if groups:
951
+ if append:
952
+ if not set(groups).issubset(existing_user["groups"]):
953
+ args.append("-a")
954
+ args.append("-G {0}".format(",".join(groups)))
955
+ elif set(existing_user["groups"]) != set(groups):
956
+ args.append("-G {0}".format(",".join(groups)))
957
+
958
+ if comment and existing_user["comment"] != comment:
959
+ args.append("-c '{0}'".format(comment))
960
+
961
+ if password and existing_user["password"] != password:
962
+ if os_type == "FreeBSD":
963
+ yield "echo '{0}' | pw usermod -n {1} -H 0".format(password, user)
964
+ else:
965
+ args.append("-p '{0}'".format(password))
966
+
967
+ # Need to mod the user?
968
+ if args:
969
+ if os_type == "FreeBSD":
970
+ yield "pw usermod -n {1} {0}".format(" ".join(args), user)
971
+ else:
972
+ yield "usermod {0} {1}".format(" ".join(args), user)
973
+
974
+ # Ensure home directory ownership
975
+ if ensure_home and home:
976
+ yield from files.directory._inner(
977
+ path=home,
978
+ user=user,
979
+ group=group or user,
980
+ # Don't fail if the home directory exists as a link
981
+ _no_fail_on_link=True,
982
+ )
983
+
984
+ # Add SSH keys
985
+ if public_keys is not None:
986
+ yield from user_authorized_keys._inner(
987
+ user=user,
988
+ public_keys=public_keys,
989
+ group=group,
990
+ delete_keys=delete_keys,
991
+ authorized_key_directory="{0}/.ssh".format(home),
992
+ authorized_key_filename=None,
993
+ )
994
+
995
+
996
+ @operation()
997
+ def locale(
998
+ locale: str,
999
+ present=True,
1000
+ ):
1001
+ """
1002
+ Enable/Disable locale.
1003
+
1004
+ + locale: name of the locale to enable/disable
1005
+ + present: whether this locale should be present or not
1006
+
1007
+ **Examples:**
1008
+
1009
+ .. code:: python
1010
+
1011
+ server.locale(
1012
+ name="Ensure en_GB.UTF-8 locale is not present",
1013
+ locale="en_GB.UTF-8",
1014
+ present=False,
1015
+ )
1016
+
1017
+ server.locale(
1018
+ name="Ensure en_GB.UTF-8 locale is present",
1019
+ locale="en_GB.UTF-8",
1020
+ )
1021
+
1022
+ """
1023
+
1024
+ locales = host.get_fact(Locales)
1025
+
1026
+ logger.debug("Enabled locales: {0}".format(locales))
1027
+
1028
+ locales_definitions_file = "/etc/locale.gen"
1029
+
1030
+ # Find the matching line in /etc/locale.gen
1031
+ matching_lines = host.get_fact(
1032
+ FindInFile, path=locales_definitions_file, pattern=rf"^.*{locale}[[:space:]]\+.*$"
1033
+ )
1034
+
1035
+ if not matching_lines:
1036
+ raise OperationError(f"Locale {locale} not found in {locales_definitions_file}")
1037
+
1038
+ if len(matching_lines) > 1:
1039
+ raise OperationError(f"Multiple locales matches for {locale} in {locales_definitions_file}")
1040
+
1041
+ matching_line = matching_lines[0]
1042
+
1043
+ # Remove locale
1044
+ if not present and locale in locales:
1045
+ logger.debug(f"Removing locale {locale}")
1046
+
1047
+ yield from files.line._inner(
1048
+ path=locales_definitions_file, line=f"^{matching_line}$", replace=f"# {matching_line}"
1049
+ )
1050
+
1051
+ yield "locale-gen"
1052
+
1053
+ # Add locale
1054
+ if present and locale not in locales:
1055
+ logger.debug(f"Adding locale {locale}")
1056
+
1057
+ yield from files.replace._inner(
1058
+ path=locales_definitions_file,
1059
+ text=f"^{matching_line}$",
1060
+ replace=f"{matching_line}".replace("# ", ""),
1061
+ )
1062
+
1063
+ yield "locale-gen"
1064
+
1065
+
1066
+ @operation()
1067
+ def security_limit(
1068
+ domain: str,
1069
+ limit_type: str,
1070
+ item: str,
1071
+ value: int,
1072
+ ):
1073
+ """
1074
+ Edit /etc/security/limits.conf configuration.
1075
+
1076
+ + domain: the domain (user, group, or wildcard) for the limit
1077
+ + limit_type: the type of limit (hard or soft)
1078
+ + item: the item to limit (e.g., nofile, nproc)
1079
+ + value: the value for the limit
1080
+
1081
+ **Example:**
1082
+
1083
+ .. code:: python
1084
+
1085
+ security_limit(
1086
+ name="Set nofile limit for all users",
1087
+ domain='*',
1088
+ limit_type='soft',
1089
+ item='nofile',
1090
+ value=1024,
1091
+ )
1092
+ """
1093
+
1094
+ line_format = f"{domain}\t{limit_type}\t{item}\t{value}"
1095
+
1096
+ yield from files.line._inner(
1097
+ path="/etc/security/limits.conf",
1098
+ line=f"^{domain}[[:space:]]+{limit_type}[[:space:]]+{item}",
1099
+ replace=line_format,
1100
+ )