summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Clay <matt@mystile.com>2022-12-05 13:33:14 -0800
committerGitHub <noreply@github.com>2022-12-05 13:33:14 -0800
commit80d2f8da02052f64396da6b8caaf820eedbf18e2 (patch)
tree68fd13b3be48588dd0cbe2d37e53a8f0d76dd6bb
parent31f95e201a564faef77473a7762a7d99583a9712 (diff)
downloadansible-80d2f8da02052f64396da6b8caaf820eedbf18e2.tar.gz
ansible-test - Fix container detection. (#79530)
-rw-r--r--changelogs/fragments/ansible-test-container-management.yml4
-rw-r--r--test/lib/ansible_test/_internal/cgroup.py49
-rw-r--r--test/lib/ansible_test/_internal/commands/env/__init__.py4
-rw-r--r--test/lib/ansible_test/_internal/dev/container_probe.py2
-rw-r--r--test/lib/ansible_test/_internal/docker_util.py58
5 files changed, 88 insertions, 29 deletions
diff --git a/changelogs/fragments/ansible-test-container-management.yml b/changelogs/fragments/ansible-test-container-management.yml
index 3dd182aaab..04961b98ee 100644
--- a/changelogs/fragments/ansible-test-container-management.yml
+++ b/changelogs/fragments/ansible-test-container-management.yml
@@ -38,6 +38,7 @@ minor_changes:
- ansible-test - Integration tests can be excluded from retries triggered by the ``--retry-on-error`` option by
adding the ``retry/never`` alias. This is useful for tests that cannot pass on a retry or are too
slow to make retries useful.
+ - ansible-test - The ``ansible-test env`` command now detects and reports the container ID if running in a container.
bugfixes:
- ansible-test - Multiple containers now work under Podman without specifying the ``--docker-network`` option.
- ansible-test - Prevent concurrent / repeat pulls of the same container image.
@@ -47,6 +48,9 @@ bugfixes:
- ansible-test - Show the exception type when reporting errors during instance provisioning.
- ansible-test - Pass the ``XDG_RUNTIME_DIR`` environment variable through to container commands.
- ansible-test - Connection attempts to managed remote instances no longer abort on ``Permission denied`` errors.
+ - ansible-test - Detection for running in a Podman or Docker container has been fixed to detect more scenarios.
+ The new detection relies on ``/proc/self/mountinfo`` instead of ``/proc/self/cpuset``.
+ Detection now works with custom cgroups and private cgroup namespaces.
known_issues:
- ansible-test - Using Docker on systems with SELinux may require setting SELinux to permissive mode.
Podman should work with SELinux in enforcing mode.
diff --git a/test/lib/ansible_test/_internal/cgroup.py b/test/lib/ansible_test/_internal/cgroup.py
index 66a72c9a7f..b55d878dc3 100644
--- a/test/lib/ansible_test/_internal/cgroup.py
+++ b/test/lib/ansible_test/_internal/cgroup.py
@@ -1,8 +1,10 @@
"""Linux control group constants, classes and utilities."""
from __future__ import annotations
+import codecs
import dataclasses
import pathlib
+import re
class CGroupPath:
@@ -55,25 +57,54 @@ class CGroupEntry:
@dataclasses.dataclass(frozen=True)
class MountEntry:
- """A single mount entry parsed from '/proc/{pid}/mounts' in the proc filesystem."""
- device: pathlib.PurePosixPath
+ """A single mount info entry parsed from '/proc/{pid}/mountinfo' in the proc filesystem."""
+ mount_id: int
+ parent_id: int
+ device_major: int
+ device_minor: int
+ root: pathlib.PurePosixPath
path: pathlib.PurePosixPath
- type: str
options: tuple[str, ...]
+ fields: tuple[str, ...]
+ type: str
+ source: pathlib.PurePosixPath
+ super_options: tuple[str, ...]
@classmethod
def parse(cls, value: str) -> MountEntry:
- """Parse the given mount line from the proc filesystem and return a mount entry."""
- device, path, mtype, options, _a, _b = value.split(' ')
+ """Parse the given mount info line from the proc filesystem and return a mount entry."""
+ # See: https://man7.org/linux/man-pages/man5/proc.5.html
+ # See: https://github.com/torvalds/linux/blob/aea23e7c464bfdec04b52cf61edb62030e9e0d0a/fs/proc_namespace.c#L135
+ mount_id, parent_id, device_major_minor, root, path, options, *remainder = value.split(' ')
+ fields = remainder[:-4]
+ separator, mtype, source, super_options = remainder[-4:]
+
+ assert separator == '-'
+
+ device_major, device_minor = device_major_minor.split(':')
return cls(
- device=pathlib.PurePosixPath(device),
- path=pathlib.PurePosixPath(path),
- type=mtype,
+ mount_id=int(mount_id),
+ parent_id=int(parent_id),
+ device_major=int(device_major),
+ device_minor=int(device_minor),
+ root=_decode_path(root),
+ path=_decode_path(path),
options=tuple(options.split(',')),
+ fields=tuple(fields),
+ type=mtype,
+ source=_decode_path(source),
+ super_options=tuple(super_options.split(',')),
)
@classmethod
def loads(cls, value: str) -> tuple[MountEntry, ...]:
- """Parse the given output from the proc filesystem and return a tuple of mount entries."""
+ """Parse the given output from the proc filesystem and return a tuple of mount info entries."""
return tuple(cls.parse(line) for line in value.splitlines())
+
+
+def _decode_path(value: str) -> pathlib.PurePosixPath:
+ """Decode and return a path which may contain octal escape sequences."""
+ # See: https://github.com/torvalds/linux/blob/aea23e7c464bfdec04b52cf61edb62030e9e0d0a/fs/proc_namespace.c#L150
+ path = re.sub(r'(\\[0-7]{3})', lambda m: codecs.decode(m.group(0).encode('ascii'), 'unicode_escape'), value)
+ return pathlib.PurePosixPath(path)
diff --git a/test/lib/ansible_test/_internal/commands/env/__init__.py b/test/lib/ansible_test/_internal/commands/env/__init__.py
index d11c4765d9..44f229f879 100644
--- a/test/lib/ansible_test/_internal/commands/env/__init__.py
+++ b/test/lib/ansible_test/_internal/commands/env/__init__.py
@@ -31,6 +31,7 @@ from ...util_common import (
from ...docker_util import (
get_docker_command,
get_docker_info,
+ get_docker_container_id,
)
from ...constants import (
@@ -69,11 +70,14 @@ def show_dump_env(args: EnvConfig) -> None:
if not args.show and not args.dump:
return
+ container_id = get_docker_container_id()
+
data = dict(
ansible=dict(
version=get_ansible_version(),
),
docker=get_docker_details(args),
+ container_id=container_id,
environ=os.environ.copy(),
location=dict(
pwd=os.environ.get('PWD', None),
diff --git a/test/lib/ansible_test/_internal/dev/container_probe.py b/test/lib/ansible_test/_internal/dev/container_probe.py
index 0a6a3b20f6..84b88f4bb6 100644
--- a/test/lib/ansible_test/_internal/dev/container_probe.py
+++ b/test/lib/ansible_test/_internal/dev/container_probe.py
@@ -73,7 +73,7 @@ class CGroupMount:
def check_container_cgroup_status(args: EnvironmentConfig, config: DockerConfig, container_name: str, expected_mounts: tuple[CGroupMount, ...]) -> None:
"""Check the running container to examine the state of the cgroup hierarchies."""
- cmd = ['sh', '-c', 'cat /proc/1/cgroup && echo && cat /proc/1/mounts']
+ cmd = ['sh', '-c', 'cat /proc/1/cgroup && echo && cat /proc/1/mountinfo']
stdout = docker_exec(args, container_name, cmd, capture=True)[0]
cgroups_stdout, mounts_stdout = stdout.split('\n\n')
diff --git a/test/lib/ansible_test/_internal/docker_util.py b/test/lib/ansible_test/_internal/docker_util.py
index efbebb6c6b..fbaca78630 100644
--- a/test/lib/ansible_test/_internal/docker_util.py
+++ b/test/lib/ansible_test/_internal/docker_util.py
@@ -6,15 +6,12 @@ import enum
import json
import os
import pathlib
+import re
import socket
import time
import urllib.parse
import typing as t
-from .io import (
- read_text_file,
-)
-
from .util import (
ApplicationError,
common_environment,
@@ -297,7 +294,7 @@ def detect_host_properties(args: CommonConfig) -> ContainerHostProperties:
multi_line_commands = (
' && '.join(single_line_commands),
'cat /proc/1/cgroup',
- 'cat /proc/1/mounts',
+ 'cat /proc/1/mountinfo',
)
options = ['--volume', '/sys/fs/cgroup:/probe:ro']
@@ -573,24 +570,47 @@ def get_podman_hostname() -> str:
@cache
def get_docker_container_id() -> t.Optional[str]:
"""Return the current container ID if running in a container, otherwise return None."""
- path = '/proc/self/cpuset'
+ mountinfo_path = pathlib.Path('/proc/self/mountinfo')
container_id = None
+ engine = None
+
+ if mountinfo_path.is_file():
+ # NOTE: This method of detecting the container engine and container ID relies on implementation details of each container engine.
+ # Although the implementation details have remained unchanged for some time, there is no guarantee they will continue to work.
+ # There have been proposals to create a standard mechanism for this, but none is currently available.
+ # See: https://github.com/opencontainers/runtime-spec/issues/1105
+
+ mounts = MountEntry.loads(mountinfo_path.read_text())
+
+ for mount in mounts:
+ if str(mount.path) == '/etc/hostname':
+ # Podman generates /etc/hostname in the makePlatformBindMounts function.
+ # That function ends up using ContainerRunDirectory to generate a path like: {prefix}/{container_id}/userdata/hostname
+ # NOTE: The {prefix} portion of the path can vary, so should not be relied upon.
+ # See: https://github.com/containers/podman/blob/480c7fbf5361f3bd8c1ed81fe4b9910c5c73b186/libpod/container_internal_linux.go#L660-L664
+ # See: https://github.com/containers/podman/blob/480c7fbf5361f3bd8c1ed81fe4b9910c5c73b186/vendor/github.com/containers/storage/store.go#L3133
+ # This behavior has existed for ~5 years and was present in Podman version 0.2.
+ # See: https://github.com/containers/podman/pull/248
+ if match := re.search('/(?P<id>[0-9a-f]{64})/userdata/hostname$', str(mount.root)):
+ container_id = match.group('id')
+ engine = 'Podman'
+ break
- if os.path.exists(path):
- # File content varies based on the environment:
- # No Container: /
- # Docker: /docker/c86f3732b5ba3d28bb83b6e14af767ab96abbc52de31313dcb1176a62d91a507
- # Azure Pipelines (Docker): /azpl_job/0f2edfed602dd6ec9f2e42c867f4d5ee640ebf4c058e6d3196d4393bb8fd0891
- # Podman: /../../../../../..
- contents = read_text_file(path)
-
- cgroup_path, cgroup_name = os.path.split(contents.strip())
-
- if cgroup_path in ('/docker', '/azpl_job'):
- container_id = cgroup_name
+ # Docker generates /etc/hostname in the BuildHostnameFile function.
+ # That function ends up using the containerRoot function to generate a path like: {prefix}/{container_id}/hostname
+ # NOTE: The {prefix} portion of the path can vary, so should not be relied upon.
+ # See: https://github.com/moby/moby/blob/cd8a090e6755bee0bdd54ac8a894b15881787097/container/container_unix.go#L58
+ # See: https://github.com/moby/moby/blob/92e954a2f05998dc05773b6c64bbe23b188cb3a0/daemon/container.go#L86
+ # This behavior has existed for at least ~7 years and was present in Docker version 1.0.1.
+ # See: https://github.com/moby/moby/blob/v1.0.1/daemon/container.go#L351
+ # See: https://github.com/moby/moby/blob/v1.0.1/daemon/daemon.go#L133
+ if match := re.search('/(?P<id>[0-9a-f]{64})/hostname$', str(mount.root)):
+ container_id = match.group('id')
+ engine = 'Docker'
+ break
if container_id:
- display.info('Detected execution in Docker container: %s' % container_id, verbosity=1)
+ display.info(f'Detected execution in {engine} container ID: {container_id}', verbosity=1)
return container_id