pyinfra 3.1.1__py2.py3-none-any.whl → 3.2__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 (45) hide show
  1. pyinfra/api/arguments.py +9 -2
  2. pyinfra/api/deploy.py +4 -2
  3. pyinfra/api/host.py +5 -3
  4. pyinfra/connectors/docker.py +17 -6
  5. pyinfra/connectors/sshuserclient/client.py +26 -14
  6. pyinfra/facts/apk.py +3 -1
  7. pyinfra/facts/apt.py +60 -0
  8. pyinfra/facts/crontab.py +190 -0
  9. pyinfra/facts/docker.py +6 -0
  10. pyinfra/facts/efibootmgr.py +108 -0
  11. pyinfra/facts/files.py +93 -6
  12. pyinfra/facts/git.py +3 -2
  13. pyinfra/facts/mysql.py +1 -2
  14. pyinfra/facts/opkg.py +233 -0
  15. pyinfra/facts/pipx.py +74 -0
  16. pyinfra/facts/podman.py +47 -0
  17. pyinfra/facts/postgres.py +2 -0
  18. pyinfra/facts/server.py +39 -77
  19. pyinfra/facts/util/units.py +30 -0
  20. pyinfra/facts/zfs.py +22 -19
  21. pyinfra/local.py +3 -2
  22. pyinfra/operations/apt.py +27 -20
  23. pyinfra/operations/crontab.py +189 -0
  24. pyinfra/operations/docker.py +13 -12
  25. pyinfra/operations/files.py +18 -0
  26. pyinfra/operations/git.py +23 -7
  27. pyinfra/operations/opkg.py +88 -0
  28. pyinfra/operations/pip.py +3 -2
  29. pyinfra/operations/pipx.py +90 -0
  30. pyinfra/operations/postgres.py +15 -11
  31. pyinfra/operations/runit.py +2 -0
  32. pyinfra/operations/server.py +3 -177
  33. pyinfra/operations/zfs.py +3 -3
  34. {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/METADATA +11 -12
  35. {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/RECORD +45 -36
  36. pyinfra_cli/inventory.py +26 -9
  37. pyinfra_cli/prints.py +18 -3
  38. pyinfra_cli/util.py +3 -0
  39. tests/test_cli/test_cli_deploy.py +15 -13
  40. tests/test_cli/test_cli_inventory.py +53 -0
  41. tests/test_connectors/test_sshuserclient.py +68 -1
  42. {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/LICENSE.md +0 -0
  43. {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/WHEEL +0 -0
  44. {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/entry_points.txt +0 -0
  45. {pyinfra-3.1.1.dist-info → pyinfra-3.2.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,21 @@
1
1
  pyinfra/__init__.py,sha256=7ZcKHGWk7_nYxsYrbFBB_vJr-J-Ddbc56ZS4sk5ArVw,535
2
2
  pyinfra/__main__.py,sha256=aVd00glLz5CMJGXgt1XxbOvC2HluqaowoTOjxgIpBaA,47
3
3
  pyinfra/context.py,sha256=S6DvGjjTEjM4u2m9oIAmAaV7kXIJzVwYf725P1muIuY,3395
4
- pyinfra/local.py,sha256=0bpIRCyDKM6i_jA1i8Ej2qr_iWIF9cUYWutXNdLj8po,2751
4
+ pyinfra/local.py,sha256=wT84xkJc9UBN5isvIVbNpm2fzZaadwE-dkbAwaFQdZk,2808
5
5
  pyinfra/progress.py,sha256=X3hXZ4Flh_L9FE4ZEWxWoG0R4dA5UPd1FCO-Exd5Xtc,4193
6
6
  pyinfra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  pyinfra/version.py,sha256=LZf50PHDzEZv65w0G-iMICoQ9US0U5LWHAOEmNtkF3I,216
8
8
  pyinfra/api/__init__.py,sha256=suGbKKM-qCduuXFYBEcyswlTqozewtYpdLRhK63PVn0,942
9
- pyinfra/api/arguments.py,sha256=51cMf5GHBAodg5SNuGAqafNYuWtCciKR6n8bn664TVI,9943
9
+ pyinfra/api/arguments.py,sha256=GdkwOpBOUKCDer6gxXtChKmRD8iDr_1pQMXXv7jzfDw,10266
10
10
  pyinfra/api/arguments_typed.py,sha256=WQKr0wDtlgJGq-Vkv_oPAz7f-LxjqQv3wJCdvVrePWk,2331
11
11
  pyinfra/api/command.py,sha256=SyUlxvhYlXpgFpg0jua8bzQ2KPtVYQXHcvD6AUL2SCI,7226
12
12
  pyinfra/api/config.py,sha256=wS6Pi4T4DxNkzO4llNY-ghLxyI5VBJ26uGvgMPZxIKY,9043
13
13
  pyinfra/api/connect.py,sha256=Z9wusMLR_jBkKKk5D4AUOj8LHl3H5MsNO5FxAeR4jac,1416
14
14
  pyinfra/api/connectors.py,sha256=nie7JuLxMSC6gqPjmjuCisQ11R-eAQDtMMWF6YbSQ48,659
15
- pyinfra/api/deploy.py,sha256=GbXtjhmUYpnzf7WNlqwIrFw6tOMiP6WjJBmJ4CyvNYM,3053
15
+ pyinfra/api/deploy.py,sha256=CllKp1QYLseeMStj35waZazB0hUuy3iGvDt7b_lH31M,3138
16
16
  pyinfra/api/exceptions.py,sha256=cCbUp1qN1QO0d9aAvOAbRgYpLi0vUI5j7ZqSjcD1_P8,1861
17
17
  pyinfra/api/facts.py,sha256=aMPtkB7vypyXRQDThjwJZzAnEgqjP0wrwyEhRHQf4Js,9449
18
- pyinfra/api/host.py,sha256=ehTWAAEwpWJ8hQU6lU1kcR0bP-WFnrB12QvnMaaSGu4,13752
18
+ pyinfra/api/host.py,sha256=bsYWn_kNQdWsk6y5ExCJ4FYVq3KlgfHSPnciC0MsrqE,13853
19
19
  pyinfra/api/inventory.py,sha256=nPITdNEJ7q71adIqS_OKHsMjD7amUuHEuTl6xzgh1Gk,7734
20
20
  pyinfra/api/operation.py,sha256=Dp7pH9H3EYs7U1ZvquYUbOtWJPO9iIAa4H7GwXdxFxs,15170
21
21
  pyinfra/api/operations.py,sha256=jvz9ISfwmQnAQVUKLnbrRdD9QHIAAfypo9l5b3fYG1w,10894
@@ -24,7 +24,7 @@ pyinfra/api/util.py,sha256=K4aFjGW7KAz2ZQqfRriRqyHMCQFFrX06WPola3epjaE,12410
24
24
  pyinfra/connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  pyinfra/connectors/base.py,sha256=2fASiV-MvpXgcZAFLM_PUwYx5ax6EHai44ri_oEKeSE,3732
26
26
  pyinfra/connectors/chroot.py,sha256=X5U3vi-zPm4DV-lHTwoHU8iEl8dppDiYJoWP_1mJVFY,5919
27
- pyinfra/connectors/docker.py,sha256=2UNHhXS4hpLo7I19ixDeSd7JR8SNo43VgqsaUIZQZJ4,8741
27
+ pyinfra/connectors/docker.py,sha256=GwWTx8Mx1AnKl0vUNSvbyoHTjQNPS0njR-tA4lNViAU,9196
28
28
  pyinfra/connectors/dockerssh.py,sha256=VWHY--jqs3yf-RuPUZXav4vLeON9SzoVC9CUyOJo1rg,8919
29
29
  pyinfra/connectors/local.py,sha256=eFDrBalS1yPCIUdQzh8h2HF3VP2nDn0uVFKOeeQZfiw,6888
30
30
  pyinfra/connectors/ssh.py,sha256=Pr7W_aiGstkWQqrwm-cNENzhzTWTBLWM-XAAbF9uOEQ,21212
@@ -33,40 +33,45 @@ pyinfra/connectors/terraform.py,sha256=Tu59cbemll5CfqlIaQtOrLa0HKzl23c64ih0DZXJu
33
33
  pyinfra/connectors/util.py,sha256=0bvoMsGMD-Tbfaer8NUhWJjBnaNKdmE83PDg48BYjcU,11374
34
34
  pyinfra/connectors/vagrant.py,sha256=oEeRglzRmemRXW3vilsp_Xg9qnZMRprRJO9fd_C-f5M,4759
35
35
  pyinfra/connectors/sshuserclient/__init__.py,sha256=Qc4RO2wknSWIiNTwOeQ0y2TeiuKHmyWDW2Dz4MOo9CE,44
36
- pyinfra/connectors/sshuserclient/client.py,sha256=24KWAAqIaUPQIod-CSeXKkA_WhQnIIGWaLKlnVUATDY,9746
36
+ pyinfra/connectors/sshuserclient/client.py,sha256=6tN-dJYxho0nmyfXRLeaEs6EtdPdDNNACROOuZSoUos,10037
37
37
  pyinfra/connectors/sshuserclient/config.py,sha256=UMwkvTgAIS7__re6Wz_pwH6EU4kO1-uMQ5zuFakH0v4,2721
38
38
  pyinfra/facts/__init__.py,sha256=myTXSOZmAqmU88Fyifn035h9Lr6Gj2mlka_jDcXyKGw,347
39
- pyinfra/facts/apk.py,sha256=q76WdaCNZGKzYia5vMscCsOi4WlnBhcj_9c7Jj-2LqQ,581
40
- pyinfra/facts/apt.py,sha256=l_ABTyUFlh6C2W6oDdv-9x4MPDJ4M47RRtx8qsTSpJc,2175
39
+ pyinfra/facts/apk.py,sha256=83kr2SpqFGXuH5Z0T5dF2zbn9rM3Eh7RzAQvOwMWuC8,717
40
+ pyinfra/facts/apt.py,sha256=Ou5FazOmJFzNtfveH-4vuZH8LdHlUBR7UGRgUX_7oEY,3904
41
41
  pyinfra/facts/brew.py,sha256=qDz89ZlZOiCZv0GLOXOjgFtqq66SLaYXgCncYP2LwDs,2584
42
42
  pyinfra/facts/bsdinit.py,sha256=hyESeGu0hPf8HY1D0bIFFFNFpXRdZB2R52aflVQPf9o,577
43
43
  pyinfra/facts/cargo.py,sha256=OQF6nOulp2TIaFK1fiAEevsXnL5OMQUL6LkFHidb1yo,605
44
44
  pyinfra/facts/choco.py,sha256=A0VCXnI5H9RocgO1IvaNWRIxnXiIZYEzIDG1F-ydJi4,790
45
+ pyinfra/facts/crontab.py,sha256=dTiAZPXV6DCi2BeEl33PP5k8c9ODkzgIDD6cPC5l2pA,5674
45
46
  pyinfra/facts/deb.py,sha256=XGyxnow9wjpE8ZKTZDa1_SNChMyMcNgFeTG1ka5uky4,1922
46
47
  pyinfra/facts/dnf.py,sha256=9rTBgLHewbk8XCJuikzAYCumfFAzbmmHMchlaXBhdWw,977
47
- pyinfra/facts/docker.py,sha256=CVSsUEiBaQrNvM1mggoKCXj5DdzwmcbufUY96koqKBw,2250
48
- pyinfra/facts/files.py,sha256=qJw5_xnPxS5orFy2Ch4oUjmiFUFGGe71UEXoXdZBSek,11765
48
+ pyinfra/facts/docker.py,sha256=tA-H2MP54iBkV5zgpCaTmPEZH6P-n59E5lRXplJ1iBk,2474
49
+ pyinfra/facts/efibootmgr.py,sha256=L8hIYLxRTNAgIdf7djZ3z-TylbnMnbagl7OuJQXHKWE,3473
50
+ pyinfra/facts/files.py,sha256=E4eS15wpNgCmlpNFWRYJuuy6DcPCzhzULwp1sJ-iyqg,15077
49
51
  pyinfra/facts/flatpak.py,sha256=lnuFZYGPtPDe35YXTVgn5M0rhgcA5ys3fMI6EmvCnm4,1536
50
52
  pyinfra/facts/gem.py,sha256=ktC2hofSwYX0fVcdWleU74ddjW1RPZvKMW-3wYp9lJE,572
51
- pyinfra/facts/git.py,sha256=6e_2GjDT2oAxdtkHTyeMYQ9N64gZDorLTTVeZhFel18,1276
53
+ pyinfra/facts/git.py,sha256=-UhY10Jx8MO537Dstm_jsn42Ezx5SsUZT315Qx7b4C0,1347
52
54
  pyinfra/facts/gpg.py,sha256=wYKoQl4aHXB1UqqbWCdVhUoa6N6Liz01AmH8fPjxR48,3813
53
55
  pyinfra/facts/hardware.py,sha256=9UOgvzlJ7jx7hPfmkhldYEymKEiKMR3ZVFGfZXl_oe0,12023
54
56
  pyinfra/facts/iptables.py,sha256=sUkywfHZUXnMZF_KshAnyJufrJvZ9fBYnERSRbwOCRE,3374
55
57
  pyinfra/facts/launchd.py,sha256=D0RSpBQXt4a4zJOxwxIb5RYY34sAx6ZrWj8NxwYqhIc,767
56
58
  pyinfra/facts/lxd.py,sha256=NeIMK9VsDXlRf7q3j17oYbdigL7XjLnhmqv63t6CrzQ,466
57
- pyinfra/facts/mysql.py,sha256=dHEn3E5rTe4aw3DNHikmgj_VBkQeJZs-gfuS5N35BqE,6135
59
+ pyinfra/facts/mysql.py,sha256=w2XphDMeEFBrvWp5MK_DC0_7GoI1RYlheovjhuIiTBw,6123
58
60
  pyinfra/facts/npm.py,sha256=rI3at2M6Rfwb4cPj5gTivMTss5POs5_W2Ilcrgv8XAA,756
59
61
  pyinfra/facts/openrc.py,sha256=gAXl3evsDXSHcIKIj4E_CRerAugEFpGGNRxJBr0rBAk,1490
62
+ pyinfra/facts/opkg.py,sha256=bnVOPnLIevAKMb_dnpSohzRmTTbVmSo7R5H1XrChfgI,7210
60
63
  pyinfra/facts/pacman.py,sha256=NJ43rYc5k4uhl5sFxd9sTg7yqTQhB3xsREsusS6BzwU,1198
61
64
  pyinfra/facts/pip.py,sha256=ORyrVxu_-8eIr0uSCkI6x4MH-sorDEavml4aozcdrOA,738
65
+ pyinfra/facts/pipx.py,sha256=5iZSnhYq8ZZ8dOhEmek254_nXT1nEm4BulU56TPnGuM,1730
62
66
  pyinfra/facts/pkg.py,sha256=iIvinsJxEaSYAaRw_5_WD5WlIlcy4ksKa0lwj0CFxco,520
63
67
  pyinfra/facts/pkgin.py,sha256=Y4QfUwtWcBH-h77O-MtiixSepHK0p-lcebrqdmABfs8,580
64
- pyinfra/facts/postgres.py,sha256=7cg0Nq8wNIWnKw3B8dJpgjSFZ7q90-_NhwEw2NsSJm8,4229
68
+ pyinfra/facts/podman.py,sha256=4vk2qhfFFd66tAE9zVjNUkvDTbntjrTkMkhMEvjQIXo,1091
69
+ pyinfra/facts/postgres.py,sha256=K_2JCJhmulJeyVPE5jAJhpgPpM7OF5LVJcfgvRg6icg,4293
65
70
  pyinfra/facts/postgresql.py,sha256=4nusMVvGhtku86KX4O4vjSxh_MamxZy_kmTQvvy0GhE,223
66
71
  pyinfra/facts/rpm.py,sha256=SzHNCNUMA-j5uJl4PKRTFpckOvNZ2zpxNeQyOCl8Usk,2225
67
72
  pyinfra/facts/runit.py,sha256=iarF_Tql8bkNeHsKGRANRKNyBWwMsflsTNXj1Wz14i8,2021
68
73
  pyinfra/facts/selinux.py,sha256=as3AvC6p88er0rYBFTdIWf6k3w0pVjDqDAV9Ur5zY90,4443
69
- pyinfra/facts/server.py,sha256=fDXSNNlZghJTGqr9CWRDob-_N-8xxb-KUZlTf5No-M0,20439
74
+ pyinfra/facts/server.py,sha256=bJ9gg8q_wUKwdJgETbXhOVRiJ-4WKkZn30qeeFhQT9A,19064
70
75
  pyinfra/facts/snap.py,sha256=6br9IMIoq88z_RS0FLXxfodIVjUmyPU9eZBa9zO8H1o,2027
71
76
  pyinfra/facts/systemd.py,sha256=RS6pdcgpIvWMbQeT93O57EKXQzFzR0tF29lCAJQmaAk,4227
72
77
  pyinfra/facts/sysvinit.py,sha256=RniaROHyeZD3jVOa_sISpZV4zx8ae8HkUQrtriLIlWc,1521
@@ -74,42 +79,46 @@ pyinfra/facts/upstart.py,sha256=HYR7vJ6oqtuRhxXQgzGDKYzyKqqVsjT-TtPPWOjBGdA,635
74
79
  pyinfra/facts/vzctl.py,sha256=S9aclpDBF3DmBLwMltsd9j3B4QxQ5-1Kb1hybZodEqI,678
75
80
  pyinfra/facts/xbps.py,sha256=4gAajBlTAg3bo7vRdx3b2TTi-vvU1y86WZqC0H9nUUk,573
76
81
  pyinfra/facts/yum.py,sha256=i42g0FIZg62TZFqFcaUQWNekFFFo4G8vf5wyaKUuh8Q,938
77
- pyinfra/facts/zfs.py,sha256=MKqh1AEa5Bpa0KDT673e92TiOb8K7YHoEg-Xo424De8,1311
82
+ pyinfra/facts/zfs.py,sha256=YC9_-BnDkmhDtUUGWy2NpE-rlz5Xzl89uvHNCFpPSYM,1501
78
83
  pyinfra/facts/zypper.py,sha256=sAIZ5SqjsJ1Dc5e3pJrOoR5Gnu9BqZHpDFI8gKLts84,873
79
84
  pyinfra/facts/util/__init__.py,sha256=f7HKu8z9_yFC899ajJ3RFiyivioaZeGfOI6nf9GviCs,521
80
85
  pyinfra/facts/util/databases.py,sha256=EphGQApzRBXI2nG1FL9h8bozY-o4SgdQgpv9YcnCkxs,730
81
86
  pyinfra/facts/util/packaging.py,sha256=4RzjDYb3HrRWZuuPlEfYHgbftLH4r1FOccN5QyIGkrk,1181
87
+ pyinfra/facts/util/units.py,sha256=SNHCisxGwZedCOqO9tfOWJpZ5Stc0Wcg9mZcXoKBY0A,714
82
88
  pyinfra/facts/util/win_files.py,sha256=S_IQ5kJD6ZgkEcVHajgh7BIMolLV-1q1ghIcwAS-E1Q,2561
83
89
  pyinfra/operations/__init__.py,sha256=SOcW337KXIzD_LH-iJJfq14BQcCs5JzwswJ0PIzDgF4,357
84
90
  pyinfra/operations/apk.py,sha256=_0vOjbSiGx6EWv9rvTmQdGnRZQ_NA_Dyd3QW1cTzFgI,2111
85
- pyinfra/operations/apt.py,sha256=JztxgKeSTienk8pFWCK9vfH-Gg7i1AtUCFbTZDptcuA,13884
91
+ pyinfra/operations/apt.py,sha256=OKAMTcvS83KwF9lCmLZ09swKezwD7GdIUJu6dGTfRao,14152
86
92
  pyinfra/operations/brew.py,sha256=aghLE4qyuhhRbt6fgSPV6_5fyWgTohA77Dc0gol19UU,5155
87
93
  pyinfra/operations/bsdinit.py,sha256=okQUQDr2H8Z-cAdfdbPJiuGujsHLuV5gpuMZ1UlICEM,1648
88
94
  pyinfra/operations/cargo.py,sha256=mXWd6pb0IR6kzJMmPHwXZN-VJ-B_y8AdOFlrRzDQOZI,1104
89
95
  pyinfra/operations/choco.py,sha256=8nG0wc1tZEA0L0HTIjgR00IDiONARokyzHyKj-R3xmo,1515
96
+ pyinfra/operations/crontab.py,sha256=qhYzj9xh-A6p95sJ0i_DDKOIm7WoNgiPjcR43ZB9iv8,6454
90
97
  pyinfra/operations/dnf.py,sha256=3154Rer6dejVB1AK-CqyJhpMVn_djaSDJrVMs62GNcE,5599
91
- pyinfra/operations/docker.py,sha256=RMkrVpS-eeN5zwGnpb3WeeOAoGvFma-A1aPvjE9M1KY,8336
92
- pyinfra/operations/files.py,sha256=M3Dj9dfBpqNnHxrm0gjDq5kNK_8ERL6iV-TEMme2S0M,54092
98
+ pyinfra/operations/docker.py,sha256=6jkeZT0JW8f5df2N3ANai4JOkGBnGd3nETE39_Ho7kM,8441
99
+ pyinfra/operations/files.py,sha256=BkDn4hTsic2T1r_0GAHz-r1Nxd6FZMWoTFqf8izu6VU,54707
93
100
  pyinfra/operations/flatpak.py,sha256=c2OAyuAvt3alVm9D8W6gCfmk5JFydcZD36gO_OhB8Bc,1891
94
101
  pyinfra/operations/gem.py,sha256=2C85sOwIRMHGvmPg4uAlUVf6MokhiA7LLPqzdJRHsBg,1132
95
- pyinfra/operations/git.py,sha256=HXBq07YtEQSo-3bkjBvMem5WzeemQ4omHkh6txQxQKs,12487
102
+ pyinfra/operations/git.py,sha256=ajyusK8rFeU_u3piR3glzJGLhomKF0IuzlbR667eyts,13035
96
103
  pyinfra/operations/iptables.py,sha256=brYa4kMhZKFTu24BNds_1b6sOaG94EfqWEoWrScx-Ck,9341
97
104
  pyinfra/operations/launchd.py,sha256=6HWvqoQ74idV_NStOEmFXwu0dmTv7YDvFtsK8An2Lu4,1177
98
105
  pyinfra/operations/lxd.py,sha256=bKm9gsgZaruKYSL7OYFMiou-wGP4BzwIMWzjW4AZYrk,1742
99
106
  pyinfra/operations/mysql.py,sha256=ctm2Z6MaB0mOArCNU4TsJzaXiKXQaa_ahmsC5Vvyi10,19857
100
107
  pyinfra/operations/npm.py,sha256=bUmfQsClZ2YcHiihiC7k5widIXIi6lbfx_32iyaAKfo,1499
101
108
  pyinfra/operations/openrc.py,sha256=GXFoCHEEKeyQyRvrZcNYx8og4fmgmtzTVAViBzt84TE,1580
109
+ pyinfra/operations/opkg.py,sha256=0P_YyXSvXUyVWJrHtnRxI_etya8yMIny7_LcgbQddEU,2627
102
110
  pyinfra/operations/pacman.py,sha256=QMjmsBiiw362nhZY0rEDVQL5A32MG3u7GcmX4q4PzfI,1702
103
- pyinfra/operations/pip.py,sha256=7PpQvZHnwBGZ60V5b0XKNR4tHLW0MXJo6_6UX7HBtGY,5856
111
+ pyinfra/operations/pip.py,sha256=MCmb9FPcyvg6M7lTlRtx2qpXHtyf-SwBVtHqcAkqVzQ,5925
112
+ pyinfra/operations/pipx.py,sha256=PFVXriRIk5gnJXWcFoghsCIVfTy7RvQyvDggjXHALQc,2188
104
113
  pyinfra/operations/pkg.py,sha256=rORQBbKeb-6gS0LYu0a0VdiWcDZoovcUONCaf6KMdeQ,2298
105
114
  pyinfra/operations/pkgin.py,sha256=zhUyGzKjnUfGoyHbMoYMbeeMzcsiOUpBz1zIzppigJ0,1992
106
- pyinfra/operations/postgres.py,sha256=eh3wjX-l4ri-q3mgfV2bdmVs3m87s3C1_EbJUFss9u4,9700
115
+ pyinfra/operations/postgres.py,sha256=qQZxTfMinl6PNR0l6Gt0dxsbnyUOZxQ2OIPi9UQh_cQ,9867
107
116
  pyinfra/operations/postgresql.py,sha256=agZjL2W4yxigk9ThIC0V_3wvmcWVdX308aJO24WkN6g,833
108
117
  pyinfra/operations/puppet.py,sha256=eDe8D9jQbHYQ4_r4-dmEZfMASKQvj36BR8z_h8aDfw8,861
109
118
  pyinfra/operations/python.py,sha256=u569cdPrPesrmzU09nwIPA3bk6TZ-Qv2QP0lJLcO_bw,2021
110
- pyinfra/operations/runit.py,sha256=jRR5kt1OUCLbYktnu7yl3YvSiTW51VvEvOuB0yfd7Ww,5126
119
+ pyinfra/operations/runit.py,sha256=-K0GhORE56J4Gjh7PCriSx9zZI7XJV-cIb-LnnSuKdY,5162
111
120
  pyinfra/operations/selinux.py,sha256=imZ4dbY4tl0GpBSkUgV983jbDDihWNs_OQkOBulT7FQ,5948
112
- pyinfra/operations/server.py,sha256=0xDKY9tM7ZvGYxj5zWppmP1fhLUBeGxSsnAxciGifnI,36572
121
+ pyinfra/operations/server.py,sha256=iz8r8MdufX6LAE6Ua4BJTIp6zPHsmY0DT-B6ifA-L9g,30700
113
122
  pyinfra/operations/snap.py,sha256=a-QtNE4Dlsavqq425TUIwpEJu4oGw8UlLRkdTFyT1F8,3049
114
123
  pyinfra/operations/ssh.py,sha256=wocoaYDlOhhItItAVQCEfnVowTtkg3AP0hQ3mnpUnl0,5634
115
124
  pyinfra/operations/systemd.py,sha256=hPHTjASj6N_fRAzLr3DNHnxxIbiiTIIT9UStSxKDkTk,3984
@@ -118,7 +127,7 @@ pyinfra/operations/upstart.py,sha256=pHb9RGnVhT14A_y6OezfOH-lmniKpiyJqpeoOJl0beE
118
127
  pyinfra/operations/vzctl.py,sha256=2u2CDkuDjzHBRQ54HfyfLpLrsbT8U7_05EEjbbhKUiU,3110
119
128
  pyinfra/operations/xbps.py,sha256=ru3_srMBUyUXGzAsPo7WwoomfM0AeDglFv8CDqB33B0,1508
120
129
  pyinfra/operations/yum.py,sha256=Ig7AzQy1C7I8XM37lWbw0nI5lzFGMoX30P8FV8-V5uA,5600
121
- pyinfra/operations/zfs.py,sha256=CYaKf_Yealluo0suk9vuhxt-vEUlLfTVbAO35oalvT8,5271
130
+ pyinfra/operations/zfs.py,sha256=FnhiXreqf5qJBusC31dwrQsEi0MvH4qxzjBayHKFcYY,5283
122
131
  pyinfra/operations/zypper.py,sha256=z1CWv2uwWBlCLIhHna7U5DojVoKZYoUYpezJ_FM_xK8,5555
123
132
  pyinfra/operations/util/__init__.py,sha256=ZAHjeCXtLo0TIOSfZ9h0Sh5IXXRCspfHs3RR1l8tQCE,366
124
133
  pyinfra/operations/util/docker.py,sha256=6CvQgeFAXH_lDqKb7RxWpMvlCDwEAXlBaDZoJ8LxrYg,4596
@@ -129,11 +138,11 @@ pyinfra_cli/__init__.py,sha256=G0X7tNdqT45uWuK3aHIKxMdDeCgJ7zHo6vbxoG6zy_8,284
129
138
  pyinfra_cli/__main__.py,sha256=WlW7eP0rrL06eguuD_q2RAqgUjg3SW-QnmrayAh2mBQ,887
130
139
  pyinfra_cli/commands.py,sha256=J-mCJYvDebJ8M7o3HreB2zToa871-xO6_KjVhPLeHho,1832
131
140
  pyinfra_cli/exceptions.py,sha256=iptx9Zj1od7VgSbOyXs7P8tD4zAZ_fwrQFKPlpPrfS0,4806
132
- pyinfra_cli/inventory.py,sha256=vuSL7dU31hxazHmJoUI0c6QjdItG78x8O5ifLUWuMeI,11292
141
+ pyinfra_cli/inventory.py,sha256=05Z0LpMW8Qt3c2X2sx-roYCQGxNlEDD0RgZILpXCcJs,11768
133
142
  pyinfra_cli/log.py,sha256=7WEGtmf3ncF1BtXL2icUjyxeRKy-7XrCcQ2Hg4GWX5Y,2201
134
143
  pyinfra_cli/main.py,sha256=5VTniMcbKuIfjPTzaUklad5fM1BW7CUEARoSV9tPf1U,19954
135
- pyinfra_cli/prints.py,sha256=heCF-ugz0F8gTSr--rYVtRqN6jpAun5DUA4cy0F8l5A,9696
136
- pyinfra_cli/util.py,sha256=s3-Y2AnRaLLULneaDw8YX1scWTFvGBPADPpYf3RtBms,6362
144
+ pyinfra_cli/prints.py,sha256=wVsTZgqEoL7Q_9dPMRoQXFZdYTIPjSGHhtGjjndEaXg,10291
145
+ pyinfra_cli/util.py,sha256=9ehdJQ8pDNBLHXoFst9p696VDT-b_qo8UFMJrqdE1rE,6424
137
146
  pyinfra_cli/virtualenv.py,sha256=6j9W54JkQLN02SrZZIVwszp0GxlaaDEUWFZjBDHIWNA,2466
138
147
  tests/test_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
148
  tests/test_api/test_api.py,sha256=Ig2ebkNACYbHcC4_zRkxS9vj5ZEogoPqGx30ErIKChg,2413
@@ -148,9 +157,9 @@ tests/test_api/test_api_operations.py,sha256=GUfnuHK2NoTAGdOT4AbytT9R8i3ZZIvGP7K
148
157
  tests/test_api/test_api_util.py,sha256=uHv4oLpoy1_tzOoqFA1zpdvC74SvjitZbxQwp0dmjTs,1716
149
158
  tests/test_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
159
  tests/test_cli/test_cli.py,sha256=IeWuhkhLzIkRbOEx5-yaW6xV5l4Y8fxaGaDGlMcOyYE,6016
151
- tests/test_cli/test_cli_deploy.py,sha256=3tlXpN_ntCvZDeymfQKrZm0kgADkiLUIAGQg5V8-KrU,5079
160
+ tests/test_cli/test_cli_deploy.py,sha256=vZC7twj8sPCy05loO50P-D_Xf73r6XN4m7yVj7TIFmo,5243
152
161
  tests/test_cli/test_cli_exceptions.py,sha256=QaUv40q6Tp0HdcVEvPggFF4dsP2qdy57y9VAcGcR1So,3060
153
- tests/test_cli/test_cli_inventory.py,sha256=xlo-p3HdfVPNqxi7SknEZ2mWrKsdDaK3PoVN-tl95Z0,2394
162
+ tests/test_cli/test_cli_inventory.py,sha256=feYSyxGBUsDGriOaqs0CBMbAIW7yUfoT1mktLwqMI0w,4036
154
163
  tests/test_cli/test_cli_util.py,sha256=Fqn_9fnG6xNfw8OMHSZs9hQoW260aSt409S1bUAJs-Q,2500
155
164
  tests/test_cli/test_context_objects.py,sha256=JiUTwQP7yvcqA47Kq9jtdsB_Z8nxGMZN46d9pR--FYA,2130
156
165
  tests/test_cli/util.py,sha256=kp_-XsGnTyDgG6IHWorYzl5VD_WLe77dKOH007TDOUE,338
@@ -160,13 +169,13 @@ tests/test_connectors/test_docker.py,sha256=0EjkfhCHpLCfL4X-AIdMNw5ASaseY0tbRAn7
160
169
  tests/test_connectors/test_dockerssh.py,sha256=MaC9IK1OZDiqoIsuLOZBJnPDglsMoPDoL19LQtXsyCE,9303
161
170
  tests/test_connectors/test_local.py,sha256=N_FkejDZKu7XLnKeApqfBARYMyxf-hRXCQJrXLHvwRg,7442
162
171
  tests/test_connectors/test_ssh.py,sha256=zYL0FbRXzqkYJslhmVeUgSkcHtozhmvZfRcaqDrYKvI,40386
163
- tests/test_connectors/test_sshuserclient.py,sha256=2PQNLPhNL6lBACc6tQuXmPoog-9L6AdDQNrA-rEw1_8,5734
172
+ tests/test_connectors/test_sshuserclient.py,sha256=Rm1zxSODDXQXuQ3qdkCA12FCUj7Zkgwwe-s2UrsZAoE,8599
164
173
  tests/test_connectors/test_terraform.py,sha256=RZInSjes394eR5CrGGEjzZEFY-UpQj47n4MZH0_ExyY,3779
165
174
  tests/test_connectors/test_util.py,sha256=hQir0WyjH0LEF6xvIyHNyqdI5pkJX6qUR9287MgO2bY,4647
166
175
  tests/test_connectors/test_vagrant.py,sha256=27qRB7ftjEPaj4ejBNZ-rR4Ou1AD1VyVcf2XjwZPG3M,3640
167
- pyinfra-3.1.1.dist-info/LICENSE.md,sha256=gwC95tUll0gwB32tHNkTAasN7Sb6vjWzXa305NwClbI,1076
168
- pyinfra-3.1.1.dist-info/METADATA,sha256=j3fopNCs_edZ2bzLHKAjoCv7_1CFph0D5q56yBJa-sg,8041
169
- pyinfra-3.1.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
170
- pyinfra-3.1.1.dist-info/entry_points.txt,sha256=BraEFyquy05M8ch33HZXOHoH_m2BTqejL3xX3NrpzOM,471
171
- pyinfra-3.1.1.dist-info/top_level.txt,sha256=2K6D1mK35JTSEBgOfEPV-N-uA2SDErxGiE0J-HUMMVI,26
172
- pyinfra-3.1.1.dist-info/RECORD,,
176
+ pyinfra-3.2.dist-info/LICENSE.md,sha256=gwC95tUll0gwB32tHNkTAasN7Sb6vjWzXa305NwClbI,1076
177
+ pyinfra-3.2.dist-info/METADATA,sha256=edyD-006yB75ALtE2Hz3_Oqj4DLFUJjEMvfs7fwQw44,8011
178
+ pyinfra-3.2.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
179
+ pyinfra-3.2.dist-info/entry_points.txt,sha256=BraEFyquy05M8ch33HZXOHoH_m2BTqejL3xX3NrpzOM,471
180
+ pyinfra-3.2.dist-info/top_level.txt,sha256=2K6D1mK35JTSEBgOfEPV-N-uA2SDErxGiE0J-HUMMVI,26
181
+ pyinfra-3.2.dist-info/RECORD,,
pyinfra_cli/inventory.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import socket
2
2
  from collections import defaultdict
3
3
  from os import listdir, path
4
- from types import GeneratorType
5
4
  from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union
6
5
 
7
6
  from pyinfra import logger
@@ -23,18 +22,37 @@ def _is_inventory_group(key: str, value: Any):
23
22
  Verify that a module-level variable (key = value) is a valid inventory group.
24
23
  """
25
24
 
26
- if key.startswith("_") or not isinstance(value, (list, tuple, GeneratorType)):
25
+ if key.startswith("__"):
26
+ # Ignore __builtins__/__file__
27
+ return False
28
+ elif key.startswith("_"):
29
+ logger.debug(
30
+ 'Ignoring variable "%s" in inventory file since it starts with a leading underscore',
31
+ key,
32
+ )
27
33
  return False
28
34
 
29
- # If the group is a tuple of (hosts, data), check the hosts
30
- if isinstance(value, tuple):
35
+ if isinstance(value, list):
36
+ pass
37
+ elif isinstance(value, tuple):
38
+ # If the group is a tuple of (hosts, data), check the hosts
31
39
  value = value[0]
40
+ else:
41
+ logger.debug(
42
+ 'Ignoring variable "%s" in inventory file since it is not a list or tuple',
43
+ key,
44
+ )
45
+ return False
32
46
 
33
- # Expand any generators of hosts
34
- if isinstance(value, GeneratorType):
35
- value = list(value)
47
+ if not all(isinstance(item, ALLOWED_HOST_TYPES) for item in value):
48
+ logger.warning(
49
+ 'Ignoring host group "%s". '
50
+ "Host groups may only contain strings (host) or tuples (host, data).",
51
+ key,
52
+ )
53
+ return False
36
54
 
37
- return all(isinstance(item, ALLOWED_HOST_TYPES) for item in value)
55
+ return True
38
56
 
39
57
 
40
58
  def _get_group_data(dirname_or_filename: str):
@@ -258,7 +276,6 @@ def make_inventory_from_files(
258
276
  for hosts in groups.values():
259
277
  # Groups can be a list of hosts or tuple of (hosts, data)
260
278
  hosts = _get_any_tuple_first(hosts)
261
-
262
279
  for host in hosts:
263
280
  # Hosts can be a hostname or tuple of (hostname, data)
264
281
  hostname = _get_any_tuple_first(host)
pyinfra_cli/prints.py CHANGED
@@ -281,6 +281,8 @@ def print_results(state: "State"):
281
281
  (logger.info, ["Operation", "Hosts", "Success", "Error", "No Change"]),
282
282
  ]
283
283
 
284
+ totals = {"hosts": 0, "success": 0, "error": 0, "no_change": 0}
285
+
284
286
  for op_hash in state.get_op_order():
285
287
  hosts_in_op = 0
286
288
  hosts_in_op_success: list[str] = []
@@ -306,19 +308,32 @@ def print_results(state: "State"):
306
308
  str(hosts_in_op),
307
309
  ]
308
310
 
311
+ totals["hosts"] += hosts_in_op
312
+
309
313
  if hosts_in_op_success:
310
- row.append(f"{len(hosts_in_op_success)}")
314
+ num_hosts_in_op_success = len(hosts_in_op_success)
315
+ row.append(str(num_hosts_in_op_success))
316
+ totals["success"] += num_hosts_in_op_success
311
317
  else:
312
318
  row.append("-")
319
+
313
320
  if hosts_in_op_error:
314
- row.append(f"{len(hosts_in_op_error)}")
321
+ num_hosts_in_op_error = len(hosts_in_op_error)
322
+ row.append(str(num_hosts_in_op_error))
323
+ totals["error"] += num_hosts_in_op_error
315
324
  else:
316
325
  row.append("-")
326
+
317
327
  if hosts_in_op_no_change:
318
- row.append(f"{len(hosts_in_op_no_change)}")
328
+ num_hosts_in_op_no_change = len(hosts_in_op_no_change)
329
+ row.append(str(num_hosts_in_op_no_change))
330
+ totals["no_change"] += num_hosts_in_op_no_change
319
331
  else:
320
332
  row.append("-")
321
333
 
322
334
  rows.append((logger.info, row))
323
335
 
336
+ totals_row = ["Grand total"] + [str(i) if i else "-" for i in totals.values()]
337
+ rows.append((logger.info, totals_row))
338
+
324
339
  print_rows(rows)
pyinfra_cli/util.py CHANGED
@@ -124,6 +124,9 @@ def json_encode(obj):
124
124
  if isinstance(obj, bytes):
125
125
  return obj.decode()
126
126
 
127
+ if hasattr(obj, "to_json"):
128
+ return obj.to_json()
129
+
127
130
  raise TypeError("Cannot serialize: {0} ({1})".format(type(obj), obj))
128
131
 
129
132
 
@@ -43,26 +43,28 @@ class TestCliDeployState(PatchSSHTestCase):
43
43
  assert executed is False
44
44
 
45
45
  def test_deploy(self):
46
- task_file_path = path.join("tasks", "a_task.py")
46
+ a_task_file_path = path.join("tasks", "a_task.py")
47
+ b_task_file_path = path.join("tasks", "b_task.py")
47
48
  nested_task_path = path.join("tasks", "another_task.py")
48
49
  correct_op_name_and_host_names = [
49
50
  ("First main operation", True), # true for all hosts
50
51
  ("Second main operation", ("somehost",)),
51
- ("{0} | First task operation".format(task_file_path), ("anotherhost",)),
52
- ("{0} | Task order loop 1".format(task_file_path), ("anotherhost",)),
53
- ("{0} | 2nd Task order loop 1".format(task_file_path), ("anotherhost",)),
54
- ("{0} | Task order loop 2".format(task_file_path), ("anotherhost",)),
55
- ("{0} | 2nd Task order loop 2".format(task_file_path), ("anotherhost",)),
52
+ ("{0} | First task operation".format(a_task_file_path), ("anotherhost",)),
53
+ ("{0} | Task order loop 1".format(a_task_file_path), ("anotherhost",)),
54
+ ("{0} | 2nd Task order loop 1".format(a_task_file_path), ("anotherhost",)),
55
+ ("{0} | Task order loop 2".format(a_task_file_path), ("anotherhost",)),
56
+ ("{0} | 2nd Task order loop 2".format(a_task_file_path), ("anotherhost",)),
56
57
  (
57
- "{0} | {1} | Second task operation".format(task_file_path, nested_task_path),
58
+ "{0} | {1} | Second task operation".format(a_task_file_path, nested_task_path),
58
59
  ("anotherhost",),
59
60
  ),
60
- ("{0} | First task operation".format(task_file_path), True),
61
- ("{0} | Task order loop 1".format(task_file_path), True),
62
- ("{0} | 2nd Task order loop 1".format(task_file_path), True),
63
- ("{0} | Task order loop 2".format(task_file_path), True),
64
- ("{0} | 2nd Task order loop 2".format(task_file_path), True),
65
- ("{0} | {1} | Second task operation".format(task_file_path, nested_task_path), True),
61
+ ("{0} | First task operation".format(a_task_file_path), True),
62
+ ("{0} | Task order loop 1".format(a_task_file_path), True),
63
+ ("{0} | 2nd Task order loop 1".format(a_task_file_path), True),
64
+ ("{0} | Task order loop 2".format(a_task_file_path), True),
65
+ ("{0} | 2nd Task order loop 2".format(a_task_file_path), True),
66
+ ("{0} | {1} | Second task operation".format(a_task_file_path, nested_task_path), True),
67
+ ("{0} | Important task operation".format(b_task_file_path), True),
66
68
  ("My deploy | First deploy operation", True),
67
69
  ("My deploy | My nested deploy | First nested deploy operation", True),
68
70
  ("My deploy | Second deploy operation", True),
@@ -64,3 +64,56 @@ class TestCliInventory(PatchSSHTestCase):
64
64
  assert "leftover_data" in inventory.group_data
65
65
  assert inventory.group_data["leftover_data"].get("still_parsed") == "never_used"
66
66
  assert inventory.group_data["leftover_data"].get("_global_arg") == "gets_parsed"
67
+
68
+ def test_ignores_variables_with_leading_underscore(self):
69
+ ctx_state.reset()
70
+ ctx_inventory.reset()
71
+
72
+ result = run_cli(
73
+ path.join("tests", "test_cli", "inventories", "invalid.py"),
74
+ "exec",
75
+ "--debug",
76
+ "--",
77
+ "echo hi",
78
+ )
79
+
80
+ assert result.exit_code == 0, result.stdout
81
+ assert (
82
+ 'Ignoring variable "_hosts" in inventory file since it starts with a leading underscore'
83
+ in result.stdout
84
+ )
85
+ assert inventory.hosts == {}
86
+
87
+ def test_only_supports_list_and_tuples(self):
88
+ ctx_state.reset()
89
+ ctx_inventory.reset()
90
+
91
+ result = run_cli(
92
+ path.join("tests", "test_cli", "inventories", "invalid.py"),
93
+ "exec",
94
+ "--debug",
95
+ "--",
96
+ "echo hi",
97
+ )
98
+
99
+ assert result.exit_code == 0, result.stdout
100
+ assert 'Ignoring variable "dict_hosts" in inventory file' in result.stdout, result.stdout
101
+ assert (
102
+ 'Ignoring variable "generator_hosts" in inventory file' in result.stdout
103
+ ), result.stdout
104
+ assert inventory.hosts == {}
105
+
106
+ def test_host_groups_may_only_contain_strings_or_tuples(self):
107
+ ctx_state.reset()
108
+ ctx_inventory.reset()
109
+
110
+ result = run_cli(
111
+ path.join("tests", "test_cli", "inventories", "invalid.py"),
112
+ "exec",
113
+ "--",
114
+ "echo hi",
115
+ )
116
+
117
+ assert result.exit_code == 0, result.stdout
118
+ assert 'Ignoring host group "issue_662"' in result.stdout, result.stdout
119
+ assert inventory.hosts == {}
@@ -1,7 +1,8 @@
1
+ from base64 import b64decode
1
2
  from unittest import TestCase
2
3
  from unittest.mock import mock_open, patch
3
4
 
4
- from paramiko import ProxyCommand
5
+ from paramiko import PKey, ProxyCommand, SSHException
5
6
 
6
7
  from pyinfra.connectors.sshuserclient import SSHClient
7
8
  from pyinfra.connectors.sshuserclient.client import AskPolicy, get_ssh_config
@@ -41,6 +42,30 @@ LOOPING_SSH_CONFIG_DATA = """
41
42
  Include other_file
42
43
  """
43
44
 
45
+ # To ensure that we don't remove things from users hostfiles
46
+ # we should test that all modifications only append to the
47
+ # hostfile, and don't delete any data or comments.
48
+ EXAMPLE_KEY_1 = (
49
+ "AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+"
50
+ "VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/"
51
+ "C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXk"
52
+ "E2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMj"
53
+ "A2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIE"
54
+ "s4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+Ej"
55
+ "qoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/Wnw"
56
+ "H6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk="
57
+ )
58
+
59
+ KNOWN_HOSTS_EXAMPLE_DATA = f"""
60
+ # this is an important comment
61
+
62
+ # another comment after the newline
63
+
64
+ @cert-authority example-domain.lan ssh-rsa {EXAMPLE_KEY_1}
65
+
66
+ 192.168.1.222 ssh-rsa {EXAMPLE_KEY_1}
67
+ """
68
+
44
69
 
45
70
  class TestSSHUserConfigMissing(TestCase):
46
71
  def setUp(self):
@@ -199,3 +224,45 @@ class TestSSHUserConfig(TestCase):
199
224
  port=22,
200
225
  test="kwarg",
201
226
  )
227
+
228
+ def test_missing_hostkey(self):
229
+ client = SSHClient()
230
+ policy = AskPolicy()
231
+ example_hostname = "new_host"
232
+ example_keytype = "ecdsa-sha2-nistp256"
233
+ example_key = (
234
+ "AAAAE2VjZHNhLXNoYTItbmlzdHAyNT"
235
+ "YAAAAIbmlzdHAyNTYAAABBBHNp1NM"
236
+ "ZjxPBuuKwIPfkVJqWaH3oUtW137kIW"
237
+ "P4PlCyACt8zVIIimFhIpwRUidcf7jw"
238
+ "VWPAJvfBjEPqewDApnZQ="
239
+ )
240
+
241
+ key = PKey.from_type_string(
242
+ example_keytype,
243
+ b64decode(example_key),
244
+ )
245
+
246
+ # Check if AskPolicy respects not importing and properly raises SSHException
247
+ with self.subTest("Check user 'no'"):
248
+ with patch("builtins.input", return_value="n"):
249
+ self.assertRaises(
250
+ SSHException, lambda: policy.missing_host_key(client, example_hostname, key)
251
+ )
252
+
253
+ # Check if AskPolicy properly appends to hostfile
254
+ with self.subTest("Check user 'yes'"):
255
+ mock_data = mock_open(read_data=KNOWN_HOSTS_EXAMPLE_DATA)
256
+ # Read mock hostfile
257
+ with patch("pyinfra.connectors.sshuserclient.client.open", mock_data):
258
+ with patch("paramiko.hostkeys.open", mock_data):
259
+ with patch("builtins.input", return_value="y"):
260
+ policy.missing_host_key(client, "new_host", key)
261
+
262
+ # Assert that we appended correctly to the file
263
+ write_call_args = mock_data.return_value.write.call_args
264
+ # Ensure we only wrote once and then closed the handle.
265
+ assert len(write_call_args) == 2
266
+ # Ensure we wrote the correct content
267
+ correct_output = f"{example_hostname} {example_keytype} {example_key}\n"
268
+ assert write_call_args[0][0] == correct_output
File without changes