ansible-core 2.19.1rc1__py3-none-any.whl → 2.19.2rc1__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.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (22) hide show
  1. ansible/executor/play_iterator.py +1 -1
  2. ansible/module_utils/ansible_release.py +1 -1
  3. ansible/plugins/filter/core.py +1 -0
  4. ansible/release.py +1 -1
  5. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/METADATA +1 -1
  6. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/RECORD +22 -22
  7. ansible_test/_internal/ci/__init__.py +64 -130
  8. ansible_test/_internal/ci/azp.py +15 -17
  9. ansible_test/_internal/ci/local.py +117 -21
  10. ansible_test/_internal/core_ci.py +31 -11
  11. ansible_test/_internal/host_profiles.py +5 -2
  12. ansible_test/_internal/provisioning.py +3 -0
  13. ansible_test/_internal/util.py +1 -0
  14. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/WHEEL +0 -0
  15. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/entry_points.txt +0 -0
  16. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/licenses/COPYING +0 -0
  17. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  18. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  19. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  20. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  21. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  22. {ansible_core-2.19.1rc1.dist-info → ansible_core-2.19.2rc1.dist-info}/top_level.txt +0 -0
@@ -574,7 +574,7 @@ class PlayIterator:
574
574
  Given the current HostState state, determines if the current block, or any child blocks,
575
575
  are in rescue mode.
576
576
  """
577
- if state.run_state == IteratingStates.TASKS and state.get_current_block().rescue:
577
+ if state.run_state in (IteratingStates.TASKS, IteratingStates.HANDLERS) and state.get_current_block().rescue:
578
578
  return True
579
579
  if state.tasks_child_state is not None:
580
580
  return self.is_any_block_rescuing(state.tasks_child_state)
@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.19.1rc1'
20
+ __version__ = '2.19.2rc1'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "What Is and What Should Never Be"
@@ -221,6 +221,7 @@ def regex_search(value, regex, *args, **kwargs):
221
221
  return items
222
222
 
223
223
 
224
+ @accept_args_markers
224
225
  def ternary(value, true_val, false_val, none_val=None):
225
226
  """ value ? true_val : false_val """
226
227
  if value is None and none_val is not None:
ansible/release.py CHANGED
@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.19.1rc1'
20
+ __version__ = '2.19.2rc1'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "What Is and What Should Never Be"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansible-core
3
- Version: 2.19.1rc1
3
+ Version: 2.19.2rc1
4
4
  Summary: Radically simple IT automation
5
5
  Author: Ansible Project
6
6
  Project-URL: Homepage, https://ansible.com/
@@ -3,7 +3,7 @@ ansible/__main__.py,sha256=24j-7-YT4lZ2fmV80JD-VRoYBnxR7YoP_VP-orJtDt0,796
3
3
  ansible/constants.py,sha256=qef45QpHi-yFFMvllvNKmqFpXdKKr304e5fEZnjgwZc,7989
4
4
  ansible/context.py,sha256=oKYyfjfWpy8vDeProtqfnqSmuij_t75_5e5t0U_hQ1g,1933
5
5
  ansible/keyword_desc.yml,sha256=5rGCsr-0B8w2D67qBD6q_2WFxfqj9ieb0V_2J-dZJ5E,7547
6
- ansible/release.py,sha256=jhEMR7ymedaoxKinJzbCkbP8rou9FlQe1kzwG1uWZ-8,855
6
+ ansible/release.py,sha256=JLb_BEVsdGYnLJUp7vyx1FkTEnTJunGAJYEFt-RZrhU,855
7
7
  ansible/_internal/__init__.py,sha256=J3yCEAZoJLwxHMPEIWHwX6seRTCQ4Sr7cfHSw11ik9k,2208
8
8
  ansible/_internal/_collection_proxy.py,sha256=V3Zns3jdWR1hTP6q4mrNWoIKL67ayiQFPDOb6F7igsc,1228
9
9
  ansible/_internal/_event_formatting.py,sha256=cHMsuYi6v2W3fgEYdKLSe8O34kW5bZE26zyj7FOt268,4222
@@ -101,7 +101,7 @@ ansible/errors/__init__.py,sha256=W1s19PaheqXMI2yKnZCuaKKjSAJRPgU1_xF2_J9B1NU,16
101
101
  ansible/executor/__init__.py,sha256=mRvbCJPA-_veSG5ka3v04G5vsarLVDeB3EWFsu6geSI,749
102
102
  ansible/executor/interpreter_discovery.py,sha256=UWeAxnHknJCci2gG3zt6edx5Nj4WbHYfJVcmW_DzItY,3858
103
103
  ansible/executor/module_common.py,sha256=sXMOvKj_9ubeBaCPVBHh76uHaRYZm-8mOhsSG55aXQ8,60450
104
- ansible/executor/play_iterator.py,sha256=OY0W7x3F7VUQCjWIogkPqhvm7SFnxOXR5anlqJjHeHY,32282
104
+ ansible/executor/play_iterator.py,sha256=ybui896hQFJ4wLsYC3fZoJY4KEsX69QkCoMfomQyEqE,32310
105
105
  ansible/executor/playbook_executor.py,sha256=5wjvqw22RG4g_JlYDQnLFrUEa8aYQBWdgKhEpNonhKQ,14806
106
106
  ansible/executor/stats.py,sha256=Rw-Q73xYvXnYOt-LJFnHAR03NvVR3ESgbMkHnVGhIPI,3180
107
107
  ansible/executor/task_executor.py,sha256=irbdKCK6BZfBVn00nbvNMZJOltAUum5ykuqpAHh40-U,61437
@@ -211,7 +211,7 @@ ansible/inventory/host.py,sha256=cZw906LeMYe6oF3ZxW6K2HWoW2Qc0jxHssg_C8cRumE,493
211
211
  ansible/inventory/manager.py,sha256=fxg2sq7s-VBJnn9TvJCgv-xvYIu0DLJTix_y3w0wLcc,31811
212
212
  ansible/module_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
213
213
  ansible/module_utils/_text.py,sha256=VkWgAnSNVCbTQqZgllUObBFsH3uM4EUW5srl1UR9t1g,544
214
- ansible/module_utils/ansible_release.py,sha256=jhEMR7ymedaoxKinJzbCkbP8rou9FlQe1kzwG1uWZ-8,855
214
+ ansible/module_utils/ansible_release.py,sha256=JLb_BEVsdGYnLJUp7vyx1FkTEnTJunGAJYEFt-RZrhU,855
215
215
  ansible/module_utils/api.py,sha256=8BmCzQtp9rClsLGlDn4I9iJrUFLCdnoEIxYX59_IL9c,5756
216
216
  ansible/module_utils/basic.py,sha256=hoGCYYxypID_5wdpwkSAhMTb5Mfkr9x0RKsJaSHMwSA,90113
217
217
  ansible/module_utils/connection.py,sha256=ZwtQEs-TtT-XecoEmFWiDevSkJLIj348YkiW6PP7G9E,7471
@@ -583,7 +583,7 @@ ansible/plugins/filter/combinations.yml,sha256=LttrIICjapNtZHWnvJD-C9Pv3PIKP16i8
583
583
  ansible/plugins/filter/combine.yml,sha256=QH2zy4qr9wPpEyr-XKmphbls60M4ZSdAkj7r3cuvC3Q,1671
584
584
  ansible/plugins/filter/comment.yml,sha256=nJVzBF2Qiwa-qQRioJK42cbWt3Rb5LYmfvGPhrhU8Rc,2139
585
585
  ansible/plugins/filter/commonpath.yml,sha256=SPx3fPy4GPJaKmY2S8aJI1I800FOgErYAKVXV1etp1Q,696
586
- ansible/plugins/filter/core.py,sha256=S0kKl61FiCIsySHYAIZ_B60fVVrkYZ5Ljyi1iwzS8ww,27372
586
+ ansible/plugins/filter/core.py,sha256=iMvK6wfEnQ53nBZos9boI72rExGeQn8ImgA3AW_E2-w,27393
587
587
  ansible/plugins/filter/dict2items.yml,sha256=A3gL25dyGrSqP44PtqQgg6paUnwReAzC7Brkqel-39E,1523
588
588
  ansible/plugins/filter/difference.yml,sha256=YJnJJMYejCcBaNgxFBhYj-z6OysRHmss1gUgrIJBQFk,1091
589
589
  ansible/plugins/filter/dirname.yml,sha256=Z7p7ay8s3_Zee6gIu7qr4wUC-an7lwLwuoVmgHQCKyg,820
@@ -789,12 +789,12 @@ ansible/vars/hostvars.py,sha256=cRK_4dssUwIN4aDxxYXEj7KzTazrykQ4PbJotne5oJc,4364
789
789
  ansible/vars/manager.py,sha256=1SNGcwMTT7m8aPC45DHdkOZRtnf7OEcuExBtocJusq4,28023
790
790
  ansible/vars/plugins.py,sha256=8svEABS2yBPzEdymdsrZ-0D70boUoCNvcgkWasvtVNo,4533
791
791
  ansible/vars/reserved.py,sha256=NgxlMBm_tloqDVb5TEX4eGhpYsz_AO6-Fmyi3kJpIFk,3107
792
- ansible_core-2.19.1rc1.dist-info/licenses/COPYING,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
793
- ansible_core-2.19.1rc1.dist-info/licenses/licenses/Apache-License.txt,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
794
- ansible_core-2.19.1rc1.dist-info/licenses/licenses/BSD-3-Clause.txt,sha256=la0N3fE3Se8vBiuvUcFKA8b-E41G7flTic6P8CkUroE,1548
795
- ansible_core-2.19.1rc1.dist-info/licenses/licenses/MIT-license.txt,sha256=jLXp2XurnyZKbye40g9tfmLGtVlxh3pPD4n8xNqX8xc,1023
796
- ansible_core-2.19.1rc1.dist-info/licenses/licenses/PSF-license.txt,sha256=g7BC_H1qyg8Q1o5F76Vrm8ChSWYI5-dyj-CdGlNKBUo,2484
797
- ansible_core-2.19.1rc1.dist-info/licenses/licenses/simplified_bsd.txt,sha256=8R5R7R7sOa0h1Fi6RNgFgHowHBfun-OVOMzJ4rKAk2w,1237
792
+ ansible_core-2.19.2rc1.dist-info/licenses/COPYING,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
793
+ ansible_core-2.19.2rc1.dist-info/licenses/licenses/Apache-License.txt,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
794
+ ansible_core-2.19.2rc1.dist-info/licenses/licenses/BSD-3-Clause.txt,sha256=la0N3fE3Se8vBiuvUcFKA8b-E41G7flTic6P8CkUroE,1548
795
+ ansible_core-2.19.2rc1.dist-info/licenses/licenses/MIT-license.txt,sha256=jLXp2XurnyZKbye40g9tfmLGtVlxh3pPD4n8xNqX8xc,1023
796
+ ansible_core-2.19.2rc1.dist-info/licenses/licenses/PSF-license.txt,sha256=g7BC_H1qyg8Q1o5F76Vrm8ChSWYI5-dyj-CdGlNKBUo,2484
797
+ ansible_core-2.19.2rc1.dist-info/licenses/licenses/simplified_bsd.txt,sha256=8R5R7R7sOa0h1Fi6RNgFgHowHBfun-OVOMzJ4rKAk2w,1237
798
798
  ansible_test/__init__.py,sha256=20VPOj11c6Ut1Av9RaurgwJvFhMqkWG3vAvcCbecNKw,66
799
799
  ansible_test/_data/ansible.cfg,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
800
800
  ansible_test/_data/coveragerc,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -854,7 +854,7 @@ ansible_test/_internal/connections.py,sha256=_wKp7ercAJlJxuB6nXGChqdIC2PGQRuwg8g
854
854
  ansible_test/_internal/constants.py,sha256=qgVmng71FWdsIZXgUaQUEZfB9V0GC4jjhYbSp4pzA_M,1970
855
855
  ansible_test/_internal/containers.py,sha256=iOqs3Bi9127cl7h7kIPw-mo3tbM-7JAkmOwB-9ESvRE,34138
856
856
  ansible_test/_internal/content_config.py,sha256=qbYoSKOBT-X7FhSCuVkYzgiIz5H4MIbua3DxcN-gYdA,5589
857
- ansible_test/_internal/core_ci.py,sha256=7ejW10CQuf1aRE64HX2tdON2cbhIRSsYsLSThdXQcHo,17310
857
+ ansible_test/_internal/core_ci.py,sha256=GSRt726aNL167p3LWLWum36EFhYhOaRvJGOjzHi7J1g,17977
858
858
  ansible_test/_internal/coverage_util.py,sha256=NnlgbpJ47h2oaAc5m3QyVs65GOq-cuRR9gEVV1ZE4rI,9408
859
859
  ansible_test/_internal/data.py,sha256=8APH-Bm6CeFO9ngWgZC8IxHeFzlxn0OQd6D5G1msSZU,11185
860
860
  ansible_test/_internal/debugging.py,sha256=bL2mT7AHiKGSmZp2lZVDWDpkeMdOg3HgCZW1m1qTaYc,15563
@@ -865,7 +865,7 @@ ansible_test/_internal/encoding.py,sha256=ymPqkmgg7mXUkW6MOARx0cYanX9TLLnu_NXp6n
865
865
  ansible_test/_internal/executor.py,sha256=-SSTYgKckI-dWltBWt67zTU6zO7NVu_O3pgFiJG4DeQ,2960
866
866
  ansible_test/_internal/git.py,sha256=TkYoTZ8CKWlP8dZZmThzzT1myItdP7_LseZ_2BMnIMA,4367
867
867
  ansible_test/_internal/host_configs.py,sha256=fuY7CAhM8Ky3cPcVhHe28Kwzuokzyg9lvr7GVL3o2Bo,18635
868
- ansible_test/_internal/host_profiles.py,sha256=q6xZfaTS3wXPQUh90rHn1lQ3YJLTTm0xCh3Z-dilHZQ,74374
868
+ ansible_test/_internal/host_profiles.py,sha256=i7qp8CBV94dK_8o7GrU-jMrofHkJAvDzGGaIMGjGj1Y,74466
869
869
  ansible_test/_internal/http.py,sha256=P_C5n8hSZ3Q1zA08smmJCh2LvOoaflGasEqnLXZP0L0,3865
870
870
  ansible_test/_internal/init.py,sha256=-OdOvJ3Fz4Sx2aTG9qq7ekKsbVVTqOvRqetOqjOvA6w,506
871
871
  ansible_test/_internal/inventory.py,sha256=8Ajk67x5a8zt1bgvT8IrR9kCuEXXfkMxO2_ioFtlgh4,7074
@@ -875,7 +875,7 @@ ansible_test/_internal/locale_util.py,sha256=tjRbwKmgMQc1ysIhvP8yBhFcNA-2UCaWfQB
875
875
  ansible_test/_internal/metadata.py,sha256=HWM-sQT-ovt2lwTSK1xN8-IYHWrB5joVY4BRh7HHHC0,7036
876
876
  ansible_test/_internal/payload.py,sha256=F9sLPiTw-zNq0-zU-L_RIYOsXZmA3nsLWha2W2MoeEs,8013
877
877
  ansible_test/_internal/processes.py,sha256=H3n7jOGzvWdeTxsTWFx4TPIjSpt40g0T6j0wvYdOsWY,2231
878
- ansible_test/_internal/provisioning.py,sha256=BIe-zIbGxFtqtaW8Cagk0cRybYthUIeGfKgmrwSwK2g,7492
878
+ ansible_test/_internal/provisioning.py,sha256=kStzjL6EYfuf_R9Xi91TO8S0fqqD7pZaj-Lt9hMjh4Y,7581
879
879
  ansible_test/_internal/pypi_proxy.py,sha256=N9_kuBk6Bko3e8dKC1zi4UfhU0untpQgOK2W984GT_0,6020
880
880
  ansible_test/_internal/python_requirements.py,sha256=0tXTRO9m8q5ORaM6lELal5n8VEqlD1f16OrN8m7C_w8,16038
881
881
  ansible_test/_internal/ssh.py,sha256=7gTyNiwszPwFSM4aYT4YtAWfAR4lYLnOi7dpnv0SqwA,10635
@@ -883,12 +883,12 @@ ansible_test/_internal/target.py,sha256=3W4J6T79Pv2kB6KOpC_lRq2qZFS7L6GobByyVshy
883
883
  ansible_test/_internal/test.py,sha256=q17SmItAsiBWrSilDBZFSEBugv9QNsG5HzFOAFXcyh4,14516
884
884
  ansible_test/_internal/thread.py,sha256=XN9jshWoLPdqTMDjcOQxGk9691FnjUo1K_5UhcRy-O8,2633
885
885
  ansible_test/_internal/timeout.py,sha256=KOYPTjgsAX4N2q-4Qn5vFpCWJWBT9YtQCpsU7ZIBvro,4073
886
- ansible_test/_internal/util.py,sha256=qL7q6KWWqvJiM7Joxh12YfIZ0ClcZcKDGxROHP33VNY,39573
886
+ ansible_test/_internal/util.py,sha256=BELYusenCvkpKJ136STWQsoAFttIJL5-erAud8JAVaE,39600
887
887
  ansible_test/_internal/util_common.py,sha256=sw8ySIrPKCTK4AH8naulDXPU7kabSHD2EJMMhQ5G1o8,17814
888
888
  ansible_test/_internal/venv.py,sha256=eb5RfjapntulFMTIQieyx8QdHo2LJfjgZY_wx3_htMw,5522
889
- ansible_test/_internal/ci/__init__.py,sha256=sZNgkICN4RRFy4Nn-ZB6dCm6Ui9d_xIrrjgaIzZ_VyI,7739
890
- ansible_test/_internal/ci/azp.py,sha256=IFNjhv3TljiPtRQqwQfggNYekMfsWx9c5_2XdU6adMg,10137
891
- ansible_test/_internal/ci/local.py,sha256=KT20UotPRg9lhfNJ-cA-LsV6risdKTZ5_zK6rIldm54,6740
889
+ ansible_test/_internal/ci/__init__.py,sha256=wNgXvmq517FQPVQIW5HeUuuFBNjBAshcNoPEO8I6jDQ,4684
890
+ ansible_test/_internal/ci/azp.py,sha256=pdMf4sgHVB8wsusY_9FTxPkhw_7Qnc9qQWgJgBZlf2w,10104
891
+ ansible_test/_internal/ci/local.py,sha256=YnpfQfvheLNwV6rzb_YElASy-S-tH0zhOXtVtABNAUs,9906
892
892
  ansible_test/_internal/classification/__init__.py,sha256=8Hzmr2pqAMR7sibHNDub1YGkcnLJzb4I_3MqeZbpJzw,34143
893
893
  ansible_test/_internal/classification/common.py,sha256=WWM6LRHcO29nRorSLveSzRLIarb5-dPbwHCf8Qd1xvU,895
894
894
  ansible_test/_internal/classification/csharp.py,sha256=EM7yxfbwnHsKFjjpiQUTsZcPY567qmue1eXR3GI5JJE,3242
@@ -1091,8 +1091,8 @@ ansible_test/config/cloud-config-vultr.ini.template,sha256=XLKHk3lg_8ReQMdWfZzhh
1091
1091
  ansible_test/config/config.yml,sha256=1zdGucnIl6nIecZA7ISIANvqXiHWqq6Dthsk_6MUwNc,2642
1092
1092
  ansible_test/config/inventory.networking.template,sha256=bFNSk8zNQOaZ_twaflrY0XZ9mLwUbRLuNT0BdIFwvn4,1335
1093
1093
  ansible_test/config/inventory.winrm.template,sha256=1QU8W-GFLnYEw8yY9bVIvUAVvJYPM3hyoijf6-M7T00,1098
1094
- ansible_core-2.19.1rc1.dist-info/METADATA,sha256=2FB5aucw77dtspB-6Skdengg8F7RJC6RtmqLguVq0Lw,7733
1095
- ansible_core-2.19.1rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1096
- ansible_core-2.19.1rc1.dist-info/entry_points.txt,sha256=S9yJij5Im6FgRQxzkqSCnPQokC7PcWrDW_NSygZczJU,451
1097
- ansible_core-2.19.1rc1.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
1098
- ansible_core-2.19.1rc1.dist-info/RECORD,,
1094
+ ansible_core-2.19.2rc1.dist-info/METADATA,sha256=GKPO88evKJ7s3y_H_Fe-y94ganzsioNiAqMnw7Do05c,7733
1095
+ ansible_core-2.19.2rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1096
+ ansible_core-2.19.2rc1.dist-info/entry_points.txt,sha256=S9yJij5Im6FgRQxzkqSCnPQokC7PcWrDW_NSygZczJU,451
1097
+ ansible_core-2.19.2rc1.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
1098
+ ansible_core-2.19.2rc1.dist-info/RECORD,,
@@ -3,22 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
- import base64
6
+ import dataclasses
7
+ import datetime
7
8
  import json
8
- import os
9
+ import pathlib
9
10
  import tempfile
10
11
  import typing as t
11
12
 
12
- from ..encoding import (
13
- to_bytes,
14
- to_text,
15
- )
16
-
17
- from ..io import (
18
- read_text_file,
19
- write_text_file,
20
- )
21
-
22
13
  from ..config import (
23
14
  CommonConfig,
24
15
  TestConfig,
@@ -34,6 +25,65 @@ from ..util import (
34
25
  )
35
26
 
36
27
 
28
+ @dataclasses.dataclass(frozen=True, kw_only=True)
29
+ class AuthContext:
30
+ """Information about the request to which authentication will be applied."""
31
+
32
+ stage: str
33
+ provider: str
34
+ request_id: str
35
+
36
+
37
+ class AuthHelper:
38
+ """Authentication helper."""
39
+
40
+ NAMESPACE: t.ClassVar = 'ci@core.ansible.com'
41
+
42
+ def __init__(self, key_file: pathlib.Path) -> None:
43
+ self.private_key_file = pathlib.Path(str(key_file).removesuffix('.pub'))
44
+ self.public_key_file = pathlib.Path(f'{self.private_key_file}.pub')
45
+
46
+ def sign_request(self, request: dict[str, object], context: AuthContext) -> None:
47
+ """Sign the given auth request using the provided context."""
48
+ request.update(
49
+ stage=context.stage,
50
+ provider=context.provider,
51
+ request_id=context.request_id,
52
+ timestamp=datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat(),
53
+ )
54
+
55
+ with tempfile.TemporaryDirectory() as temp_dir:
56
+ payload_path = pathlib.Path(temp_dir) / 'auth.json'
57
+ payload_path.write_text(json.dumps(request, sort_keys=True))
58
+
59
+ cmd = ['ssh-keygen', '-q', '-Y', 'sign', '-f', str(self.private_key_file), '-n', self.NAMESPACE, str(payload_path)]
60
+ raw_command(cmd, capture=False, interactive=True)
61
+
62
+ signature_path = pathlib.Path(f'{payload_path}.sig')
63
+ signature = signature_path.read_text()
64
+
65
+ request.update(signature=signature)
66
+
67
+
68
+ class GeneratingAuthHelper(AuthHelper, metaclass=abc.ABCMeta):
69
+ """Authentication helper which generates a key pair on demand."""
70
+
71
+ def __init__(self) -> None:
72
+ super().__init__(pathlib.Path('~/.ansible/test/ansible-core-ci').expanduser())
73
+
74
+ def sign_request(self, request: dict[str, object], context: AuthContext) -> None:
75
+ if not self.private_key_file.exists():
76
+ self.generate_key_pair()
77
+
78
+ super().sign_request(request, context)
79
+
80
+ def generate_key_pair(self) -> None:
81
+ """Generate key pair."""
82
+ self.private_key_file.parent.mkdir(parents=True, exist_ok=True)
83
+
84
+ raw_command(['ssh-keygen', '-q', '-f', str(self.private_key_file), '-N', ''], capture=True)
85
+
86
+
37
87
  class ChangeDetectionNotSupported(ApplicationError):
38
88
  """Exception for cases where change detection is not supported."""
39
89
 
@@ -75,8 +125,8 @@ class CIProvider(metaclass=abc.ABCMeta):
75
125
  """Return True if Ansible Core CI is supported."""
76
126
 
77
127
  @abc.abstractmethod
78
- def prepare_core_ci_auth(self) -> dict[str, t.Any]:
79
- """Return authentication details for Ansible Core CI."""
128
+ def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
129
+ """Prepare an Ansible Core CI request using the given config and context."""
80
130
 
81
131
  @abc.abstractmethod
82
132
  def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]:
@@ -101,119 +151,3 @@ def get_ci_provider() -> CIProvider:
101
151
  display.info('Detected CI provider: %s' % provider.name)
102
152
 
103
153
  return provider
104
-
105
-
106
- class AuthHelper(metaclass=abc.ABCMeta):
107
- """Public key based authentication helper for Ansible Core CI."""
108
-
109
- def sign_request(self, request: dict[str, t.Any]) -> None:
110
- """Sign the given auth request and make the public key available."""
111
- payload_bytes = to_bytes(json.dumps(request, sort_keys=True))
112
- signature_raw_bytes = self.sign_bytes(payload_bytes)
113
- signature = to_text(base64.b64encode(signature_raw_bytes))
114
-
115
- request.update(signature=signature)
116
-
117
- def initialize_private_key(self) -> str:
118
- """
119
- Initialize and publish a new key pair (if needed) and return the private key.
120
- The private key is cached across ansible-test invocations, so it is only generated and published once per CI job.
121
- """
122
- path = os.path.expanduser('~/.ansible-core-ci-private.key')
123
-
124
- if os.path.exists(to_bytes(path)):
125
- private_key_pem = read_text_file(path)
126
- else:
127
- private_key_pem = self.generate_private_key()
128
- write_text_file(path, private_key_pem)
129
-
130
- return private_key_pem
131
-
132
- @abc.abstractmethod
133
- def sign_bytes(self, payload_bytes: bytes) -> bytes:
134
- """Sign the given payload and return the signature, initializing a new key pair if required."""
135
-
136
- @abc.abstractmethod
137
- def publish_public_key(self, public_key_pem: str) -> None:
138
- """Publish the given public key."""
139
-
140
- @abc.abstractmethod
141
- def generate_private_key(self) -> str:
142
- """Generate a new key pair, publishing the public key and returning the private key."""
143
-
144
-
145
- class CryptographyAuthHelper(AuthHelper, metaclass=abc.ABCMeta):
146
- """Cryptography based public key based authentication helper for Ansible Core CI."""
147
-
148
- def sign_bytes(self, payload_bytes: bytes) -> bytes:
149
- """Sign the given payload and return the signature, initializing a new key pair if required."""
150
- # import cryptography here to avoid overhead and failures in environments which do not use/provide it
151
- from cryptography.hazmat.backends import default_backend
152
- from cryptography.hazmat.primitives import hashes
153
- from cryptography.hazmat.primitives.asymmetric import ec
154
- from cryptography.hazmat.primitives.serialization import load_pem_private_key
155
-
156
- private_key_pem = self.initialize_private_key()
157
- private_key = load_pem_private_key(to_bytes(private_key_pem), None, default_backend())
158
-
159
- assert isinstance(private_key, ec.EllipticCurvePrivateKey)
160
-
161
- signature_raw_bytes = private_key.sign(payload_bytes, ec.ECDSA(hashes.SHA256()))
162
-
163
- return signature_raw_bytes
164
-
165
- def generate_private_key(self) -> str:
166
- """Generate a new key pair, publishing the public key and returning the private key."""
167
- # import cryptography here to avoid overhead and failures in environments which do not use/provide it
168
- from cryptography.hazmat.backends import default_backend
169
- from cryptography.hazmat.primitives import serialization
170
- from cryptography.hazmat.primitives.asymmetric import ec
171
-
172
- private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())
173
- public_key = private_key.public_key()
174
-
175
- private_key_pem = to_text(private_key.private_bytes( # type: ignore[attr-defined] # documented method, but missing from type stubs
176
- encoding=serialization.Encoding.PEM,
177
- format=serialization.PrivateFormat.PKCS8,
178
- encryption_algorithm=serialization.NoEncryption(),
179
- ))
180
-
181
- public_key_pem = to_text(public_key.public_bytes(
182
- encoding=serialization.Encoding.PEM,
183
- format=serialization.PublicFormat.SubjectPublicKeyInfo,
184
- ))
185
-
186
- self.publish_public_key(public_key_pem)
187
-
188
- return private_key_pem
189
-
190
-
191
- class OpenSSLAuthHelper(AuthHelper, metaclass=abc.ABCMeta):
192
- """OpenSSL based public key based authentication helper for Ansible Core CI."""
193
-
194
- def sign_bytes(self, payload_bytes: bytes) -> bytes:
195
- """Sign the given payload and return the signature, initializing a new key pair if required."""
196
- private_key_pem = self.initialize_private_key()
197
-
198
- with tempfile.NamedTemporaryFile() as private_key_file:
199
- private_key_file.write(to_bytes(private_key_pem))
200
- private_key_file.flush()
201
-
202
- with tempfile.NamedTemporaryFile() as payload_file:
203
- payload_file.write(payload_bytes)
204
- payload_file.flush()
205
-
206
- with tempfile.NamedTemporaryFile() as signature_file:
207
- raw_command(['openssl', 'dgst', '-sha256', '-sign', private_key_file.name, '-out', signature_file.name, payload_file.name], capture=True)
208
- signature_raw_bytes = signature_file.read()
209
-
210
- return signature_raw_bytes
211
-
212
- def generate_private_key(self) -> str:
213
- """Generate a new key pair, publishing the public key and returning the private key."""
214
- private_key_pem = raw_command(['openssl', 'ecparam', '-genkey', '-name', 'secp384r1', '-noout'], capture=True)[0]
215
- public_key_pem = raw_command(['openssl', 'ec', '-pubout'], data=private_key_pem, capture=True)[0]
216
-
217
- self.publish_public_key(public_key_pem)
218
-
219
- return private_key_pem
@@ -31,9 +31,10 @@ from ..util import (
31
31
  )
32
32
 
33
33
  from . import (
34
+ AuthContext,
34
35
  ChangeDetectionNotSupported,
35
36
  CIProvider,
36
- CryptographyAuthHelper,
37
+ GeneratingAuthHelper,
37
38
  )
38
39
 
39
40
  CODE = 'azp'
@@ -112,10 +113,11 @@ class AzurePipelines(CIProvider):
112
113
  """Return True if Ansible Core CI is supported."""
113
114
  return True
114
115
 
115
- def prepare_core_ci_auth(self) -> dict[str, t.Any]:
116
- """Return authentication details for Ansible Core CI."""
116
+ def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
117
117
  try:
118
- request = dict(
118
+ request: dict[str, object] = dict(
119
+ type="azp:ssh",
120
+ config=config,
119
121
  org_name=os.environ['SYSTEM_COLLECTIONURI'].strip('/').split('/')[-1],
120
122
  project_name=os.environ['SYSTEM_TEAMPROJECT'],
121
123
  build_id=int(os.environ['BUILD_BUILDID']),
@@ -124,13 +126,9 @@ class AzurePipelines(CIProvider):
124
126
  except KeyError as ex:
125
127
  raise MissingEnvironmentVariable(name=ex.args[0]) from None
126
128
 
127
- self.auth.sign_request(request)
129
+ self.auth.sign_request(request, context)
128
130
 
129
- auth = dict(
130
- azp=request,
131
- )
132
-
133
- return auth
131
+ return request
134
132
 
135
133
  def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]:
136
134
  """Return details about git in the current environment."""
@@ -144,14 +142,14 @@ class AzurePipelines(CIProvider):
144
142
  return details
145
143
 
146
144
 
147
- class AzurePipelinesAuthHelper(CryptographyAuthHelper):
148
- """
149
- Authentication helper for Azure Pipelines.
150
- Based on cryptography since it is provided by the default Azure Pipelines environment.
151
- """
145
+ class AzurePipelinesAuthHelper(GeneratingAuthHelper):
146
+ """Authentication helper for Azure Pipelines."""
147
+
148
+ def generate_key_pair(self) -> None:
149
+ super().generate_key_pair()
150
+
151
+ public_key_pem = self.public_key_file.read_text()
152
152
 
153
- def publish_public_key(self, public_key_pem: str) -> None:
154
- """Publish the given public key."""
155
153
  try:
156
154
  agent_temp_directory = os.environ['AGENT_TEMPDIRECTORY']
157
155
  except KeyError as ex:
@@ -2,10 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
5
+ import abc
6
+ import inspect
6
7
  import platform
7
8
  import random
8
9
  import re
10
+ import pathlib
9
11
  import typing as t
10
12
 
11
13
  from ..config import (
@@ -24,11 +26,14 @@ from ..git import (
24
26
  from ..util import (
25
27
  ApplicationError,
26
28
  display,
29
+ get_subclasses,
27
30
  is_binary_file,
28
31
  SubprocessError,
29
32
  )
30
33
 
31
34
  from . import (
35
+ AuthContext,
36
+ AuthHelper,
32
37
  CIProvider,
33
38
  )
34
39
 
@@ -120,34 +125,20 @@ class Local(CIProvider):
120
125
 
121
126
  def supports_core_ci_auth(self) -> bool:
122
127
  """Return True if Ansible Core CI is supported."""
123
- path = self._get_aci_key_path()
124
- return os.path.exists(path)
128
+ return Authenticator.available()
125
129
 
126
- def prepare_core_ci_auth(self) -> dict[str, t.Any]:
127
- """Return authentication details for Ansible Core CI."""
128
- path = self._get_aci_key_path()
129
- auth_key = read_text_file(path).strip()
130
+ def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
131
+ if not (authenticator := Authenticator.load()):
132
+ raise ApplicationError('Ansible Core CI authentication has not been configured.')
130
133
 
131
- request = dict(
132
- key=auth_key,
133
- nonce=None,
134
- )
134
+ display.info(f'Using {authenticator} for Ansible Core CI.', verbosity=1)
135
135
 
136
- auth = dict(
137
- remote=request,
138
- )
139
-
140
- return auth
136
+ return authenticator.prepare_auth_request(config, context)
141
137
 
142
138
  def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]:
143
139
  """Return details about git in the current environment."""
144
140
  return None # not yet implemented for local
145
141
 
146
- @staticmethod
147
- def _get_aci_key_path() -> str:
148
- path = os.path.expanduser('~/.ansible-core-ci.key')
149
- return path
150
-
151
142
 
152
143
  class InvalidBranch(ApplicationError):
153
144
  """Exception for invalid branch specification."""
@@ -214,3 +205,108 @@ class LocalChanges:
214
205
  return True
215
206
 
216
207
  return False
208
+
209
+
210
+ class Authenticator(metaclass=abc.ABCMeta):
211
+ """Base class for authenticators."""
212
+
213
+ @staticmethod
214
+ def list() -> list[type[Authenticator]]:
215
+ """List all authenticators in priority order."""
216
+ return sorted((sc for sc in get_subclasses(Authenticator) if not inspect.isabstract(sc)), key=lambda obj: obj.priority())
217
+
218
+ @staticmethod
219
+ def load() -> Authenticator | None:
220
+ """Load an authenticator instance, returning None if not configured."""
221
+ for implementation in Authenticator.list():
222
+ if implementation.config_file().exists():
223
+ return implementation()
224
+
225
+ return None
226
+
227
+ @staticmethod
228
+ def available() -> bool:
229
+ """Return True if an authenticator is available, otherwise False."""
230
+ return bool(Authenticator.load())
231
+
232
+ @classmethod
233
+ @abc.abstractmethod
234
+ def priority(cls) -> int:
235
+ """Priority used to determine which authenticator is tried first, from lowest to highest."""
236
+
237
+ @classmethod
238
+ @abc.abstractmethod
239
+ def config_file(cls) -> pathlib.Path:
240
+ """Path to the config file for this authenticator."""
241
+
242
+ @abc.abstractmethod
243
+ def prepare_auth_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
244
+ """Prepare an authenticated Ansible Core CI request using the given config and context."""
245
+
246
+ def __str__(self) -> str:
247
+ return self.__class__.__name__
248
+
249
+
250
+ class PasswordAuthenticator(Authenticator):
251
+ """Authenticate using a password."""
252
+
253
+ @classmethod
254
+ def priority(cls) -> int:
255
+ return 200
256
+
257
+ @classmethod
258
+ def config_file(cls) -> pathlib.Path:
259
+ return pathlib.Path('~/.ansible-core-ci.key').expanduser()
260
+
261
+ def prepare_auth_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
262
+ parts = self.config_file().read_text().strip().split(maxsplit=1)
263
+
264
+ if len(parts) == 1: # temporary backward compatibility for legacy API keys
265
+ request = dict(
266
+ config=config,
267
+ auth=dict(
268
+ remote=dict(
269
+ key=parts[0],
270
+ ),
271
+ ),
272
+ )
273
+
274
+ return request
275
+
276
+ username, password = parts
277
+
278
+ request = dict(
279
+ type="remote:password",
280
+ config=config,
281
+ username=username,
282
+ password=password,
283
+ )
284
+
285
+ return request
286
+
287
+
288
+ class SshAuthenticator(Authenticator):
289
+ """Authenticate using an SSH key."""
290
+
291
+ @classmethod
292
+ def priority(cls) -> int:
293
+ return 100
294
+
295
+ @classmethod
296
+ def config_file(cls) -> pathlib.Path:
297
+ return pathlib.Path('~/.ansible-core-ci.auth').expanduser()
298
+
299
+ def prepare_auth_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
300
+ parts = self.config_file().read_text().strip().split(maxsplit=1)
301
+ username, key_file = parts
302
+
303
+ request: dict[str, object] = dict(
304
+ type="remote:ssh",
305
+ config=config,
306
+ username=username,
307
+ )
308
+
309
+ auth_helper = AuthHelper(pathlib.Path(key_file).expanduser())
310
+ auth_helper.sign_request(request, context)
311
+
312
+ return request
@@ -42,6 +42,7 @@ from .config import (
42
42
  )
43
43
 
44
44
  from .ci import (
45
+ AuthContext,
45
46
  get_ci_provider,
46
47
  )
47
48
 
@@ -68,6 +69,10 @@ class Resource(metaclass=abc.ABCMeta):
68
69
  def persist(self) -> bool:
69
70
  """True if the resource is persistent, otherwise false."""
70
71
 
72
+ @abc.abstractmethod
73
+ def get_config(self, core_ci: AnsibleCoreCI) -> dict[str, object]:
74
+ """Return the configuration for this resource."""
75
+
71
76
 
72
77
  @dataclasses.dataclass(frozen=True)
73
78
  class VmResource(Resource):
@@ -92,6 +97,16 @@ class VmResource(Resource):
92
97
  """True if the resource is persistent, otherwise false."""
93
98
  return True
94
99
 
100
+ def get_config(self, core_ci: AnsibleCoreCI) -> dict[str, object]:
101
+ """Return the configuration for this resource."""
102
+ return dict(
103
+ type="vm",
104
+ platform=self.platform,
105
+ version=self.version,
106
+ architecture=self.architecture,
107
+ public_key=core_ci.ssh_key.pub_contents,
108
+ )
109
+
95
110
 
96
111
  @dataclasses.dataclass(frozen=True)
97
112
  class CloudResource(Resource):
@@ -112,6 +127,12 @@ class CloudResource(Resource):
112
127
  """True if the resource is persistent, otherwise false."""
113
128
  return False
114
129
 
130
+ def get_config(self, core_ci: AnsibleCoreCI) -> dict[str, object]:
131
+ """Return the configuration for this resource."""
132
+ return dict(
133
+ type="cloud",
134
+ )
135
+
115
136
 
116
137
  class AnsibleCoreCI:
117
138
  """Client for Ansible Core CI services."""
@@ -189,7 +210,7 @@ class AnsibleCoreCI:
189
210
  display.info(f'Skipping started {self.label} instance.', verbosity=1)
190
211
  return None
191
212
 
192
- return self._start(self.ci_provider.prepare_core_ci_auth())
213
+ return self._start()
193
214
 
194
215
  def stop(self) -> None:
195
216
  """Stop instance."""
@@ -288,26 +309,25 @@ class AnsibleCoreCI:
288
309
  def _uri(self) -> str:
289
310
  return f'{self.endpoint}/{self.stage}/{self.provider}/{self.instance_id}'
290
311
 
291
- def _start(self, auth) -> dict[str, t.Any]:
312
+ def _start(self) -> dict[str, t.Any]:
292
313
  """Start instance."""
293
314
  display.info(f'Initializing new {self.label} instance using: {self._uri}', verbosity=1)
294
315
 
295
- data = dict(
296
- config=dict(
297
- platform=self.platform,
298
- version=self.version,
299
- architecture=self.arch,
300
- public_key=self.ssh_key.pub_contents,
301
- )
316
+ config = self.resource.get_config(self)
317
+
318
+ context = AuthContext(
319
+ request_id=self.instance_id,
320
+ stage=self.stage,
321
+ provider=self.provider,
302
322
  )
303
323
 
304
- data.update(auth=auth)
324
+ request = self.ci_provider.prepare_core_ci_request(config, context)
305
325
 
306
326
  headers = {
307
327
  'Content-Type': 'application/json',
308
328
  }
309
329
 
310
- response = self._start_endpoint(data, headers)
330
+ response = self._start_endpoint(request, headers)
311
331
 
312
332
  self.started = True
313
333
  self._save()
@@ -265,6 +265,9 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
265
265
  def name(self) -> str:
266
266
  """The name of the host profile."""
267
267
 
268
+ def pre_provision(self) -> None:
269
+ """Pre-provision the host profile."""
270
+
268
271
  def provision(self) -> None:
269
272
  """Provision the host before delegation."""
270
273
 
@@ -522,8 +525,8 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
522
525
  """The saved Ansible Core CI state."""
523
526
  self.state['core_ci'] = value
524
527
 
525
- def provision(self) -> None:
526
- """Provision the host before delegation."""
528
+ def pre_provision(self) -> None:
529
+ """Pre-provision the host before delegation."""
527
530
  self.core_ci = self.create_core_ci(load=True)
528
531
  self.core_ci.start()
529
532
 
@@ -132,6 +132,9 @@ def prepare_profiles(
132
132
 
133
133
  ExitHandler.register(functools.partial(cleanup_profiles, host_state))
134
134
 
135
+ for pre_profile in host_state.profiles:
136
+ pre_profile.pre_provision()
137
+
135
138
  def provision(profile: HostProfile) -> None:
136
139
  """Provision the given profile."""
137
140
  profile.provision()
@@ -707,6 +707,7 @@ def common_environment() -> dict[str, str]:
707
707
  optional = (
708
708
  'LD_LIBRARY_PATH',
709
709
  'SSH_AUTH_SOCK',
710
+ 'SSH_SK_PROVIDER',
710
711
  # MacOS High Sierra Compatibility
711
712
  # http://sealiesoftware.com/blog/archive/2017/6/5/Objective-C_and_fork_in_macOS_1013.html
712
713
  # Example configuration for macOS: