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
@@ -113,23 +113,27 @@ def delegate(args: CommonConfig, host_state: HostState, exclude: list[str], requ
|
|
113
113
|
assert isinstance(args, EnvironmentConfig)
|
114
114
|
|
115
115
|
with delegation_context(args, host_state):
|
116
|
-
|
117
|
-
args.metadata.ci_provider = get_ci_provider().code
|
116
|
+
args.metadata.ci_provider = get_ci_provider().code
|
118
117
|
|
119
|
-
|
118
|
+
make_dirs(ResultType.TMP.path)
|
120
119
|
|
121
|
-
|
122
|
-
args.metadata_path = os.path.join(ResultType.TMP.relative_path, os.path.basename(metadata_fd.name))
|
123
|
-
args.metadata.to_file(args.metadata_path)
|
124
|
-
|
125
|
-
try:
|
126
|
-
delegate_command(args, host_state, exclude, require)
|
127
|
-
finally:
|
128
|
-
args.metadata_path = None
|
129
|
-
else:
|
120
|
+
with metadata_context(args):
|
130
121
|
delegate_command(args, host_state, exclude, require)
|
131
122
|
|
132
123
|
|
124
|
+
@contextlib.contextmanager
|
125
|
+
def metadata_context(args: EnvironmentConfig) -> t.Generator[None]:
|
126
|
+
"""A context manager which exports delegation metadata."""
|
127
|
+
with tempfile.NamedTemporaryFile(prefix='metadata-', suffix='.json', dir=ResultType.TMP.path) as metadata_fd:
|
128
|
+
args.metadata_path = os.path.join(ResultType.TMP.relative_path, os.path.basename(metadata_fd.name))
|
129
|
+
args.metadata.to_file(args.metadata_path)
|
130
|
+
|
131
|
+
try:
|
132
|
+
yield
|
133
|
+
finally:
|
134
|
+
args.metadata_path = None
|
135
|
+
|
136
|
+
|
133
137
|
def delegate_command(args: EnvironmentConfig, host_state: HostState, exclude: list[str], require: list[str]) -> None:
|
134
138
|
"""Delegate execution based on the provided host state."""
|
135
139
|
con = host_state.controller_profile.get_origin_controller_connection()
|
@@ -189,6 +193,10 @@ def delegate_command(args: EnvironmentConfig, host_state: HostState, exclude: li
|
|
189
193
|
networks = container.get_network_names()
|
190
194
|
|
191
195
|
if networks is not None:
|
196
|
+
if args.metadata.debugger_flags.enable:
|
197
|
+
networks = []
|
198
|
+
display.warning('Skipping network isolation to enable remote debugging.')
|
199
|
+
|
192
200
|
for network in networks:
|
193
201
|
try:
|
194
202
|
con.disconnect_network(network)
|
@@ -334,6 +342,7 @@ def filter_options(
|
|
334
342
|
('--redact', 0, False),
|
335
343
|
('--no-redact', 0, not args.redact),
|
336
344
|
('--host-path', 1, args.host_path),
|
345
|
+
('--metadata', 1, args.metadata_path),
|
337
346
|
]
|
338
347
|
|
339
348
|
if isinstance(args, TestConfig):
|
@@ -346,7 +355,6 @@ def filter_options(
|
|
346
355
|
('--ignore-unstaged', 0, False),
|
347
356
|
('--changed-from', 1, False),
|
348
357
|
('--changed-path', 1, False),
|
349
|
-
('--metadata', 1, args.metadata_path),
|
350
358
|
('--exclude', 1, exclude),
|
351
359
|
('--require', 1, require),
|
352
360
|
('--base-branch', 1, False),
|
@@ -4,7 +4,10 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import abc
|
6
6
|
import dataclasses
|
7
|
+
import importlib
|
8
|
+
import json
|
7
9
|
import os
|
10
|
+
import pathlib
|
8
11
|
import shlex
|
9
12
|
import tempfile
|
10
13
|
import time
|
@@ -58,6 +61,9 @@ from .util import (
|
|
58
61
|
HostConnectionError,
|
59
62
|
ANSIBLE_TEST_TARGET_ROOT,
|
60
63
|
WINDOWS_CONNECTION_VARIABLES,
|
64
|
+
ANSIBLE_SOURCE_ROOT,
|
65
|
+
ANSIBLE_LIB_ROOT,
|
66
|
+
ANSIBLE_TEST_ROOT,
|
61
67
|
)
|
62
68
|
|
63
69
|
from .util_common import (
|
@@ -92,6 +98,8 @@ from .venv import (
|
|
92
98
|
|
93
99
|
from .ssh import (
|
94
100
|
SshConnectionDetail,
|
101
|
+
create_ssh_port_forwards,
|
102
|
+
SshProcess,
|
95
103
|
)
|
96
104
|
|
97
105
|
from .ansible_util import (
|
@@ -284,6 +292,176 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
|
|
284
292
|
return f'{self.__class__.__name__}: {self.name}'
|
285
293
|
|
286
294
|
|
295
|
+
class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
|
296
|
+
"""Base class for profiles remote debugging."""
|
297
|
+
|
298
|
+
__PYDEVD_PORT_KEY = 'pydevd_port'
|
299
|
+
__DEBUGGING_FORWARDER_KEY = 'debugging_forwarder'
|
300
|
+
|
301
|
+
@property
|
302
|
+
def debugging_enabled(self) -> bool:
|
303
|
+
"""Returns `True` if debugging is enabled for this profile, otherwise `False`."""
|
304
|
+
if self.controller:
|
305
|
+
return self.args.metadata.debugger_flags.enable
|
306
|
+
|
307
|
+
return self.args.metadata.debugger_flags.ansiballz
|
308
|
+
|
309
|
+
@property
|
310
|
+
def pydevd_port(self) -> int:
|
311
|
+
"""The pydevd port to use."""
|
312
|
+
return self.state.get(self.__PYDEVD_PORT_KEY) or self.origin_pydev_port
|
313
|
+
|
314
|
+
@property
|
315
|
+
def debugging_forwarder(self) -> SshProcess | None:
|
316
|
+
"""The SSH forwarding process, if enabled."""
|
317
|
+
return self.cache.get(self.__DEBUGGING_FORWARDER_KEY)
|
318
|
+
|
319
|
+
@debugging_forwarder.setter
|
320
|
+
def debugging_forwarder(self, value: SshProcess) -> None:
|
321
|
+
"""The SSH forwarding process, if enabled."""
|
322
|
+
self.cache[self.__DEBUGGING_FORWARDER_KEY] = value
|
323
|
+
|
324
|
+
@property
|
325
|
+
def origin_pydev_port(self) -> int:
|
326
|
+
"""The pydevd port on the origin."""
|
327
|
+
return self.args.metadata.debugger_settings.port
|
328
|
+
|
329
|
+
def enable_debugger_forwarding(self, ssh: SshConnectionDetail) -> None:
|
330
|
+
"""Enable pydevd port forwarding from the origin."""
|
331
|
+
if not self.debugging_enabled:
|
332
|
+
return
|
333
|
+
|
334
|
+
endpoint = ('localhost', self.origin_pydev_port)
|
335
|
+
forwards = [endpoint]
|
336
|
+
|
337
|
+
self.debugging_forwarder = create_ssh_port_forwards(self.args, ssh, forwards)
|
338
|
+
|
339
|
+
port_forwards = self.debugging_forwarder.collect_port_forwards()
|
340
|
+
|
341
|
+
self.state[self.__PYDEVD_PORT_KEY] = port = port_forwards[endpoint]
|
342
|
+
|
343
|
+
display.info(f'Remote debugging of {self.name!r} is available on port {port}.', verbosity=1)
|
344
|
+
|
345
|
+
def deprovision(self) -> None:
|
346
|
+
"""Deprovision the host after delegation has completed."""
|
347
|
+
super().deprovision()
|
348
|
+
|
349
|
+
if not self.debugging_forwarder:
|
350
|
+
return # forwarding not in use
|
351
|
+
|
352
|
+
self.debugging_forwarder.terminate()
|
353
|
+
|
354
|
+
display.info(f'Waiting for the {self.name!r} remote debugging SSH port forwarding process to terminate.', verbosity=1)
|
355
|
+
|
356
|
+
self.debugging_forwarder.wait()
|
357
|
+
|
358
|
+
def get_pydevd_settrace_arguments(self) -> dict[str, object]:
|
359
|
+
"""Get settrace arguments for pydevd."""
|
360
|
+
return self.args.metadata.debugger_settings.settrace | dict(
|
361
|
+
host="localhost",
|
362
|
+
port=self.pydevd_port,
|
363
|
+
)
|
364
|
+
|
365
|
+
def get_pydevd_environment_variables(self) -> dict[str, str]:
|
366
|
+
"""Get environment variables needed to configure pydevd for debugging."""
|
367
|
+
return dict(
|
368
|
+
PATHS_FROM_ECLIPSE_TO_PYTHON=json.dumps(list(self.get_source_mapping().items())),
|
369
|
+
)
|
370
|
+
|
371
|
+
def get_source_mapping(self) -> dict[str, str]:
|
372
|
+
"""Get the source mapping from the given metadata."""
|
373
|
+
from . import data_context
|
374
|
+
|
375
|
+
if collection := data_context().content.collection:
|
376
|
+
source_mapping = {
|
377
|
+
f"{self.args.metadata.ansible_test_root}/": f'{ANSIBLE_TEST_ROOT}/',
|
378
|
+
f"{self.args.metadata.ansible_lib_root}/": f'{ANSIBLE_LIB_ROOT}/',
|
379
|
+
f'{self.args.metadata.collection_root}/ansible_collections/': f'{collection.root}/ansible_collections/',
|
380
|
+
}
|
381
|
+
else:
|
382
|
+
ansible_source_root = pathlib.Path(self.args.metadata.ansible_lib_root).parent.parent
|
383
|
+
|
384
|
+
source_mapping = {
|
385
|
+
f"{ansible_source_root}/": f'{ANSIBLE_SOURCE_ROOT}/',
|
386
|
+
}
|
387
|
+
|
388
|
+
source_mapping = {key: value for key, value in source_mapping.items() if key != value}
|
389
|
+
|
390
|
+
return source_mapping
|
391
|
+
|
392
|
+
def activate_debugger(self) -> None:
|
393
|
+
"""Activate the debugger after delegation."""
|
394
|
+
if not self.args.metadata.loaded or not self.args.metadata.debugger_flags.self:
|
395
|
+
return
|
396
|
+
|
397
|
+
display.info('Activating remote debugging of ansible-test.', verbosity=1)
|
398
|
+
|
399
|
+
os.environ.update(self.get_pydevd_environment_variables())
|
400
|
+
|
401
|
+
debugging_module = importlib.import_module(self.args.metadata.debugger_settings.module)
|
402
|
+
debugging_module.settrace(**self.get_pydevd_settrace_arguments())
|
403
|
+
|
404
|
+
pass # pylint: disable=unnecessary-pass # when suspend is True, execution pauses here -- it's also a convenient place to put a breakpoint
|
405
|
+
|
406
|
+
def get_ansiballz_inventory_variables(self) -> dict[str, t.Any]:
|
407
|
+
"""
|
408
|
+
Return inventory variables for remote debugging of AnsiballZ modules.
|
409
|
+
When delegating, this function must be called after delegation.
|
410
|
+
"""
|
411
|
+
if not self.args.metadata.debugger_flags.ansiballz:
|
412
|
+
return {}
|
413
|
+
|
414
|
+
return dict(
|
415
|
+
_ansible_ansiballz_debugger_config=json.dumps(self.get_ansiballz_debugger_config()),
|
416
|
+
)
|
417
|
+
|
418
|
+
def get_ansiballz_environment_variables(self) -> dict[str, t.Any]:
|
419
|
+
"""
|
420
|
+
Return environment variables for remote debugging of AnsiballZ modules.
|
421
|
+
When delegating, this function must be called after delegation.
|
422
|
+
"""
|
423
|
+
if not self.args.metadata.debugger_flags.ansiballz:
|
424
|
+
return {}
|
425
|
+
|
426
|
+
return dict(
|
427
|
+
_ANSIBLE_ANSIBALLZ_DEBUGGER_CONFIG=json.dumps(self.get_ansiballz_debugger_config()),
|
428
|
+
)
|
429
|
+
|
430
|
+
def get_ansiballz_debugger_config(self) -> dict[str, t.Any]:
|
431
|
+
"""
|
432
|
+
Return config for remote debugging of AnsiballZ modules.
|
433
|
+
When delegating, this function must be called after delegation.
|
434
|
+
"""
|
435
|
+
debugger_config = dict(
|
436
|
+
module=self.args.metadata.debugger_settings.module,
|
437
|
+
settrace=self.get_pydevd_settrace_arguments(),
|
438
|
+
source_mapping=self.get_source_mapping(),
|
439
|
+
)
|
440
|
+
|
441
|
+
display.info(f'>>> Debugger Config ({self.name} AnsiballZ)\n{json.dumps(debugger_config, indent=4)}', verbosity=3)
|
442
|
+
|
443
|
+
return debugger_config
|
444
|
+
|
445
|
+
def get_ansible_cli_environment_variables(self) -> dict[str, t.Any]:
|
446
|
+
"""
|
447
|
+
Return environment variables for remote debugging of the Ansible CLI.
|
448
|
+
When delegating, this function must be called after delegation.
|
449
|
+
"""
|
450
|
+
if not self.args.metadata.debugger_flags.cli:
|
451
|
+
return {}
|
452
|
+
|
453
|
+
debugger_config = dict(
|
454
|
+
args=['-m', 'pydevd', '--client', 'localhost', '--port', str(self.pydevd_port)] + self.args.metadata.debugger_settings.args + ['--file'],
|
455
|
+
env=self.get_pydevd_environment_variables(),
|
456
|
+
)
|
457
|
+
|
458
|
+
display.info(f'>>> Debugger Config ({self.name} Ansible CLI)\n{json.dumps(debugger_config, indent=4)}', verbosity=3)
|
459
|
+
|
460
|
+
return dict(
|
461
|
+
ANSIBLE_TEST_DEBUGGER_CONFIG=json.dumps(debugger_config),
|
462
|
+
)
|
463
|
+
|
464
|
+
|
287
465
|
class PosixProfile(HostProfile[TPosixConfig], metaclass=abc.ABCMeta):
|
288
466
|
"""Base class for POSIX host profiles."""
|
289
467
|
|
@@ -306,7 +484,7 @@ class PosixProfile(HostProfile[TPosixConfig], metaclass=abc.ABCMeta):
|
|
306
484
|
return python
|
307
485
|
|
308
486
|
|
309
|
-
class ControllerHostProfile(PosixProfile[TControllerHostConfig], metaclass=abc.ABCMeta):
|
487
|
+
class ControllerHostProfile(PosixProfile[TControllerHostConfig], DebuggableProfile[TControllerHostConfig], metaclass=abc.ABCMeta):
|
310
488
|
"""Base class for profiles usable as a controller."""
|
311
489
|
|
312
490
|
@abc.abstractmethod
|
@@ -410,7 +588,7 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
|
|
410
588
|
)
|
411
589
|
|
412
590
|
|
413
|
-
class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[ControllerConfig]):
|
591
|
+
class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[ControllerConfig], DebuggableProfile[ControllerConfig]):
|
414
592
|
"""Host profile for the controller as a target."""
|
415
593
|
|
416
594
|
@property
|
@@ -418,6 +596,11 @@ class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[Con
|
|
418
596
|
"""The name of the host profile."""
|
419
597
|
return self.controller_profile.name
|
420
598
|
|
599
|
+
@property
|
600
|
+
def pydevd_port(self) -> int:
|
601
|
+
"""The pydevd port to use."""
|
602
|
+
return self.controller_profile.pydevd_port
|
603
|
+
|
421
604
|
def get_controller_target_connections(self) -> list[SshConnection]:
|
422
605
|
"""Return SSH connection(s) for accessing the host as a target from the controller."""
|
423
606
|
settings = SshConnectionDetail(
|
@@ -432,7 +615,7 @@ class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[Con
|
|
432
615
|
return [SshConnection(self.args, settings)]
|
433
616
|
|
434
617
|
|
435
|
-
class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[DockerConfig]):
|
618
|
+
class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[DockerConfig], DebuggableProfile[DockerConfig]):
|
436
619
|
"""Host profile for a docker instance."""
|
437
620
|
|
438
621
|
MARKER = 'ansible-test-marker'
|
@@ -487,7 +670,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
487
670
|
image=self.config.image,
|
488
671
|
name=f'ansible-test-{self.label}',
|
489
672
|
ports=[22],
|
490
|
-
publish_ports=not self.controller, #
|
673
|
+
publish_ports=self.debugging_enabled or not self.controller, # SSH to the controller is not required unless remote debugging is enabled
|
491
674
|
options=init_config.options,
|
492
675
|
cleanup=False,
|
493
676
|
cmd=self.build_init_command(init_config, init_probe),
|
@@ -627,7 +810,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
627
810
|
# The host namespace must be used to permit the container to access the cgroup v1 systemd hierarchy created by Podman.
|
628
811
|
'--cgroupns', 'host',
|
629
812
|
# Mask the host cgroup tmpfs mount to avoid exposing the host cgroup v1 hierarchies (or cgroup v2 hybrid) to the container.
|
630
|
-
# Podman will provide a cgroup v1 systemd
|
813
|
+
# Podman will provide a cgroup v1 systemd hierarchy on top of this.
|
631
814
|
'--tmpfs', '/sys/fs/cgroup',
|
632
815
|
))
|
633
816
|
|
@@ -1000,6 +1183,9 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
1000
1183
|
docker_logs(self.args, self.container_name)
|
1001
1184
|
raise
|
1002
1185
|
|
1186
|
+
if self.debugging_enabled:
|
1187
|
+
self.enable_debugger_forwarding(self.get_ssh_connection_detail(HostType.origin))
|
1188
|
+
|
1003
1189
|
def deprovision(self) -> None:
|
1004
1190
|
"""Deprovision the host after delegation has completed."""
|
1005
1191
|
super().deprovision()
|
@@ -1248,13 +1434,18 @@ class OriginProfile(ControllerHostProfile[OriginConfig]):
|
|
1248
1434
|
return os.getcwd()
|
1249
1435
|
|
1250
1436
|
|
1251
|
-
class PosixRemoteProfile(ControllerHostProfile[PosixRemoteConfig], RemoteProfile[PosixRemoteConfig]):
|
1437
|
+
class PosixRemoteProfile(ControllerHostProfile[PosixRemoteConfig], RemoteProfile[PosixRemoteConfig], DebuggableProfile[PosixRemoteConfig]):
|
1252
1438
|
"""Host profile for a POSIX remote instance."""
|
1253
1439
|
|
1254
1440
|
def wait(self) -> None:
|
1255
1441
|
"""Wait for the instance to be ready. Executed before delegation for the controller and after delegation for targets."""
|
1256
1442
|
self.wait_until_ready()
|
1257
1443
|
|
1444
|
+
def setup(self) -> None:
|
1445
|
+
"""Perform out-of-band setup before delegation."""
|
1446
|
+
if self.debugging_enabled:
|
1447
|
+
self.enable_debugger_forwarding(self.get_origin_controller_connection().settings)
|
1448
|
+
|
1258
1449
|
def configure(self) -> None:
|
1259
1450
|
"""Perform in-band configuration. Executed before delegation for the controller and after delegation for targets."""
|
1260
1451
|
# a target uses a single python version, but a controller may include additional versions for targets running on the controller
|
@@ -30,6 +30,7 @@ from .host_profiles import (
|
|
30
30
|
SshTargetHostProfile,
|
31
31
|
WindowsInventoryProfile,
|
32
32
|
WindowsRemoteProfile,
|
33
|
+
DebuggableProfile,
|
33
34
|
)
|
34
35
|
|
35
36
|
from .ssh import (
|
@@ -59,6 +60,9 @@ def get_common_variables(target_profile: HostProfile, controller: bool = False)
|
|
59
60
|
# To compensate for this we'll perform a `cd /` before running any commands after `sudo` succeeds.
|
60
61
|
common_variables.update(ansible_sudo_chdir='/')
|
61
62
|
|
63
|
+
if isinstance(target_profile, DebuggableProfile):
|
64
|
+
common_variables.update(target_profile.get_ansiballz_inventory_variables())
|
65
|
+
|
62
66
|
return common_variables
|
63
67
|
|
64
68
|
|
@@ -1,11 +1,15 @@
|
|
1
1
|
"""Test metadata for passing data to delegated tests."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
|
+
|
5
|
+
import dataclasses
|
4
6
|
import typing as t
|
5
7
|
|
6
8
|
from .util import (
|
7
9
|
display,
|
8
10
|
generate_name,
|
11
|
+
ANSIBLE_TEST_ROOT,
|
12
|
+
ANSIBLE_LIB_ROOT,
|
9
13
|
)
|
10
14
|
|
11
15
|
from .io import (
|
@@ -22,13 +26,19 @@ from .diff import (
|
|
22
26
|
class Metadata:
|
23
27
|
"""Metadata object for passing data to delegated tests."""
|
24
28
|
|
25
|
-
def __init__(self) -> None:
|
29
|
+
def __init__(self, debugger_flags: DebuggerFlags) -> None:
|
26
30
|
"""Initialize metadata."""
|
27
31
|
self.changes: dict[str, tuple[tuple[int, int], ...]] = {}
|
28
32
|
self.cloud_config: t.Optional[dict[str, dict[str, t.Union[int, str, bool]]]] = None
|
29
33
|
self.change_description: t.Optional[ChangeDescription] = None
|
30
34
|
self.ci_provider: t.Optional[str] = None
|
31
35
|
self.session_id = generate_name()
|
36
|
+
self.ansible_lib_root = ANSIBLE_LIB_ROOT
|
37
|
+
self.ansible_test_root = ANSIBLE_TEST_ROOT
|
38
|
+
self.collection_root: str | None = None
|
39
|
+
self.debugger_flags = debugger_flags
|
40
|
+
self.debugger_settings: DebuggerSettings | None = None
|
41
|
+
self.loaded = False
|
32
42
|
|
33
43
|
def populate_changes(self, diff: t.Optional[list[str]]) -> None:
|
34
44
|
"""Populate the changeset using the given diff."""
|
@@ -55,8 +65,13 @@ class Metadata:
|
|
55
65
|
changes=self.changes,
|
56
66
|
cloud_config=self.cloud_config,
|
57
67
|
ci_provider=self.ci_provider,
|
58
|
-
change_description=self.change_description.to_dict(),
|
68
|
+
change_description=self.change_description.to_dict() if self.change_description else None,
|
59
69
|
session_id=self.session_id,
|
70
|
+
ansible_lib_root=self.ansible_lib_root,
|
71
|
+
ansible_test_root=self.ansible_test_root,
|
72
|
+
collection_root=self.collection_root,
|
73
|
+
debugger_flags=dataclasses.asdict(self.debugger_flags),
|
74
|
+
debugger_settings=dataclasses.asdict(self.debugger_settings) if self.debugger_settings else None,
|
60
75
|
)
|
61
76
|
|
62
77
|
def to_file(self, path: str) -> None:
|
@@ -76,12 +91,20 @@ class Metadata:
|
|
76
91
|
@staticmethod
|
77
92
|
def from_dict(data: dict[str, t.Any]) -> Metadata:
|
78
93
|
"""Return metadata loaded from the specified dictionary."""
|
79
|
-
metadata = Metadata(
|
94
|
+
metadata = Metadata(
|
95
|
+
debugger_flags=DebuggerFlags(**data['debugger_flags']),
|
96
|
+
)
|
97
|
+
|
80
98
|
metadata.changes = data['changes']
|
81
99
|
metadata.cloud_config = data['cloud_config']
|
82
100
|
metadata.ci_provider = data['ci_provider']
|
83
|
-
metadata.change_description = ChangeDescription.from_dict(data['change_description'])
|
101
|
+
metadata.change_description = ChangeDescription.from_dict(data['change_description']) if data['change_description'] else None
|
84
102
|
metadata.session_id = data['session_id']
|
103
|
+
metadata.ansible_lib_root = data['ansible_lib_root']
|
104
|
+
metadata.ansible_test_root = data['ansible_test_root']
|
105
|
+
metadata.collection_root = data['collection_root']
|
106
|
+
metadata.debugger_settings = DebuggerSettings(**data['debugger_settings']) if data['debugger_settings'] else None
|
107
|
+
metadata.loaded = True
|
85
108
|
|
86
109
|
return metadata
|
87
110
|
|
@@ -130,3 +153,70 @@ class ChangeDescription:
|
|
130
153
|
changes.no_integration_paths = data['no_integration_paths']
|
131
154
|
|
132
155
|
return changes
|
156
|
+
|
157
|
+
|
158
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
159
|
+
class DebuggerSettings:
|
160
|
+
"""Settings for remote debugging."""
|
161
|
+
|
162
|
+
module: str | None = None
|
163
|
+
"""
|
164
|
+
The Python module to import.
|
165
|
+
This should be pydevd or a derivative.
|
166
|
+
If not provided it will be auto-detected.
|
167
|
+
"""
|
168
|
+
|
169
|
+
package: str | None = None
|
170
|
+
"""
|
171
|
+
The Python package to install for debugging.
|
172
|
+
If `None` then the package will be auto-detected.
|
173
|
+
If an empty string, then no package will be installed.
|
174
|
+
"""
|
175
|
+
|
176
|
+
settrace: dict[str, object] = dataclasses.field(default_factory=dict)
|
177
|
+
"""
|
178
|
+
Options to pass to the `{module}.settrace` method.
|
179
|
+
Used for running AnsiballZ modules only.
|
180
|
+
The `host` and `port` options will be provided by ansible-test.
|
181
|
+
The `suspend` option defaults to `False`.
|
182
|
+
"""
|
183
|
+
|
184
|
+
args: list[str] = dataclasses.field(default_factory=list)
|
185
|
+
"""
|
186
|
+
Arguments to pass to `pydevd` on the command line.
|
187
|
+
Used for running Ansible CLI programs only.
|
188
|
+
The `--client` and `--port` options will be provided by ansible-test.
|
189
|
+
"""
|
190
|
+
|
191
|
+
port: int = 5678
|
192
|
+
"""
|
193
|
+
The port on the origin host which is listening for incoming connections from pydevd.
|
194
|
+
SSH port forwarding will be automatically configured for non-local hosts to connect to this port as needed.
|
195
|
+
"""
|
196
|
+
|
197
|
+
|
198
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
199
|
+
class DebuggerFlags:
|
200
|
+
"""Flags for enabling specific debugging features."""
|
201
|
+
|
202
|
+
self: bool = False
|
203
|
+
"""Debug ansible-test itself."""
|
204
|
+
|
205
|
+
ansiballz: bool = False
|
206
|
+
"""Debug AnsiballZ modules."""
|
207
|
+
|
208
|
+
cli: bool = False
|
209
|
+
"""Debug Ansible CLI programs other than ansible-test."""
|
210
|
+
|
211
|
+
on_demand: bool = False
|
212
|
+
"""Enable debugging features only when ansible-test is running under a debugger."""
|
213
|
+
|
214
|
+
@property
|
215
|
+
def enable(self) -> bool:
|
216
|
+
"""Return `True` if any debugger feature other than on-demand is enabled."""
|
217
|
+
return any(getattr(self, field.name) for field in dataclasses.fields(self) if field.name != 'on_demand')
|
218
|
+
|
219
|
+
@classmethod
|
220
|
+
def all(cls, enabled: bool) -> t.Self:
|
221
|
+
"""Return a `DebuggerFlags` instance with all flags enabled or disabled."""
|
222
|
+
return cls(**{field.name: enabled for field in dataclasses.fields(cls)})
|
@@ -0,0 +1,80 @@
|
|
1
|
+
"""Wrappers around `ps` for querying running processes."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import collections
|
6
|
+
import dataclasses
|
7
|
+
import os
|
8
|
+
import pathlib
|
9
|
+
import shlex
|
10
|
+
|
11
|
+
from ansible_test._internal.util import raw_command
|
12
|
+
|
13
|
+
|
14
|
+
@dataclasses.dataclass(frozen=True)
|
15
|
+
class ProcessData:
|
16
|
+
"""Data about a running process."""
|
17
|
+
|
18
|
+
pid: int
|
19
|
+
ppid: int
|
20
|
+
command: str
|
21
|
+
|
22
|
+
|
23
|
+
@dataclasses.dataclass(frozen=True)
|
24
|
+
class Process:
|
25
|
+
"""A process in the process tree."""
|
26
|
+
|
27
|
+
pid: int
|
28
|
+
command: str
|
29
|
+
parent: Process | None = None
|
30
|
+
children: tuple[Process, ...] = dataclasses.field(default_factory=tuple)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def args(self) -> list[str]:
|
34
|
+
"""The list of arguments that make up `command`."""
|
35
|
+
return shlex.split(self.command)
|
36
|
+
|
37
|
+
@property
|
38
|
+
def path(self) -> pathlib.Path:
|
39
|
+
"""The path to the process."""
|
40
|
+
return pathlib.Path(self.args[0])
|
41
|
+
|
42
|
+
|
43
|
+
def get_process_data(pids: list[int] | None = None) -> list[ProcessData]:
|
44
|
+
"""Return a list of running processes."""
|
45
|
+
if pids:
|
46
|
+
args = ['-p', ','.join(map(str, pids))]
|
47
|
+
else:
|
48
|
+
args = ['-A']
|
49
|
+
|
50
|
+
lines = raw_command(['ps'] + args + ['-o', 'pid,ppid,command'], capture=True)[0].splitlines()[1:]
|
51
|
+
processes = [ProcessData(pid=int(pid), ppid=int(ppid), command=command) for pid, ppid, command in (line.split(maxsplit=2) for line in lines)]
|
52
|
+
|
53
|
+
return processes
|
54
|
+
|
55
|
+
|
56
|
+
def get_process_tree() -> dict[int, Process]:
|
57
|
+
"""Return the process tree."""
|
58
|
+
processes = get_process_data()
|
59
|
+
pid_to_process: dict[int, Process] = {}
|
60
|
+
pid_to_children: dict[int, list[Process]] = collections.defaultdict(list)
|
61
|
+
|
62
|
+
for data in processes:
|
63
|
+
pid_to_process[data.pid] = process = Process(pid=data.pid, command=data.command)
|
64
|
+
|
65
|
+
if data.ppid:
|
66
|
+
pid_to_children[data.ppid].append(process)
|
67
|
+
|
68
|
+
for data in processes:
|
69
|
+
pid_to_process[data.pid] = dataclasses.replace(
|
70
|
+
pid_to_process[data.pid],
|
71
|
+
parent=pid_to_process.get(data.ppid),
|
72
|
+
children=tuple(pid_to_children[data.pid]),
|
73
|
+
)
|
74
|
+
|
75
|
+
return pid_to_process
|
76
|
+
|
77
|
+
|
78
|
+
def get_current_process() -> Process:
|
79
|
+
"""Return the current process along with its ancestors and descendants."""
|
80
|
+
return get_process_tree()[os.getpid()]
|
@@ -55,6 +55,11 @@ from .coverage_util import (
|
|
55
55
|
get_coverage_version,
|
56
56
|
)
|
57
57
|
|
58
|
+
if t.TYPE_CHECKING:
|
59
|
+
from .host_profiles import (
|
60
|
+
HostProfile,
|
61
|
+
)
|
62
|
+
|
58
63
|
QUIET_PIP_SCRIPT_PATH = os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'quiet_pip.py')
|
59
64
|
REQUIREMENTS_SCRIPT_PATH = os.path.join(ANSIBLE_TEST_TARGET_ROOT, 'setup', 'requirements.py')
|
60
65
|
|
@@ -122,6 +127,7 @@ class PipBootstrap(PipCommand):
|
|
122
127
|
|
123
128
|
def install_requirements(
|
124
129
|
args: EnvironmentConfig,
|
130
|
+
host_profile: HostProfile | None,
|
125
131
|
python: PythonConfig,
|
126
132
|
ansible: bool = False,
|
127
133
|
command: bool = False,
|
@@ -133,6 +139,7 @@ def install_requirements(
|
|
133
139
|
create_result_directories(args)
|
134
140
|
|
135
141
|
if not requirements_allowed(args, controller):
|
142
|
+
post_install(host_profile)
|
136
143
|
return
|
137
144
|
|
138
145
|
if command and isinstance(args, (UnitsConfig, IntegrationConfig)) and args.coverage:
|
@@ -161,7 +168,17 @@ def install_requirements(
|
|
161
168
|
sanity=None,
|
162
169
|
)
|
163
170
|
|
171
|
+
from .host_profiles import DebuggableProfile
|
172
|
+
|
173
|
+
if isinstance(host_profile, DebuggableProfile) and host_profile.debugging_enabled and args.metadata.debugger_settings.package:
|
174
|
+
commands.append(PipInstall(
|
175
|
+
requirements=[],
|
176
|
+
constraints=[],
|
177
|
+
packages=[args.metadata.debugger_settings.package],
|
178
|
+
))
|
179
|
+
|
164
180
|
if not commands:
|
181
|
+
post_install(host_profile)
|
165
182
|
return
|
166
183
|
|
167
184
|
run_pip(args, python, commands, connection)
|
@@ -170,6 +187,16 @@ def install_requirements(
|
|
170
187
|
if any(isinstance(command, PipInstall) and command.has_package('pyyaml') for command in commands):
|
171
188
|
check_pyyaml(python)
|
172
189
|
|
190
|
+
post_install(host_profile)
|
191
|
+
|
192
|
+
|
193
|
+
def post_install(host_profile: HostProfile) -> None:
|
194
|
+
"""Operations to perform after requirements are installed."""
|
195
|
+
from .host_profiles import DebuggableProfile
|
196
|
+
|
197
|
+
if isinstance(host_profile, DebuggableProfile):
|
198
|
+
host_profile.activate_debugger()
|
199
|
+
|
173
200
|
|
174
201
|
def collect_bootstrap(python: PythonConfig) -> list[PipCommand]:
|
175
202
|
"""Return the details necessary to bootstrap pip into an empty virtual environment."""
|
ansible_test/_internal/target.py
CHANGED
@@ -582,6 +582,14 @@ class IntegrationTarget(CompletionTarget):
|
|
582
582
|
else:
|
583
583
|
static_aliases = tuple()
|
584
584
|
|
585
|
+
# non-group aliases which need to be extracted before group mangling occurs
|
586
|
+
|
587
|
+
self.env_set: dict[str, str] = {
|
588
|
+
match.group('key'): match.group('value') for match in (
|
589
|
+
re.match(r'env/set/(?P<key>[^/]+)/(?P<value>.*)', alias) for alias in static_aliases
|
590
|
+
) if match
|
591
|
+
}
|
592
|
+
|
585
593
|
# modules
|
586
594
|
|
587
595
|
if self.name in modules:
|