ansible-core 2.18.8__py3-none-any.whl → 2.18.9rc1__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.

@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.18.8'
20
+ __version__ = '2.18.9rc1'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "Fool in the Rain"
ansible/modules/dnf.py CHANGED
@@ -526,7 +526,10 @@ class DnfModule(YumDnf):
526
526
  conf.config_file_path = conf_file
527
527
 
528
528
  # Read the configuration file
529
- conf.read()
529
+ try:
530
+ conf.read()
531
+ except dnf.exceptions.ConfigError as e:
532
+ self.module.fail_json(msg=f"Failed to read configuration file: {e}", conf_file=conf_file)
530
533
 
531
534
  # Turn off debug messages in the output
532
535
  conf.debuglevel = 0
@@ -209,8 +209,8 @@ class ServiceScanService(BaseService):
209
209
 
210
210
  def _list_openrc(self, services):
211
211
  all_services_runlevels = {}
212
- rc, stdout, stderr = self.module.run_command("%s -a -s -m 2>&1 | grep '^ ' | tr -d '[]'" % self.rc_status_path, use_unsafe_shell=True)
213
- rc_u, stdout_u, stderr_u = self.module.run_command("%s show -v 2>&1 | grep '|'" % self.rc_update_path, use_unsafe_shell=True)
212
+ dummy, stdout, dummy = self.module.run_command("%s -a -s -m 2>&1 | grep '^ ' | tr -d '[]'" % self.rc_status_path, use_unsafe_shell=True)
213
+ dummy, stdout_u, dummy = self.module.run_command("%s show -v 2>&1 | grep '|'" % self.rc_update_path, use_unsafe_shell=True)
214
214
  for line in stdout_u.split('\n'):
215
215
  line_data = line.split('|')
216
216
  if len(line_data) < 2:
@@ -226,8 +226,15 @@ class ServiceScanService(BaseService):
226
226
  if len(line_data) < 2:
227
227
  continue
228
228
  service_name = line_data[0]
229
+ # Skip lines which are not service names
230
+ if service_name == "*":
231
+ continue
229
232
  service_state = line_data[1]
230
- service_runlevels = all_services_runlevels[service_name]
233
+ try:
234
+ service_runlevels = all_services_runlevels[service_name]
235
+ except KeyError:
236
+ self.module.warn(f"Service {service_name} not found in the service list")
237
+ continue
231
238
  service_data = {"name": service_name, "runlevels": service_runlevels, "state": service_state, "source": "openrc"}
232
239
  services[service_name] = service_data
233
240
 
ansible/modules/user.py CHANGED
@@ -1376,16 +1376,24 @@ class User(object):
1376
1376
  self.module.exit_json(failed=True, msg="%s" % to_native(e))
1377
1377
  # get umask from /etc/login.defs and set correct home mode
1378
1378
  if os.path.exists(self.LOGIN_DEFS):
1379
- with open(self.LOGIN_DEFS, 'r') as f:
1380
- for line in f:
1381
- m = re.match(r'^UMASK\s+(\d+)$', line)
1382
- if m:
1383
- umask = int(m.group(1), 8)
1379
+ # fallback if neither HOME_MODE nor UMASK are set;
1380
+ # follow behaviour of useradd initializing UMASK = 022
1381
+ mode = 0o755
1382
+ with open(self.LOGIN_DEFS, 'r') as fh:
1383
+ for line in fh:
1384
+ # HOME_MODE has higher precedence as UMASK
1385
+ match = re.match(r'^HOME_MODE\s+(\d+)$', line)
1386
+ if match:
1387
+ mode = int(match.group(1), 8)
1388
+ break # higher precedence
1389
+ match = re.match(r'^UMASK\s+(\d+)$', line)
1390
+ if match:
1391
+ umask = int(match.group(1), 8)
1384
1392
  mode = 0o777 & ~umask
1385
- try:
1386
- os.chmod(path, mode)
1387
- except OSError as e:
1388
- self.module.exit_json(failed=True, msg="%s" % to_native(e))
1393
+ try:
1394
+ os.chmod(path, mode)
1395
+ except OSError as e:
1396
+ self.module.exit_json(failed=True, msg=to_native(e))
1389
1397
 
1390
1398
  def chown_homedir(self, uid, gid, path):
1391
1399
  try:
ansible/release.py CHANGED
@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.18.8'
20
+ __version__ = '2.18.9rc1'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "Fool in the Rain"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansible-core
3
- Version: 2.18.8
3
+ Version: 2.18.9rc1
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=dSgbrzNsmhYc4GQOWZvRm4XKgf--_MUWcMa_9_7l5Pc,9757
4
4
  ansible/context.py,sha256=oKYyfjfWpy8vDeProtqfnqSmuij_t75_5e5t0U_hQ1g,1933
5
5
  ansible/keyword_desc.yml,sha256=xD-MRMB8mSRaj2ADwRnjIEbOwJKbc6BYadouGPfS0mI,7462
6
- ansible/release.py,sha256=9gq7Q1GbBxC3GTVQHdxbwP6GkRpZWnuB_EVmsfq6wVc,836
6
+ ansible/release.py,sha256=cz6pm0cY8y6gC4Z_HVFz2ylNhzHGCZKUZzWN0IwYwys,839
7
7
  ansible/_vendor/__init__.py,sha256=2QBeBwT7uG7M3Aw-pIdCpt6XPtHMCpbEKfACYKA7xIg,2033
8
8
  ansible/cli/__init__.py,sha256=e0KjeLfG1Ketbwl-uOmQ-zXoq3_El80LnHTGu80d1gs,28111
9
9
  ansible/cli/adhoc.py,sha256=quJ9WzRzf3dz_dtDGmahNMffqyNVy1jzQCMo21YL5Qg,8194
@@ -141,7 +141,7 @@ ansible/inventory/host.py,sha256=PDb5OTplhfpUIvdHiP2BckUOB1gUl302N-3sW0_sTyg,503
141
141
  ansible/inventory/manager.py,sha256=45mHgZTAkQ3IjAtrgsNzJXvynC-HIEor-JJE-V3xXN4,29454
142
142
  ansible/module_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
143
  ansible/module_utils/_text.py,sha256=VkWgAnSNVCbTQqZgllUObBFsH3uM4EUW5srl1UR9t1g,544
144
- ansible/module_utils/ansible_release.py,sha256=9gq7Q1GbBxC3GTVQHdxbwP6GkRpZWnuB_EVmsfq6wVc,836
144
+ ansible/module_utils/ansible_release.py,sha256=cz6pm0cY8y6gC4Z_HVFz2ylNhzHGCZKUZzWN0IwYwys,839
145
145
  ansible/module_utils/api.py,sha256=r4wd6XZGhUnxMF416Ry6ebgq8BIhjCPSPOvO2ZtrYxE,5785
146
146
  ansible/module_utils/basic.py,sha256=fogfpo_l7JtS34WvgwwOebmPfMhFjQaJN5CwjKgUJVE,86291
147
147
  ansible/module_utils/connection.py,sha256=8TviwCucQ7d_JILwaUHE4tCuNfR3U1WFkmxLMxWa8Rw,7671
@@ -295,7 +295,7 @@ ansible/modules/cron.py,sha256=qR5ePdI3GZQeBSCn9YqxTxWlNEblCaTFnBRZqLjtnPo,26353
295
295
  ansible/modules/deb822_repository.py,sha256=SLJM8bBLc70WYu3-OA67wd5hMft3pznYAMIidYOtmUU,15791
296
296
  ansible/modules/debconf.py,sha256=Y49U5pM6UpKvYAvDbOhYe6kmQFAaxjl7YoYnPrOaGGU,9362
297
297
  ansible/modules/debug.py,sha256=BFbzrU_vl-Try5DuLV20_sLgqxEJlPV9uOrgAtby2e8,2908
298
- ansible/modules/dnf.py,sha256=rsb28kjMMnTu-rPW0Pdnbs2RPyvfdhWgXeUORUSgzEI,52288
298
+ ansible/modules/dnf.py,sha256=NsraDYODkJAS2BgaHwBa866qk1Qg8bQ0rXgq_4rHWRM,52455
299
299
  ansible/modules/dnf5.py,sha256=VZ5KpEO1lZScawgmnsrdd2FVW2vyRpg4dikXvWcD6pA,30288
300
300
  ansible/modules/dpkg_selections.py,sha256=lTWBhmVFrf6PsV4_BoR23wVTJOloCH1YNPcAn0m7DTY,2805
301
301
  ansible/modules/expect.py,sha256=O4emRoJ09i3OLmVX5j84WHkGKWg6bMytYpZlExOrSmc,9369
@@ -332,7 +332,7 @@ ansible/modules/replace.py,sha256=L7JI3VFY4NKPlQAt2s27sngHBFSGLVG_l2sWUqwekck,11
332
332
  ansible/modules/rpm_key.py,sha256=BXFIAY6FOxA5LCvu6RaneVPqzVqptF5hdWmFdJQ63Bg,9325
333
333
  ansible/modules/script.py,sha256=bcn4C3BuCvwEE1Ebc7IA5NGOjEcmkiAkx8jBDjQyOAw,4410
334
334
  ansible/modules/service.py,sha256=kLRfjE2Ns99FwoY8gh5i283ArBGVcWXIjrxd3PsDHtw,62341
335
- ansible/modules/service_facts.py,sha256=9AmYTye-EfpArLolOJoIFTQB7MdzaH7e0TB0UKXRacM,18389
335
+ ansible/modules/service_facts.py,sha256=WMhBOCjGxq8rjWED0N5Sv5Y4o306OXnUsiu7-q1VPGw,18668
336
336
  ansible/modules/set_fact.py,sha256=iT7kQWXdUoSYWue66dmqYDUW74s_wUlO7dixnawPkgs,5721
337
337
  ansible/modules/set_stats.py,sha256=RlZZuMJ4aW1Z8Eba7s-yDlBxHnXS12OmYoiWOooyDzY,2641
338
338
  ansible/modules/setup.py,sha256=OyJ3aXrbVW5ybwpyIiA-MzS2PeX_XGK7VpG9saHpjFY,11034
@@ -347,7 +347,7 @@ ansible/modules/tempfile.py,sha256=lA9e8lyFXf9J5ud0R6Jkt8sIFyRcOwzhc9Jz-5_HOZQ,3
347
347
  ansible/modules/template.py,sha256=D1sm36GB_mEimH0CfWq1cJ4w1eRvpcsHwZ-ufVzC_Gs,4537
348
348
  ansible/modules/unarchive.py,sha256=wdSOFKhZqbAFq5j_tBZtUSamfr_EEW6_cTZDw-erMjs,45405
349
349
  ansible/modules/uri.py,sha256=UCANRxecG45I4HGtj1AX9k0I01w8dfTplWksejLHrIg,27846
350
- ansible/modules/user.py,sha256=0lEGObBxw_SfjJGJGi2sk6-V8Rwkvt1VfhdfWhuWyrA,123562
350
+ ansible/modules/user.py,sha256=tQK7ZloLNe485D-FD0ZQsDNhxjOVDlbvs6kqYW2pSh0,123973
351
351
  ansible/modules/validate_argument_spec.py,sha256=XbWlUr4ElgLfdxo3qCN7M-IES_X2iTl3AgawzCOMQpo,3042
352
352
  ansible/modules/wait_for.py,sha256=rsx_PR73-BRImLNErVUM1o9wRdPlXkGu5y6FhJ8wLcY,27355
353
353
  ansible/modules/wait_for_connection.py,sha256=8ySz5bhK7LGSaT_7Jzk3jvACcnngyR2g_ziyFLKk6Rk,3367
@@ -687,11 +687,11 @@ ansible/vars/hostvars.py,sha256=o11xrzDVYn23renGbb3lx3R-nH9qOjLFju5IYJanDxg,5324
687
687
  ansible/vars/manager.py,sha256=JF2KTL4iYSbcdnFNjhQPktwH05YhWJhTWtjSlF0qg9E,31260
688
688
  ansible/vars/plugins.py,sha256=PocWZPMqFl1LoNgWlGFNxwg9nZnUzhQmlXO4g7bcP2A,4503
689
689
  ansible/vars/reserved.py,sha256=Tsc4m2UwVce3dOvSWrjT2wB3lpNJtUyNZn45zNhsW0I,2869
690
- ansible_core-2.18.8.dist-info/licenses/COPYING,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
691
- ansible_core-2.18.8.dist-info/licenses/licenses/Apache-License.txt,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
692
- ansible_core-2.18.8.dist-info/licenses/licenses/MIT-license.txt,sha256=jLXp2XurnyZKbye40g9tfmLGtVlxh3pPD4n8xNqX8xc,1023
693
- ansible_core-2.18.8.dist-info/licenses/licenses/PSF-license.txt,sha256=g7BC_H1qyg8Q1o5F76Vrm8ChSWYI5-dyj-CdGlNKBUo,2484
694
- ansible_core-2.18.8.dist-info/licenses/licenses/simplified_bsd.txt,sha256=8R5R7R7sOa0h1Fi6RNgFgHowHBfun-OVOMzJ4rKAk2w,1237
690
+ ansible_core-2.18.9rc1.dist-info/licenses/COPYING,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
691
+ ansible_core-2.18.9rc1.dist-info/licenses/licenses/Apache-License.txt,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
692
+ ansible_core-2.18.9rc1.dist-info/licenses/licenses/MIT-license.txt,sha256=jLXp2XurnyZKbye40g9tfmLGtVlxh3pPD4n8xNqX8xc,1023
693
+ ansible_core-2.18.9rc1.dist-info/licenses/licenses/PSF-license.txt,sha256=g7BC_H1qyg8Q1o5F76Vrm8ChSWYI5-dyj-CdGlNKBUo,2484
694
+ ansible_core-2.18.9rc1.dist-info/licenses/licenses/simplified_bsd.txt,sha256=8R5R7R7sOa0h1Fi6RNgFgHowHBfun-OVOMzJ4rKAk2w,1237
695
695
  ansible_test/__init__.py,sha256=20VPOj11c6Ut1Av9RaurgwJvFhMqkWG3vAvcCbecNKw,66
696
696
  ansible_test/_data/ansible.cfg,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
697
697
  ansible_test/_data/coveragerc,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -751,7 +751,7 @@ ansible_test/_internal/connections.py,sha256=-gK9FqvmpsjENdYNkvWgFgqYHJSS_F2XkvQ
751
751
  ansible_test/_internal/constants.py,sha256=Zwgp8wtUuge_8xMPg0pDUt58fBd9KA7YEPTQqAQv8ac,1969
752
752
  ansible_test/_internal/containers.py,sha256=TrkHL4ntmb7HrmD55BzSdqF35s7oFKu0vCywpWfRt-k,34137
753
753
  ansible_test/_internal/content_config.py,sha256=QKR_XVBgYRNZL-XawF2pN2ERTZ6lSm1AJg9ZQRD6IHE,5588
754
- ansible_test/_internal/core_ci.py,sha256=pyiwFG_TgDSQw34qW-PG8T2VYS6XxiF0zOEWGYXRRek,17309
754
+ ansible_test/_internal/core_ci.py,sha256=9RQU92QfSlEPcf9Wet8c3UWmZGrkmZCMQrT1h51iQuk,17976
755
755
  ansible_test/_internal/coverage_util.py,sha256=6SFmRqRPiv5l5Aj3VV1PtnHIgM5CNVvsWrilUch1NtY,9407
756
756
  ansible_test/_internal/data.py,sha256=OFDpRa47yqBqQO1aSvTZVQQpScHvBHsr861586MQEUI,11184
757
757
  ansible_test/_internal/delegation.py,sha256=D8hluDQf_YN3DtVG_8HW0iumRBY3gjp_zP-rlc3VNY4,13418
@@ -761,7 +761,7 @@ ansible_test/_internal/encoding.py,sha256=4GAcVhcswUJW77XSBTLRIP7ll6kT9J-yIBtVsF
761
761
  ansible_test/_internal/executor.py,sha256=KW5yI-f-giErQ077MTj707fTtFkf_Kr8IV_Nr36NNmc,2959
762
762
  ansible_test/_internal/git.py,sha256=njtciWq2DlzZ1DAkQi08HRRP-TgH0mgeGZsWcsJGctI,4366
763
763
  ansible_test/_internal/host_configs.py,sha256=ElAo2t3kSyb7bShqBENiHH0oEe-8Am8JXG9xY5l7HY4,18634
764
- ansible_test/_internal/host_profiles.py,sha256=Y2_LM5nH_J0Yb0Cq9CX8D0VSM2JHJWY6Xs-ZUFX-opU,65629
764
+ ansible_test/_internal/host_profiles.py,sha256=pTKpncORGaRPaDPE-dDVXlSus2CkbrjkM66cxNG5KH8,65721
765
765
  ansible_test/_internal/http.py,sha256=ENuIPnBXIuvgDSxC-r5eOxfGzscxB6MOVJzT4OQXQSA,3864
766
766
  ansible_test/_internal/init.py,sha256=f2ZN7F-FyjMgN73SUgxwbVtWNhkJv7BIlZ-q4ALHyjM,505
767
767
  ansible_test/_internal/inventory.py,sha256=c79s-xc1uv2nD7rPISv0JKkKspY-X2-kHoozF2R4e1Q,5408
@@ -770,7 +770,7 @@ ansible_test/_internal/junit_xml.py,sha256=5op7cjGK7Et0OSjcAAuUEqNWNAv5ZoNI0rkLx
770
770
  ansible_test/_internal/locale_util.py,sha256=tjRbwKmgMQc1ysIhvP8yBhFcNA-2UCaWfQBDgrRFUxU,2161
771
771
  ansible_test/_internal/metadata.py,sha256=c9ThXPUlgeKYhaTUmfCSS4INRNQ1JhN2KEOVaX3m1Gk,4791
772
772
  ansible_test/_internal/payload.py,sha256=1Pw05OEHvP3LMQnoLXch8631c94YMklWlpDn0CvQECw,8012
773
- ansible_test/_internal/provisioning.py,sha256=9Zl3xQqljx0MGDTp55Q4LZPWQ7Afj5K87cGsXzPGS5Y,7320
773
+ ansible_test/_internal/provisioning.py,sha256=owGvyyBmMjtdAw0x41X9qYtOJnkvskpQRcpJ-pmLwlo,7409
774
774
  ansible_test/_internal/pypi_proxy.py,sha256=g2FbZh1ZaLWgj_mfgGNSynMX76s4F4DUKMOYavsWIuA,6019
775
775
  ansible_test/_internal/python_requirements.py,sha256=x9u_9ABfvRx-aeQ-oyoTmlF78mmJ483wBGjxT5PuJfA,15205
776
776
  ansible_test/_internal/ssh.py,sha256=WeVvn3ReHmjg6Im5BdSBRl1YIj1lOmi71jO9T5fTkik,10781
@@ -778,12 +778,12 @@ ansible_test/_internal/target.py,sha256=Whtb_n0jn4zbiMmX7je5jewgzsRczfXRm_ndYtjT
778
778
  ansible_test/_internal/test.py,sha256=znQmGjKACqDU8T0EAPqcv2qyy0J7M2w4OmyYhwHLqT0,14515
779
779
  ansible_test/_internal/thread.py,sha256=WQoZ2q2ljmEkKHRDkIqwxW7eZbkCKDrG3YZfcaxHzHw,2596
780
780
  ansible_test/_internal/timeout.py,sha256=X8LxoMSU85lw4MszfEljaG6V7Aff4Btveg3r89Fe25U,4052
781
- ansible_test/_internal/util.py,sha256=o8efNGzuieD3mf3B7Ash1r25dEcePB7L-HOwu5EPFto,38908
781
+ ansible_test/_internal/util.py,sha256=VLPU4b0sEvb7fZ8pimnJS--5_fjQnrVmckRvm88dEuA,38935
782
782
  ansible_test/_internal/util_common.py,sha256=wSygjQRWlkFNpqQnPSSXnz_3Cr0pWrPvCP-6lMdfuAk,17490
783
783
  ansible_test/_internal/venv.py,sha256=k7L9_Ocpsdwp4kQFLF59BVguymd2nqJ-bLHH1NlMET0,5521
784
- ansible_test/_internal/ci/__init__.py,sha256=QOaC_8_wUzqFEbsFCXYAnElWoUo6gB40CXvP9RJ-Iyo,7738
785
- ansible_test/_internal/ci/azp.py,sha256=DwSL1UiPwmfHYOBNMs4e3iN7tonPneod7Dw1BycnfNM,10136
786
- ansible_test/_internal/ci/local.py,sha256=E4nnerMKdBoVEbsT8IBkl0nSdXyxO2gT8WAaxyzA1EY,6739
784
+ ansible_test/_internal/ci/__init__.py,sha256=wFAyQVsPJbHEI94EN3F3yLBP8aZ4Zlm4U1jka0U8Z8M,4683
785
+ ansible_test/_internal/ci/azp.py,sha256=voqqYVP9OEccLIsx_Y9RyUCR9pL8xz3uu91hwGDac8w,10103
786
+ ansible_test/_internal/ci/local.py,sha256=Yd2fqBs9KcbnF-TEx9PyEslCNF69WNUlM5Bm3-vzq9c,9905
787
787
  ansible_test/_internal/classification/__init__.py,sha256=LVyApjvPsofXjdDaHrXnWl71Z61k5jFJYN3WrBeeFDs,34142
788
788
  ansible_test/_internal/classification/common.py,sha256=jd5VLRegcOX-GNTZqN_7PBzwKF6akFQYsPEltfynGtU,894
789
789
  ansible_test/_internal/classification/csharp.py,sha256=3QpVZjamTTG7h86oeVm7d4UMbyojPbBALHVqCpxS1ic,3241
@@ -985,8 +985,8 @@ ansible_test/config/cloud-config-vultr.ini.template,sha256=XLKHk3lg_8ReQMdWfZzhh
985
985
  ansible_test/config/config.yml,sha256=1zdGucnIl6nIecZA7ISIANvqXiHWqq6Dthsk_6MUwNc,2642
986
986
  ansible_test/config/inventory.networking.template,sha256=bFNSk8zNQOaZ_twaflrY0XZ9mLwUbRLuNT0BdIFwvn4,1335
987
987
  ansible_test/config/inventory.winrm.template,sha256=1QU8W-GFLnYEw8yY9bVIvUAVvJYPM3hyoijf6-M7T00,1098
988
- ansible_core-2.18.8.dist-info/METADATA,sha256=syPAmnkZT9EKQc_Kgn9-d4RDe6MjgwGvtVdi9IdsIeg,7690
989
- ansible_core-2.18.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
990
- ansible_core-2.18.8.dist-info/entry_points.txt,sha256=S9yJij5Im6FgRQxzkqSCnPQokC7PcWrDW_NSygZczJU,451
991
- ansible_core-2.18.8.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
992
- ansible_core-2.18.8.dist-info/RECORD,,
988
+ ansible_core-2.18.9rc1.dist-info/METADATA,sha256=Q5A08nuTPaLqmLm5WcWX5qZhKrFUyYum1dpwEQw40_k,7693
989
+ ansible_core-2.18.9rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
990
+ ansible_core-2.18.9rc1.dist-info/entry_points.txt,sha256=S9yJij5Im6FgRQxzkqSCnPQokC7PcWrDW_NSygZczJU,451
991
+ ansible_core-2.18.9rc1.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
992
+ ansible_core-2.18.9rc1.dist-info/RECORD,,
@@ -2,22 +2,13 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import abc
5
- import base64
5
+ import dataclasses
6
+ import datetime
6
7
  import json
7
- import os
8
+ import pathlib
8
9
  import tempfile
9
10
  import typing as t
10
11
 
11
- from ..encoding import (
12
- to_bytes,
13
- to_text,
14
- )
15
-
16
- from ..io import (
17
- read_text_file,
18
- write_text_file,
19
- )
20
-
21
12
  from ..config import (
22
13
  CommonConfig,
23
14
  TestConfig,
@@ -33,6 +24,65 @@ from ..util import (
33
24
  )
34
25
 
35
26
 
27
+ @dataclasses.dataclass(frozen=True, kw_only=True)
28
+ class AuthContext:
29
+ """Information about the request to which authentication will be applied."""
30
+
31
+ stage: str
32
+ provider: str
33
+ request_id: str
34
+
35
+
36
+ class AuthHelper:
37
+ """Authentication helper."""
38
+
39
+ NAMESPACE: t.ClassVar = 'ci@core.ansible.com'
40
+
41
+ def __init__(self, key_file: pathlib.Path) -> None:
42
+ self.private_key_file = pathlib.Path(str(key_file).removesuffix('.pub'))
43
+ self.public_key_file = pathlib.Path(f'{self.private_key_file}.pub')
44
+
45
+ def sign_request(self, request: dict[str, object], context: AuthContext) -> None:
46
+ """Sign the given auth request using the provided context."""
47
+ request.update(
48
+ stage=context.stage,
49
+ provider=context.provider,
50
+ request_id=context.request_id,
51
+ timestamp=datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat(),
52
+ )
53
+
54
+ with tempfile.TemporaryDirectory() as temp_dir:
55
+ payload_path = pathlib.Path(temp_dir) / 'auth.json'
56
+ payload_path.write_text(json.dumps(request, sort_keys=True))
57
+
58
+ cmd = ['ssh-keygen', '-q', '-Y', 'sign', '-f', str(self.private_key_file), '-n', self.NAMESPACE, str(payload_path)]
59
+ raw_command(cmd, capture=False, interactive=True)
60
+
61
+ signature_path = pathlib.Path(f'{payload_path}.sig')
62
+ signature = signature_path.read_text()
63
+
64
+ request.update(signature=signature)
65
+
66
+
67
+ class GeneratingAuthHelper(AuthHelper, metaclass=abc.ABCMeta):
68
+ """Authentication helper which generates a key pair on demand."""
69
+
70
+ def __init__(self) -> None:
71
+ super().__init__(pathlib.Path('~/.ansible/test/ansible-core-ci').expanduser())
72
+
73
+ def sign_request(self, request: dict[str, object], context: AuthContext) -> None:
74
+ if not self.private_key_file.exists():
75
+ self.generate_key_pair()
76
+
77
+ super().sign_request(request, context)
78
+
79
+ def generate_key_pair(self) -> None:
80
+ """Generate key pair."""
81
+ self.private_key_file.parent.mkdir(parents=True, exist_ok=True)
82
+
83
+ raw_command(['ssh-keygen', '-q', '-f', str(self.private_key_file), '-N', ''], capture=True)
84
+
85
+
36
86
  class ChangeDetectionNotSupported(ApplicationError):
37
87
  """Exception for cases where change detection is not supported."""
38
88
 
@@ -74,8 +124,8 @@ class CIProvider(metaclass=abc.ABCMeta):
74
124
  """Return True if Ansible Core CI is supported."""
75
125
 
76
126
  @abc.abstractmethod
77
- def prepare_core_ci_auth(self) -> dict[str, t.Any]:
78
- """Return authentication details for Ansible Core CI."""
127
+ def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
128
+ """Prepare an Ansible Core CI request using the given config and context."""
79
129
 
80
130
  @abc.abstractmethod
81
131
  def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]:
@@ -100,119 +150,3 @@ def get_ci_provider() -> CIProvider:
100
150
  display.info('Detected CI provider: %s' % provider.name)
101
151
 
102
152
  return provider
103
-
104
-
105
- class AuthHelper(metaclass=abc.ABCMeta):
106
- """Public key based authentication helper for Ansible Core CI."""
107
-
108
- def sign_request(self, request: dict[str, t.Any]) -> None:
109
- """Sign the given auth request and make the public key available."""
110
- payload_bytes = to_bytes(json.dumps(request, sort_keys=True))
111
- signature_raw_bytes = self.sign_bytes(payload_bytes)
112
- signature = to_text(base64.b64encode(signature_raw_bytes))
113
-
114
- request.update(signature=signature)
115
-
116
- def initialize_private_key(self) -> str:
117
- """
118
- Initialize and publish a new key pair (if needed) and return the private key.
119
- The private key is cached across ansible-test invocations, so it is only generated and published once per CI job.
120
- """
121
- path = os.path.expanduser('~/.ansible-core-ci-private.key')
122
-
123
- if os.path.exists(to_bytes(path)):
124
- private_key_pem = read_text_file(path)
125
- else:
126
- private_key_pem = self.generate_private_key()
127
- write_text_file(path, private_key_pem)
128
-
129
- return private_key_pem
130
-
131
- @abc.abstractmethod
132
- def sign_bytes(self, payload_bytes: bytes) -> bytes:
133
- """Sign the given payload and return the signature, initializing a new key pair if required."""
134
-
135
- @abc.abstractmethod
136
- def publish_public_key(self, public_key_pem: str) -> None:
137
- """Publish the given public key."""
138
-
139
- @abc.abstractmethod
140
- def generate_private_key(self) -> str:
141
- """Generate a new key pair, publishing the public key and returning the private key."""
142
-
143
-
144
- class CryptographyAuthHelper(AuthHelper, metaclass=abc.ABCMeta):
145
- """Cryptography based public key based authentication helper for Ansible Core CI."""
146
-
147
- def sign_bytes(self, payload_bytes: bytes) -> bytes:
148
- """Sign the given payload and return the signature, initializing a new key pair if required."""
149
- # import cryptography here to avoid overhead and failures in environments which do not use/provide it
150
- from cryptography.hazmat.backends import default_backend
151
- from cryptography.hazmat.primitives import hashes
152
- from cryptography.hazmat.primitives.asymmetric import ec
153
- from cryptography.hazmat.primitives.serialization import load_pem_private_key
154
-
155
- private_key_pem = self.initialize_private_key()
156
- private_key = load_pem_private_key(to_bytes(private_key_pem), None, default_backend())
157
-
158
- assert isinstance(private_key, ec.EllipticCurvePrivateKey)
159
-
160
- signature_raw_bytes = private_key.sign(payload_bytes, ec.ECDSA(hashes.SHA256()))
161
-
162
- return signature_raw_bytes
163
-
164
- def generate_private_key(self) -> str:
165
- """Generate a new key pair, publishing the public key and returning the private key."""
166
- # import cryptography here to avoid overhead and failures in environments which do not use/provide it
167
- from cryptography.hazmat.backends import default_backend
168
- from cryptography.hazmat.primitives import serialization
169
- from cryptography.hazmat.primitives.asymmetric import ec
170
-
171
- private_key = ec.generate_private_key(ec.SECP384R1(), default_backend())
172
- public_key = private_key.public_key()
173
-
174
- private_key_pem = to_text(private_key.private_bytes( # type: ignore[attr-defined] # documented method, but missing from type stubs
175
- encoding=serialization.Encoding.PEM,
176
- format=serialization.PrivateFormat.PKCS8,
177
- encryption_algorithm=serialization.NoEncryption(),
178
- ))
179
-
180
- public_key_pem = to_text(public_key.public_bytes(
181
- encoding=serialization.Encoding.PEM,
182
- format=serialization.PublicFormat.SubjectPublicKeyInfo,
183
- ))
184
-
185
- self.publish_public_key(public_key_pem)
186
-
187
- return private_key_pem
188
-
189
-
190
- class OpenSSLAuthHelper(AuthHelper, metaclass=abc.ABCMeta):
191
- """OpenSSL based public key based authentication helper for Ansible Core CI."""
192
-
193
- def sign_bytes(self, payload_bytes: bytes) -> bytes:
194
- """Sign the given payload and return the signature, initializing a new key pair if required."""
195
- private_key_pem = self.initialize_private_key()
196
-
197
- with tempfile.NamedTemporaryFile() as private_key_file:
198
- private_key_file.write(to_bytes(private_key_pem))
199
- private_key_file.flush()
200
-
201
- with tempfile.NamedTemporaryFile() as payload_file:
202
- payload_file.write(payload_bytes)
203
- payload_file.flush()
204
-
205
- with tempfile.NamedTemporaryFile() as signature_file:
206
- raw_command(['openssl', 'dgst', '-sha256', '-sign', private_key_file.name, '-out', signature_file.name, payload_file.name], capture=True)
207
- signature_raw_bytes = signature_file.read()
208
-
209
- return signature_raw_bytes
210
-
211
- def generate_private_key(self) -> str:
212
- """Generate a new key pair, publishing the public key and returning the private key."""
213
- private_key_pem = raw_command(['openssl', 'ecparam', '-genkey', '-name', 'secp384r1', '-noout'], capture=True)[0]
214
- public_key_pem = raw_command(['openssl', 'ec', '-pubout'], data=private_key_pem, capture=True)[0]
215
-
216
- self.publish_public_key(public_key_pem)
217
-
218
- return private_key_pem
@@ -30,9 +30,10 @@ from ..util import (
30
30
  )
31
31
 
32
32
  from . import (
33
+ AuthContext,
33
34
  ChangeDetectionNotSupported,
34
35
  CIProvider,
35
- CryptographyAuthHelper,
36
+ GeneratingAuthHelper,
36
37
  )
37
38
 
38
39
  CODE = 'azp'
@@ -111,10 +112,11 @@ class AzurePipelines(CIProvider):
111
112
  """Return True if Ansible Core CI is supported."""
112
113
  return True
113
114
 
114
- def prepare_core_ci_auth(self) -> dict[str, t.Any]:
115
- """Return authentication details for Ansible Core CI."""
115
+ def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
116
116
  try:
117
- request = dict(
117
+ request: dict[str, object] = dict(
118
+ type="azp:ssh",
119
+ config=config,
118
120
  org_name=os.environ['SYSTEM_COLLECTIONURI'].strip('/').split('/')[-1],
119
121
  project_name=os.environ['SYSTEM_TEAMPROJECT'],
120
122
  build_id=int(os.environ['BUILD_BUILDID']),
@@ -123,13 +125,9 @@ class AzurePipelines(CIProvider):
123
125
  except KeyError as ex:
124
126
  raise MissingEnvironmentVariable(name=ex.args[0]) from None
125
127
 
126
- self.auth.sign_request(request)
128
+ self.auth.sign_request(request, context)
127
129
 
128
- auth = dict(
129
- azp=request,
130
- )
131
-
132
- return auth
130
+ return request
133
131
 
134
132
  def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]:
135
133
  """Return details about git in the current environment."""
@@ -143,14 +141,14 @@ class AzurePipelines(CIProvider):
143
141
  return details
144
142
 
145
143
 
146
- class AzurePipelinesAuthHelper(CryptographyAuthHelper):
147
- """
148
- Authentication helper for Azure Pipelines.
149
- Based on cryptography since it is provided by the default Azure Pipelines environment.
150
- """
144
+ class AzurePipelinesAuthHelper(GeneratingAuthHelper):
145
+ """Authentication helper for Azure Pipelines."""
146
+
147
+ def generate_key_pair(self) -> None:
148
+ super().generate_key_pair()
149
+
150
+ public_key_pem = self.public_key_file.read_text()
151
151
 
152
- def publish_public_key(self, public_key_pem: str) -> None:
153
- """Publish the given public key."""
154
152
  try:
155
153
  agent_temp_directory = os.environ['AGENT_TEMPDIRECTORY']
156
154
  except KeyError as ex:
@@ -1,10 +1,12 @@
1
1
  """Support code for working without a supported CI provider."""
2
2
  from __future__ import annotations
3
3
 
4
- import os
4
+ import abc
5
+ import inspect
5
6
  import platform
6
7
  import random
7
8
  import re
9
+ import pathlib
8
10
  import typing as t
9
11
 
10
12
  from ..config import (
@@ -23,11 +25,14 @@ from ..git import (
23
25
  from ..util import (
24
26
  ApplicationError,
25
27
  display,
28
+ get_subclasses,
26
29
  is_binary_file,
27
30
  SubprocessError,
28
31
  )
29
32
 
30
33
  from . import (
34
+ AuthContext,
35
+ AuthHelper,
31
36
  CIProvider,
32
37
  )
33
38
 
@@ -119,34 +124,20 @@ class Local(CIProvider):
119
124
 
120
125
  def supports_core_ci_auth(self) -> bool:
121
126
  """Return True if Ansible Core CI is supported."""
122
- path = self._get_aci_key_path()
123
- return os.path.exists(path)
127
+ return Authenticator.available()
124
128
 
125
- def prepare_core_ci_auth(self) -> dict[str, t.Any]:
126
- """Return authentication details for Ansible Core CI."""
127
- path = self._get_aci_key_path()
128
- auth_key = read_text_file(path).strip()
129
+ def prepare_core_ci_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
130
+ if not (authenticator := Authenticator.load()):
131
+ raise ApplicationError('Ansible Core CI authentication has not been configured.')
129
132
 
130
- request = dict(
131
- key=auth_key,
132
- nonce=None,
133
- )
133
+ display.info(f'Using {authenticator} for Ansible Core CI.', verbosity=1)
134
134
 
135
- auth = dict(
136
- remote=request,
137
- )
138
-
139
- return auth
135
+ return authenticator.prepare_auth_request(config, context)
140
136
 
141
137
  def get_git_details(self, args: CommonConfig) -> t.Optional[dict[str, t.Any]]:
142
138
  """Return details about git in the current environment."""
143
139
  return None # not yet implemented for local
144
140
 
145
- @staticmethod
146
- def _get_aci_key_path() -> str:
147
- path = os.path.expanduser('~/.ansible-core-ci.key')
148
- return path
149
-
150
141
 
151
142
  class InvalidBranch(ApplicationError):
152
143
  """Exception for invalid branch specification."""
@@ -213,3 +204,108 @@ class LocalChanges:
213
204
  return True
214
205
 
215
206
  return False
207
+
208
+
209
+ class Authenticator(metaclass=abc.ABCMeta):
210
+ """Base class for authenticators."""
211
+
212
+ @staticmethod
213
+ def list() -> list[type[Authenticator]]:
214
+ """List all authenticators in priority order."""
215
+ return sorted((sc for sc in get_subclasses(Authenticator) if not inspect.isabstract(sc)), key=lambda obj: obj.priority())
216
+
217
+ @staticmethod
218
+ def load() -> Authenticator | None:
219
+ """Load an authenticator instance, returning None if not configured."""
220
+ for implementation in Authenticator.list():
221
+ if implementation.config_file().exists():
222
+ return implementation()
223
+
224
+ return None
225
+
226
+ @staticmethod
227
+ def available() -> bool:
228
+ """Return True if an authenticator is available, otherwise False."""
229
+ return bool(Authenticator.load())
230
+
231
+ @classmethod
232
+ @abc.abstractmethod
233
+ def priority(cls) -> int:
234
+ """Priority used to determine which authenticator is tried first, from lowest to highest."""
235
+
236
+ @classmethod
237
+ @abc.abstractmethod
238
+ def config_file(cls) -> pathlib.Path:
239
+ """Path to the config file for this authenticator."""
240
+
241
+ @abc.abstractmethod
242
+ def prepare_auth_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
243
+ """Prepare an authenticated Ansible Core CI request using the given config and context."""
244
+
245
+ def __str__(self) -> str:
246
+ return self.__class__.__name__
247
+
248
+
249
+ class PasswordAuthenticator(Authenticator):
250
+ """Authenticate using a password."""
251
+
252
+ @classmethod
253
+ def priority(cls) -> int:
254
+ return 200
255
+
256
+ @classmethod
257
+ def config_file(cls) -> pathlib.Path:
258
+ return pathlib.Path('~/.ansible-core-ci.key').expanduser()
259
+
260
+ def prepare_auth_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
261
+ parts = self.config_file().read_text().strip().split(maxsplit=1)
262
+
263
+ if len(parts) == 1: # temporary backward compatibility for legacy API keys
264
+ request = dict(
265
+ config=config,
266
+ auth=dict(
267
+ remote=dict(
268
+ key=parts[0],
269
+ ),
270
+ ),
271
+ )
272
+
273
+ return request
274
+
275
+ username, password = parts
276
+
277
+ request = dict(
278
+ type="remote:password",
279
+ config=config,
280
+ username=username,
281
+ password=password,
282
+ )
283
+
284
+ return request
285
+
286
+
287
+ class SshAuthenticator(Authenticator):
288
+ """Authenticate using an SSH key."""
289
+
290
+ @classmethod
291
+ def priority(cls) -> int:
292
+ return 100
293
+
294
+ @classmethod
295
+ def config_file(cls) -> pathlib.Path:
296
+ return pathlib.Path('~/.ansible-core-ci.auth').expanduser()
297
+
298
+ def prepare_auth_request(self, config: dict[str, object], context: AuthContext) -> dict[str, object]:
299
+ parts = self.config_file().read_text().strip().split(maxsplit=1)
300
+ username, key_file = parts
301
+
302
+ request: dict[str, object] = dict(
303
+ type="remote:ssh",
304
+ config=config,
305
+ username=username,
306
+ )
307
+
308
+ auth_helper = AuthHelper(pathlib.Path(key_file).expanduser())
309
+ auth_helper.sign_request(request, context)
310
+
311
+ return request
@@ -41,6 +41,7 @@ from .config import (
41
41
  )
42
42
 
43
43
  from .ci import (
44
+ AuthContext,
44
45
  get_ci_provider,
45
46
  )
46
47
 
@@ -67,6 +68,10 @@ class Resource(metaclass=abc.ABCMeta):
67
68
  def persist(self) -> bool:
68
69
  """True if the resource is persistent, otherwise false."""
69
70
 
71
+ @abc.abstractmethod
72
+ def get_config(self, core_ci: AnsibleCoreCI) -> dict[str, object]:
73
+ """Return the configuration for this resource."""
74
+
70
75
 
71
76
  @dataclasses.dataclass(frozen=True)
72
77
  class VmResource(Resource):
@@ -91,6 +96,16 @@ class VmResource(Resource):
91
96
  """True if the resource is persistent, otherwise false."""
92
97
  return True
93
98
 
99
+ def get_config(self, core_ci: AnsibleCoreCI) -> dict[str, object]:
100
+ """Return the configuration for this resource."""
101
+ return dict(
102
+ type="vm",
103
+ platform=self.platform,
104
+ version=self.version,
105
+ architecture=self.architecture,
106
+ public_key=core_ci.ssh_key.pub_contents,
107
+ )
108
+
94
109
 
95
110
  @dataclasses.dataclass(frozen=True)
96
111
  class CloudResource(Resource):
@@ -111,6 +126,12 @@ class CloudResource(Resource):
111
126
  """True if the resource is persistent, otherwise false."""
112
127
  return False
113
128
 
129
+ def get_config(self, core_ci: AnsibleCoreCI) -> dict[str, object]:
130
+ """Return the configuration for this resource."""
131
+ return dict(
132
+ type="cloud",
133
+ )
134
+
114
135
 
115
136
  class AnsibleCoreCI:
116
137
  """Client for Ansible Core CI services."""
@@ -188,7 +209,7 @@ class AnsibleCoreCI:
188
209
  display.info(f'Skipping started {self.label} instance.', verbosity=1)
189
210
  return None
190
211
 
191
- return self._start(self.ci_provider.prepare_core_ci_auth())
212
+ return self._start()
192
213
 
193
214
  def stop(self) -> None:
194
215
  """Stop instance."""
@@ -287,26 +308,25 @@ class AnsibleCoreCI:
287
308
  def _uri(self) -> str:
288
309
  return f'{self.endpoint}/{self.stage}/{self.provider}/{self.instance_id}'
289
310
 
290
- def _start(self, auth) -> dict[str, t.Any]:
311
+ def _start(self) -> dict[str, t.Any]:
291
312
  """Start instance."""
292
313
  display.info(f'Initializing new {self.label} instance using: {self._uri}', verbosity=1)
293
314
 
294
- data = dict(
295
- config=dict(
296
- platform=self.platform,
297
- version=self.version,
298
- architecture=self.arch,
299
- public_key=self.ssh_key.pub_contents,
300
- )
315
+ config = self.resource.get_config(self)
316
+
317
+ context = AuthContext(
318
+ request_id=self.instance_id,
319
+ stage=self.stage,
320
+ provider=self.provider,
301
321
  )
302
322
 
303
- data.update(auth=auth)
323
+ request = self.ci_provider.prepare_core_ci_request(config, context)
304
324
 
305
325
  headers = {
306
326
  'Content-Type': 'application/json',
307
327
  }
308
328
 
309
- response = self._start_endpoint(data, headers)
329
+ response = self._start_endpoint(request, headers)
310
330
 
311
331
  self.started = True
312
332
  self._save()
@@ -246,6 +246,9 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
246
246
  self.cache: dict[str, t.Any] = {}
247
247
  """Cache that must not be persisted across delegation."""
248
248
 
249
+ def pre_provision(self) -> None:
250
+ """Pre-provision the host profile."""
251
+
249
252
  def provision(self) -> None:
250
253
  """Provision the host before delegation."""
251
254
 
@@ -329,8 +332,8 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
329
332
  """The saved Ansible Core CI state."""
330
333
  self.state['core_ci'] = value
331
334
 
332
- def provision(self) -> None:
333
- """Provision the host before delegation."""
335
+ def pre_provision(self) -> None:
336
+ """Pre-provision the host before delegation."""
334
337
  self.core_ci = self.create_core_ci(load=True)
335
338
  self.core_ci.start()
336
339
 
@@ -129,6 +129,9 @@ def prepare_profiles(
129
129
 
130
130
  ExitHandler.register(functools.partial(cleanup_profiles, host_state))
131
131
 
132
+ for pre_profile in host_state.profiles:
133
+ pre_profile.pre_provision()
134
+
132
135
  def provision(profile: HostProfile) -> None:
133
136
  """Provision the given profile."""
134
137
  profile.provision()
@@ -678,6 +678,7 @@ def common_environment() -> dict[str, str]:
678
678
  optional = (
679
679
  'LD_LIBRARY_PATH',
680
680
  'SSH_AUTH_SOCK',
681
+ 'SSH_SK_PROVIDER',
681
682
  # MacOS High Sierra Compatibility
682
683
  # http://sealiesoftware.com/blog/archive/2017/6/5/Objective-C_and_fork_in_macOS_1013.html
683
684
  # Example configuration for macOS: