ansible-core 2.17.13rc1__py3-none-any.whl → 2.17.14rc1__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.
- ansible/module_utils/ansible_release.py +1 -1
- ansible/release.py +1 -1
- {ansible_core-2.17.13rc1.dist-info → ansible_core-2.17.14rc1.dist-info}/METADATA +1 -1
- {ansible_core-2.17.13rc1.dist-info → ansible_core-2.17.14rc1.dist-info}/RECORD +18 -18
- ansible_test/_data/completion/remote.txt +1 -1
- ansible_test/_internal/ci/__init__.py +64 -130
- ansible_test/_internal/ci/azp.py +15 -17
- ansible_test/_internal/ci/local.py +117 -21
- ansible_test/_internal/core_ci.py +31 -11
- ansible_test/_internal/host_profiles.py +5 -2
- ansible_test/_internal/provisioning.py +3 -0
- ansible_test/_internal/util.py +1 -0
- ansible_test/_util/target/setup/bootstrap.sh +41 -31
- {ansible_core-2.17.13rc1.data → ansible_core-2.17.14rc1.data}/scripts/ansible-test +0 -0
- {ansible_core-2.17.13rc1.dist-info → ansible_core-2.17.14rc1.dist-info}/WHEEL +0 -0
- {ansible_core-2.17.13rc1.dist-info → ansible_core-2.17.14rc1.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.17.13rc1.dist-info → ansible_core-2.17.14rc1.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.17.13rc1.dist-info → ansible_core-2.17.14rc1.dist-info}/top_level.txt +0 -0
ansible/release.py
CHANGED
|
@@ -3,7 +3,7 @@ ansible/__main__.py,sha256=EnLcULXNtSXkuJ8igEHPPLBTZKAwqXv4PvMEhvzp2Oo,1430
|
|
|
3
3
|
ansible/constants.py,sha256=vRwEcoynqtuKDPKsxKUY94XzrTSV3J0y1slb907DioU,9140
|
|
4
4
|
ansible/context.py,sha256=oKYyfjfWpy8vDeProtqfnqSmuij_t75_5e5t0U_hQ1g,1933
|
|
5
5
|
ansible/keyword_desc.yml,sha256=vE9joFgSeHR4Djl7Bd-HHVCrGByRCrTUmWYZ8LKPZKk,7412
|
|
6
|
-
ansible/release.py,sha256=
|
|
6
|
+
ansible/release.py,sha256=uy2FZVAk8RGRrhMFnOC8NXgkrzn3OxSZCCtWww_PpTk,836
|
|
7
7
|
ansible/_vendor/__init__.py,sha256=2QBeBwT7uG7M3Aw-pIdCpt6XPtHMCpbEKfACYKA7xIg,2033
|
|
8
8
|
ansible/cli/__init__.py,sha256=fzgR82NIGBH3GujIMehhAaP4KYszn4uztuCaFYRUpGk,28718
|
|
9
9
|
ansible/cli/adhoc.py,sha256=quJ9WzRzf3dz_dtDGmahNMffqyNVy1jzQCMo21YL5Qg,8194
|
|
@@ -140,7 +140,7 @@ ansible/inventory/host.py,sha256=PDb5OTplhfpUIvdHiP2BckUOB1gUl302N-3sW0_sTyg,503
|
|
|
140
140
|
ansible/inventory/manager.py,sha256=45mHgZTAkQ3IjAtrgsNzJXvynC-HIEor-JJE-V3xXN4,29454
|
|
141
141
|
ansible/module_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
142
142
|
ansible/module_utils/_text.py,sha256=VkWgAnSNVCbTQqZgllUObBFsH3uM4EUW5srl1UR9t1g,544
|
|
143
|
-
ansible/module_utils/ansible_release.py,sha256=
|
|
143
|
+
ansible/module_utils/ansible_release.py,sha256=uy2FZVAk8RGRrhMFnOC8NXgkrzn3OxSZCCtWww_PpTk,836
|
|
144
144
|
ansible/module_utils/api.py,sha256=DWIuLW5gDWuyyDHLLgGnub42Qa8kagDdkf1xDeLAFl4,5784
|
|
145
145
|
ansible/module_utils/basic.py,sha256=UcDamm_6bkL3HXxKvQcSUlzDOHkIlvd8AYGuqJNmZeI,86113
|
|
146
146
|
ansible/module_utils/connection.py,sha256=q_BdUaST6E44ltHsWPOFOheXK9vKmzaJvP-eQOrOrmE,8394
|
|
@@ -678,14 +678,14 @@ ansible/vars/hostvars.py,sha256=o11xrzDVYn23renGbb3lx3R-nH9qOjLFju5IYJanDxg,5324
|
|
|
678
678
|
ansible/vars/manager.py,sha256=Yuo51lu4UVfzxMS63zYtZMcI8iFYgLXtg0p8fnq3Y7E,38871
|
|
679
679
|
ansible/vars/plugins.py,sha256=RsRU9fiLcJwPIAyTYnmVZglsiEOMCIgQskflavE-XnE,4546
|
|
680
680
|
ansible/vars/reserved.py,sha256=Tsc4m2UwVce3dOvSWrjT2wB3lpNJtUyNZn45zNhsW0I,2869
|
|
681
|
-
ansible_core-2.17.
|
|
682
|
-
ansible_core-2.17.
|
|
681
|
+
ansible_core-2.17.14rc1.data/scripts/ansible-test,sha256=dyY2HtRZotRQO3b89HGXY_KnJgBvgsm4eLIe4B2LUoA,1637
|
|
682
|
+
ansible_core-2.17.14rc1.dist-info/licenses/COPYING,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
683
683
|
ansible_test/__init__.py,sha256=20VPOj11c6Ut1Av9RaurgwJvFhMqkWG3vAvcCbecNKw,66
|
|
684
684
|
ansible_test/_data/ansible.cfg,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
685
685
|
ansible_test/_data/coveragerc,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
686
686
|
ansible_test/_data/completion/docker.txt,sha256=ddsWorTETn1pF9n5coT-tVRC1Hizf9vp6_q0t28S3I0,642
|
|
687
687
|
ansible_test/_data/completion/network.txt,sha256=BxVN0UxlVkRUrPi9MBArQOe6nR8exaow0oCAznUdfKQ,100
|
|
688
|
-
ansible_test/_data/completion/remote.txt,sha256=
|
|
688
|
+
ansible_test/_data/completion/remote.txt,sha256=Diq2dsppMJCspO6m_eVJy3xdSKHCQwgoCVauyfIVtbw,914
|
|
689
689
|
ansible_test/_data/completion/windows.txt,sha256=LunFLE7xMeoS9TVDuE58nUBVzsz-Wh-9wfL80mGiUmo,147
|
|
690
690
|
ansible_test/_data/playbooks/posix_coverage_setup.yml,sha256=PgQNVzVTsNmfnu0sT2SAYiWtkMSOppfmh0oVmAsb7TQ,594
|
|
691
691
|
ansible_test/_data/playbooks/posix_coverage_teardown.yml,sha256=xHci5QllwJymFtig-hsOXm-Wdrxz063JH14aIyRXhyc,212
|
|
@@ -741,7 +741,7 @@ ansible_test/_internal/connections.py,sha256=-gK9FqvmpsjENdYNkvWgFgqYHJSS_F2XkvQ
|
|
|
741
741
|
ansible_test/_internal/constants.py,sha256=djMgWI_xR1Yg6M9Au8dEtao6yTYIzeLA-Ctxb1sKnHg,2056
|
|
742
742
|
ansible_test/_internal/containers.py,sha256=8uRbrDtQKJznPYHbrCDuxZI0teyhcL8qT3mAO2M_DU8,33905
|
|
743
743
|
ansible_test/_internal/content_config.py,sha256=QKR_XVBgYRNZL-XawF2pN2ERTZ6lSm1AJg9ZQRD6IHE,5588
|
|
744
|
-
ansible_test/_internal/core_ci.py,sha256=
|
|
744
|
+
ansible_test/_internal/core_ci.py,sha256=9RQU92QfSlEPcf9Wet8c3UWmZGrkmZCMQrT1h51iQuk,17976
|
|
745
745
|
ansible_test/_internal/coverage_util.py,sha256=p8zcoN6DyyNcLWHzAOtGMeN_6BHTCD1jR9Hm-g-IPIY,9332
|
|
746
746
|
ansible_test/_internal/data.py,sha256=OFDpRa47yqBqQO1aSvTZVQQpScHvBHsr861586MQEUI,11184
|
|
747
747
|
ansible_test/_internal/delegation.py,sha256=D8hluDQf_YN3DtVG_8HW0iumRBY3gjp_zP-rlc3VNY4,13418
|
|
@@ -751,7 +751,7 @@ ansible_test/_internal/encoding.py,sha256=E61EfXbQw0uQoFhbN3SYx3Oy_1tAMCPAAvY9hk
|
|
|
751
751
|
ansible_test/_internal/executor.py,sha256=KW5yI-f-giErQ077MTj707fTtFkf_Kr8IV_Nr36NNmc,2959
|
|
752
752
|
ansible_test/_internal/git.py,sha256=njtciWq2DlzZ1DAkQi08HRRP-TgH0mgeGZsWcsJGctI,4366
|
|
753
753
|
ansible_test/_internal/host_configs.py,sha256=0S6EfSE2QMkOi4-ySxM6A4hlGxfb3aSjJKUHOC4wiwM,18283
|
|
754
|
-
ansible_test/_internal/host_profiles.py,sha256=
|
|
754
|
+
ansible_test/_internal/host_profiles.py,sha256=auM8hVRk-1lxNlM1llz6BlOJv5lft2dfBfO1kFnt1Iw,65581
|
|
755
755
|
ansible_test/_internal/http.py,sha256=ENuIPnBXIuvgDSxC-r5eOxfGzscxB6MOVJzT4OQXQSA,3864
|
|
756
756
|
ansible_test/_internal/init.py,sha256=f2ZN7F-FyjMgN73SUgxwbVtWNhkJv7BIlZ-q4ALHyjM,505
|
|
757
757
|
ansible_test/_internal/inventory.py,sha256=c79s-xc1uv2nD7rPISv0JKkKspY-X2-kHoozF2R4e1Q,5408
|
|
@@ -760,7 +760,7 @@ ansible_test/_internal/junit_xml.py,sha256=5op7cjGK7Et0OSjcAAuUEqNWNAv5ZoNI0rkLx
|
|
|
760
760
|
ansible_test/_internal/locale_util.py,sha256=tjRbwKmgMQc1ysIhvP8yBhFcNA-2UCaWfQBDgrRFUxU,2161
|
|
761
761
|
ansible_test/_internal/metadata.py,sha256=c9ThXPUlgeKYhaTUmfCSS4INRNQ1JhN2KEOVaX3m1Gk,4791
|
|
762
762
|
ansible_test/_internal/payload.py,sha256=1Pw05OEHvP3LMQnoLXch8631c94YMklWlpDn0CvQECw,8012
|
|
763
|
-
ansible_test/_internal/provisioning.py,sha256=
|
|
763
|
+
ansible_test/_internal/provisioning.py,sha256=owGvyyBmMjtdAw0x41X9qYtOJnkvskpQRcpJ-pmLwlo,7409
|
|
764
764
|
ansible_test/_internal/pypi_proxy.py,sha256=1y21FjIyzXMdbFFWiOQWr3BocxXTsavw_NCagSkD0uM,6019
|
|
765
765
|
ansible_test/_internal/python_requirements.py,sha256=tilVPxEthIWBYd7PGx89cVyYX_Ahy9CVxlJ10PfkzUU,15672
|
|
766
766
|
ansible_test/_internal/ssh.py,sha256=WeVvn3ReHmjg6Im5BdSBRl1YIj1lOmi71jO9T5fTkik,10781
|
|
@@ -768,12 +768,12 @@ ansible_test/_internal/target.py,sha256=Whtb_n0jn4zbiMmX7je5jewgzsRczfXRm_ndYtjT
|
|
|
768
768
|
ansible_test/_internal/test.py,sha256=znQmGjKACqDU8T0EAPqcv2qyy0J7M2w4OmyYhwHLqT0,14515
|
|
769
769
|
ansible_test/_internal/thread.py,sha256=WQoZ2q2ljmEkKHRDkIqwxW7eZbkCKDrG3YZfcaxHzHw,2596
|
|
770
770
|
ansible_test/_internal/timeout.py,sha256=hT-LirImhAh1iCGIh8JpmECXsiGu6Zetw8BWl1iBIC8,4050
|
|
771
|
-
ansible_test/_internal/util.py,sha256=
|
|
771
|
+
ansible_test/_internal/util.py,sha256=dkDOAbm3e9IxvMDTta23hCxlca3dpq10pVP2QK-clmc,37859
|
|
772
772
|
ansible_test/_internal/util_common.py,sha256=W5mkR0sevcyMWsMPYcpxRN-b8It8N9g6PqkophHCI9U,17385
|
|
773
773
|
ansible_test/_internal/venv.py,sha256=k7L9_Ocpsdwp4kQFLF59BVguymd2nqJ-bLHH1NlMET0,5521
|
|
774
|
-
ansible_test/_internal/ci/__init__.py,sha256=
|
|
775
|
-
ansible_test/_internal/ci/azp.py,sha256=
|
|
776
|
-
ansible_test/_internal/ci/local.py,sha256=
|
|
774
|
+
ansible_test/_internal/ci/__init__.py,sha256=wFAyQVsPJbHEI94EN3F3yLBP8aZ4Zlm4U1jka0U8Z8M,4683
|
|
775
|
+
ansible_test/_internal/ci/azp.py,sha256=i2Th7ZWJhefYR2oxcsK8ZlU11xXtMbCJBjBQa3kAj_8,10104
|
|
776
|
+
ansible_test/_internal/ci/local.py,sha256=Yd2fqBs9KcbnF-TEx9PyEslCNF69WNUlM5Bm3-vzq9c,9905
|
|
777
777
|
ansible_test/_internal/classification/__init__.py,sha256=ZhYq3YHtd5iO8yFWcnWqwg_JIGWOYHmFoxwoUzKrZmM,34199
|
|
778
778
|
ansible_test/_internal/classification/common.py,sha256=jd5VLRegcOX-GNTZqN_7PBzwKF6akFQYsPEltfynGtU,894
|
|
779
779
|
ansible_test/_internal/classification/csharp.py,sha256=3QpVZjamTTG7h86oeVm7d4UMbyojPbBALHVqCpxS1ic,3241
|
|
@@ -959,7 +959,7 @@ ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py,sha256=vn
|
|
|
959
959
|
ansible_test/_util/target/pytest/plugins/ansible_pytest_coverage.py,sha256=RAEMJ4N88UhwlCcD3gRG78ERb12uW2FMYI4j1tOiEhU,1546
|
|
960
960
|
ansible_test/_util/target/sanity/compile/compile.py,sha256=iTRgiZHNO8DwjSqHBw8gPBbFtWnr-Zbd_ybymeazdtA,1302
|
|
961
961
|
ansible_test/_util/target/sanity/import/importer.py,sha256=BLQN6NmdaMgbI6mu_AdkL4AeD5LxYUi-JXEBGJTuhnU,25148
|
|
962
|
-
ansible_test/_util/target/setup/bootstrap.sh,sha256=
|
|
962
|
+
ansible_test/_util/target/setup/bootstrap.sh,sha256=ZXMU6ps2bfZYnumw7OZ-4Zbr7erVsBf6qqSb95ZGsOo,12848
|
|
963
963
|
ansible_test/_util/target/setup/check_systemd_cgroup_v1.sh,sha256=Aq0T62x_KLtkGaWzYqWjvhchTqYFflrTbQET3h6xrT0,395
|
|
964
964
|
ansible_test/_util/target/setup/probe_cgroups.py,sha256=wUHvjW_GXpcyMGw308w26T09cOtBW5EU7i9WagGDQ7o,659
|
|
965
965
|
ansible_test/_util/target/setup/quiet_pip.py,sha256=d3bvh9k2XI_z8-vb3ZoI4lwL8LaFkwvjJE7PpApBlcw,1979
|
|
@@ -980,8 +980,8 @@ ansible_test/config/cloud-config-vultr.ini.template,sha256=XLKHk3lg_8ReQMdWfZzhh
|
|
|
980
980
|
ansible_test/config/config.yml,sha256=wb3knoBmZewG3GWOMnRHoVPQWW4vPixKLPMNS6vJmTc,2620
|
|
981
981
|
ansible_test/config/inventory.networking.template,sha256=bFNSk8zNQOaZ_twaflrY0XZ9mLwUbRLuNT0BdIFwvn4,1335
|
|
982
982
|
ansible_test/config/inventory.winrm.template,sha256=1QU8W-GFLnYEw8yY9bVIvUAVvJYPM3hyoijf6-M7T00,1098
|
|
983
|
-
ansible_core-2.17.
|
|
984
|
-
ansible_core-2.17.
|
|
985
|
-
ansible_core-2.17.
|
|
986
|
-
ansible_core-2.17.
|
|
987
|
-
ansible_core-2.17.
|
|
983
|
+
ansible_core-2.17.14rc1.dist-info/METADATA,sha256=gQ_JZpOu3czuT-G_edIR9-z4GPZRlOREJsByiU_IQqM,6991
|
|
984
|
+
ansible_core-2.17.14rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
985
|
+
ansible_core-2.17.14rc1.dist-info/entry_points.txt,sha256=0mpmsrIhODChxKl3eS-NcVQCaMetBn8KdPLtVxQgR64,453
|
|
986
|
+
ansible_core-2.17.14rc1.dist-info/top_level.txt,sha256=IFbRLjAvih1DYzJWg3_F6t4sCzEMxRO7TOMNs6GkYHo,21
|
|
987
|
+
ansible_core-2.17.14rc1.dist-info/RECORD,,
|
|
@@ -2,7 +2,7 @@ alpine/3.19 python=3.11 become=doas_sudo provider=aws arch=x86_64
|
|
|
2
2
|
alpine become=doas_sudo provider=aws arch=x86_64
|
|
3
3
|
fedora/39 python=3.12 become=sudo provider=aws arch=x86_64
|
|
4
4
|
fedora become=sudo provider=aws arch=x86_64
|
|
5
|
-
freebsd/13.
|
|
5
|
+
freebsd/13.5 python=3.11 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
|
|
6
6
|
freebsd/14.1 python=3.9,3.11 python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
|
|
7
7
|
freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
|
|
8
8
|
macos/14.3 python=3.11 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
|
|
@@ -2,22 +2,13 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import abc
|
|
5
|
-
import
|
|
5
|
+
import dataclasses
|
|
6
|
+
import datetime
|
|
6
7
|
import json
|
|
7
|
-
import
|
|
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
|
|
78
|
-
"""
|
|
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
|
ansible_test/_internal/ci/azp.py
CHANGED
|
@@ -30,9 +30,10 @@ from ..util import (
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
from . import (
|
|
33
|
+
AuthContext,
|
|
33
34
|
ChangeDetectionNotSupported,
|
|
34
35
|
CIProvider,
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
147
|
-
"""
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
return os.path.exists(path)
|
|
127
|
+
return Authenticator.available()
|
|
124
128
|
|
|
125
|
-
def
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
key=auth_key,
|
|
132
|
-
nonce=None,
|
|
133
|
-
)
|
|
133
|
+
display.info(f'Using {authenticator} for Ansible Core CI.', verbosity=1)
|
|
134
134
|
|
|
135
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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(
|
|
329
|
+
response = self._start_endpoint(request, headers)
|
|
310
330
|
|
|
311
331
|
self.started = True
|
|
312
332
|
self._save()
|
|
@@ -245,6 +245,9 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
|
|
|
245
245
|
self.cache: dict[str, t.Any] = {}
|
|
246
246
|
"""Cache that must not be persisted across delegation."""
|
|
247
247
|
|
|
248
|
+
def pre_provision(self) -> None:
|
|
249
|
+
"""Pre-provision the host profile."""
|
|
250
|
+
|
|
248
251
|
def provision(self) -> None:
|
|
249
252
|
"""Provision the host before delegation."""
|
|
250
253
|
|
|
@@ -328,8 +331,8 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
|
|
|
328
331
|
"""The saved Ansible Core CI state."""
|
|
329
332
|
self.state['core_ci'] = value
|
|
330
333
|
|
|
331
|
-
def
|
|
332
|
-
"""
|
|
334
|
+
def pre_provision(self) -> None:
|
|
335
|
+
"""Pre-provision the host before delegation."""
|
|
333
336
|
self.core_ci = self.create_core_ci(load=True)
|
|
334
337
|
self.core_ci.start()
|
|
335
338
|
|
|
@@ -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()
|
ansible_test/_internal/util.py
CHANGED
|
@@ -638,6 +638,7 @@ def common_environment() -> dict[str, str]:
|
|
|
638
638
|
optional = (
|
|
639
639
|
'LD_LIBRARY_PATH',
|
|
640
640
|
'SSH_AUTH_SOCK',
|
|
641
|
+
'SSH_SK_PROVIDER',
|
|
641
642
|
# MacOS High Sierra Compatibility
|
|
642
643
|
# http://sealiesoftware.com/blog/archive/2017/6/5/Objective-C_and_fork_in_macOS_1013.html
|
|
643
644
|
# Example configuration for macOS:
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
set -eu
|
|
4
4
|
|
|
5
|
+
retry_init()
|
|
6
|
+
{
|
|
7
|
+
attempt=0
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
retry_or_fail()
|
|
11
|
+
{
|
|
12
|
+
attempt=$((attempt + 1))
|
|
13
|
+
|
|
14
|
+
if [ $attempt -gt 5 ]; then
|
|
15
|
+
echo "Failed to install packages. Giving up."
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "Failed to install packages. Sleeping before trying again..."
|
|
20
|
+
sleep 10
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
remove_externally_managed_marker()
|
|
6
24
|
{
|
|
7
25
|
"${python_interpreter}" -c '
|
|
@@ -26,13 +44,13 @@ install_ssh_keys()
|
|
|
26
44
|
echo "${ssh_private_key}" > "${ssh_private_key_path}"
|
|
27
45
|
|
|
28
46
|
# add public key to authorized_keys
|
|
29
|
-
|
|
47
|
+
authorized_keys_path="${HOME}/.ssh/authorized_keys"
|
|
30
48
|
|
|
31
49
|
# the existing file is overwritten to avoid conflicts (ex: RHEL on EC2 blocks root login)
|
|
32
|
-
cat "${public_key_path}" > "${
|
|
33
|
-
chmod 0600 "${
|
|
50
|
+
cat "${public_key_path}" > "${authorized_keys_path}"
|
|
51
|
+
chmod 0600 "${authorized_keys_path}"
|
|
34
52
|
|
|
35
|
-
# add localhost
|
|
53
|
+
# add localhost server keys to known_hosts
|
|
36
54
|
known_hosts_path="${HOME}/.ssh/known_hosts"
|
|
37
55
|
|
|
38
56
|
for key in /etc/ssh/ssh_host_*_key.pub; do
|
|
@@ -64,13 +82,13 @@ install_pip() {
|
|
|
64
82
|
;;
|
|
65
83
|
esac
|
|
66
84
|
|
|
85
|
+
retry_init
|
|
67
86
|
while true; do
|
|
68
87
|
curl --silent --show-error "${pip_bootstrap_url}" -o /tmp/get-pip.py && \
|
|
69
88
|
"${python_interpreter}" /tmp/get-pip.py --disable-pip-version-check --quiet && \
|
|
70
89
|
rm /tmp/get-pip.py \
|
|
71
90
|
&& break
|
|
72
|
-
|
|
73
|
-
sleep 10
|
|
91
|
+
retry_or_fail
|
|
74
92
|
done
|
|
75
93
|
fi
|
|
76
94
|
}
|
|
@@ -99,21 +117,21 @@ bootstrap_remote_alpine()
|
|
|
99
117
|
"
|
|
100
118
|
fi
|
|
101
119
|
|
|
120
|
+
retry_init
|
|
102
121
|
while true; do
|
|
103
122
|
# shellcheck disable=SC2086
|
|
104
123
|
apk add -q ${packages} \
|
|
105
124
|
&& break
|
|
106
|
-
|
|
107
|
-
sleep 10
|
|
125
|
+
retry_or_fail
|
|
108
126
|
done
|
|
109
127
|
|
|
110
128
|
# Upgrade the `libexpat` package to ensure that an upgraded Python (`pyexpat`) continues to work.
|
|
129
|
+
retry_init
|
|
111
130
|
while true; do
|
|
112
131
|
# shellcheck disable=SC2086
|
|
113
132
|
apk upgrade -q libexpat \
|
|
114
133
|
&& break
|
|
115
|
-
|
|
116
|
-
sleep 10
|
|
134
|
+
retry_or_fail
|
|
117
135
|
done
|
|
118
136
|
}
|
|
119
137
|
|
|
@@ -138,12 +156,12 @@ bootstrap_remote_fedora()
|
|
|
138
156
|
"
|
|
139
157
|
fi
|
|
140
158
|
|
|
159
|
+
retry_init
|
|
141
160
|
while true; do
|
|
142
161
|
# shellcheck disable=SC2086
|
|
143
162
|
dnf install -q -y ${packages} \
|
|
144
163
|
&& break
|
|
145
|
-
|
|
146
|
-
sleep 10
|
|
164
|
+
retry_or_fail
|
|
147
165
|
done
|
|
148
166
|
}
|
|
149
167
|
|
|
@@ -162,22 +180,14 @@ bootstrap_remote_freebsd()
|
|
|
162
180
|
if [ "${controller}" ]; then
|
|
163
181
|
jinja2_pkg="py${python_package_version}-jinja2"
|
|
164
182
|
cryptography_pkg="py${python_package_version}-cryptography"
|
|
165
|
-
pyyaml_pkg="py${python_package_version}-
|
|
183
|
+
pyyaml_pkg="py${python_package_version}-pyyaml"
|
|
166
184
|
packaging_pkg="py${python_package_version}-packaging"
|
|
167
185
|
|
|
168
186
|
# Declare platform/python version combinations which do not have supporting OS packages available.
|
|
169
187
|
# For these combinations ansible-test will use pip to install the requirements instead.
|
|
170
188
|
case "${platform_version}/${python_version}" in
|
|
171
|
-
13.
|
|
172
|
-
# defaults
|
|
173
|
-
;;
|
|
174
|
-
13.3/3.11)
|
|
175
|
-
jinja2_pkg="" # not available
|
|
176
|
-
cryptography_pkg="" # not available
|
|
177
|
-
pyyaml_pkg="" # not available
|
|
178
|
-
;;
|
|
179
|
-
14.1/3.9)
|
|
180
|
-
# defaults above 'just work'TM
|
|
189
|
+
13.5/3.11)
|
|
190
|
+
# defaults available
|
|
181
191
|
;;
|
|
182
192
|
14.1/3.11)
|
|
183
193
|
cryptography_pkg="" # not available
|
|
@@ -203,13 +213,13 @@ bootstrap_remote_freebsd()
|
|
|
203
213
|
"
|
|
204
214
|
fi
|
|
205
215
|
|
|
216
|
+
retry_init
|
|
206
217
|
while true; do
|
|
207
218
|
# shellcheck disable=SC2086
|
|
208
219
|
env ASSUME_ALWAYS_YES=YES pkg bootstrap && \
|
|
209
220
|
pkg install -q -y ${packages} \
|
|
210
221
|
&& break
|
|
211
|
-
|
|
212
|
-
sleep 10
|
|
222
|
+
retry_or_fail
|
|
213
223
|
done
|
|
214
224
|
|
|
215
225
|
install_pip
|
|
@@ -290,12 +300,12 @@ bootstrap_remote_rhel_9()
|
|
|
290
300
|
"
|
|
291
301
|
fi
|
|
292
302
|
|
|
303
|
+
retry_init
|
|
293
304
|
while true; do
|
|
294
305
|
# shellcheck disable=SC2086
|
|
295
306
|
dnf install -q -y ${packages} \
|
|
296
307
|
&& break
|
|
297
|
-
|
|
298
|
-
sleep 10
|
|
308
|
+
retry_or_fail
|
|
299
309
|
done
|
|
300
310
|
}
|
|
301
311
|
|
|
@@ -320,12 +330,12 @@ bootstrap_remote_rhel_10()
|
|
|
320
330
|
"
|
|
321
331
|
fi
|
|
322
332
|
|
|
333
|
+
retry_init
|
|
323
334
|
while true; do
|
|
324
335
|
# shellcheck disable=SC2086
|
|
325
336
|
dnf install -q -y ${packages} \
|
|
326
337
|
&& break
|
|
327
|
-
|
|
328
|
-
sleep 10
|
|
338
|
+
retry_or_fail
|
|
329
339
|
done
|
|
330
340
|
}
|
|
331
341
|
|
|
@@ -376,13 +386,13 @@ bootstrap_remote_ubuntu()
|
|
|
376
386
|
"
|
|
377
387
|
fi
|
|
378
388
|
|
|
389
|
+
retry_init
|
|
379
390
|
while true; do
|
|
380
391
|
# shellcheck disable=SC2086
|
|
381
392
|
apt-get update -qq -y && \
|
|
382
393
|
DEBIAN_FRONTEND=noninteractive apt-get install -qq -y --no-install-recommends ${packages} \
|
|
383
394
|
&& break
|
|
384
|
-
|
|
385
|
-
sleep 10
|
|
395
|
+
retry_or_fail
|
|
386
396
|
done
|
|
387
397
|
}
|
|
388
398
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|