Skip to content

Commit

Permalink
Use config object from run() API call
Browse files Browse the repository at this point in the history
  • Loading branch information
Shrews committed Oct 11, 2024
1 parent 8d072b2 commit 708feee
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 249 deletions.
Empty file.
124 changes: 124 additions & 0 deletions src/ansible_runner/_internal/_dump_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from __future__ import annotations

import fcntl
import hashlib
import json
import os
import stat
import tempfile

from collections.abc import MutableMapping

from ansible_runner.config.runner import RunnerConfig
from ansible_runner.utils import isinventory, isplaybook


def dump_artifacts(config: RunnerConfig) -> None:
"""Introspect the arguments and dump objects to disk"""
if config.role:
role = {'name': config.role}
if config.role_vars:
role['vars'] = config.role_vars

hosts = config.host_pattern or 'all'
play = [{'hosts': hosts, 'roles': [role]}]

if config.role_skip_facts:
play[0]['gather_facts'] = False

config.playbook = play

if config.envvars is None:
config.envvars = {}

roles_path = config.roles_path
if not roles_path:
roles_path = os.path.join(config.private_data_dir, 'roles')
else:
roles_path += f":{os.path.join(config.private_data_dir, 'roles')}"

config.envvars['ANSIBLE_ROLES_PATH'] = roles_path

playbook = config.playbook
if playbook:
# Ensure the play is a list of dictionaries
if isinstance(playbook, MutableMapping):
playbook = [playbook]

if isplaybook(playbook):
path = os.path.join(config.private_data_dir, 'project')
config.playbook = dump_artifact(json.dumps(playbook), path, 'main.json')

obj = config.inventory
if obj and isinventory(obj):
path = os.path.join(config.private_data_dir, 'inventory')
if isinstance(obj, MutableMapping):
config.inventory = dump_artifact(json.dumps(obj), path, 'hosts.json')
elif isinstance(obj, str):
if not os.path.exists(os.path.join(path, obj)):
config.inventory = dump_artifact(obj, path, 'hosts')
elif os.path.isabs(obj):
config.inventory = obj
else:
config.inventory = os.path.join(path, obj)

if not config.suppress_env_files:
for key in ('envvars', 'extravars', 'passwords', 'settings'):
obj = getattr(config, key, None)
if obj and not os.path.exists(os.path.join(config.private_data_dir, 'env', key)):
path = os.path.join(config.private_data_dir, 'env')
dump_artifact(json.dumps(obj), path, key)

for key in ('ssh_key', 'cmdline'):
obj = getattr(config, key, None)
if obj and not os.path.exists(os.path.join(config.private_data_dir, 'env', key)):
path = os.path.join(config.private_data_dir, 'env')
dump_artifact(obj, path, key)


def dump_artifact(obj: str,
path: str,
filename: str | None = None
) -> str:
"""Write the artifact to disk at the specified path
:param str obj: The string object to be dumped to disk in the specified
path. The artifact filename will be automatically created.
:param str path: The full path to the artifacts data directory.
:param str filename: The name of file to write the artifact to.
If the filename is not provided, then one will be generated.
:return: The full path filename for the artifact that was generated.
"""
if not os.path.exists(path):
os.makedirs(path, mode=0o700)

p_sha1 = hashlib.sha1()
p_sha1.update(obj.encode(encoding='UTF-8'))

if filename is None:
_, fn = tempfile.mkstemp(dir=path)
else:
fn = os.path.join(path, filename)

if os.path.exists(fn):
c_sha1 = hashlib.sha1()
with open(fn) as f:
contents = f.read()
c_sha1.update(contents.encode(encoding='UTF-8'))

if not os.path.exists(fn) or p_sha1.hexdigest() != c_sha1.hexdigest():
lock_fp = os.path.join(path, '.artifact_write_lock')
lock_fd = os.open(lock_fp, os.O_RDWR | os.O_CREAT, stat.S_IRUSR | stat.S_IWUSR)
fcntl.lockf(lock_fd, fcntl.LOCK_EX)

try:
with open(fn, 'w') as f:
os.chmod(fn, stat.S_IRUSR | stat.S_IWUSR)
f.write(str(obj))
finally:
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
os.close(lock_fd)
os.remove(lock_fp)

return fn
20 changes: 18 additions & 2 deletions src/ansible_runner/config/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from dataclasses import dataclass, field
from enum import Enum
from uuid import uuid4
from collections.abc import Mapping
from collections.abc import Callable, Mapping
from typing import Any

import pexpect
Expand Down Expand Up @@ -64,6 +64,13 @@ class BaseExecutionMode(Enum):

@dataclass
class BaseConfig:
"""The base configuration object.
This object has multiple initialization responsibilities, including:
- guaranteeing the `private_data_dir` directory exists
- guaranteeing that `ident` value is set
- setting the various work directory attributes based on `private_data_dir`
"""

# This MUST be the first field we define to handle the use case where a RunnerConfig object
# is instantiated with private_data_dir as the first positional argument (non-keyword).
Expand Down Expand Up @@ -97,6 +104,12 @@ class BaseConfig:
suppress_env_files: bool = field(metadata={}, default=False)
timeout: int | None = field(metadata={}, default=None)

event_handler: Callable[[dict], None] | None = None
status_handler: Callable[[dict, BaseConfig], bool] | None = None
artifacts_handler: Callable[[str], None] | None = None
cancel_callback: Callable[[], bool] | None = None
finished_callback: Callable[[BaseConfig], None] | None = None

_CONTAINER_ENGINES = ('docker', 'podman')

def __post_init__(self) -> None:
Expand All @@ -115,7 +128,10 @@ def __post_init__(self) -> None:
# Note that os.makedirs, exist_ok=True is dangerous. If there's a directory writable
# by someone other than the user anywhere in the path to be created, an attacker can
# attempt to compromise the directories via a race.
os.makedirs(self.private_data_dir, exist_ok=True, mode=0o700)
try:
os.makedirs(self.private_data_dir, exist_ok=True, mode=0o700)
except Exception as error:
raise ConfigurationError(f"Unable to create private_data_dir {self.private_data_dir}") from error
else:
self.private_data_dir = tempfile.mkdtemp(prefix=defaults.AUTO_CREATE_NAMING, dir=defaults.AUTO_CREATE_DIR)

Expand Down
20 changes: 19 additions & 1 deletion src/ansible_runner/config/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@ class RunnerConfig(BaseConfig):
module_args: str | None = field(metadata={}, default=None)
omit_event_data: bool = field(metadata={}, default=False)
only_failed_event_data: bool = field(metadata={}, default=False)
playbook: str | None = field(metadata={}, default=None)
playbook: str | dict | list | None = field(metadata={}, default=None)
process_isolation_hide_paths: str | list | None = field(metadata={}, default=None)
process_isolation_ro_paths: str | list | None = field(metadata={}, default=None)
process_isolation_show_paths: str | list | None = field(metadata={}, default=None)
process_isolation_path: str | None = field(metadata={}, default=None)
role: str = ""
role_skip_facts: bool = False
roles_path: str | None = field(metadata={}, default=None)
role_vars: dict[str, str] | None = None
skip_tags: str | None = field(metadata={}, default=None)
suppress_ansible_output: bool = field(metadata={}, default=False)
suppress_output_file: bool = field(metadata={}, default=False)
Expand Down Expand Up @@ -121,6 +124,21 @@ def directory_isolation_path(self):
def directory_isolation_path(self, value):
self.directory_isolation_base_path = value

@property
def hosts(self):
"""
Alias for backward compatibility.
dump_artifacts() makes reference to 'hosts' kwargs (API) value, even though it
is undocumented as an API parameter to interface.run(). We make it equivalent
to 'host_pattern' here to not break anyone.
"""
return self.host_pattern

@hosts.setter
def hosts(self, value):
self.host_pattern = value

@property
def extra_vars(self):
""" Alias for backward compatibility. """
Expand Down
Loading

0 comments on commit 708feee

Please sign in to comment.