pyinfra 2.9.2__py2.py3-none-any.whl → 3.0__py2.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 (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.2.dist-info/RECORD +0 -170
  151. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.2.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
@@ -3,11 +3,14 @@ The server module takes care of os-level state. Targets POSIX compatibility, tes
3
3
  Linux/BSD.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  import shlex
7
9
  from io import StringIO
8
10
  from itertools import filterfalse, tee
9
11
  from os import path
10
12
  from time import sleep
13
+ from typing import TYPE_CHECKING
11
14
 
12
15
  from pyinfra import host, logger, state
13
16
  from pyinfra.api import FunctionCommand, OperationError, StringCommand, operation
@@ -17,6 +20,7 @@ from pyinfra.facts.files import Directory, FindInFile, Link
17
20
  from pyinfra.facts.server import (
18
21
  Crontab,
19
22
  Groups,
23
+ Home,
20
24
  Hostname,
21
25
  KernelModules,
22
26
  Locales,
@@ -37,6 +41,7 @@ from . import (
37
41
  openrc,
38
42
  pacman,
39
43
  pkg,
44
+ runit,
40
45
  systemd,
41
46
  sysvinit,
42
47
  upstart,
@@ -46,6 +51,9 @@ from . import (
46
51
  )
47
52
  from .util.files import chmod, sed_replace
48
53
 
54
+ if TYPE_CHECKING:
55
+ from pyinfra.api.arguments_typed import PyinfraOperation
56
+
49
57
 
50
58
  @operation(is_idempotent=False)
51
59
  def reboot(delay=10, interval=1, reboot_timeout=300):
@@ -75,7 +83,7 @@ def reboot(delay=10, interval=1, reboot_timeout=300):
75
83
 
76
84
  yield FunctionCommand(remove_any_askpass_file, (), {})
77
85
 
78
- yield StringCommand("reboot", success_exit_codes=[0, -1]) # -1 being error/disconnected
86
+ yield StringCommand("reboot", _success_exit_codes=[0, -1]) # -1 being error/disconnected
79
87
 
80
88
  def wait_and_reconnect(state, host): # pragma: no cover
81
89
  sleep(delay)
@@ -135,7 +143,7 @@ def wait(port: int):
135
143
 
136
144
 
137
145
  @operation(is_idempotent=False)
138
- def shell(commands):
146
+ def shell(commands: str | list[str]):
139
147
  """
140
148
  Run raw shell code on server during a deploy. If the command would
141
149
  modify data that would be in a fact, the fact would not be updated
@@ -162,7 +170,7 @@ def shell(commands):
162
170
 
163
171
 
164
172
  @operation(is_idempotent=False)
165
- def script(src, args=()):
173
+ def script(src: str, args=()):
166
174
  """
167
175
  Upload and execute a local script on the remote host.
168
176
 
@@ -187,15 +195,15 @@ def script(src, args=()):
187
195
  )
188
196
  """
189
197
 
190
- temp_file = state.get_temp_filename()
191
- yield from files.put(src, temp_file)
198
+ temp_file = host.get_temp_filename()
199
+ yield from files.put._inner(src=src, dest=temp_file)
192
200
 
193
201
  yield chmod(temp_file, "+x")
194
202
  yield StringCommand(temp_file, *args)
195
203
 
196
204
 
197
205
  @operation(is_idempotent=False)
198
- def script_template(src, args=(), **data):
206
+ def script_template(src: str, args=(), **data):
199
207
  """
200
208
  Generate, upload and execute a local script template on the remote host.
201
209
 
@@ -217,15 +225,15 @@ def script_template(src, args=(), **data):
217
225
  )
218
226
  """
219
227
 
220
- temp_file = state.get_temp_filename("{0}{1}".format(src, data))
221
- yield from files.template(src, temp_file, **data)
228
+ temp_file = host.get_temp_filename("{0}{1}".format(src, data))
229
+ yield from files.template._inner(src, temp_file, **data)
222
230
 
223
231
  yield chmod(temp_file, "+x")
224
232
  yield StringCommand(temp_file, *args)
225
233
 
226
234
 
227
- @operation
228
- def modprobe(module, present=True, force=False):
235
+ @operation()
236
+ def modprobe(module: str, present=True, force=False):
229
237
  """
230
238
  Load/unload kernel modules.
231
239
 
@@ -259,14 +267,10 @@ def modprobe(module, present=True, force=False):
259
267
  # Module is loaded and we don't want it?
260
268
  if not present and present_mods:
261
269
  yield "modprobe{0} -r -a {1}".format(args, " ".join(present_mods))
262
- for mod in present_mods:
263
- modules.pop(mod)
264
270
 
265
271
  # Module isn't loaded and we want it?
266
272
  elif present and missing_mods:
267
273
  yield "modprobe{0} -a {1}".format(args, " ".join(missing_mods))
268
- for mod in missing_mods:
269
- modules[mod] = {}
270
274
 
271
275
  else:
272
276
  host.noop(
@@ -279,13 +283,13 @@ def modprobe(module, present=True, force=False):
279
283
  )
280
284
 
281
285
 
282
- @operation
286
+ @operation()
283
287
  def mount(
284
- path,
288
+ path: str,
285
289
  mounted=True,
286
- options=None,
287
- device=None,
288
- fs_type=None,
290
+ options: list[str] | None = None,
291
+ device: str | None = None,
292
+ fs_type: str | None = None,
289
293
  # TODO: do we want to manage fstab here?
290
294
  # update_fstab=False,
291
295
  ):
@@ -322,13 +326,10 @@ def mount(
322
326
  args.append(path)
323
327
 
324
328
  yield StringCommand("mount", *args)
325
- # Should we update facts with fs_type, device, etc?
326
- mounts[path] = {"options": options}
327
329
 
328
330
  # Want no mount but mounted?
329
331
  elif mounted is False and is_mounted:
330
332
  yield "umount {0}".format(path)
331
- mounts.pop(path)
332
333
 
333
334
  # Want mount and is mounted! Check the options
334
335
  elif is_mounted and mounted and options:
@@ -336,7 +337,6 @@ def mount(
336
337
  needed_options = set(options) - set(mounted_options)
337
338
  if needed_options:
338
339
  yield "mount -o remount,{0} {1}".format(options_string, path)
339
- mounts[path]["options"] = options
340
340
 
341
341
  else:
342
342
  host.noop(
@@ -347,8 +347,8 @@ def mount(
347
347
  )
348
348
 
349
349
 
350
- @operation
351
- def hostname(hostname, hostname_file=None):
350
+ @operation()
351
+ def hostname(hostname: str, hostname_file: str | None = None):
352
352
  """
353
353
  Set the system hostname using ``hostnamectl`` or ``hostname`` on older systems.
354
354
 
@@ -379,7 +379,6 @@ def hostname(hostname, hostname_file=None):
379
379
  if host.get_fact(Which, command="hostnamectl"):
380
380
  if current_hostname != hostname:
381
381
  yield "hostnamectl set-hostname {0}".format(hostname)
382
- host.create_fact(Hostname, data=hostname)
383
382
  else:
384
383
  host.noop("hostname is set")
385
384
  return
@@ -394,7 +393,6 @@ def hostname(hostname, hostname_file=None):
394
393
 
395
394
  if current_hostname != hostname:
396
395
  yield "hostname {0}".format(hostname)
397
- host.create_fact(Hostname, data=hostname)
398
396
  else:
399
397
  host.noop("hostname is set")
400
398
 
@@ -403,13 +401,13 @@ def hostname(hostname, hostname_file=None):
403
401
  file = StringIO("{0}\n".format(hostname))
404
402
 
405
403
  # And ensure it exists
406
- yield from files.put(file, hostname_file)
404
+ yield from files.put._inner(src=file, dest=hostname_file)
407
405
 
408
406
 
409
- @operation
407
+ @operation()
410
408
  def sysctl(
411
- key,
412
- value,
409
+ key: str,
410
+ value: str | int | list[str | int],
413
411
  persist=False,
414
412
  persist_file="/etc/sysctl.conf",
415
413
  ):
@@ -437,35 +435,34 @@ def sysctl(
437
435
 
438
436
  value = [try_int(v) for v in value] if isinstance(value, list) else try_int(value)
439
437
 
440
- existing_sysctls = host.get_fact(Sysctl)
441
-
438
+ existing_sysctls = host.get_fact(Sysctl, keys=[key])
442
439
  existing_value = existing_sysctls.get(key)
440
+
443
441
  if not existing_value or existing_value != value:
444
442
  yield "sysctl {0}='{1}'".format(key, string_value)
445
- existing_sysctls[key] = value
446
443
  else:
447
444
  host.noop("sysctl {0} is set to {1}".format(key, string_value))
448
445
 
449
446
  if persist:
450
- yield from files.line(
447
+ yield from files.line._inner(
451
448
  path=persist_file,
452
449
  line="{0}[[:space:]]*=[[:space:]]*{1}".format(key, string_value),
453
450
  replace="{0} = {1}".format(key, string_value),
454
451
  )
455
452
 
456
453
 
457
- @operation
454
+ @operation()
458
455
  def service(
459
- service,
456
+ service: str,
460
457
  running=True,
461
458
  restarted=False,
462
459
  reloaded=False,
463
- command=None,
464
- enabled=None,
460
+ command: str | None = None,
461
+ enabled: bool | None = None,
465
462
  ):
466
463
  """
467
464
  Manage the state of services. This command checks for the presence of all the
468
- Linux init systems ``pyinfra`` can handle and executes the relevant operation.
465
+ Linux init systems pyinfra can handle and executes the relevant operation.
469
466
 
470
467
  + service: name of the service to manage
471
468
  + running: whether the service should be running
@@ -485,6 +482,8 @@ def service(
485
482
  )
486
483
  """
487
484
 
485
+ service_operation: "PyinfraOperation"
486
+
488
487
  if host.get_fact(Which, command="systemctl"):
489
488
  service_operation = systemd.service
490
489
 
@@ -494,6 +493,9 @@ def service(
494
493
  elif host.get_fact(Which, command="initctl"):
495
494
  service_operation = upstart.service
496
495
 
496
+ elif host.get_fact(Which, command="sv"):
497
+ service_operation = runit.service
498
+
497
499
  elif (
498
500
  host.get_fact(Which, command="service")
499
501
  or host.get_fact(Link, path="/etc/init.d")
@@ -503,7 +505,7 @@ def service(
503
505
 
504
506
  # NOTE: important that we are not Linux here because /etc/rc.d will exist but checking it's
505
507
  # contents may trigger things (like a reboot: https://github.com/Fizzadar/pyinfra/issues/819)
506
- elif host.get_fact(Os) != "Linux" and host.get_fact(Directory, path="/etc/rc.d"):
508
+ elif host.get_fact(Os) != "Linux" and bool(host.get_fact(Directory, path="/etc/rc.d")):
507
509
  service_operation = bsdinit.service
508
510
 
509
511
  else:
@@ -511,8 +513,8 @@ def service(
511
513
  ("No init system found " "(no systemctl, initctl, /etc/init.d or /etc/rc.d found)"),
512
514
  )
513
515
 
514
- yield from service_operation(
515
- service,
516
+ yield from service_operation._inner(
517
+ service=service,
516
518
  running=running,
517
519
  restarted=restarted,
518
520
  reloaded=reloaded,
@@ -521,14 +523,14 @@ def service(
521
523
  )
522
524
 
523
525
 
524
- @operation
526
+ @operation()
525
527
  def packages(
526
- packages,
528
+ packages: str | list[str],
527
529
  present=True,
528
530
  ):
529
531
  """
530
532
  Add or remove system packages. This command checks for the presence of all the
531
- system package managers ``pyinfra`` can handle and executes the relevant operation.
533
+ system package managers pyinfra can handle and executes the relevant operation.
532
534
 
533
535
  + packages: list of packages to ensure
534
536
  + present: whether the packages should be installed
@@ -543,6 +545,8 @@ def packages(
543
545
  )
544
546
  """
545
547
 
548
+ package_operation: "PyinfraOperation"
549
+
546
550
  # TODO: improve this - use LinuxDistribution fact + mapping with fallback below?
547
551
  # Here to be preferred on openSUSE which also provides aptitude
548
552
  # See: https://github.com/Fizzadar/pyinfra/issues/799
@@ -581,21 +585,21 @@ def packages(
581
585
  ),
582
586
  )
583
587
 
584
- yield from package_operation(packages=packages, present=present)
588
+ yield from package_operation._inner(packages=packages, present=present)
585
589
 
586
590
 
587
- @operation
591
+ @operation()
588
592
  def crontab(
589
- command,
593
+ command: str,
590
594
  present=True,
591
- user=None,
592
- cron_name=None,
595
+ user: str | None = None,
596
+ cron_name: str | None = None,
593
597
  minute="*",
594
598
  hour="*",
595
599
  month="*",
596
600
  day_of_week="*",
597
601
  day_of_month="*",
598
- special_time=None,
602
+ special_time: str | None = None,
599
603
  interpolate_variables=False,
600
604
  ):
601
605
  """
@@ -657,6 +661,8 @@ def crontab(
657
661
 
658
662
  if not existing_crontab and cron_name: # find the crontab by name if provided
659
663
  for cmd, details in crontab.items():
664
+ if not details["comments"]:
665
+ continue
660
666
  if name_comment in details["comments"]:
661
667
  existing_crontab = details
662
668
  existing_crontab_match = cmd
@@ -664,8 +670,8 @@ def crontab(
664
670
 
665
671
  exists = existing_crontab is not None
666
672
 
667
- edit_commands = []
668
- temp_filename = state.get_temp_filename()
673
+ edit_commands: list[str | StringCommand] = []
674
+ temp_filename = host.get_temp_filename()
669
675
 
670
676
  if special_time:
671
677
  new_crontab_line = "{0} {1}".format(special_time, command)
@@ -713,6 +719,7 @@ def crontab(
713
719
 
714
720
  # We have the cron and it exists, do it's details? If not, replace the line
715
721
  elif present and exists:
722
+ assert existing_crontab is not None
716
723
  if any(
717
724
  (
718
725
  special_time != existing_crontab.get("special_time"),
@@ -748,20 +755,6 @@ def crontab(
748
755
 
749
756
  # Finally, use the tempfile to write a new crontab
750
757
  yield "crontab {0} {1}".format(" ".join(crontab_args), temp_filename)
751
-
752
- # Update the crontab fact
753
- if present:
754
- crontab[command] = {
755
- "special_time": special_time,
756
- "minute": minute,
757
- "hour": hour,
758
- "month": month,
759
- "day_of_week": day_of_week,
760
- "day_of_month": day_of_month,
761
- "comments": [cron_name] if cron_name else [],
762
- }
763
- else:
764
- crontab.pop(command)
765
758
  else:
766
759
  host.noop(
767
760
  "crontab {0} {1}".format(
@@ -771,8 +764,8 @@ def crontab(
771
764
  )
772
765
 
773
766
 
774
- @operation
775
- def group(group, present=True, system=False, gid=None):
767
+ @operation()
768
+ def group(group: str, present=True, system=False, gid: int | str | None = None):
776
769
  """
777
770
  Add/remove system groups.
778
771
 
@@ -811,7 +804,6 @@ def group(group, present=True, system=False, gid=None):
811
804
  yield "pw groupdel -n {0}".format(group)
812
805
  else:
813
806
  yield "groupdel {0}".format(group)
814
- groups.remove(group)
815
807
 
816
808
  # Group doesn't exist and we want it?
817
809
  elif present and not is_present:
@@ -837,18 +829,17 @@ def group(group, present=True, system=False, gid=None):
837
829
  group_add_command = "groupadd"
838
830
  if os_type == "FreeBSD":
839
831
  group_add_command = "pw groupadd"
840
- yield "grep '^{0}:' /etc/group || {2} {1}".format(group, " ".join(args), group_add_command)
841
- groups.append(group)
832
+ yield "{0} {1}".format(group_add_command, " ".join(args))
842
833
 
843
834
 
844
- @operation
835
+ @operation()
845
836
  def user_authorized_keys(
846
- user,
847
- public_keys,
848
- group=None,
837
+ user: str,
838
+ public_keys: str | list[str],
839
+ group: str | None = None,
849
840
  delete_keys=False,
850
- authorized_key_directory=None,
851
- authorized_key_filename=None,
841
+ authorized_key_directory: str | None = None,
842
+ authorized_key_filename: str | None = None,
852
843
  ):
853
844
  """
854
845
  Manage `authorized_keys` of system users.
@@ -860,7 +851,7 @@ def user_authorized_keys(
860
851
 
861
852
  Public keys:
862
853
  These can be provided as strings containing the public key or as a path to
863
- a public key file which ``pyinfra`` will read.
854
+ a public key file which pyinfra will read.
864
855
 
865
856
  **Examples:**
866
857
 
@@ -872,8 +863,11 @@ def user_authorized_keys(
872
863
  public_keys=["ed25519..."],
873
864
  )
874
865
  """
866
+
875
867
  if not authorized_key_directory:
876
- authorized_key_directory = f"/home/{user}/.ssh/"
868
+ home = host.get_fact(Home, user=user)
869
+ authorized_key_directory = f"{home}/.ssh"
870
+
877
871
  if not authorized_key_filename:
878
872
  authorized_key_filename = "authorized_keys"
879
873
 
@@ -889,15 +883,15 @@ def user_authorized_keys(
889
883
  with open(try_path, "r") as f:
890
884
  return f.read().strip()
891
885
 
892
- return key
886
+ return key.strip()
893
887
 
894
888
  public_keys = list(map(read_any_pub_key_file, public_keys))
895
889
 
896
890
  # Ensure .ssh directory
897
891
  # note that this always outputs commands unless the SSH user has access to the
898
892
  # authorized_keys file, ie the SSH user is the user defined in this function
899
- yield from files.directory(
900
- authorized_key_directory,
893
+ yield from files.directory._inner(
894
+ path=authorized_key_directory,
901
895
  user=user,
902
896
  group=group or user,
903
897
  mode=700,
@@ -914,7 +908,7 @@ def user_authorized_keys(
914
908
  )
915
909
 
916
910
  # And ensure it exists
917
- yield from files.put(
911
+ yield from files.put._inner(
918
912
  src=keys_file,
919
913
  dest=authorized_key_file,
920
914
  user=user,
@@ -924,7 +918,7 @@ def user_authorized_keys(
924
918
 
925
919
  else:
926
920
  # Ensure authorized_keys exists
927
- yield from files.file(
921
+ yield from files.file._inner(
928
922
  path=authorized_key_file,
929
923
  user=user,
930
924
  group=group or user,
@@ -933,27 +927,27 @@ def user_authorized_keys(
933
927
 
934
928
  # And every public key is present
935
929
  for key in public_keys:
936
- yield from files.line(path=authorized_key_file, line=key, ensure_newline=True)
930
+ yield from files.line._inner(path=authorized_key_file, line=key, ensure_newline=True)
937
931
 
938
932
 
939
- @operation
933
+ @operation()
940
934
  def user(
941
- user,
935
+ user: str,
942
936
  present=True,
943
- home=None,
944
- shell=None,
945
- group=None,
946
- groups=None,
947
- public_keys=None,
937
+ home: str | None = None,
938
+ shell: str | None = None,
939
+ group: str | None = None,
940
+ groups: list[str] | None = None,
941
+ public_keys: str | list[str] | None = None,
948
942
  delete_keys=False,
949
943
  ensure_home=True,
950
944
  create_home=False,
951
945
  system=False,
952
- uid=None,
953
- comment=None,
946
+ uid: int | None = None,
947
+ comment: str | None = None,
954
948
  add_deploy_dir=True,
955
949
  unique=True,
956
- password=None,
950
+ password: str | None = None,
957
951
  ):
958
952
  """
959
953
  Add/remove/update system users & their ssh `authorized_keys`.
@@ -983,7 +977,7 @@ def user(
983
977
 
984
978
  Public keys:
985
979
  These can be provided as strings containing the public key or as a path to
986
- a public key file which ``pyinfra`` will read.
980
+ a public key file which pyinfra will read.
987
981
 
988
982
  **Examples:**
989
983
 
@@ -1029,7 +1023,6 @@ def user(
1029
1023
  yield "pw userdel -n {0}".format(user)
1030
1024
  else:
1031
1025
  yield "userdel {0}".format(user)
1032
- users.pop(user)
1033
1026
  return
1034
1027
 
1035
1028
  # User doesn't exist but we want them?
@@ -1079,29 +1072,20 @@ def user(
1079
1072
 
1080
1073
  # Users are often added by other operations (package installs), so check
1081
1074
  # for the user at runtime before adding.
1082
-
1083
1075
  add_user_command = "useradd"
1084
1076
  if os_type == "FreeBSD":
1085
1077
  add_user_command = "pw useradd"
1086
- yield "grep '^{2}:' /etc/passwd || {0} -n {2} {1}".format(
1078
+ yield "{0} -n {2} {1}".format(
1087
1079
  add_user_command,
1088
1080
  " ".join(args),
1089
1081
  user,
1090
1082
  )
1091
1083
  else:
1092
- yield "grep '^{2}:' /etc/passwd || {0} {1} {2}".format(
1084
+ yield "{0} {1} {2}".format(
1093
1085
  add_user_command,
1094
1086
  " ".join(args),
1095
1087
  user,
1096
1088
  )
1097
- users[user] = {
1098
- "comment": comment,
1099
- "home": home,
1100
- "shell": shell,
1101
- "group": group,
1102
- "groups": groups,
1103
- "password": password,
1104
- }
1105
1089
 
1106
1090
  # User exists and we want them, check home/shell/keys/password
1107
1091
  else:
@@ -1149,9 +1133,9 @@ def user(
1149
1133
  existing_user["password"] = password
1150
1134
 
1151
1135
  # Ensure home directory ownership
1152
- if ensure_home:
1153
- yield from files.directory(
1154
- home,
1136
+ if ensure_home and home:
1137
+ yield from files.directory._inner(
1138
+ path=home,
1155
1139
  user=user,
1156
1140
  group=group or user,
1157
1141
  # Don't fail if the home directory exists as a link
@@ -1160,9 +1144,9 @@ def user(
1160
1144
 
1161
1145
  # Add SSH keys
1162
1146
  if public_keys is not None:
1163
- yield from user_authorized_keys(
1164
- user,
1165
- public_keys,
1147
+ yield from user_authorized_keys._inner(
1148
+ user=user,
1149
+ public_keys=public_keys,
1166
1150
  group=group,
1167
1151
  delete_keys=delete_keys,
1168
1152
  authorized_key_directory="{0}/.ssh".format(home),
@@ -1170,9 +1154,9 @@ def user(
1170
1154
  )
1171
1155
 
1172
1156
 
1173
- @operation
1157
+ @operation()
1174
1158
  def locale(
1175
- locale,
1159
+ locale: str,
1176
1160
  present=True,
1177
1161
  ):
1178
1162
  """
@@ -1221,7 +1205,7 @@ def locale(
1221
1205
  if not present and locale in locales:
1222
1206
  logger.debug(f"Removing locale {locale}")
1223
1207
 
1224
- yield from files.line(
1208
+ yield from files.line._inner(
1225
1209
  path=locales_definitions_file, line=f"^{matching_line}$", replace=f"# {matching_line}"
1226
1210
  )
1227
1211
 
@@ -1231,7 +1215,7 @@ def locale(
1231
1215
  if present and locale not in locales:
1232
1216
  logger.debug(f"Adding locale {locale}")
1233
1217
 
1234
- yield from files.replace(
1218
+ yield from files.replace._inner(
1235
1219
  path=locales_definitions_file,
1236
1220
  text=f"^{matching_line}$",
1237
1221
  replace=f"{matching_line}".replace("# ", ""),
@@ -1240,12 +1224,12 @@ def locale(
1240
1224
  yield "locale-gen"
1241
1225
 
1242
1226
 
1243
- @operation
1227
+ @operation()
1244
1228
  def security_limit(
1245
- domain,
1246
- limit_type,
1247
- item,
1248
- value,
1229
+ domain: str,
1230
+ limit_type: str,
1231
+ item: str,
1232
+ value: int,
1249
1233
  ):
1250
1234
  """
1251
1235
  Edit /etc/security/limits.conf configuration.
@@ -1264,13 +1248,13 @@ def security_limit(
1264
1248
  domain='*',
1265
1249
  limit_type='soft',
1266
1250
  item='nofile',
1267
- value='1024',
1251
+ value=1024,
1268
1252
  )
1269
1253
  """
1270
1254
 
1271
1255
  line_format = f"{domain}\t{limit_type}\t{item}\t{value}"
1272
1256
 
1273
- yield from files.line(
1257
+ yield from files.line._inner(
1274
1258
  path="/etc/security/limits.conf",
1275
1259
  line=f"^{domain}[[:space:]]+{limit_type}[[:space:]]+{item}",
1276
1260
  replace=line_format,
@@ -2,14 +2,16 @@
2
2
  Manage snap packages. See https://snapcraft.io/
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  from pyinfra import host
6
8
  from pyinfra.api import operation
7
9
  from pyinfra.facts.snap import SnapPackage, SnapPackages
8
10
 
9
11
 
10
- @operation
12
+ @operation()
11
13
  def package(
12
- packages=None,
14
+ packages: str | list[str] | None = None,
13
15
  channel="latest/stable",
14
16
  classic=False,
15
17
  present=True,
@@ -91,14 +93,12 @@ def package(
91
93
  else:
92
94
  # we don't want it
93
95
  remove_packages.append(package)
94
- snap_packages.remove(package)
95
96
 
96
97
  # it's not installed
97
98
  if package not in snap_packages:
98
99
  # we want it
99
100
  if present:
100
101
  install_packages.append(package)
101
- snap_packages.append(package)
102
102
 
103
103
  # we don't want it
104
104
  else: