ansible-test - Fix container detection. (#79530)

pull/79541/head
Matt Clay 2 years ago committed by GitHub
parent 31f95e201a
commit 80d2f8da02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.

@ -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)

@ -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),

@ -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')

@ -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

Loading…
Cancel
Save