|
|
|
"""Payload management for sending Ansible files and test content to other systems (VMs, containers)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import atexit
|
|
|
|
import os
|
|
|
|
import stat
|
|
|
|
import tarfile
|
|
|
|
import tempfile
|
|
|
|
import time
|
|
|
|
import typing as t
|
|
|
|
|
|
|
|
from .constants import (
|
|
|
|
ANSIBLE_BIN_SYMLINK_MAP,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .config import (
|
|
|
|
IntegrationConfig,
|
|
|
|
ShellConfig,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .util import (
|
|
|
|
display,
|
|
|
|
ANSIBLE_SOURCE_ROOT,
|
|
|
|
remove_tree,
|
|
|
|
is_subdir,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .data import (
|
|
|
|
data_context,
|
|
|
|
)
|
|
|
|
|
|
|
|
from .util_common import (
|
|
|
|
CommonConfig,
|
|
|
|
)
|
|
|
|
|
|
|
|
# improve performance by disabling uid/gid lookups
|
|
|
|
tarfile.pwd = None # type: ignore[attr-defined] # undocumented attribute
|
|
|
|
tarfile.grp = None # type: ignore[attr-defined] # undocumented attribute
|
|
|
|
|
|
|
|
|
|
|
|
def create_payload(args: CommonConfig, dst_path: str) -> None:
|
|
|
|
"""Create a payload for delegation."""
|
|
|
|
if args.explain:
|
|
|
|
return
|
|
|
|
|
|
|
|
files = list(data_context().ansible_source)
|
|
|
|
filters = {}
|
|
|
|
|
|
|
|
def make_executable(tar_info: tarfile.TarInfo) -> t.Optional[tarfile.TarInfo]:
|
|
|
|
"""Make the given file executable."""
|
|
|
|
tar_info.mode |= stat.S_IXUSR | stat.S_IXOTH | stat.S_IXGRP
|
|
|
|
return tar_info
|
|
|
|
|
|
|
|
if not ANSIBLE_SOURCE_ROOT:
|
|
|
|
# reconstruct the bin directory which is not available when running from an ansible install
|
|
|
|
files.extend(create_temporary_bin_files(args))
|
|
|
|
filters.update(dict((os.path.join('ansible', path[3:]), make_executable) for path in ANSIBLE_BIN_SYMLINK_MAP.values() if path.startswith('../')))
|
|
|
|
|
|
|
|
if not data_context().content.is_ansible:
|
|
|
|
# exclude unnecessary files when not testing ansible itself
|
|
|
|
files = [f for f in files if
|
|
|
|
is_subdir(f[1], 'bin/') or
|
|
|
|
is_subdir(f[1], 'lib/ansible/') or
|
|
|
|
is_subdir(f[1], 'test/lib/ansible_test/')]
|
|
|
|
|
|
|
|
if not isinstance(args, (ShellConfig, IntegrationConfig)):
|
|
|
|
# exclude built-in ansible modules when they are not needed
|
|
|
|
files = [f for f in files if not is_subdir(f[1], 'lib/ansible/modules/') or f[1] == 'lib/ansible/modules/__init__.py']
|
|
|
|
|
|
|
|
collection_layouts = data_context().create_collection_layouts()
|
|
|
|
|
|
|
|
content_files: list[tuple[str, str]] = []
|
|
|
|
extra_files: list[tuple[str, str]] = []
|
|
|
|
|
|
|
|
for layout in collection_layouts:
|
|
|
|
if layout == data_context().content:
|
|
|
|
# include files from the current collection (layout.collection.directory will be added later)
|
|
|
|
content_files.extend((os.path.join(layout.root, path), path) for path in data_context().content.all_files())
|
|
|
|
else:
|
|
|
|
# include files from each collection in the same collection root as the content being tested
|
|
|
|
extra_files.extend((os.path.join(layout.root, path), os.path.join(layout.collection.directory, path)) for path in layout.all_files())
|
|
|
|
else:
|
|
|
|
# when testing ansible itself the ansible source is the content
|
|
|
|
content_files = files
|
|
|
|
# there are no extra files when testing ansible itself
|
|
|
|
extra_files = []
|
|
|
|
|
|
|
|
for callback in data_context().payload_callbacks:
|
|
|
|
# execute callbacks only on the content paths
|
|
|
|
# this is done before placing them in the appropriate subdirectory (see below)
|
|
|
|
callback(content_files)
|
|
|
|
|
|
|
|
# place ansible source files under the 'ansible' directory on the delegated host
|
|
|
|
files = [(src, os.path.join('ansible', dst)) for src, dst in files]
|
|
|
|
|
|
|
|
if data_context().content.collection:
|
|
|
|
# place collection files under the 'ansible_collections/{namespace}/{collection}' directory on the delegated host
|
|
|
|
files.extend((src, os.path.join(data_context().content.collection.directory, dst)) for src, dst in content_files)
|
|
|
|
# extra files already have the correct destination path
|
|
|
|
files.extend(extra_files)
|
|
|
|
|
|
|
|
# maintain predictable file order
|
|
|
|
files = sorted(set(files))
|
|
|
|
|
|
|
|
display.info('Creating a payload archive containing %d files...' % len(files), verbosity=1)
|
|
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
|
|
|
with tarfile.open(dst_path, mode='w:gz', compresslevel=4, format=tarfile.GNU_FORMAT) as tar:
|
|
|
|
for src, dst in files:
|
|
|
|
display.info('%s -> %s' % (src, dst), verbosity=4)
|
|
|
|
tar.add(src, dst, filter=filters.get(dst))
|
|
|
|
|
|
|
|
duration = time.time() - start
|
|
|
|
payload_size_bytes = os.path.getsize(dst_path)
|
|
|
|
|
|
|
|
display.info('Created a %d byte payload archive containing %d files in %d seconds.' % (payload_size_bytes, len(files), duration), verbosity=1)
|
|
|
|
|
|
|
|
|
|
|
|
def create_temporary_bin_files(args: CommonConfig) -> tuple[tuple[str, str], ...]:
|
|
|
|
"""Create a temporary ansible bin directory populated using the symlink map."""
|
|
|
|
if args.explain:
|
|
|
|
temp_path = '/tmp/ansible-tmp-bin'
|
|
|
|
else:
|
|
|
|
temp_path = tempfile.mkdtemp(prefix='ansible', suffix='bin')
|
|
|
|
atexit.register(remove_tree, temp_path)
|
|
|
|
|
|
|
|
for name, dest in ANSIBLE_BIN_SYMLINK_MAP.items():
|
|
|
|
path = os.path.join(temp_path, name)
|
|
|
|
os.symlink(dest, path)
|
|
|
|
|
|
|
|
return tuple((os.path.join(temp_path, name), os.path.join('bin', name)) for name in sorted(ANSIBLE_BIN_SYMLINK_MAP))
|