From 766267412978d6b4e22a5952f146bcd84e47656b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 10 Nov 2021 11:28:00 -0500 Subject: [PATCH 1/7] New interface methods for role list and role argspec --- ansible_runner/__init__.py | 1 + ansible_runner/config/doc.py | 25 +++++++++++++++++ ansible_runner/interface.py | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/ansible_runner/__init__.py b/ansible_runner/__init__.py index 44ccf1f6c..40a4be7b2 100644 --- a/ansible_runner/__init__.py +++ b/ansible_runner/__init__.py @@ -3,6 +3,7 @@ from .interface import run, run_async, \ run_command, run_command_async, \ get_plugin_docs, get_plugin_docs_async, get_plugin_list, \ + get_role_list, get_role_argspec, \ get_inventory, \ get_ansible_config # noqa from .exceptions import AnsibleRunnerException, ConfigurationError, CallbackError # noqa diff --git a/ansible_runner/config/doc.py b/ansible_runner/config/doc.py index 6612baf3c..bb7bf1214 100644 --- a/ansible_runner/config/doc.py +++ b/ansible_runner/config/doc.py @@ -121,3 +121,28 @@ def prepare_plugin_list_command(self, list_files=None, response_format=None, plu self.command = [self._ansible_doc_exec_path] + self.cmdline_args self._handle_command_wrap(self.execution_mode, self.cmdline_args) + + def prepare_role_list_command(self, collection_name): + ''' + ansible-doc -t role -l -j + ''' + self._prepare_env(runner_mode=self.runner_mode) + self.cmdline_args = ['-t', 'role', '-l', '-j'] + if collection_name: + self.cmdline_args.append(collection_name) + + self.command = [self._ansible_doc_exec_path] + self.cmdline_args + self._handle_command_wrap(self.execution_mode, self.cmdline_args) + + def prepare_role_argspec_command(self, role_name, collection_name): + ''' + ansible-doc -t role -j . + ''' + + if collection_name: + role_name = ".".join([collection_name, role_name]) + + self._prepare_env(runner_mode=self.runner_mode) + self.cmdline_args = ['-t', 'role', '-j', role_name] + self.command = [self._ansible_doc_exec_path] + self.cmdline_args + self._handle_command_wrap(self.execution_mode, self.cmdline_args) diff --git a/ansible_runner/interface.py b/ansible_runner/interface.py index 4c8714a47..344297ca3 100644 --- a/ansible_runner/interface.py +++ b/ansible_runner/interface.py @@ -897,3 +897,55 @@ def get_ansible_config(action, config_file=None, only_changed=None, **kwargs): response = r.stdout.read() error = r.stderr.read() return response, error + + +def get_role_list(collection=None, **kwargs): + ''' + Run an ``ansible-doc`` command to get list of installed collection roles. + ''' + event_callback_handler = kwargs.pop('event_handler', None) + status_callback_handler = kwargs.pop('status_handler', None) + artifacts_handler = kwargs.pop('artifacts_handler', None) + cancel_callback = kwargs.pop('cancel_callback', None) + finished_callback = kwargs.pop('finished_callback', None) + + rd = DocConfig(**kwargs) + rd.prepare_role_list_command(collection) + r = Runner(rd, + event_handler=event_callback_handler, + status_handler=status_callback_handler, + artifacts_handler=artifacts_handler, + cancel_callback=cancel_callback, + finished_callback=finished_callback) + r.run() + response = r.stdout.read() + error = r.stderr.read() + if response: + response = json.loads(santize_json_response(response)) + return response, error + + +def get_role_argspec(role, collection=None, **kwargs): + ''' + Run an ``ansible-doc`` command to get a collection role argument spec. + ''' + event_callback_handler = kwargs.pop('event_handler', None) + status_callback_handler = kwargs.pop('status_handler', None) + artifacts_handler = kwargs.pop('artifacts_handler', None) + cancel_callback = kwargs.pop('cancel_callback', None) + finished_callback = kwargs.pop('finished_callback', None) + + rd = DocConfig(**kwargs) + rd.prepare_role_argspec_command(role, collection) + r = Runner(rd, + event_handler=event_callback_handler, + status_handler=status_callback_handler, + artifacts_handler=artifacts_handler, + cancel_callback=cancel_callback, + finished_callback=finished_callback) + r.run() + response = r.stdout.read() + error = r.stderr.read() + if response: + response = json.loads(santize_json_response(response)) + return response, error From 72629e96977f214ba074e9c841b78a09e14f425a Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 16 Nov 2021 14:53:11 -0500 Subject: [PATCH 2/7] Add get_role_list tests, fix func name --- ansible_runner/config/doc.py | 4 +- ansible_runner/interface.py | 64 ++++++++++++++++--- ansible_runner/utils/__init__.py | 2 +- .../Into_The_Mystic/meta/argument_specs.yml | 17 +++++ test/integration/test_interface.py | 38 ++++++++++- 5 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 test/integration/fixtures/projects/music/project/roles/Into_The_Mystic/meta/argument_specs.yml diff --git a/ansible_runner/config/doc.py b/ansible_runner/config/doc.py index bb7bf1214..8c08c3f8a 100644 --- a/ansible_runner/config/doc.py +++ b/ansible_runner/config/doc.py @@ -122,12 +122,14 @@ def prepare_plugin_list_command(self, list_files=None, response_format=None, plu self.command = [self._ansible_doc_exec_path] + self.cmdline_args self._handle_command_wrap(self.execution_mode, self.cmdline_args) - def prepare_role_list_command(self, collection_name): + def prepare_role_list_command(self, collection_name, playbook_dir): ''' ansible-doc -t role -l -j ''' self._prepare_env(runner_mode=self.runner_mode) self.cmdline_args = ['-t', 'role', '-l', '-j'] + if playbook_dir: + self.cmdline_args.extend(['--playbook-dir', playbook_dir]) if collection_name: self.cmdline_args.append(collection_name) diff --git a/ansible_runner/interface.py b/ansible_runner/interface.py index 344297ca3..22f284862 100644 --- a/ansible_runner/interface.py +++ b/ansible_runner/interface.py @@ -34,7 +34,7 @@ from ansible_runner.utils import ( dump_artifacts, check_isolation_executable_installed, - santize_json_response, + sanitize_json_response, signal_handler, ) @@ -540,7 +540,7 @@ def get_plugin_docs(plugin_names, plugin_type=None, response_format=None, snippe response = r.stdout.read() error = r.stderr.read() if response and response_format == 'json': - response = json.loads(santize_json_response(response)) + response = json.loads(sanitize_json_response(response)) return response, error @@ -647,7 +647,7 @@ def get_plugin_list(list_files=None, response_format=None, plugin_type=None, pla :type check_job_event_data: bool :returns: Returns a tuple of response and error string. In case if ``runner_mode`` is set to ``pexpect`` the error value is empty as - ``pexpect`` uses same output descriptor for stdout and stderr. If the vaue of ``response_format`` is ``json`` + ``pexpect`` uses same output descriptor for stdout and stderr. If the value of ``response_format`` is ``json`` it returns a python dictionary object. ''' event_callback_handler = kwargs.pop('event_handler', None) @@ -669,7 +669,7 @@ def get_plugin_list(list_files=None, response_format=None, plugin_type=None, pla response = r.stdout.read() error = r.stderr.read() if response and response_format == 'json': - response = json.loads(santize_json_response(response)) + response = json.loads(sanitize_json_response(response)) return response, error @@ -792,7 +792,7 @@ def get_inventory(action, inventories, response_format=None, host=None, playbook response = r.stdout.read() error = r.stderr.read() if response and response_format == 'json': - response = json.loads(santize_json_response(response)) + response = json.loads(sanitize_json_response(response)) return response, error @@ -899,9 +899,55 @@ def get_ansible_config(action, config_file=None, only_changed=None, **kwargs): return response, error -def get_role_list(collection=None, **kwargs): +def get_role_list(collection=None, playbook_dir=None, **kwargs): ''' Run an ``ansible-doc`` command to get list of installed collection roles. + + :param str collection: A fully qualified collection name used to filter the results. + :param str playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed roles. + + :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param dict envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param dict passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param dict settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param str ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param bool quiet: Disable all output + :param bool json_mode: Store event data in place of stdout on the console and in the stdout file + :param str artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the execution. + :param bool process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param str container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param list container_options: List of container options to pass to execution engine. + :param str container_workdir: The working directory within the container. + :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param str fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param str private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param str ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param function event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param function cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param function finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param function status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param function artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, if set to 'False' it log a debug message and continue execution. + Default value is 'False' + + :returns: A tuple of response and error string. The response is a python dictionary object + (as returned by ansible-doc JSON output) containing each role found, or an empty dict + if none are found. ''' event_callback_handler = kwargs.pop('event_handler', None) status_callback_handler = kwargs.pop('status_handler', None) @@ -910,7 +956,7 @@ def get_role_list(collection=None, **kwargs): finished_callback = kwargs.pop('finished_callback', None) rd = DocConfig(**kwargs) - rd.prepare_role_list_command(collection) + rd.prepare_role_list_command(collection, playbook_dir) r = Runner(rd, event_handler=event_callback_handler, status_handler=status_callback_handler, @@ -921,7 +967,7 @@ def get_role_list(collection=None, **kwargs): response = r.stdout.read() error = r.stderr.read() if response: - response = json.loads(santize_json_response(response)) + response = json.loads(sanitize_json_response(response)) return response, error @@ -947,5 +993,5 @@ def get_role_argspec(role, collection=None, **kwargs): response = r.stdout.read() error = r.stderr.read() if response: - response = json.loads(santize_json_response(response)) + response = json.loads(sanitize_json_response(response)) return response, error diff --git a/ansible_runner/utils/__init__.py b/ansible_runner/utils/__init__.py index cde9525ed..79b87ae2d 100644 --- a/ansible_runner/utils/__init__.py +++ b/ansible_runner/utils/__init__.py @@ -454,7 +454,7 @@ def cli_mounts(): ] -def santize_json_response(data): +def sanitize_json_response(data): ''' Removes warning message from response message emitted by ansible command line utilities. diff --git a/test/integration/fixtures/projects/music/project/roles/Into_The_Mystic/meta/argument_specs.yml b/test/integration/fixtures/projects/music/project/roles/Into_The_Mystic/meta/argument_specs.yml new file mode 100644 index 000000000..5d6c0639f --- /dev/null +++ b/test/integration/fixtures/projects/music/project/roles/Into_The_Mystic/meta/argument_specs.yml @@ -0,0 +1,17 @@ +--- +argument_specs: + main: + short_description: The main entry point for the Into_The_Mystic role. + options: + foghorn: + type: "bool" + required: false + default: true + description: "If true, the foghorn blows." + soul: + type: "str" + required: true + description: "Type of soul to rock" + choices: + - "gypsy" + - "normal" diff --git a/test/integration/test_interface.py b/test/integration/test_interface.py index d1d5298c9..ced4156cb 100644 --- a/test/integration/test_interface.py +++ b/test/integration/test_interface.py @@ -3,7 +3,8 @@ from ansible_runner import defaults from ansible_runner.interface import run, run_async, run_command, run_command_async, get_plugin_docs, \ - get_plugin_docs_async, get_plugin_list, get_ansible_config, get_inventory + get_plugin_docs_async, get_plugin_list, get_ansible_config, get_inventory, \ + get_role_list def test_run(): @@ -355,3 +356,38 @@ def test_run_role(project_fixtures): stdout = res.stdout.read() assert res.rc == 0, stdout assert 'Hello World!' in stdout + + +def test_get_role_list(project_fixtures): + ''' + Test get_role_list() running locally, specifying a playbook directory + containing our test role. + ''' + use_role_example = str(project_fixtures / 'music' / 'project') + expected_role = { + "collection": "", + "entry_points": { + "main": "The main entry point for the Into_The_Mystic role." + } + } + + resp, err = get_role_list(playbook_dir=use_role_example) + assert isinstance(resp, dict) + assert 'Into_The_Mystic' in resp + assert resp['Into_The_Mystic'] == expected_role + + +@pytest.mark.test_all_runtimes +def test_get_role_list_within_container(project_fixtures, runtime): + ''' + Test get_role_list() running in a container. Because the test container + has no roles/collections installed, the returned output should be empty. + ''' + container_kwargs = { + 'process_isolation_executable': runtime, + 'process_isolation': True, + 'container_image': defaults.default_container_image + } + resp, err = get_role_list(**container_kwargs) + assert isinstance(resp, dict) + assert resp == {} From 1070c4cc195f13869ad7d5cd912e9fc316469d07 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 17 Nov 2021 11:17:19 -0500 Subject: [PATCH 3/7] Add fixture to test pre-2.11 ansible --- test/conftest.py | 27 +++++++++++++++++++++++++++ test/integration/test_interface.py | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 594c0d6ec..b31f92414 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -27,12 +27,39 @@ def is_pre_ansible28(): pass +@pytest.fixture(scope='session') +def is_pre_ansible211(): + """ + Check if the version of Ansible is less than 2.11. + + We test with either ansible-core (>=2.11), ansible-base (==2.10), and ansible (<=2.9). + Since either might be installed, based on the test environment, test in succession + until one is found. + """ + + try: + if pkg_resources.get_distribution('ansible-core').version: + return False + except pkg_resources.DistributionNotFound: + try: + if pkg_resources.get_distribution('ansible-base').version: + return True + except pkg_resources.DistributionNotFound: + return True + + @pytest.fixture(scope='session') def skipif_pre_ansible28(is_pre_ansible28): if is_pre_ansible28: pytest.skip("Valid only on Ansible 2.8+") +@pytest.fixture(scope='session') +def skipif_pre_ansible211(is_pre_ansible211): + if is_pre_ansible211: + pytest.skip("Valid only on Ansible 2.11+") + + # TODO: determine if we want to add docker / podman # to zuul instances in order to run these tests def pytest_generate_tests(metafunc): diff --git a/test/integration/test_interface.py b/test/integration/test_interface.py index ced4156cb..02025f3c8 100644 --- a/test/integration/test_interface.py +++ b/test/integration/test_interface.py @@ -358,7 +358,7 @@ def test_run_role(project_fixtures): assert 'Hello World!' in stdout -def test_get_role_list(project_fixtures): +def test_get_role_list(project_fixtures, skipif_pre_ansible211): ''' Test get_role_list() running locally, specifying a playbook directory containing our test role. @@ -378,7 +378,7 @@ def test_get_role_list(project_fixtures): @pytest.mark.test_all_runtimes -def test_get_role_list_within_container(project_fixtures, runtime): +def test_get_role_list_within_container(project_fixtures, runtime, skipif_pre_ansible211): ''' Test get_role_list() running in a container. Because the test container has no roles/collections installed, the returned output should be empty. From 289e6eb7ba5387327f5fdd57a0a901246290622e Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 17 Nov 2021 11:44:49 -0500 Subject: [PATCH 4/7] Add some docs --- ansible_runner/interface.py | 6 +++++- docs/python_interface.rst | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ansible_runner/interface.py b/ansible_runner/interface.py index 22f284862..6486087b4 100644 --- a/ansible_runner/interface.py +++ b/ansible_runner/interface.py @@ -903,6 +903,8 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): ''' Run an ``ansible-doc`` command to get list of installed collection roles. + .. note:: Version added: 2.2 + :param str collection: A fully qualified collection name used to filter the results. :param str playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed roles. @@ -973,7 +975,9 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): def get_role_argspec(role, collection=None, **kwargs): ''' - Run an ``ansible-doc`` command to get a collection role argument spec. + Run an ``ansible-doc`` command to get a collection role argument specification. + + .. note:: Version added: 2.2 ''' event_callback_handler = kwargs.pop('event_handler', None) status_callback_handler = kwargs.pop('status_handler', None) diff --git a/docs/python_interface.rst b/docs/python_interface.rst index 06719e0d0..fd909885b 100644 --- a/docs/python_interface.rst +++ b/docs/python_interface.rst @@ -99,6 +99,33 @@ and it can be customized to return only the changed configuration value by setti view of the active configuration file. The exectuin will be in the foreground and return a tuple of output and error response when finished. While running the command within the container the current local working diretory will be volume mounted within the container. +``get_role_list()`` helper function +----------------------------------- + +:meth:`ansible_runner.interface.get_role_list` + +*Version added: 2.2* + +This function will execute the ``ansible-doc`` command to return the list of installed roles. +This data can be fetched from either the local environment or from within a container image +based on the parameters passed. It will run in the foreground and return a tuple of output +and error response when finished. Successful output will be in JSON format as returned from +``ansible-doc``. + +``get_role_argspec()`` helper function +-------------------------------------- + +:meth:`ansible_runner.interface.get_role_argspec` + +*Version added: 2.2* + +This function will execute the ``ansible-doc`` command to return a role argument specification. +This data can be fetched from either the local environment or from within a container image +based on the parameters passed. It will run in the foreground and return a tuple of output +and error response when finished. Successful output will be in JSON format as returned from +``ansible-doc``. + + The ``Runner`` object --------------------- From 8c649549431a97a9726476257df08f0931e0a7cb Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Thu, 18 Nov 2021 15:46:28 -0500 Subject: [PATCH 5/7] Add test for get_role_argspec() --- ansible_runner/config/doc.py | 18 +++++----- ansible_runner/interface.py | 54 ++++++++++++++++++++++++++++-- test/conftest.py | 11 ++---- test/integration/test_interface.py | 45 ++++++++++++++++++++++--- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/ansible_runner/config/doc.py b/ansible_runner/config/doc.py index 8c08c3f8a..7fded7717 100644 --- a/ansible_runner/config/doc.py +++ b/ansible_runner/config/doc.py @@ -123,9 +123,9 @@ def prepare_plugin_list_command(self, list_files=None, response_format=None, plu self._handle_command_wrap(self.execution_mode, self.cmdline_args) def prepare_role_list_command(self, collection_name, playbook_dir): - ''' + """ ansible-doc -t role -l -j - ''' + """ self._prepare_env(runner_mode=self.runner_mode) self.cmdline_args = ['-t', 'role', '-l', '-j'] if playbook_dir: @@ -136,15 +136,17 @@ def prepare_role_list_command(self, collection_name, playbook_dir): self.command = [self._ansible_doc_exec_path] + self.cmdline_args self._handle_command_wrap(self.execution_mode, self.cmdline_args) - def prepare_role_argspec_command(self, role_name, collection_name): - ''' + def prepare_role_argspec_command(self, role_name, collection_name, playbook_dir): + """ ansible-doc -t role -j . - ''' - + """ + self._prepare_env(runner_mode=self.runner_mode) + self.cmdline_args = ['-t', 'role', '-j'] + if playbook_dir: + self.cmdline_args.extend(['--playbook-dir', playbook_dir]) if collection_name: role_name = ".".join([collection_name, role_name]) + self.cmdline_args.append(role_name) - self._prepare_env(runner_mode=self.runner_mode) - self.cmdline_args = ['-t', 'role', '-j', role_name] self.command = [self._ansible_doc_exec_path] + self.cmdline_args self._handle_command_wrap(self.execution_mode, self.cmdline_args) diff --git a/ansible_runner/interface.py b/ansible_runner/interface.py index 6486087b4..de909e1ca 100644 --- a/ansible_runner/interface.py +++ b/ansible_runner/interface.py @@ -973,11 +973,59 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): return response, error -def get_role_argspec(role, collection=None, **kwargs): +def get_role_argspec(role, collection=None, playbook_dir=None, **kwargs): ''' - Run an ``ansible-doc`` command to get a collection role argument specification. + Run an ``ansible-doc`` command to get a role argument specification. .. note:: Version added: 2.2 + + :param str role: Simple role name, or fully qualified collection role name, to query. + :param str collection: If specified, will be combined with the role name to form a fully qualified collection role name. + If this is supplied, the ``role`` param should not be fully qualified. + :param str playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed roles. + + :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. + :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be + the work directory within container. + :param dict envvars: Environment variables to be used when running Ansible. Environment variables will also be + read from ``env/envvars`` in ``private_data_dir`` + :param dict passwords: A dictionary containing password prompt patterns and response values used when processing output from + Ansible. Passwords will also be read from ``env/passwords`` in ``private_data_dir``. + :param dict settings: A dictionary containing settings values for the ``ansible-runner`` runtime environment. These will also + be read from ``env/settings`` in ``private_data_dir``. + :param str ssh_key: The ssh private key passed to ``ssh-agent`` as part of the ansible-playbook run. + :param bool quiet: Disable all output + :param bool json_mode: Store event data in place of stdout on the console and in the stdout file + :param str artifact_dir: The path to the directory where artifacts should live, this defaults to 'artifacts' under the private data dir + :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir + :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default + :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation + (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the execution. + :param bool process_isolation: Enable process isolation, using a container engine (e.g. podman). + :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) + :param str container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param list container_options: List of container options to pass to execution engine. + :param str container_workdir: The working directory within the container. + :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. + This is only used for 'jsonfile' type fact caches. + :param str fact_cache_type: A string of the type of fact cache to use. Defaults to 'jsonfile'. + :param str private_data_dir: The directory containing all runner metadata needed to invoke the runner + module. Output artifacts will also be stored here for later consumption. + :param str ident: The run identifier for this invocation of Runner. Will be used to create and name + the artifact directory holding the results of the invocation. + :param function event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event + :param function cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) + :param function finished_callback: An optional callback that will be invoked at shutdown after process cleanup. + :param function status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param function artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. + :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if + value is set to 'True' it will raise 'AnsibleRunnerException' exception, if set to 'False' it log a debug message and continue execution. + Default value is 'False' + + :returns: A tuple of response and error string. The response is a python dictionary object + (as returned by ansible-doc JSON output) containing each role found, or an empty dict + if none are found. ''' event_callback_handler = kwargs.pop('event_handler', None) status_callback_handler = kwargs.pop('status_handler', None) @@ -986,7 +1034,7 @@ def get_role_argspec(role, collection=None, **kwargs): finished_callback = kwargs.pop('finished_callback', None) rd = DocConfig(**kwargs) - rd.prepare_role_argspec_command(role, collection) + rd.prepare_role_argspec_command(role, collection, playbook_dir) r = Runner(rd, event_handler=event_callback_handler, status_handler=status_callback_handler, diff --git a/test/conftest.py b/test/conftest.py index b31f92414..6e361811b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -32,20 +32,15 @@ def is_pre_ansible211(): """ Check if the version of Ansible is less than 2.11. - We test with either ansible-core (>=2.11), ansible-base (==2.10), and ansible (<=2.9). - Since either might be installed, based on the test environment, test in succession - until one is found. + CI tests with either ansible-core (>=2.11), ansible-base (==2.10), and ansible (<=2.9). """ try: if pkg_resources.get_distribution('ansible-core').version: return False except pkg_resources.DistributionNotFound: - try: - if pkg_resources.get_distribution('ansible-base').version: - return True - except pkg_resources.DistributionNotFound: - return True + # Must be ansible-base or ansible + return True @pytest.fixture(scope='session') diff --git a/test/integration/test_interface.py b/test/integration/test_interface.py index 02025f3c8..b34a65081 100644 --- a/test/integration/test_interface.py +++ b/test/integration/test_interface.py @@ -4,7 +4,7 @@ from ansible_runner import defaults from ansible_runner.interface import run, run_async, run_command, run_command_async, get_plugin_docs, \ get_plugin_docs_async, get_plugin_list, get_ansible_config, get_inventory, \ - get_role_list + get_role_list, get_role_argspec def test_run(): @@ -359,10 +359,10 @@ def test_run_role(project_fixtures): def test_get_role_list(project_fixtures, skipif_pre_ansible211): - ''' + """ Test get_role_list() running locally, specifying a playbook directory containing our test role. - ''' + """ use_role_example = str(project_fixtures / 'music' / 'project') expected_role = { "collection": "", @@ -379,10 +379,10 @@ def test_get_role_list(project_fixtures, skipif_pre_ansible211): @pytest.mark.test_all_runtimes def test_get_role_list_within_container(project_fixtures, runtime, skipif_pre_ansible211): - ''' + """ Test get_role_list() running in a container. Because the test container has no roles/collections installed, the returned output should be empty. - ''' + """ container_kwargs = { 'process_isolation_executable': runtime, 'process_isolation': True, @@ -391,3 +391,38 @@ def test_get_role_list_within_container(project_fixtures, runtime, skipif_pre_an resp, err = get_role_list(**container_kwargs) assert isinstance(resp, dict) assert resp == {} + + +def test_get_role_argspec(project_fixtures, skipif_pre_ansible211): + """ + Test get_role_argspec() running locally, specifying a playbook directory + containing our test role. + """ + use_role_example = str(project_fixtures / 'music' / 'project') + expected_epoint = { + "main": { + "options": { + "foghorn": { + "default": True, + "description": "If true, the foghorn blows.", + "required": False, + "type": "bool" + }, + "soul": { + "choices": [ + "gypsy", + "normal" + ], + "description": "Type of soul to rock", + "required": True, + "type": "str" + } + }, + "short_description": "The main entry point for the Into_The_Mystic role." + } + } + + resp, err = get_role_argspec('Into_The_Mystic', playbook_dir=use_role_example) + assert isinstance(resp, dict) + assert 'Into_The_Mystic' in resp + assert resp['Into_The_Mystic']['entry_points'] == expected_epoint From 12ee3d8925d4857afa6320685a16a5e24428f004 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Fri, 19 Nov 2021 08:44:22 -0500 Subject: [PATCH 6/7] More tests and documentation --- ansible_runner/interface.py | 2 + docs/python_interface.rst | 38 ++++++++++++++--- test/integration/test_interface.py | 65 +++++++++++++++++++++++++++--- 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/ansible_runner/interface.py b/ansible_runner/interface.py index de909e1ca..90f7801f8 100644 --- a/ansible_runner/interface.py +++ b/ansible_runner/interface.py @@ -903,6 +903,8 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): ''' Run an ``ansible-doc`` command to get list of installed collection roles. + Only roles that have an argument specification defined are returned. + .. note:: Version added: 2.2 :param str collection: A fully qualified collection name used to filter the results. diff --git a/docs/python_interface.rst b/docs/python_interface.rst index fd909885b..a35a57609 100644 --- a/docs/python_interface.rst +++ b/docs/python_interface.rst @@ -106,11 +106,11 @@ While running the command within the container the current local working diretor *Version added: 2.2* -This function will execute the ``ansible-doc`` command to return the list of installed roles. -This data can be fetched from either the local environment or from within a container image -based on the parameters passed. It will run in the foreground and return a tuple of output -and error response when finished. Successful output will be in JSON format as returned from -``ansible-doc``. +This function will execute the ``ansible-doc`` command to return the list of installed roles +that have an argument specification defined. This data can be fetched from either the local +environment or from within a container image based on the parameters passed. It will run in +the foreground and return a tuple of output and error response when finished. Successful output +will be in JSON format as returned from ``ansible-doc``. ``get_role_argspec()`` helper function -------------------------------------- @@ -328,6 +328,34 @@ Usage examples print("out: {}".format(out)) print("err: {}".format(err)) +.. code-block:: python + + # get all roles with an arg spec installed locally + out, err = ansible_runner.get_role_list() + print("out: {}".format(out)) + print("err: {}".format(err)) + +.. code-block:: python + + # get roles with an arg spec from the `foo.bar` collection in a container + out, err = ansible_runner.get_role_list(collection='foo.bar', process_isolation=True, container_image='network-ee') + print("out: {}".format(out)) + print("err: {}".format(err)) + +.. code-block:: python + + # get the arg spec for role `baz` from the locally installed `foo.bar` collection + out, err = ansible_runner.get_role_argspec('baz', collection='foo.bar') + print("out: {}".format(out)) + print("err: {}".format(err)) + +.. code-block:: python + + # get the arg spec for role `baz` from the `foo.bar` collection installed in a container + out, err = ansible_runner.get_role_argspec('baz', collection='foo.bar', process_isolation=True, container_image='network-ee') + print("out: {}".format(out)) + print("err: {}".format(err)) + Providing custom behavior and inputs ------------------------------------ diff --git a/test/integration/test_interface.py b/test/integration/test_interface.py index b34a65081..73d60cdb4 100644 --- a/test/integration/test_interface.py +++ b/test/integration/test_interface.py @@ -363,7 +363,7 @@ def test_get_role_list(project_fixtures, skipif_pre_ansible211): Test get_role_list() running locally, specifying a playbook directory containing our test role. """ - use_role_example = str(project_fixtures / 'music' / 'project') + pdir = str(project_fixtures / 'music' / 'project') expected_role = { "collection": "", "entry_points": { @@ -371,8 +371,11 @@ def test_get_role_list(project_fixtures, skipif_pre_ansible211): } } - resp, err = get_role_list(playbook_dir=use_role_example) + resp, err = get_role_list(playbook_dir=pdir) assert isinstance(resp, dict) + + # So that tests can work locally, where multiple roles might be returned, + # we check for this single role. assert 'Into_The_Mystic' in resp assert resp['Into_The_Mystic'] == expected_role @@ -380,17 +383,25 @@ def test_get_role_list(project_fixtures, skipif_pre_ansible211): @pytest.mark.test_all_runtimes def test_get_role_list_within_container(project_fixtures, runtime, skipif_pre_ansible211): """ - Test get_role_list() running in a container. Because the test container - has no roles/collections installed, the returned output should be empty. + Test get_role_list() running in a container. """ + pdir = str(project_fixtures / 'music') + expected = { + "Into_The_Mystic": { + "collection": "", + "entry_points": { + "main": "The main entry point for the Into_The_Mystic role." + } + } + } container_kwargs = { 'process_isolation_executable': runtime, 'process_isolation': True, 'container_image': defaults.default_container_image } - resp, err = get_role_list(**container_kwargs) + resp, err = get_role_list(private_data_dir=pdir, playbook_dir="/runner/project", **container_kwargs) assert isinstance(resp, dict) - assert resp == {} + assert resp == expected def test_get_role_argspec(project_fixtures, skipif_pre_ansible211): @@ -426,3 +437,45 @@ def test_get_role_argspec(project_fixtures, skipif_pre_ansible211): assert isinstance(resp, dict) assert 'Into_The_Mystic' in resp assert resp['Into_The_Mystic']['entry_points'] == expected_epoint + + +@pytest.mark.test_all_runtimes +def test_get_role_argspec_within_container(project_fixtures, runtime, skipif_pre_ansible211): + """ + Test get_role_argspec() running inside a container. Since the test container + does not currently contain any collections or roles, specify playbook_dir + pointing to the project dir of private_data_dir so that we will find a role. + """ + pdir = str(project_fixtures / 'music') + expected_epoint = { + "main": { + "options": { + "foghorn": { + "default": True, + "description": "If true, the foghorn blows.", + "required": False, + "type": "bool" + }, + "soul": { + "choices": [ + "gypsy", + "normal" + ], + "description": "Type of soul to rock", + "required": True, + "type": "str" + } + }, + "short_description": "The main entry point for the Into_The_Mystic role." + } + } + + container_kwargs = { + 'process_isolation_executable': runtime, + 'process_isolation': True, + 'container_image': defaults.default_container_image + } + resp, err = get_role_argspec('Into_The_Mystic', private_data_dir=pdir, playbook_dir="/runner/project", **container_kwargs) + assert isinstance(resp, dict) + assert 'Into_The_Mystic' in resp + assert resp['Into_The_Mystic']['entry_points'] == expected_epoint From 3689a65341f39ecaf09e60fa32c4a2dab34b89c1 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 30 Nov 2021 10:03:17 -0500 Subject: [PATCH 7/7] More cleanup --- ansible_runner/interface.py | 34 ++++++++++++++++-------------- test/integration/test_interface.py | 16 +++++++++++--- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/ansible_runner/interface.py b/ansible_runner/interface.py index 90f7801f8..48e6ad2c7 100644 --- a/ansible_runner/interface.py +++ b/ansible_runner/interface.py @@ -908,7 +908,7 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): .. note:: Version added: 2.2 :param str collection: A fully qualified collection name used to filter the results. - :param str playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed roles. + :param str playbook_dir: This parameter is used to set the relative path to handle playbook adjacent installed roles. :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be @@ -926,11 +926,11 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation - (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the execution. - :param bool process_isolation: Enable process isolation, using a container engine (e.g. podman). + (based on ``runner_mode`` selected) while executing command. If the timeout is triggered, it will force cancel the execution. + :param bool process_isolation: Enable process isolation using a container engine, such as podman. :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) - :param str container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) - :param list container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param str container_image: Container image to use when running an Ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form ``host_dir:/container_dir:labels``. (default: None) :param list container_options: List of container options to pass to execution engine. :param str container_workdir: The working directory within the container. :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. @@ -943,13 +943,14 @@ def get_role_list(collection=None, playbook_dir=None, **kwargs): :param function event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event :param function cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) :param function finished_callback: An optional callback that will be invoked at shutdown after process cleanup. - :param function status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param function status_handler: An optional callback that will be invoked any time the status changes + (for example: started, running, failed, successful, timeout) :param function artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if - value is set to 'True' it will raise 'AnsibleRunnerException' exception, if set to 'False' it log a debug message and continue execution. + value is set to 'True' it will raise 'AnsibleRunnerException' exception. If set to 'False', log a debug message and continue execution. Default value is 'False' - :returns: A tuple of response and error string. The response is a python dictionary object + :returns: A tuple of response and error string. The response is a dictionary object (as returned by ansible-doc JSON output) containing each role found, or an empty dict if none are found. ''' @@ -984,7 +985,7 @@ def get_role_argspec(role, collection=None, playbook_dir=None, **kwargs): :param str role: Simple role name, or fully qualified collection role name, to query. :param str collection: If specified, will be combined with the role name to form a fully qualified collection role name. If this is supplied, the ``role`` param should not be fully qualified. - :param str playbook_dir: This parameter is used to sets the relative path to handle playbook adjacent installed roles. + :param str playbook_dir: This parameter is used to set the relative path to handle playbook adjacent installed roles. :param str runner_mode: The applicable values are ``pexpect`` and ``subprocess``. Default is set to ``subprocess``. :param str host_cwd: The host current working directory to be mounted within the container (if enabled) and will be @@ -1002,11 +1003,11 @@ def get_role_argspec(role, collection=None, playbook_dir=None, **kwargs): :param str project_dir: The path to the playbook content, this defaults to 'project' within the private data dir :param int rotate_artifacts: Keep at most n artifact directories, disable with a value of 0 which is the default :param int timeout: The timeout value in seconds that will be passed to either ``pexpect`` of ``subprocess`` invocation - (based on ``runner_mode`` selected) while executing command. It the timeout is triggered it will force cancel the execution. - :param bool process_isolation: Enable process isolation, using a container engine (e.g. podman). + (based on ``runner_mode`` selected) while executing command. If the timeout is triggered, it will force cancel the execution. + :param bool process_isolation: Enable process isolation using a container engine, such as podman. :param str process_isolation_executable: Process isolation executable or container engine used to isolate execution. (default: podman) - :param str container_image: Container image to use when running an ansible task (default: quay.io/ansible/ansible-runner:devel) - :param list container_volume_mounts: List of bind mounts in the form 'host_dir:/container_dir:labels. (default: None) + :param str container_image: Container image to use when running an Ansible task (default: quay.io/ansible/ansible-runner:devel) + :param list container_volume_mounts: List of bind mounts in the form ``host_dir:/container_dir:labels``. (default: None) :param list container_options: List of container options to pass to execution engine. :param str container_workdir: The working directory within the container. :param str fact_cache: A string that will be used as the name for the subdirectory of the fact cache in artifacts directory. @@ -1019,13 +1020,14 @@ def get_role_argspec(role, collection=None, playbook_dir=None, **kwargs): :param function event_handler: An optional callback that will be invoked any time an event is received by Runner itself, return True to keep the event :param function cancel_callback: An optional callback that can inform runner to cancel (returning True) or not (returning False) :param function finished_callback: An optional callback that will be invoked at shutdown after process cleanup. - :param function status_handler: An optional callback that will be invoked any time the status changes (e.g...started, running, failed, successful, timeout) + :param function status_handler: An optional callback that will be invoked any time the status changes + (for example: started, running, failed, successful, timeout) :param function artifacts_handler: An optional callback that will be invoked at the end of the run to deal with the artifacts from the run. :param bool check_job_event_data: Check if job events data is completely generated. If event data is not completely generated and if - value is set to 'True' it will raise 'AnsibleRunnerException' exception, if set to 'False' it log a debug message and continue execution. + value is set to 'True' it will raise 'AnsibleRunnerException' exception. If set to 'False', log a debug message and continue execution. Default value is 'False' - :returns: A tuple of response and error string. The response is a python dictionary object + :returns: A tuple of response and error string. The response is a dictionary object (as returned by ansible-doc JSON output) containing each role found, or an empty dict if none are found. ''' diff --git a/test/integration/test_interface.py b/test/integration/test_interface.py index 73d60cdb4..36dd22e70 100644 --- a/test/integration/test_interface.py +++ b/test/integration/test_interface.py @@ -2,9 +2,19 @@ import pytest from ansible_runner import defaults -from ansible_runner.interface import run, run_async, run_command, run_command_async, get_plugin_docs, \ - get_plugin_docs_async, get_plugin_list, get_ansible_config, get_inventory, \ - get_role_list, get_role_argspec +from ansible_runner.interface import ( + get_ansible_config, + get_inventory, + get_plugin_docs, + get_plugin_docs_async, + get_plugin_list, + get_role_argspec, + get_role_list, + run, + run_async, + run_command, + run_command_async, +) def test_run():