pyinfra 2.9.2__py2.py3-none-any.whl → 3.0b1__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 (126) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +261 -255
  3. pyinfra/api/arguments_typed.py +77 -0
  4. pyinfra/api/command.py +66 -53
  5. pyinfra/api/config.py +27 -22
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +2 -24
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +77 -113
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +17 -25
  13. pyinfra/api/operation.py +232 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +55 -70
  17. pyinfra/connectors/base.py +150 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +227 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +195 -207
  22. pyinfra/connectors/ssh.py +528 -615
  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 +212 -137
  27. pyinfra/connectors/vagrant.py +55 -48
  28. pyinfra/context.py +3 -2
  29. pyinfra/facts/docker.py +1 -0
  30. pyinfra/facts/files.py +45 -32
  31. pyinfra/facts/git.py +3 -1
  32. pyinfra/facts/gpg.py +1 -1
  33. pyinfra/facts/hardware.py +4 -2
  34. pyinfra/facts/iptables.py +5 -3
  35. pyinfra/facts/mysql.py +1 -0
  36. pyinfra/facts/postgres.py +168 -0
  37. pyinfra/facts/postgresql.py +5 -161
  38. pyinfra/facts/selinux.py +3 -1
  39. pyinfra/facts/server.py +77 -30
  40. pyinfra/facts/systemd.py +29 -12
  41. pyinfra/facts/sysvinit.py +10 -10
  42. pyinfra/facts/util/packaging.py +4 -2
  43. pyinfra/local.py +4 -5
  44. pyinfra/operations/apk.py +3 -3
  45. pyinfra/operations/apt.py +25 -47
  46. pyinfra/operations/brew.py +7 -14
  47. pyinfra/operations/bsdinit.py +4 -4
  48. pyinfra/operations/cargo.py +1 -1
  49. pyinfra/operations/choco.py +1 -1
  50. pyinfra/operations/dnf.py +4 -4
  51. pyinfra/operations/files.py +108 -321
  52. pyinfra/operations/gem.py +1 -1
  53. pyinfra/operations/git.py +6 -37
  54. pyinfra/operations/iptables.py +2 -10
  55. pyinfra/operations/launchd.py +1 -1
  56. pyinfra/operations/lxd.py +1 -9
  57. pyinfra/operations/mysql.py +5 -28
  58. pyinfra/operations/npm.py +1 -1
  59. pyinfra/operations/openrc.py +1 -1
  60. pyinfra/operations/pacman.py +3 -3
  61. pyinfra/operations/pip.py +14 -15
  62. pyinfra/operations/pkg.py +1 -1
  63. pyinfra/operations/pkgin.py +3 -3
  64. pyinfra/operations/postgres.py +347 -0
  65. pyinfra/operations/postgresql.py +17 -380
  66. pyinfra/operations/python.py +2 -17
  67. pyinfra/operations/selinux.py +5 -28
  68. pyinfra/operations/server.py +59 -84
  69. pyinfra/operations/snap.py +1 -3
  70. pyinfra/operations/ssh.py +8 -23
  71. pyinfra/operations/systemd.py +7 -7
  72. pyinfra/operations/sysvinit.py +3 -12
  73. pyinfra/operations/upstart.py +4 -4
  74. pyinfra/operations/util/__init__.py +12 -0
  75. pyinfra/operations/util/files.py +2 -2
  76. pyinfra/operations/util/packaging.py +6 -24
  77. pyinfra/operations/util/service.py +18 -37
  78. pyinfra/operations/vzctl.py +2 -2
  79. pyinfra/operations/xbps.py +3 -3
  80. pyinfra/operations/yum.py +4 -4
  81. pyinfra/operations/zypper.py +4 -4
  82. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/METADATA +19 -22
  83. pyinfra-3.0b1.dist-info/RECORD +163 -0
  84. pyinfra-3.0b1.dist-info/entry_points.txt +11 -0
  85. pyinfra_cli/__main__.py +2 -0
  86. pyinfra_cli/commands.py +7 -2
  87. pyinfra_cli/exceptions.py +83 -42
  88. pyinfra_cli/inventory.py +19 -4
  89. pyinfra_cli/log.py +17 -3
  90. pyinfra_cli/main.py +133 -90
  91. pyinfra_cli/prints.py +93 -129
  92. pyinfra_cli/util.py +60 -29
  93. tests/test_api/test_api.py +2 -0
  94. tests/test_api/test_api_arguments.py +13 -13
  95. tests/test_api/test_api_deploys.py +28 -29
  96. tests/test_api/test_api_facts.py +60 -98
  97. tests/test_api/test_api_operations.py +100 -200
  98. tests/test_cli/test_cli.py +18 -49
  99. tests/test_cli/test_cli_deploy.py +11 -37
  100. tests/test_cli/test_cli_exceptions.py +50 -19
  101. tests/test_cli/util.py +1 -1
  102. tests/test_connectors/test_chroot.py +6 -6
  103. tests/test_connectors/test_docker.py +4 -4
  104. tests/test_connectors/test_dockerssh.py +38 -50
  105. tests/test_connectors/test_local.py +11 -12
  106. tests/test_connectors/test_ssh.py +66 -107
  107. tests/test_connectors/test_terraform.py +9 -15
  108. tests/test_connectors/test_util.py +24 -46
  109. tests/test_connectors/test_vagrant.py +4 -4
  110. pyinfra/api/operation.pyi +0 -117
  111. pyinfra/connectors/ansible.py +0 -171
  112. pyinfra/connectors/mech.py +0 -186
  113. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  114. pyinfra/connectors/winrm.py +0 -320
  115. pyinfra/facts/windows.py +0 -366
  116. pyinfra/facts/windows_files.py +0 -90
  117. pyinfra/operations/windows.py +0 -59
  118. pyinfra/operations/windows_files.py +0 -551
  119. pyinfra-2.9.2.dist-info/RECORD +0 -170
  120. pyinfra-2.9.2.dist-info/entry_points.txt +0 -14
  121. tests/test_connectors/test_ansible.py +0 -64
  122. tests/test_connectors/test_mech.py +0 -126
  123. tests/test_connectors/test_winrm.py +0 -76
  124. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/LICENSE.md +0 -0
  125. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/WHEEL +0 -0
  126. {pyinfra-2.9.2.dist-info → pyinfra-3.0b1.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,8 @@
2
2
  The files operations handles filesystem state, file uploads and template generation.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import os
6
8
  import posixpath
7
9
  import sys
@@ -9,10 +11,11 @@ import traceback
9
11
  from datetime import timedelta
10
12
  from fnmatch import fnmatch
11
13
  from io import StringIO
14
+ from pathlib import Path
15
+ from typing import Union
12
16
 
13
17
  from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError
14
18
 
15
- import pyinfra
16
19
  from pyinfra import host, logger, state
17
20
  from pyinfra.api import (
18
21
  FileDownloadCommand,
@@ -28,6 +31,7 @@ from pyinfra.api import (
28
31
  from pyinfra.api.command import make_formatted_string_command
29
32
  from pyinfra.api.util import (
30
33
  get_call_location,
34
+ get_file_io,
31
35
  get_file_sha1,
32
36
  get_path_permissions_mode,
33
37
  get_template,
@@ -54,9 +58,7 @@ from .util import files as file_utils
54
58
  from .util.files import adjust_regex, ensure_mode_int, get_timestamp, sed_replace, unix_path_join
55
59
 
56
60
 
57
- @operation(
58
- pipeline_facts={"file": "dest"},
59
- )
61
+ @operation()
60
62
  def download(
61
63
  src,
62
64
  dest,
@@ -101,7 +103,6 @@ def download(
101
103
  """
102
104
 
103
105
  info = host.get_fact(File, path=dest)
104
- host_datetime = host.get_fact(Date).replace(tzinfo=None)
105
106
 
106
107
  # Destination is a directory?
107
108
  if info is False:
@@ -140,11 +141,10 @@ def download(
140
141
 
141
142
  # If we download, always do user/group/mode as SSH user may be different
142
143
  if download:
143
- temp_file = state.get_temp_filename(dest)
144
+ temp_file = host.get_temp_filename(dest)
144
145
 
145
- curl_args = ["-sSLf"]
146
-
147
- wget_args = ["-q"]
146
+ curl_args: list[Union[str, StringCommand]] = ["-sSLf"]
147
+ wget_args: list[Union[str, StringCommand]] = ["-q"]
148
148
 
149
149
  if proxy:
150
150
  curl_args.append(f"--proxy {proxy}")
@@ -221,30 +221,11 @@ def download(
221
221
  md5sum,
222
222
  QuoteString("MD5 did not match!"),
223
223
  )
224
-
225
- host.create_fact(
226
- File,
227
- kwargs={"path": dest},
228
- data={"mode": mode, "group": group, "user": user, "mtime": host_datetime},
229
- )
230
-
231
- # Remove any checksum facts as we don't know the correct values
232
- for value, fact_cls in (
233
- (sha1sum, Sha1File),
234
- (sha256sum, Sha256File),
235
- (md5sum, Md5File),
236
- ):
237
- fact_kwargs = {"path": dest}
238
- if value:
239
- host.create_fact(fact_cls, kwargs=fact_kwargs, data=value)
240
- else:
241
- host.delete_fact(fact_cls, kwargs=fact_kwargs)
242
-
243
224
  else:
244
225
  host.noop("file {0} has already been downloaded".format(dest))
245
226
 
246
227
 
247
- @operation
228
+ @operation()
248
229
  def line(
249
230
  path,
250
231
  line,
@@ -254,7 +235,6 @@ def line(
254
235
  backup=False,
255
236
  interpolate_variables=False,
256
237
  escape_regex_characters=False,
257
- assume_present=False,
258
238
  ensure_newline=False,
259
239
  ):
260
240
  """
@@ -267,7 +247,6 @@ def line(
267
247
  + flags: list of flags to pass to sed when replacing/deleting
268
248
  + backup: whether to backup the file (see below)
269
249
  + interpolate_variables: whether to interpolate variables in ``replace``
270
- + assume_present: whether to assume a matching line already exists in the file
271
250
  + escape_regex_characters: whether to escape regex characters from the matching line
272
251
  + ensure_newline: ensures that the appended line is on a new line
273
252
 
@@ -354,15 +333,12 @@ def line(
354
333
  # match_line = "{0}.*$".format(match_line)
355
334
 
356
335
  # Is there a matching line in this file?
357
- if assume_present:
358
- present_lines = [line]
359
- else:
360
- present_lines = host.get_fact(
361
- FindInFile,
362
- path=path,
363
- pattern=match_line,
364
- interpolate_variables=interpolate_variables,
365
- )
336
+ present_lines = host.get_fact(
337
+ FindInFile,
338
+ path=path,
339
+ pattern=match_line,
340
+ interpolate_variables=interpolate_variables,
341
+ )
366
342
 
367
343
  # If replace present, use that over the matching line
368
344
  if replace:
@@ -407,58 +383,27 @@ def line(
407
383
 
408
384
  # No line and we want it, append it
409
385
  if not present_lines and present:
410
- # If the file does not exist - it *might* be created, so we handle it
411
- # dynamically with a little script.
412
- if present_lines is None:
413
- yield make_formatted_string_command(
414
- """
415
- if [ -f '{target}' ]; then
416
- ( grep {match_line} '{target}' && \
417
- {sed_replace_command}) 2> /dev/null || \
418
- {echo_command} ;
419
- else
420
- {echo_command} ;
421
- fi
422
- """,
423
- target=QuoteString(path),
424
- match_line=QuoteString(match_line),
425
- echo_command=echo_command,
426
- sed_replace_command=sed_replace_command,
386
+ # If we're doing replacement, only append if the *replacement* line
387
+ # does not exist (as we are appending the replacement).
388
+ if replace:
389
+ # Ensure replace explicitly matches a whole line
390
+ replace_line = replace
391
+ if not replace_line.startswith("^"):
392
+ replace_line = f"^{replace_line}"
393
+ if not replace_line.endswith("$"):
394
+ replace_line = f"{replace_line}$"
395
+
396
+ present_lines = host.get_fact(
397
+ FindInFile,
398
+ path=path,
399
+ pattern=replace_line,
400
+ interpolate_variables=interpolate_variables,
427
401
  )
428
402
 
429
- # File exists but has no matching lines - append it.
403
+ if not present_lines:
404
+ yield echo_command
430
405
  else:
431
- # If we're doing replacement, only append if the *replacement* line
432
- # does not exist (as we are appending the replacement).
433
- if replace:
434
- # Ensure replace explicitly matches a whole line
435
- replace_line = replace
436
- if not replace_line.startswith("^"):
437
- replace_line = f"^{replace_line}"
438
- if not replace_line.endswith("$"):
439
- replace_line = f"{replace_line}$"
440
-
441
- present_lines = host.get_fact(
442
- FindInFile,
443
- path=path,
444
- pattern=replace_line,
445
- interpolate_variables=interpolate_variables,
446
- )
447
-
448
- if not present_lines:
449
- yield echo_command
450
- else:
451
- host.noop('line "{0}" exists in {1}'.format(replace or line, path))
452
-
453
- host.create_fact(
454
- FindInFile,
455
- kwargs={
456
- "path": path,
457
- "pattern": match_line,
458
- "interpolate_variables": interpolate_variables,
459
- },
460
- data=[replace or line],
461
- )
406
+ host.noop('line "{0}" exists in {1}'.format(replace or line, path))
462
407
 
463
408
  # Line(s) exists and we want to remove them, replace with nothing
464
409
  elif present_lines and not present:
@@ -471,27 +416,16 @@ def line(
471
416
  interpolate_variables=interpolate_variables,
472
417
  )
473
418
 
474
- host.delete_fact(
475
- FindInFile,
476
- kwargs={
477
- "path": path,
478
- "pattern": match_line,
479
- "interpolate_variables": interpolate_variables,
480
- },
481
- )
482
-
483
419
  # Line(s) exists and we have want to ensure they're correct
484
420
  elif present_lines and present:
485
421
  # If any of lines are different, sed replace them
486
422
  if replace and any(line != replace for line in present_lines):
487
423
  yield sed_replace_command
488
- del present_lines[:] # TODO: use .clear() when py3+
489
- present_lines.append(replace)
490
424
  else:
491
425
  host.noop('line "{0}" exists in {1}'.format(replace or line, path))
492
426
 
493
427
 
494
- @operation
428
+ @operation()
495
429
  def replace(
496
430
  path,
497
431
  text=None,
@@ -561,22 +495,11 @@ def replace(
561
495
  backup=backup,
562
496
  interpolate_variables=interpolate_variables,
563
497
  )
564
- host.create_fact(
565
- FindInFile,
566
- kwargs={
567
- "path": path,
568
- "pattern": text,
569
- "interpolate_variables": interpolate_variables,
570
- },
571
- data=[],
572
- )
573
498
  else:
574
499
  host.noop('string "{0}" does not exist in {1}'.format(text, path))
575
500
 
576
501
 
577
- @operation(
578
- pipeline_facts={"find_files": "destination"},
579
- )
502
+ @operation()
580
503
  def sync(
581
504
  src,
582
505
  dest,
@@ -648,7 +571,7 @@ def sync(
648
571
  put_files = []
649
572
  ensure_dirnames = []
650
573
  for dirpath, dirnames, filenames in os.walk(src, topdown=True):
651
- remote_dirpath = os.path.normpath(os.path.relpath(dirpath, src))
574
+ remote_dirpath = Path(os.path.normpath(os.path.relpath(dirpath, src))).as_posix()
652
575
 
653
576
  # Filter excluded dirs
654
577
  for child_dir in dirnames[:]:
@@ -682,8 +605,8 @@ def sync(
682
605
  if dest_link_info:
683
606
  dest_to_ensure = dest_link_info["link_target"]
684
607
 
685
- yield from directory(
686
- dest_to_ensure,
608
+ yield from directory._inner(
609
+ path=dest_to_ensure,
687
610
  user=user,
688
611
  group=group,
689
612
  mode=dir_mode or get_path_permissions_mode(src),
@@ -691,8 +614,8 @@ def sync(
691
614
 
692
615
  # Ensure any remote dirnames
693
616
  for dir_path_curr, dir_mode_curr in ensure_dirnames:
694
- yield from directory(
695
- unix_path_join(dest, dir_path_curr),
617
+ yield from directory._inner(
618
+ path=unix_path_join(dest, dir_path_curr),
696
619
  user=user,
697
620
  group=group,
698
621
  mode=dir_mode or dir_mode_curr,
@@ -700,9 +623,9 @@ def sync(
700
623
 
701
624
  # Put each file combination
702
625
  for local_filename, remote_filename in put_files:
703
- yield from put(
704
- local_filename,
705
- remote_filename,
626
+ yield from put._inner(
627
+ src=local_filename,
628
+ dest=remote_filename,
706
629
  user=user,
707
630
  group=group,
708
631
  mode=mode or get_path_permissions_mode(local_filename),
@@ -720,7 +643,7 @@ def sync(
720
643
  if exclude and any(fnmatch(filename, match) for match in exclude):
721
644
  continue
722
645
 
723
- yield from file(filename, present=False)
646
+ yield from file._inner(path=filename, present=False)
724
647
 
725
648
 
726
649
  @memoize
@@ -754,11 +677,11 @@ def rsync(src, dest, flags=["-ax", "--delete"]):
754
677
  yield RsyncCommand(src, dest, flags)
755
678
 
756
679
 
757
- def _create_remote_dir(state, host, remote_filename, user, group):
680
+ def _create_remote_dir(remote_filename, user, group):
758
681
  # Always use POSIX style path as local might be Windows, remote always *nix
759
682
  remote_dirname = posixpath.dirname(remote_filename)
760
683
  if remote_dirname:
761
- yield from directory(
684
+ yield from directory._inner(
762
685
  path=remote_dirname,
763
686
  user=user,
764
687
  group=group,
@@ -771,10 +694,6 @@ def _create_remote_dir(state, host, remote_filename, user, group):
771
694
  # We don't (currently) cache the local state, so there's nothing we can
772
695
  # update to flag the local file as present.
773
696
  is_idempotent=False,
774
- pipeline_facts={
775
- "file": "src",
776
- "sha1_file": "src",
777
- },
778
697
  )
779
698
  def get(
780
699
  src,
@@ -819,11 +738,11 @@ def get(
819
738
 
820
739
  # No remote file, so assume exists and download it "blind"
821
740
  if not remote_file or force:
822
- yield FileDownloadCommand(src, dest, remote_temp_filename=state.get_temp_filename(dest))
741
+ yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
823
742
 
824
743
  # No local file, so always download
825
744
  elif not os.path.exists(dest):
826
- yield FileDownloadCommand(src, dest, remote_temp_filename=state.get_temp_filename(dest))
745
+ yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
827
746
 
828
747
  # Remote file exists - check if it matches our local
829
748
  else:
@@ -832,15 +751,10 @@ def get(
832
751
 
833
752
  # Check sha1sum, upload if needed
834
753
  if local_sum != remote_sum:
835
- yield FileDownloadCommand(src, dest, remote_temp_filename=state.get_temp_filename(dest))
754
+ yield FileDownloadCommand(src, dest, remote_temp_filename=host.get_temp_filename(dest))
836
755
 
837
756
 
838
- @operation(
839
- pipeline_facts={
840
- "file": "dest",
841
- "sha1_file": "dest",
842
- },
843
- )
757
+ @operation()
844
758
  def put(
845
759
  src,
846
760
  dest,
@@ -934,19 +848,19 @@ def put(
934
848
 
935
849
  remote_file = host.get_fact(File, path=dest)
936
850
 
937
- if not remote_file and host.get_fact(Directory, path=dest):
851
+ if not remote_file and bool(host.get_fact(Directory, path=dest)):
938
852
  dest = unix_path_join(dest, os.path.basename(src))
939
853
  remote_file = host.get_fact(File, path=dest)
940
854
 
941
855
  if create_remote_dir:
942
- yield from _create_remote_dir(state, host, dest, user, group)
856
+ yield from _create_remote_dir(dest, user, group)
943
857
 
944
858
  # No remote file, always upload and user/group/mode if supplied
945
859
  if not remote_file or force:
946
860
  yield FileUploadCommand(
947
861
  local_file,
948
862
  dest,
949
- remote_temp_filename=state.get_temp_filename(dest),
863
+ remote_temp_filename=host.get_temp_filename(dest),
950
864
  )
951
865
 
952
866
  if user or group:
@@ -964,7 +878,7 @@ def put(
964
878
  yield FileUploadCommand(
965
879
  local_file,
966
880
  dest,
967
- remote_temp_filename=state.get_temp_filename(dest),
881
+ remote_temp_filename=host.get_temp_filename(dest),
968
882
  )
969
883
 
970
884
  if user or group:
@@ -989,16 +903,8 @@ def put(
989
903
  if not changed:
990
904
  host.noop("file {0} is already uploaded".format(dest))
991
905
 
992
- # Now we've uploaded the file and ensured user/group/mode, update the relevant fact data
993
- host.create_fact(Sha1File, kwargs={"path": dest}, data=local_sum)
994
- host.create_fact(
995
- File,
996
- kwargs={"path": dest},
997
- data={"user": user, "group": group, "mode": mode},
998
- )
999
-
1000
906
 
1001
- @operation
907
+ @operation()
1002
908
  def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True, **data):
1003
909
  '''
1004
910
  Generate a template using jinja2 and write it to the remote system.
@@ -1081,23 +987,21 @@ def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True
1081
987
  data.setdefault("host", host)
1082
988
  data.setdefault("state", state)
1083
989
  data.setdefault("inventory", state.inventory)
1084
- data.setdefault("facts", pyinfra.facts)
1085
990
 
1086
991
  # Render and make file-like it's output
1087
992
  try:
1088
993
  output = get_template(src).render(data)
1089
994
  except (TemplateRuntimeError, TemplateSyntaxError, UndefinedError) as e:
1090
- trace_frames = traceback.extract_tb(sys.exc_info()[2])
1091
995
  trace_frames = [
1092
996
  frame
1093
- for frame in trace_frames
997
+ for frame in traceback.extract_tb(sys.exc_info()[2])
1094
998
  if frame[2] in ("template", "<module>", "top-level template code")
1095
999
  ] # thank you https://github.com/saltstack/salt/blob/master/salt/utils/templates.py
1096
1000
 
1097
1001
  line_number = trace_frames[-1][1]
1098
1002
 
1099
1003
  # Quickly read the line in question and one above/below for nicer debugging
1100
- with open(src, "r") as f:
1004
+ with get_file_io(src, "r") as f:
1101
1005
  template_lines = f.readlines()
1102
1006
 
1103
1007
  template_lines = [line.strip() for line in template_lines]
@@ -1110,16 +1014,16 @@ def template(src, dest, user=None, group=None, mode=None, create_remote_dir=True
1110
1014
  e,
1111
1015
  "\n".join(relevant_lines),
1112
1016
  ),
1113
- )
1017
+ ) from None
1114
1018
 
1115
1019
  output_file = StringIO(output)
1116
1020
  # Set the template attribute for nicer debugging
1117
- output_file.template = src
1021
+ output_file.template = src # type: ignore[attr-defined]
1118
1022
 
1119
1023
  # Pass to the put function
1120
- yield from put(
1121
- output_file,
1122
- dest,
1024
+ yield from put._inner(
1025
+ src=output_file,
1026
+ dest=dest,
1123
1027
  user=user,
1124
1028
  group=group,
1125
1029
  mode=mode,
@@ -1149,14 +1053,11 @@ def _raise_or_remove_invalid_path(fs_type, path, force, force_backup, force_back
1149
1053
  raise OperationError("{0} exists and is not a {1}".format(path, fs_type))
1150
1054
 
1151
1055
 
1152
- @operation(
1153
- pipeline_facts={"link": "path"},
1154
- )
1056
+ @operation()
1155
1057
  def link(
1156
1058
  path,
1157
1059
  target=None,
1158
1060
  present=True,
1159
- assume_present=False,
1160
1061
  user=None,
1161
1062
  group=None,
1162
1063
  symbolic=True,
@@ -1171,7 +1072,6 @@ def link(
1171
1072
  + path: the name of the link
1172
1073
  + target: the file/directory the link points to
1173
1074
  + present: whether the link should exist
1174
- + assume_present: whether to assume the link exists
1175
1075
  + user: user to own the link
1176
1076
  + group: group to own the link
1177
1077
  + symbolic: whether to make a symbolic link (vs hard link)
@@ -1198,22 +1098,6 @@ def link(
1198
1098
  path="/etc/issue2",
1199
1099
  target="/etc/issue",
1200
1100
  )
1201
-
1202
-
1203
- # Complex example demonstrating the assume_present option
1204
- from pyinfra.operations import apt, files
1205
-
1206
- install_nginx = apt.packages(
1207
- name="Install nginx",
1208
- packages=["nginx"],
1209
- )
1210
-
1211
- files.link(
1212
- name="Remove default nginx site",
1213
- path="/etc/nginx/sites-enabled/default",
1214
- present=False,
1215
- assume_present=install_nginx.changed,
1216
- )
1217
1101
  """
1218
1102
 
1219
1103
  path = _validate_path(path)
@@ -1223,8 +1107,7 @@ def link(
1223
1107
 
1224
1108
  info = host.get_fact(Link, path=path)
1225
1109
 
1226
- # Not a link?
1227
- if info is False:
1110
+ if info is False: # not a link
1228
1111
  yield from _raise_or_remove_invalid_path(
1229
1112
  "link",
1230
1113
  path,
@@ -1238,49 +1121,28 @@ def link(
1238
1121
  if symbolic:
1239
1122
  add_args.append("-s")
1240
1123
 
1241
- add_cmd = StringCommand(" ".join(add_args), QuoteString(target), QuoteString(path))
1242
1124
  remove_cmd = StringCommand("rm", "-f", QuoteString(path))
1243
1125
 
1244
- # No link and we want it
1245
- if not assume_present and info is None and present:
1126
+ if not present:
1127
+ if info:
1128
+ yield remove_cmd
1129
+ else:
1130
+ host.noop("link {link} does not exist")
1131
+ return
1132
+
1133
+ assert target is not None # appease typing QuoteString below
1134
+ add_cmd = StringCommand(" ".join(add_args), QuoteString(target), QuoteString(path))
1135
+
1136
+ if info is None: # create
1246
1137
  if create_remote_dir:
1247
- yield from _create_remote_dir(state, host, path, user, group)
1138
+ yield from _create_remote_dir(path, user, group)
1248
1139
 
1249
1140
  yield add_cmd
1250
1141
 
1251
1142
  if user or group:
1252
1143
  yield file_utils.chown(path, user, group, dereference=False)
1253
1144
 
1254
- host.create_fact(
1255
- Link,
1256
- kwargs={"path": path},
1257
- data={"link_target": target, "group": group, "user": user},
1258
- )
1259
-
1260
- # It exists and we don't want it
1261
- elif (assume_present or info) and not present:
1262
- yield remove_cmd
1263
- host.delete_fact(Link, kwargs={"path": path})
1264
-
1265
- # Exists and want to ensure it's state
1266
- elif (assume_present or info) and present:
1267
- if assume_present and not info:
1268
- info = {"link_target": None, "group": None, "user": None}
1269
- host.create_fact(Link, kwargs={"path": path}, data=info)
1270
-
1271
- # If we have an absolute path - prepend to any non-absolute values from the fact
1272
- # and/or the source.
1273
- if os.path.isabs(path):
1274
- link_dirname = os.path.dirname(path)
1275
-
1276
- if not os.path.isabs(target):
1277
- target = os.path.normpath(unix_path_join(link_dirname, target))
1278
-
1279
- if info and not os.path.isabs(info["link_target"]):
1280
- info["link_target"] = os.path.normpath(
1281
- unix_path_join(link_dirname, info["link_target"]),
1282
- )
1283
-
1145
+ else: # edit
1284
1146
  changed = False
1285
1147
 
1286
1148
  # If the target is wrong, remove & recreate the link
@@ -1288,32 +1150,20 @@ def link(
1288
1150
  changed = True
1289
1151
  yield remove_cmd
1290
1152
  yield add_cmd
1291
- info["link_target"] = target
1292
1153
 
1293
1154
  # Check user/group
1294
- if (
1295
- (not info and (user or group))
1296
- or (user and info["user"] != user)
1297
- or (group and info["group"] != group)
1298
- ):
1155
+ if (user and info["user"] != user) or (group and info["group"] != group):
1299
1156
  yield file_utils.chown(path, user, group, dereference=False)
1300
1157
  changed = True
1301
- if user:
1302
- info["user"] = user
1303
- if group:
1304
- info["group"] = group
1305
1158
 
1306
1159
  if not changed:
1307
1160
  host.noop("link {0} already exists".format(path))
1308
1161
 
1309
1162
 
1310
- @operation(
1311
- pipeline_facts={"file": "path"},
1312
- )
1163
+ @operation()
1313
1164
  def file(
1314
1165
  path,
1315
1166
  present=True,
1316
- assume_present=False,
1317
1167
  user=None,
1318
1168
  group=None,
1319
1169
  mode=None,
@@ -1328,7 +1178,6 @@ def file(
1328
1178
 
1329
1179
  + path: name/path of the remote file
1330
1180
  + present: whether the file should exist
1331
- + assume_present: whether to assume the file exists
1332
1181
  + user: user to own the files
1333
1182
  + group: group to own the files
1334
1183
  + mode: permissions of the files as an integer, eg: 755
@@ -1364,8 +1213,7 @@ def file(
1364
1213
  mode = ensure_mode_int(mode)
1365
1214
  info = host.get_fact(File, path=path)
1366
1215
 
1367
- # Not a file?!
1368
- if info is False:
1216
+ if info is False: # not a file
1369
1217
  yield from _raise_or_remove_invalid_path(
1370
1218
  "file",
1371
1219
  path,
@@ -1375,10 +1223,16 @@ def file(
1375
1223
  )
1376
1224
  info = None
1377
1225
 
1378
- # Doesn't exist & we want it
1379
- if not assume_present and info is None and present:
1226
+ if not present:
1227
+ if info:
1228
+ yield StringCommand("rm", "-f", QuoteString(path))
1229
+ else:
1230
+ host.noop("file {0} does not exist")
1231
+ return
1232
+
1233
+ if info is None: # create
1380
1234
  if create_remote_dir:
1381
- yield from _create_remote_dir(state, host, path, user, group)
1235
+ yield from _create_remote_dir(path, user, group)
1382
1236
 
1383
1237
  yield StringCommand("touch", QuoteString(path))
1384
1238
 
@@ -1387,23 +1241,7 @@ def file(
1387
1241
  if user or group:
1388
1242
  yield file_utils.chown(path, user, group)
1389
1243
 
1390
- host.create_fact(
1391
- File,
1392
- kwargs={"path": path},
1393
- data={"mode": mode, "group": group, "user": user},
1394
- )
1395
-
1396
- # It exists and we don't want it
1397
- elif (assume_present or info) and not present:
1398
- yield StringCommand("rm", "-f", QuoteString(path))
1399
- host.delete_fact(File, kwargs={"path": path})
1400
-
1401
- # It exists & we want to ensure its state
1402
- elif (assume_present or info) and present:
1403
- if assume_present and not info:
1404
- info = {"mode": None, "group": None, "user": None}
1405
- host.create_fact(File, kwargs={"path": path}, data=info)
1406
-
1244
+ else: # update
1407
1245
  changed = False
1408
1246
 
1409
1247
  if touch:
@@ -1413,33 +1251,21 @@ def file(
1413
1251
  # Check mode
1414
1252
  if mode and (not info or info["mode"] != mode):
1415
1253
  yield file_utils.chmod(path, mode)
1416
- info["mode"] = mode
1417
1254
  changed = True
1418
1255
 
1419
1256
  # Check user/group
1420
- if (
1421
- (not info and (user or group))
1422
- or (user and info["user"] != user)
1423
- or (group and info["group"] != group)
1424
- ):
1257
+ if (user and info["user"] != user) or (group and info["group"] != group):
1425
1258
  yield file_utils.chown(path, user, group)
1426
1259
  changed = True
1427
- if user:
1428
- info["user"] = user
1429
- if group:
1430
- info["group"] = group
1431
1260
 
1432
1261
  if not changed:
1433
1262
  host.noop("file {0} already exists".format(path))
1434
1263
 
1435
1264
 
1436
- @operation(
1437
- pipeline_facts={"directory": "path"},
1438
- )
1265
+ @operation()
1439
1266
  def directory(
1440
1267
  path,
1441
1268
  present=True,
1442
- assume_present=False,
1443
1269
  user=None,
1444
1270
  group=None,
1445
1271
  mode=None,
@@ -1455,7 +1281,6 @@ def directory(
1455
1281
 
1456
1282
  + path: path of the remote folder
1457
1283
  + present: whether the folder should exist
1458
- + assume_present: whether to assume the directory exists
1459
1284
  + user: user to own the folder
1460
1285
  + group: group to own the folder
1461
1286
  + mode: permissions of the folder
@@ -1494,8 +1319,7 @@ def directory(
1494
1319
  mode = ensure_mode_int(mode)
1495
1320
  info = host.get_fact(Directory, path=path)
1496
1321
 
1497
- # Not a directory?!
1498
- if info is False:
1322
+ if info is False: # not a directory
1499
1323
  if _no_fail_on_link and host.get_fact(Link, path=path):
1500
1324
  host.noop("directory {0} already exists (as a link)".format(path))
1501
1325
  return
@@ -1508,31 +1332,21 @@ def directory(
1508
1332
  )
1509
1333
  info = None
1510
1334
 
1511
- # Doesn't exist & we want it
1512
- if not assume_present and info is None and present:
1335
+ if not present:
1336
+ if info:
1337
+ yield StringCommand("rm", "-rf", QuoteString(path))
1338
+ else:
1339
+ host.noop("directory {0} does not exist")
1340
+ return
1341
+
1342
+ if info is None: # create
1513
1343
  yield StringCommand("mkdir", "-p", QuoteString(path))
1514
1344
  if mode:
1515
1345
  yield file_utils.chmod(path, mode, recursive=recursive)
1516
1346
  if user or group:
1517
1347
  yield file_utils.chown(path, user, group, recursive=recursive)
1518
1348
 
1519
- host.create_fact(
1520
- Directory,
1521
- kwargs={"path": path},
1522
- data={"mode": mode, "group": group, "user": user},
1523
- )
1524
-
1525
- # It exists and we don't want it
1526
- elif (assume_present or info) and not present:
1527
- yield StringCommand("rm", "-rf", QuoteString(path))
1528
- host.delete_fact(Directory, kwargs={"path": path})
1529
-
1530
- # It exists & we want to ensure its state
1531
- elif (assume_present or info) and present:
1532
- if assume_present and not info:
1533
- info = {"mode": None, "group": None, "user": None}
1534
- host.create_fact(Directory, kwargs={"path": path}, data=info)
1535
-
1349
+ else: # update
1536
1350
  if _no_check_owner_mode:
1537
1351
  return
1538
1352
 
@@ -1540,26 +1354,17 @@ def directory(
1540
1354
 
1541
1355
  if mode and (not info or info["mode"] != mode):
1542
1356
  yield file_utils.chmod(path, mode, recursive=recursive)
1543
- info["mode"] = mode
1544
1357
  changed = True
1545
1358
 
1546
- if (
1547
- (not info and (user or group))
1548
- or (user and info["user"] != user)
1549
- or (group and info["group"] != group)
1550
- ):
1359
+ if (user and info["user"] != user) or (group and info["group"] != group):
1551
1360
  yield file_utils.chown(path, user, group, recursive=recursive)
1552
1361
  changed = True
1553
- if user:
1554
- info["user"] = user
1555
- if group:
1556
- info["group"] = group
1557
1362
 
1558
1363
  if not changed:
1559
1364
  host.noop("directory {0} already exists".format(path))
1560
1365
 
1561
1366
 
1562
- @operation(pipeline_facts={"flags": "path"})
1367
+ @operation()
1563
1368
  def flags(path, flags=None, present=True):
1564
1369
  """
1565
1370
  Set/clear file flags.
@@ -1601,22 +1406,13 @@ def flags(path, flags=None, present=True):
1601
1406
  prefix = "" if present else "no"
1602
1407
  new_flags = ",".join([prefix + flag for flag in sorted(to_change)])
1603
1408
  yield StringCommand("chflags", new_flags, QuoteString(path))
1604
- host.create_fact(
1605
- Flags,
1606
- kwargs={"path": path},
1607
- data=list(current_set | set(to_change))
1608
- if present
1609
- else list(current_set - set(to_change)),
1610
- )
1611
1409
  else:
1612
1410
  host.noop(
1613
1411
  f'\'{path}\' already has \'{",".join(flags)}\' {"set" if present else "clear"}',
1614
1412
  )
1615
1413
 
1616
1414
 
1617
- @operation(
1618
- pipeline_facts={"file": "path"},
1619
- )
1415
+ @operation()
1620
1416
  def block(
1621
1417
  path,
1622
1418
  content=None,
@@ -1718,7 +1514,7 @@ def block(
1718
1514
  # standard awk doesn't have an "in-place edit" option so we write to a tempfile and
1719
1515
  # if edits were successful move to dest i.e. we do: <out_prep> ... do some work ... <real_out>
1720
1516
  q_path = QuoteString(path)
1721
- out_prep = 'OUT="$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)" && '
1517
+ out_prep = StringCommand('OUT="$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)" && ')
1722
1518
  if backup:
1723
1519
  out_prep = StringCommand(
1724
1520
  "cp",
@@ -1816,11 +1612,6 @@ def block(
1816
1612
 
1817
1613
  if cmd:
1818
1614
  yield cmd
1819
- host.create_fact(
1820
- Block,
1821
- kwargs={"path": path, "marker": marker, "begin": begin, "end": end},
1822
- data=content,
1823
- )
1824
1615
  else: # remove the marked_block
1825
1616
  if content:
1826
1617
  logger.warning("'content' ignored when removing a marked_block")
@@ -1829,9 +1620,5 @@ def block(
1829
1620
  elif current == []:
1830
1621
  host.noop("no remove required: markers not found")
1831
1622
  else:
1832
- cmd = f"awk '/{mark_1}/,/{mark_2}/ {{next}} 1'"
1623
+ cmd = StringCommand(f"awk '/{mark_1}/,/{mark_2}/ {{next}} 1'")
1833
1624
  yield StringCommand(out_prep, cmd, q_path, "> $OUT &&", real_out)
1834
- host.delete_fact(
1835
- Block,
1836
- kwargs={"path": path, "marker": marker, "begin": begin, "end": end},
1837
- )