ansible-core 2.19.0b6__py3-none-any.whl → 2.19.0b7__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.
- ansible/_internal/_templating/_jinja_bits.py +16 -1
- ansible/_internal/_templating/_jinja_common.py +1 -1
- ansible/cli/__init__.py +2 -2
- ansible/cli/adhoc.py +6 -3
- ansible/cli/console.py +1 -1
- ansible/cli/doc.py +2 -2
- ansible/config/base.yml +9 -6
- ansible/executor/module_common.py +8 -5
- ansible/executor/powershell/psrp_put_file.ps1 +1 -1
- ansible/executor/task_executor.py +2 -2
- ansible/executor/task_queue_manager.py +34 -70
- ansible/executor/task_result.py +1 -1
- ansible/galaxy/api.py +2 -2
- ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
- ansible/galaxy/dependency_resolution/providers.py +3 -3
- ansible/inventory/group.py +6 -1
- ansible/inventory/host.py +6 -1
- ansible/module_utils/_internal/_deprecator.py +12 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +14 -16
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
- ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
- ansible/module_utils/facts/hardware/base.py +1 -1
- ansible/module_utils/facts/other/facter.py +1 -1
- ansible/module_utils/facts/system/distribution.py +2 -2
- ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
- ansible/module_utils/urls.py +1 -1
- ansible/modules/apt.py +9 -3
- ansible/modules/assemble.py +5 -3
- ansible/modules/expect.py +5 -5
- ansible/modules/hostname.py +2 -2
- ansible/modules/pip.py +9 -11
- ansible/modules/raw.py +2 -2
- ansible/modules/stat.py +1 -1
- ansible/modules/wait_for.py +10 -3
- ansible/parsing/mod_args.py +38 -20
- ansible/parsing/vault/__init__.py +3 -3
- ansible/playbook/base.py +0 -2
- ansible/playbook/helpers.py +1 -1
- ansible/playbook/playbook_include.py +23 -56
- ansible/playbook/role/__init__.py +38 -21
- ansible/plugins/action/__init__.py +2 -2
- ansible/plugins/action/assemble.py +2 -1
- ansible/plugins/action/assert.py +2 -2
- ansible/plugins/action/script.py +5 -4
- ansible/plugins/action/template.py +1 -1
- ansible/plugins/callback/__init__.py +77 -87
- ansible/plugins/callback/default.py +0 -3
- ansible/plugins/callback/junit.py +0 -6
- ansible/plugins/connection/ssh.py +1 -1
- ansible/plugins/filter/pow.yml +1 -1
- ansible/plugins/filter/root.yml +1 -1
- ansible/plugins/filter/strftime.yml +3 -3
- ansible/plugins/filter/to_uuid.yml +1 -1
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/loader.py +5 -0
- ansible/plugins/lookup/password.py +4 -6
- ansible/release.py +1 -1
- ansible/utils/display.py +16 -26
- ansible/utils/path.py +1 -1
- ansible/utils/vars.py +4 -1
- ansible/vars/manager.py +6 -3
- ansible/vars/reserved.py +6 -4
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/RECORD +101 -99
- ansible_test/_internal/__init__.py +5 -0
- ansible_test/_internal/ansible_util.py +1 -1
- ansible_test/_internal/classification/python.py +6 -0
- ansible_test/_internal/cli/commands/__init__.py +0 -5
- ansible_test/_internal/cli/environments.py +51 -5
- ansible_test/_internal/commands/coverage/__init__.py +1 -1
- ansible_test/_internal/commands/integration/__init__.py +18 -5
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/sanity/__init__.py +3 -1
- ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
- ansible_test/_internal/commands/shell/__init__.py +43 -4
- ansible_test/_internal/commands/units/__init__.py +4 -1
- ansible_test/_internal/config.py +21 -13
- ansible_test/_internal/debugging.py +166 -0
- ansible_test/_internal/delegation.py +21 -13
- ansible_test/_internal/host_profiles.py +197 -6
- ansible_test/_internal/inventory.py +4 -0
- ansible_test/_internal/metadata.py +94 -4
- ansible_test/_internal/processes.py +80 -0
- ansible_test/_internal/python_requirements.py +27 -0
- ansible_test/_internal/target.py +8 -0
- ansible_test/_internal/util_common.py +13 -3
- ansible_test/_util/target/injector/python.py +8 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0b7.dist-info}/top_level.txt +0 -0
@@ -59,6 +59,10 @@ from .config import (
|
|
59
59
|
TestConfig,
|
60
60
|
)
|
61
61
|
|
62
|
+
from .debugging import (
|
63
|
+
initialize_debugger,
|
64
|
+
)
|
65
|
+
|
62
66
|
|
63
67
|
def main(cli_args: t.Optional[list[str]] = None) -> None:
|
64
68
|
"""Wrapper around the main program function to invoke cleanup functions at exit."""
|
@@ -77,6 +81,7 @@ def main_internal(cli_args: t.Optional[list[str]] = None) -> None:
|
|
77
81
|
display.redact = config.redact
|
78
82
|
display.color = config.color
|
79
83
|
display.fd = sys.stderr if config.display_stderr else sys.stdout
|
84
|
+
initialize_debugger(config)
|
80
85
|
configure_timeout(config)
|
81
86
|
report_locale(isinstance(config, TestConfig) and not config.delegate)
|
82
87
|
|
@@ -330,6 +330,6 @@ def run_playbook(
|
|
330
330
|
if args.verbosity:
|
331
331
|
cmd.append('-%s' % ('v' * args.verbosity))
|
332
332
|
|
333
|
-
install_requirements(args, args.controller_python, ansible=True) # run_playbook()
|
333
|
+
install_requirements(args, None, args.controller_python, ansible=True) # run_playbook()
|
334
334
|
env = ansible_environment(args)
|
335
335
|
intercept_python(args, args.controller_python, cmd, env, capture=capture)
|
@@ -221,6 +221,12 @@ def relative_to_absolute(name: str, level: int, module: str, path: str, lineno:
|
|
221
221
|
else:
|
222
222
|
parts = module.split('.')
|
223
223
|
|
224
|
+
if path.endswith('/__init__.py'):
|
225
|
+
# Ensure the correct relative module is calculated for both not_init.py and __init__.py:
|
226
|
+
# a/b/not_init.py -> a.b.not_init # used as-is
|
227
|
+
# a/b/__init__.py -> a.b # needs "__init__" part appended to ensure relative imports work
|
228
|
+
parts.append('__init__')
|
229
|
+
|
224
230
|
if level >= len(parts):
|
225
231
|
display.warning('Cannot resolve relative import "%s%s" above module "%s" at %s:%d' % ('.' * level, name, module, path, lineno))
|
226
232
|
absolute_name = 'relative.abovelevel'
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import argparse
|
6
6
|
import enum
|
7
7
|
import functools
|
8
|
+
import os
|
8
9
|
import typing as t
|
9
10
|
|
10
11
|
from ..constants import (
|
@@ -147,8 +148,20 @@ def add_global_options(
|
|
147
148
|
help='install command requirements',
|
148
149
|
)
|
149
150
|
|
151
|
+
global_parser.add_argument(
|
152
|
+
'--host-path',
|
153
|
+
help=argparse.SUPPRESS, # for internal use only by ansible-test
|
154
|
+
)
|
155
|
+
|
156
|
+
global_parser.add_argument(
|
157
|
+
'--metadata',
|
158
|
+
default=os.environ.get('ANSIBLE_TEST_METADATA_PATH'),
|
159
|
+
help=argparse.SUPPRESS, # for internal use only by ansible-test
|
160
|
+
)
|
161
|
+
|
150
162
|
add_global_remote(global_parser, controller_mode)
|
151
163
|
add_global_docker(global_parser, controller_mode)
|
164
|
+
add_global_debug(global_parser)
|
152
165
|
|
153
166
|
|
154
167
|
def add_composite_environment_options(
|
@@ -161,11 +174,6 @@ def add_composite_environment_options(
|
|
161
174
|
composite_parser = t.cast(argparse.ArgumentParser, parser.add_argument_group(
|
162
175
|
title='composite environment arguments (mutually exclusive with "environment arguments" above)'))
|
163
176
|
|
164
|
-
composite_parser.add_argument(
|
165
|
-
'--host-path',
|
166
|
-
help=argparse.SUPPRESS,
|
167
|
-
)
|
168
|
-
|
169
177
|
action_types: list[t.Type[CompositeAction]] = []
|
170
178
|
|
171
179
|
def register_action_type(action_type: t.Type[CompositeAction]) -> t.Type[CompositeAction]:
|
@@ -440,6 +448,44 @@ def add_global_docker(
|
|
440
448
|
)
|
441
449
|
|
442
450
|
|
451
|
+
def add_global_debug(
|
452
|
+
parser: argparse.ArgumentParser,
|
453
|
+
) -> None:
|
454
|
+
"""Add global debug options."""
|
455
|
+
# These `--dev-*` options are experimental features that may change or be removed without regard for backward compatibility.
|
456
|
+
# Additionally, they're features that are not likely to be used by most users.
|
457
|
+
# To avoid confusion, they're hidden from `--help` and tab completion by default, except for ansible-core-ci users.
|
458
|
+
suppress = None if get_ci_provider().supports_core_ci_auth() else argparse.SUPPRESS
|
459
|
+
|
460
|
+
parser.add_argument(
|
461
|
+
'--dev-debug-on-demand',
|
462
|
+
action='store_true',
|
463
|
+
default=False,
|
464
|
+
help=suppress or 'enable remote debugging only under a debugger',
|
465
|
+
)
|
466
|
+
|
467
|
+
parser.add_argument(
|
468
|
+
'--dev-debug-cli',
|
469
|
+
action='store_true',
|
470
|
+
default=False,
|
471
|
+
help=suppress or 'enable remote debugging for the Ansible CLI',
|
472
|
+
)
|
473
|
+
|
474
|
+
parser.add_argument(
|
475
|
+
'--dev-debug-ansiballz',
|
476
|
+
action='store_true',
|
477
|
+
default=False,
|
478
|
+
help=suppress or 'enable remote debugging for AnsiballZ modules',
|
479
|
+
)
|
480
|
+
|
481
|
+
parser.add_argument(
|
482
|
+
'--dev-debug-self',
|
483
|
+
action='store_true',
|
484
|
+
default=False,
|
485
|
+
help=suppress or 'enable remote debugging for ansible-test',
|
486
|
+
)
|
487
|
+
|
488
|
+
|
443
489
|
def add_environment_docker(
|
444
490
|
exclusive_parser: argparse.ArgumentParser,
|
445
491
|
environments_parser: argparse.ArgumentParser,
|
@@ -77,7 +77,7 @@ class CoverageConfig(EnvironmentConfig):
|
|
77
77
|
def initialize_coverage(args: CoverageConfig, host_state: HostState) -> coverage_module:
|
78
78
|
"""Delegate execution if requested, install requirements, then import and return the coverage module. Raises an exception if coverage is not available."""
|
79
79
|
configure_pypi_proxy(args, host_state.controller_profile) # coverage
|
80
|
-
install_requirements(args, host_state.controller_profile.python, coverage=True) # coverage
|
80
|
+
install_requirements(args, host_state.controller_profile, host_state.controller_profile.python, coverage=True) # coverage
|
81
81
|
|
82
82
|
try:
|
83
83
|
import coverage
|
@@ -105,6 +105,7 @@ from ...host_profiles import (
|
|
105
105
|
HostProfile,
|
106
106
|
PosixProfile,
|
107
107
|
SshTargetHostProfile,
|
108
|
+
DebuggableProfile,
|
108
109
|
)
|
109
110
|
|
110
111
|
from ...provisioning import (
|
@@ -459,10 +460,10 @@ def command_integration_filtered(
|
|
459
460
|
|
460
461
|
if isinstance(target_profile, ControllerProfile):
|
461
462
|
if host_state.controller_profile.python.path != target_profile.python.path:
|
462
|
-
install_requirements(args, target_python, command=True, controller=False) # integration
|
463
|
+
install_requirements(args, target_profile, target_python, command=True, controller=False) # integration
|
463
464
|
elif isinstance(target_profile, SshTargetHostProfile):
|
464
465
|
connection = target_profile.get_controller_target_connections()[0]
|
465
|
-
install_requirements(args, target_python, command=True, controller=False, connection=connection) # integration
|
466
|
+
install_requirements(args, target_profile, target_python, command=True, controller=False, connection=connection) # integration
|
466
467
|
|
467
468
|
coverage_manager = CoverageManager(args, host_state, inventory_path)
|
468
469
|
coverage_manager.setup()
|
@@ -616,7 +617,7 @@ def command_integration_script(
|
|
616
617
|
if args.verbosity:
|
617
618
|
cmd.append('-' + ('v' * args.verbosity))
|
618
619
|
|
619
|
-
env = integration_environment(args, target, test_dir, test_env.inventory_path, test_env.ansible_config, env_config, test_env)
|
620
|
+
env = integration_environment(args, target, test_dir, test_env.inventory_path, test_env.ansible_config, env_config, test_env, host_state)
|
620
621
|
cwd = os.path.join(test_env.targets_dir, target.relative_path)
|
621
622
|
|
622
623
|
env.update(
|
@@ -737,7 +738,7 @@ def command_integration_role(
|
|
737
738
|
if args.verbosity:
|
738
739
|
cmd.append('-' + ('v' * args.verbosity))
|
739
740
|
|
740
|
-
env = integration_environment(args, target, test_dir, test_env.inventory_path, test_env.ansible_config, env_config, test_env)
|
741
|
+
env = integration_environment(args, target, test_dir, test_env.inventory_path, test_env.ansible_config, env_config, test_env, host_state)
|
741
742
|
cwd = test_env.integration_dir
|
742
743
|
|
743
744
|
env.update(
|
@@ -793,6 +794,7 @@ def integration_environment(
|
|
793
794
|
ansible_config: t.Optional[str],
|
794
795
|
env_config: t.Optional[CloudEnvironmentConfig],
|
795
796
|
test_env: IntegrationEnvironment,
|
797
|
+
host_state: HostState,
|
796
798
|
) -> dict[str, str]:
|
797
799
|
"""Return a dictionary of environment variables to use when running the given integration test target."""
|
798
800
|
env = ansible_environment(args, ansible_config=ansible_config)
|
@@ -813,6 +815,9 @@ def integration_environment(
|
|
813
815
|
if args.debug_strategy:
|
814
816
|
env.update(ANSIBLE_STRATEGY='debug')
|
815
817
|
|
818
|
+
if isinstance(host_state.controller_profile, DebuggableProfile):
|
819
|
+
env.update(host_state.controller_profile.get_ansible_cli_environment_variables())
|
820
|
+
|
816
821
|
if 'non_local/' in target.aliases:
|
817
822
|
if args.coverage:
|
818
823
|
display.warning('Skipping coverage reporting on Ansible modules for non-local test: %s' % target.name)
|
@@ -820,6 +825,7 @@ def integration_environment(
|
|
820
825
|
env.update(ANSIBLE_TEST_REMOTE_INTERPRETER='')
|
821
826
|
|
822
827
|
env.update(integration)
|
828
|
+
env.update(target.env_set)
|
823
829
|
|
824
830
|
return env
|
825
831
|
|
@@ -974,6 +980,13 @@ def requirements(host_profile: HostProfile) -> None:
|
|
974
980
|
"""Install requirements after bootstrapping and delegation."""
|
975
981
|
if isinstance(host_profile, ControllerHostProfile) and host_profile.controller:
|
976
982
|
configure_pypi_proxy(host_profile.args, host_profile) # integration, windows-integration, network-integration
|
977
|
-
|
983
|
+
|
984
|
+
install_requirements( # integration, windows-integration, network-integration
|
985
|
+
args=host_profile.args,
|
986
|
+
host_profile=host_profile,
|
987
|
+
python=host_profile.python,
|
988
|
+
ansible=True,
|
989
|
+
command=True,
|
990
|
+
)
|
978
991
|
elif isinstance(host_profile, PosixProfile) and not isinstance(host_profile, ControllerProfile):
|
979
992
|
configure_pypi_proxy(host_profile.args, host_profile) # integration
|
@@ -71,7 +71,7 @@ class HttptesterProvider(CloudProvider):
|
|
71
71
|
return
|
72
72
|
|
73
73
|
# Read the password from the container environment.
|
74
|
-
# This allows the tests to work when
|
74
|
+
# This allows the tests to work when reusing an existing container.
|
75
75
|
# The password is marked as sensitive, since it may differ from the one we generated.
|
76
76
|
krb5_password = descriptor.details.container.env_dict()[KRB5_PASSWORD_ENV]
|
77
77
|
display.sensitive.add(krb5_password)
|
@@ -76,6 +76,7 @@ from ...python_requirements import (
|
|
76
76
|
PipInstall,
|
77
77
|
collect_requirements,
|
78
78
|
run_pip,
|
79
|
+
install_requirements,
|
79
80
|
)
|
80
81
|
|
81
82
|
from ...config import (
|
@@ -178,6 +179,7 @@ def command_sanity(args: SanityConfig) -> None:
|
|
178
179
|
if args.delegate:
|
179
180
|
raise Delegate(host_state=host_state, require=changes, exclude=args.exclude)
|
180
181
|
|
182
|
+
install_requirements(args, host_state.controller_profile, host_state.controller_profile.python) # sanity
|
181
183
|
configure_pypi_proxy(args, host_state.controller_profile) # sanity
|
182
184
|
|
183
185
|
if disabled:
|
@@ -1085,7 +1087,7 @@ class SanityScript(SanityTest, metaclass=abc.ABCMeta):
|
|
1085
1087
|
|
1086
1088
|
|
1087
1089
|
class SanityVersionNeutral(SanityTest, metaclass=abc.ABCMeta):
|
1088
|
-
"""Base class for sanity test plugins which are
|
1090
|
+
"""Base class for sanity test plugins which are independent of the python version being used."""
|
1089
1091
|
|
1090
1092
|
@abc.abstractmethod
|
1091
1093
|
def test(self, args: SanityConfig, targets: SanityTargets) -> TestResult:
|
@@ -181,6 +181,8 @@ class IntegrationAliasesTest(SanitySingleVersion):
|
|
181
181
|
group_numbers = self.ci_test_groups.get(name, None)
|
182
182
|
|
183
183
|
if group_numbers:
|
184
|
+
group_numbers = [num for num in group_numbers if num not in (6, 7)] # HACK: ignore special groups 6 and 7
|
185
|
+
|
184
186
|
if min(group_numbers) != 1:
|
185
187
|
display.warning('Min test group "%s" in %s is %d instead of 1.' % (name, self.CI_YML, min(group_numbers)), unique=True)
|
186
188
|
|
@@ -291,6 +293,9 @@ class IntegrationAliasesTest(SanitySingleVersion):
|
|
291
293
|
if target.name == 'ansible-test-container':
|
292
294
|
continue # special test target which uses group 6 -- nothing else should be in that group
|
293
295
|
|
296
|
+
if target.name in ('dnf-oldest', 'dnf-latest'):
|
297
|
+
continue # special test targets which use group 7 -- nothing else should be in that group
|
298
|
+
|
294
299
|
if f'{self.TEST_ALIAS_PREFIX}/posix/' not in target.aliases:
|
295
300
|
continue
|
296
301
|
|
@@ -351,6 +356,12 @@ class IntegrationAliasesTest(SanitySingleVersion):
|
|
351
356
|
if path == 'test/integration/targets/ansible-test-container':
|
352
357
|
continue # special test target which uses group 6 -- nothing else should be in that group
|
353
358
|
|
359
|
+
if path in (
|
360
|
+
'test/integration/targets/dnf-oldest',
|
361
|
+
'test/integration/targets/dnf-latest',
|
362
|
+
):
|
363
|
+
continue # special test targets which use group 7 -- nothing else should be in that group
|
364
|
+
|
354
365
|
messages.append(SanityMessage(unassigned_message, '%s/aliases' % path))
|
355
366
|
|
356
367
|
for path in conflicting_paths:
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import dataclasses
|
5
6
|
import os
|
6
7
|
import sys
|
7
8
|
import typing as t
|
@@ -14,6 +15,10 @@ from ...util import (
|
|
14
15
|
HostConnectionError,
|
15
16
|
)
|
16
17
|
|
18
|
+
from ...ansible_util import (
|
19
|
+
ansible_environment,
|
20
|
+
)
|
21
|
+
|
17
22
|
from ...config import (
|
18
23
|
ShellConfig,
|
19
24
|
)
|
@@ -32,6 +37,7 @@ from ...host_profiles import (
|
|
32
37
|
ControllerProfile,
|
33
38
|
PosixProfile,
|
34
39
|
SshTargetHostProfile,
|
40
|
+
DebuggableProfile,
|
35
41
|
)
|
36
42
|
|
37
43
|
from ...provisioning import (
|
@@ -39,7 +45,6 @@ from ...provisioning import (
|
|
39
45
|
)
|
40
46
|
|
41
47
|
from ...host_configs import (
|
42
|
-
ControllerConfig,
|
43
48
|
OriginConfig,
|
44
49
|
)
|
45
50
|
|
@@ -48,12 +53,21 @@ from ...inventory import (
|
|
48
53
|
create_posix_inventory,
|
49
54
|
)
|
50
55
|
|
56
|
+
from ...python_requirements import (
|
57
|
+
install_requirements,
|
58
|
+
)
|
59
|
+
|
60
|
+
from ...util_common import (
|
61
|
+
get_injector_env,
|
62
|
+
)
|
63
|
+
|
64
|
+
from ...delegation import (
|
65
|
+
metadata_context,
|
66
|
+
)
|
67
|
+
|
51
68
|
|
52
69
|
def command_shell(args: ShellConfig) -> None:
|
53
70
|
"""Entry point for the `shell` command."""
|
54
|
-
if args.raw and isinstance(args.targets[0], ControllerConfig):
|
55
|
-
raise ApplicationError('The --raw option has no effect on the controller.')
|
56
|
-
|
57
71
|
if not args.export and not args.cmd and not sys.stdin.isatty():
|
58
72
|
raise ApplicationError('Standard input must be a TTY to launch a shell.')
|
59
73
|
|
@@ -62,6 +76,8 @@ def command_shell(args: ShellConfig) -> None:
|
|
62
76
|
if args.delegate:
|
63
77
|
raise Delegate(host_state=host_state)
|
64
78
|
|
79
|
+
install_requirements(args, host_state.controller_profile, host_state.controller_profile.python) # shell
|
80
|
+
|
65
81
|
if args.raw and not isinstance(args.controller, OriginConfig):
|
66
82
|
display.warning('The --raw option will only be applied to the target.')
|
67
83
|
|
@@ -92,6 +108,20 @@ def command_shell(args: ShellConfig) -> None:
|
|
92
108
|
con.run(args.cmd, capture=False, interactive=False, output_stream=OutputStream.ORIGINAL)
|
93
109
|
return
|
94
110
|
|
111
|
+
if isinstance(con, LocalConnection) and isinstance(target_profile, DebuggableProfile) and target_profile.debugging_enabled:
|
112
|
+
# HACK: ensure the pydevd port visible in the shell is the forwarded port, not the original
|
113
|
+
args.metadata.debugger_settings = dataclasses.replace(args.metadata.debugger_settings, port=target_profile.pydevd_port)
|
114
|
+
|
115
|
+
with metadata_context(args):
|
116
|
+
interactive_shell(args, target_profile, con)
|
117
|
+
|
118
|
+
|
119
|
+
def interactive_shell(
|
120
|
+
args: ShellConfig,
|
121
|
+
target_profile: SshTargetHostProfile,
|
122
|
+
con: Connection,
|
123
|
+
) -> None:
|
124
|
+
"""Run an interactive shell."""
|
95
125
|
if isinstance(con, SshConnection) and args.raw:
|
96
126
|
cmd: list[str] = []
|
97
127
|
elif isinstance(target_profile, PosixProfile):
|
@@ -111,6 +141,15 @@ def command_shell(args: ShellConfig) -> None:
|
|
111
141
|
|
112
142
|
env = {name: os.environ[name] for name in optional_vars if name in os.environ}
|
113
143
|
|
144
|
+
if isinstance(con, LocalConnection): # configure the controller environment
|
145
|
+
env.update(ansible_environment(args))
|
146
|
+
env.update(get_injector_env(target_profile.python, env))
|
147
|
+
env.update(ANSIBLE_TEST_METADATA_PATH=os.path.abspath(args.metadata_path))
|
148
|
+
|
149
|
+
if isinstance(target_profile, DebuggableProfile):
|
150
|
+
env.update(target_profile.get_ansiballz_environment_variables())
|
151
|
+
env.update(target_profile.get_ansible_cli_environment_variables())
|
152
|
+
|
114
153
|
if env:
|
115
154
|
cmd = ['/usr/bin/env'] + [f'{name}={value}' for name, value in env.items()]
|
116
155
|
|
@@ -64,6 +64,7 @@ from ...executor import (
|
|
64
64
|
|
65
65
|
from ...python_requirements import (
|
66
66
|
install_requirements,
|
67
|
+
post_install,
|
67
68
|
)
|
68
69
|
|
69
70
|
from ...content_config import (
|
@@ -230,7 +231,9 @@ def command_units(args: UnitsConfig) -> None:
|
|
230
231
|
controller = any(test_context == TestContext.controller for test_context, python, paths, env in final_candidates)
|
231
232
|
|
232
233
|
if args.requirements_mode != 'skip':
|
233
|
-
install_requirements(args, target_profile.python, ansible=controller, command=True, controller=False) # units
|
234
|
+
install_requirements(args, target_profile, target_profile.python, ansible=controller, command=True, controller=False) # units
|
235
|
+
else:
|
236
|
+
post_install(target_profile)
|
234
237
|
|
235
238
|
test_sets.extend(final_candidates)
|
236
239
|
|
ansible_test/_internal/config.py
CHANGED
@@ -20,6 +20,7 @@ from .util_common import (
|
|
20
20
|
|
21
21
|
from .metadata import (
|
22
22
|
Metadata,
|
23
|
+
DebuggerFlags,
|
23
24
|
)
|
24
25
|
|
25
26
|
from .data import (
|
@@ -118,6 +119,26 @@ class EnvironmentConfig(CommonConfig):
|
|
118
119
|
self.dev_systemd_debug: bool = args.dev_systemd_debug
|
119
120
|
self.dev_probe_cgroups: t.Optional[str] = args.dev_probe_cgroups
|
120
121
|
|
122
|
+
debugger_flags = DebuggerFlags(
|
123
|
+
on_demand=args.dev_debug_on_demand,
|
124
|
+
cli=args.dev_debug_cli,
|
125
|
+
ansiballz=args.dev_debug_ansiballz,
|
126
|
+
self=args.dev_debug_self,
|
127
|
+
)
|
128
|
+
|
129
|
+
self.metadata = Metadata.from_file(args.metadata) if args.metadata else Metadata(debugger_flags=debugger_flags)
|
130
|
+
self.metadata_path: t.Optional[str] = None
|
131
|
+
|
132
|
+
def metadata_callback(payload_config: PayloadConfig) -> None:
|
133
|
+
"""Add the metadata file to the payload file list."""
|
134
|
+
config = self
|
135
|
+
files = payload_config.files
|
136
|
+
|
137
|
+
if config.metadata_path:
|
138
|
+
files.append((os.path.abspath(config.metadata_path), config.metadata_path))
|
139
|
+
|
140
|
+
data_context().register_payload_callback(metadata_callback)
|
141
|
+
|
121
142
|
def host_callback(payload_config: PayloadConfig) -> None:
|
122
143
|
"""Add the host files to the payload file list."""
|
123
144
|
config = self
|
@@ -220,22 +241,9 @@ class TestConfig(EnvironmentConfig):
|
|
220
241
|
self.junit: bool = getattr(args, 'junit', False)
|
221
242
|
self.failure_ok: bool = getattr(args, 'failure_ok', False)
|
222
243
|
|
223
|
-
self.metadata = Metadata.from_file(args.metadata) if args.metadata else Metadata()
|
224
|
-
self.metadata_path: t.Optional[str] = None
|
225
|
-
|
226
244
|
if self.coverage_check:
|
227
245
|
self.coverage = True
|
228
246
|
|
229
|
-
def metadata_callback(payload_config: PayloadConfig) -> None:
|
230
|
-
"""Add the metadata file to the payload file list."""
|
231
|
-
config = self
|
232
|
-
files = payload_config.files
|
233
|
-
|
234
|
-
if config.metadata_path:
|
235
|
-
files.append((os.path.abspath(config.metadata_path), config.metadata_path))
|
236
|
-
|
237
|
-
data_context().register_payload_callback(metadata_callback)
|
238
|
-
|
239
247
|
|
240
248
|
class ShellConfig(EnvironmentConfig):
|
241
249
|
"""Configuration for the shell command."""
|
@@ -0,0 +1,166 @@
|
|
1
|
+
"""Setup and configure remote debugging."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import dataclasses
|
6
|
+
import json
|
7
|
+
import os
|
8
|
+
import re
|
9
|
+
|
10
|
+
from .util import (
|
11
|
+
cache,
|
12
|
+
display,
|
13
|
+
raw_command,
|
14
|
+
ApplicationError,
|
15
|
+
)
|
16
|
+
|
17
|
+
from .processes import (
|
18
|
+
Process,
|
19
|
+
get_current_process,
|
20
|
+
)
|
21
|
+
|
22
|
+
from .config import (
|
23
|
+
EnvironmentConfig,
|
24
|
+
)
|
25
|
+
|
26
|
+
from .metadata import (
|
27
|
+
DebuggerSettings,
|
28
|
+
DebuggerFlags,
|
29
|
+
)
|
30
|
+
|
31
|
+
from . import (
|
32
|
+
data_context,
|
33
|
+
CommonConfig,
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
def initialize_debugger(args: CommonConfig) -> None:
|
38
|
+
"""Initialize the debugger settings before delegation."""
|
39
|
+
if not isinstance(args, EnvironmentConfig):
|
40
|
+
return
|
41
|
+
|
42
|
+
if args.metadata.loaded:
|
43
|
+
return # after delegation
|
44
|
+
|
45
|
+
if collection := data_context().content.collection:
|
46
|
+
args.metadata.collection_root = collection.root
|
47
|
+
|
48
|
+
load_debugger_settings(args)
|
49
|
+
|
50
|
+
|
51
|
+
def parse_debugger_settings(value: str) -> DebuggerSettings:
|
52
|
+
"""Parse remote debugger settings and apply defaults."""
|
53
|
+
try:
|
54
|
+
settings = DebuggerSettings(**json.loads(value))
|
55
|
+
except Exception as ex:
|
56
|
+
raise ApplicationError(f"Invalid debugger settings: {ex}") from ex
|
57
|
+
|
58
|
+
if not settings.module:
|
59
|
+
if not settings.package or 'pydevd-pycharm' in settings.package:
|
60
|
+
module = 'pydevd_pycharm'
|
61
|
+
else:
|
62
|
+
module = 'pydevd'
|
63
|
+
|
64
|
+
settings = dataclasses.replace(settings, module=module)
|
65
|
+
|
66
|
+
if settings.package is None:
|
67
|
+
if settings.module == 'pydevd_pycharm':
|
68
|
+
if pycharm_version := detect_pycharm_version():
|
69
|
+
package = f'pydevd-pycharm~={pycharm_version}'
|
70
|
+
else:
|
71
|
+
package = None
|
72
|
+
else:
|
73
|
+
package = 'pydevd'
|
74
|
+
|
75
|
+
settings = dataclasses.replace(settings, package=package)
|
76
|
+
|
77
|
+
settings.settrace.setdefault('suspend', False)
|
78
|
+
|
79
|
+
if port := detect_pydevd_port():
|
80
|
+
settings = dataclasses.replace(settings, port=port)
|
81
|
+
|
82
|
+
if detect_pycharm_process():
|
83
|
+
# This only works with the default PyCharm debugger.
|
84
|
+
# Using it with PyCharm's "Python Debug Server" results in hangs in Ansible workers.
|
85
|
+
# Further investigation is required to understand the cause.
|
86
|
+
settings = dataclasses.replace(settings, args=settings.args + ['--multiprocess'])
|
87
|
+
|
88
|
+
return settings
|
89
|
+
|
90
|
+
|
91
|
+
def load_debugger_settings(args: EnvironmentConfig) -> None:
|
92
|
+
"""Load the remote debugger settings."""
|
93
|
+
if args.metadata.debugger_flags.on_demand:
|
94
|
+
# On-demand debugging only enables debugging if we're running under a debugger, otherwise it's a no-op.
|
95
|
+
|
96
|
+
if not detect_pydevd_port():
|
97
|
+
display.info('Debugging disabled because no debugger was detected.', verbosity=1)
|
98
|
+
args.metadata.debugger_flags = DebuggerFlags.all(False)
|
99
|
+
return
|
100
|
+
|
101
|
+
display.info('Enabling on-demand debugging.', verbosity=1)
|
102
|
+
|
103
|
+
if not args.metadata.debugger_flags.enable:
|
104
|
+
# Assume the user wants all debugging features enabled, since on-demand debugging with no features is pointless.
|
105
|
+
args.metadata.debugger_flags = DebuggerFlags.all(True)
|
106
|
+
|
107
|
+
if not args.metadata.debugger_flags.enable:
|
108
|
+
return
|
109
|
+
|
110
|
+
value = os.environ.get('ANSIBLE_TEST_REMOTE_DEBUGGER') or '{}'
|
111
|
+
settings = parse_debugger_settings(value)
|
112
|
+
|
113
|
+
display.info(f'>>> Debugger Settings\n{json.dumps(dataclasses.asdict(settings), indent=4)}', verbosity=3)
|
114
|
+
|
115
|
+
args.metadata.debugger_settings = settings
|
116
|
+
|
117
|
+
|
118
|
+
@cache
|
119
|
+
def detect_pydevd_port() -> int | None:
|
120
|
+
"""Return the port for the pydevd instance hosting this process, or `None` if not detected."""
|
121
|
+
current_process = get_current_process_cached()
|
122
|
+
args = current_process.args
|
123
|
+
|
124
|
+
if any('/pydevd.py' in arg for arg in args) and (port_idx := args.index('--port')):
|
125
|
+
port = int(args[port_idx + 1])
|
126
|
+
display.info(f'Detected pydevd debugger port {port}.', verbosity=1)
|
127
|
+
return port
|
128
|
+
|
129
|
+
return None
|
130
|
+
|
131
|
+
|
132
|
+
@cache
|
133
|
+
def detect_pycharm_version() -> str | None:
|
134
|
+
"""Return the version of PyCharm running ansible-test, or `None` if PyCharm was not detected. The result is cached."""
|
135
|
+
if pycharm := detect_pycharm_process():
|
136
|
+
output = raw_command([pycharm.args[0], '--version'], capture=True)[0]
|
137
|
+
|
138
|
+
if match := re.search('^Build #PY-(?P<version>[0-9.]+)$', output, flags=re.MULTILINE):
|
139
|
+
version = match.group('version')
|
140
|
+
display.info(f'Detected PyCharm version {version}.', verbosity=1)
|
141
|
+
return version
|
142
|
+
|
143
|
+
display.warning('Skipping installation of `pydevd-pycharm` since the running PyCharm version could not be detected.')
|
144
|
+
|
145
|
+
return None
|
146
|
+
|
147
|
+
|
148
|
+
@cache
|
149
|
+
def detect_pycharm_process() -> Process | None:
|
150
|
+
"""Return the PyCharm process running ansible-test, or `None` if PyCharm was not detected. The result is cached."""
|
151
|
+
current_process = get_current_process_cached()
|
152
|
+
parent = current_process.parent
|
153
|
+
|
154
|
+
while parent:
|
155
|
+
if parent.path.name == 'pycharm':
|
156
|
+
return parent
|
157
|
+
|
158
|
+
parent = parent.parent
|
159
|
+
|
160
|
+
return None
|
161
|
+
|
162
|
+
|
163
|
+
@cache
|
164
|
+
def get_current_process_cached() -> Process:
|
165
|
+
"""Return the current process. The result is cached."""
|
166
|
+
return get_current_process()
|