summaryrefslogtreecommitdiffstats
path: root/ansible_collections/cloud
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/cloud')
-rw-r--r--ansible_collections/cloud/common/CHANGELOG.rst135
-rw-r--r--ansible_collections/cloud/common/FILES.json467
-rw-r--r--ansible_collections/cloud/common/LICENSE674
-rw-r--r--ansible_collections/cloud/common/MANIFEST.json31
-rw-r--r--ansible_collections/cloud/common/README.md202
-rw-r--r--ansible_collections/cloud/common/changelogs/changelog.yaml145
-rw-r--r--ansible_collections/cloud/common/changelogs/config.yaml29
-rw-r--r--ansible_collections/cloud/common/changelogs/fragments/.keep0
-rw-r--r--ansible_collections/cloud/common/meta/runtime.yml2
-rw-r--r--ansible_collections/cloud/common/plugins/lookup/turbo_demo.py69
-rw-r--r--ansible_collections/cloud/common/plugins/module_utils/turbo/common.py125
-rw-r--r--ansible_collections/cloud/common/plugins/module_utils/turbo/exceptions.py65
-rw-r--r--ansible_collections/cloud/common/plugins/module_utils/turbo/module.py169
-rw-r--r--ansible_collections/cloud/common/plugins/module_utils/turbo/server.py396
-rw-r--r--ansible_collections/cloud/common/plugins/module_utils/turbo_demo.py1
-rw-r--r--ansible_collections/cloud/common/plugins/modules/turbo_demo.py74
-rw-r--r--ansible_collections/cloud/common/plugins/modules/turbo_fail.py59
-rw-r--r--ansible_collections/cloud/common/plugins/modules/turbo_import.py46
-rw-r--r--ansible_collections/cloud/common/plugins/plugin_utils/turbo/lookup.py90
-rw-r--r--ansible_collections/cloud/common/requirements.txt0
-rw-r--r--ansible_collections/cloud/common/test-requirements.txt2
-rw-r--r--ansible_collections/cloud/common/tests/config.yml2
-rw-r--r--ansible_collections/cloud/common/tests/integration/targets/turbo_fail/tasks/main.yaml24
-rw-r--r--ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/playbook.yaml81
-rwxr-xr-xansible_collections/cloud/common/tests/integration/targets/turbo_lookup/runme.sh3
-rw-r--r--ansible_collections/cloud/common/tests/integration/targets/turbo_mode/playbook.yaml5
-rwxr-xr-xansible_collections/cloud/common/tests/integration/targets/turbo_mode/runme.sh4
-rw-r--r--ansible_collections/cloud/common/tests/integration/targets/turbo_mode/tasks/main.yaml77
-rw-r--r--ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/inventory50
-rw-r--r--ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/playbook.yaml33
-rwxr-xr-xansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/runme.sh4
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.10.txt76
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.11.txt84
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.12.txt1
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.13.txt1
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.14.txt1
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.15.txt1
-rw-r--r--ansible_collections/cloud/common/tests/sanity/ignore-2.9.txt76
-rw-r--r--ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/conftest.py20
-rw-r--r--ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_module.py59
-rw-r--r--ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_turbo_module.py140
-rw-r--r--ansible_collections/cloud/common/tests/unit/requirements.txt4
-rw-r--r--ansible_collections/cloud/common/tox.ini39
43 files changed, 3566 insertions, 0 deletions
diff --git a/ansible_collections/cloud/common/CHANGELOG.rst b/ansible_collections/cloud/common/CHANGELOG.rst
new file mode 100644
index 000000000..af0074c06
--- /dev/null
+++ b/ansible_collections/cloud/common/CHANGELOG.rst
@@ -0,0 +1,135 @@
+==========================
+cloud.common Release Notes
+==========================
+
+.. contents:: Topics
+
+
+v2.1.3
+======
+
+Minor Changes
+-------------
+
+- sanity - fix sanity errors (https://github.com/ansible-collections/cloud.common/issues/106)
+- units - ensure tests/units follow the Ansible-defined unit tests structure (https://github.com/ansible-collections/cloud.common/issues/89)
+
+Bugfixes
+--------
+
+- module_utils/turbo/server - import needed library into the right place (https://github.com/ansible-collections/cloud.common/pull/120)
+
+v2.1.2
+======
+
+Bugfixes
+--------
+
+- Ensure we don't shutdown the server when we've still got some ongoing tasks (https://github.com/ansible-collections/cloud.common/pull/109).
+
+v2.1.1
+======
+
+Minor Changes
+-------------
+
+- Move the content of README_ansible_turbo.module.rst in the main README.md to get visibility on Ansible Galaxy.
+
+Bugfixes
+--------
+
+- fix parameters with aliases not being passed through (https://github.com/ansible-collections/cloud.common/issues/91).
+- fix turbo mode loading incorrect module (https://github.com/ansible-collections/cloud.common/pull/102).
+- turbo - Ensure we don't call the module with duplicated aliased parameters.
+
+v2.1.0
+======
+
+Minor Changes
+-------------
+
+- Cosmetic changes in the documentation for the inclusion in the Ansible collection.
+- turbo - Extend the unit-test coverage.
+- turbo - Use a BSD license for the module_utils and plugin_utils files.
+- turbo - add support for coroutine for lookup plugins (https://github.com/ansible-collections/cloud.common/pull/75).
+
+v2.0.4
+======
+
+Major Changes
+-------------
+
+- turbo - enable turbo mode for lookup plugins
+
+Bugfixes
+--------
+
+- add exception handler to main async loop (https://github.com/ansible-collections/cloud.common/pull/67).
+- pass current task's environment through to execution (https://github.com/ansible-collections/cloud.common/pull/69).
+- turbo - AnsibleTurboModule was missing some _ansible_facts variable like _diff, _ansible_tmpdir. (https://github.com/ansible-collections/cloud.common/issues/65)
+- turbo - honor the ``remote_tmp`` configuration key.
+
+v2.0.3
+======
+
+Bugfixes
+--------
+
+- Introduces a fix for the future Python 3.10 (#53)
+- turbo - make sure socket doesn't close prematurely, preventing issues with large amounts of data passed as module parameters (https://github.com/ansible-collections/cloud.common/issues/61)
+
+v2.0.2
+======
+
+Bugfixes
+--------
+
+- Introduces a fix for the future Python 3.10 (#53)
+- fail_json method should honor kwargs now when running embedded in server.
+
+v2.0.1
+======
+
+Bugfixes
+--------
+
+- The profiler is now properly initialized.
+- Use the argument_spec values to determine which option should actually be used.
+- fix exception messages containing extra single quotes (https://github.com/ansible-collections/cloud.common/pull/46).
+
+v2.0.0
+======
+
+Minor Changes
+-------------
+
+- The ``EmbeddedModuleFailure`` and ``EmbeddedModuleUnexpectedFailure`` exceptions now handle the ``__repr__`` and ``__str__`` method. This means Python is able to print a meaningful output.
+- The modules must now set the ``collection_name`` of the ``AnsibleTurboModule`` class. The content of this attribute is used to build the path of the UNIX socket.
+- When the background service is started in a console without the ``--daemon`` flag, it now prints information what it runs.
+- ``argument_spec`` is now evaluated server-side.
+- fail_json now accept and collect extra named arguments.
+- raise an exception if the output of module execution cannot be parsed.
+- the ``turbo_demo`` module now return the value of counter.
+- the user get an error now an error if a module don't raise ``exit_json()`` or ``fail_json()``.
+
+Bugfixes
+--------
+
+- the debug mode now work as expected. The ``_ansible_*`` variables are properly passed to the module.
+
+v1.1.0
+======
+
+Minor Changes
+-------------
+
+- ansible_module.turbo - the cache is now associated with the collection, if two collections use a cache, two background services will be started.
+
+Bugfixes
+--------
+
+- Ensure the background service starts properly on MacOS (https://github.com/ansible-collections/cloud.common/pull/16)
+- do not silently skip parameters when the value is ``False``
+
+v1.0.2
+======
diff --git a/ansible_collections/cloud/common/FILES.json b/ansible_collections/cloud/common/FILES.json
new file mode 100644
index 000000000..21e0a535e
--- /dev/null
+++ b/ansible_collections/cloud/common/FILES.json
@@ -0,0 +1,467 @@
+{
+ "files": [
+ {
+ "name": ".",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs/fragments/.keep",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "changelogs/changelog.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1227088c4dfdbd9290835fcb4dbc00bb0376c326e8c9d8fbc38f87ca4e881fd5",
+ "format": 1
+ },
+ {
+ "name": "changelogs/config.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8ed2dc9b735615a898401d871695d090d74624a617d80f7bde0b59abd79fae6e",
+ "format": 1
+ },
+ {
+ "name": "meta",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "meta/runtime.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "df18179bb2f5447a56ac92261a911649b96821c0b2c08eea62d5cc6b0195203f",
+ "format": 1
+ },
+ {
+ "name": "plugins",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/lookup",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/lookup/turbo_demo.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "36a1cac200d74a970d78687b8ce86c3e18c201f6bb4d9da7d1c4b5448f822eda",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/turbo",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/turbo/common.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0cb8fa84023f04361a4e2c4fc87ab381e5b238772ab34ec0b258a0fcc2868e14",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/turbo/exceptions.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "10e897e6a947d1d9971b1a0e10694caeba7f6c52af69e5e8d7f32d4648362185",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/turbo/module.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4489bf197e29debb7b46d3ebbd306e5feec859a94f7f18ff87f73b15fe562801",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/turbo/server.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2ce2d1796d3e23eacbc70a25b0decdbbb16132535b322cfba8d8c3af2224bb3c",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/turbo_demo.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7401a0c8d754a4f09267b95ba7a898b77c2b5b2422341966d83ec81fe406e1bb",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/turbo_demo.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8c9439d9544ca0b74e0800c257f3472f59e85f65f0fd55a2b0d8c8ee82e674fd",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/turbo_fail.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0fadd01d905bf6bf22d8d8e59e66f30302afb419af84fed0b385b479da5a2db8",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/turbo_import.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a68d01cddbbe3254294930452900cc648ce1cdc0d75d6fde1f57f718e0d8e8ac",
+ "format": 1
+ },
+ {
+ "name": "plugins/plugin_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/plugin_utils/turbo",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/plugin_utils/turbo/lookup.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d2665aa7917683e56d9bbc381cc4f1a72d64b28918606ca410a77d0d59d31fc6",
+ "format": 1
+ },
+ {
+ "name": "tests",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_fail",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_fail/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_fail/tasks/main.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2ed77ad416a544c74f1109f5fbb190de18ffb324cd10659d529f88d816a49bd5",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_lookup",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_lookup/playbook.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9d726f50446ac7219b0d1a9343dee16fd9a39f81292f1735ba711a7b82740046",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_lookup/runme.sh",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "74c081ba1aa13beb5aa8e4a29a60967eeaa5fdaff101ca722614e45c7d2d24c0",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode/tasks/main.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b4b6c68ae9c6513cc6a7c9020a981d74f6e025ee6fa62be484db39b628defdef",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode/playbook.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "80493725349cabb96ef26aaf52c9da3f3eafae1977a85cbeac4e272e3868e69d",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode/runme.sh",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "16f894cf82c43e1a5a569d3f80c8f3a6db2bd27aa29144be3c1f992c1d771d2a",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode_parallel_exec",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode_parallel_exec/inventory",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ecae869ac5b55848215776b0da1fc1b5e659116c008e3c0091f2d7917f2d22db",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode_parallel_exec/playbook.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "16e8c8f7d0c12baadd2c1415a46f81f340713bf9e4ebe078bccc70d32b34a1ae",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/turbo_mode_parallel_exec/runme.sh",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "32f07f09fdc38e292cbd2f1919b06fe0c1de8cd49beded2b1484387b03b774cf",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.10.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2ba79e49dde0ab4db7726d03ab0350834d8a2130ce3880cc9e90788a03e633f3",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.11.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "811e9919d1846ef277f0d158c1d87640854725ed73b8939d7d7f71c7ce19ce6e",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.12.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6ade72ead3ea9bfa27bc079c9f4f9dddc8fc29e3e5ae5cabb2e2e7666317485e",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.13.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6ade72ead3ea9bfa27bc079c9f4f9dddc8fc29e3e5ae5cabb2e2e7666317485e",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.14.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6ade72ead3ea9bfa27bc079c9f4f9dddc8fc29e3e5ae5cabb2e2e7666317485e",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.15.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6ade72ead3ea9bfa27bc079c9f4f9dddc8fc29e3e5ae5cabb2e2e7666317485e",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.9.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2ba79e49dde0ab4db7726d03ab0350834d8a2130ce3880cc9e90788a03e633f3",
+ "format": 1
+ },
+ {
+ "name": "tests/unit",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/module_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/module_utils/turbo",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/module_utils/turbo/conftest.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "99a3352cf6215e0a3d767c048fbfefa66aec5cead1330b0b029975e019faeb8c",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/module_utils/turbo/test_module.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5b7ed86bd31bd4bf172cf9e8b6d1c8715bd0d6128713e6b59418b55ad33ad417",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/plugins/module_utils/turbo/test_turbo_module.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4d3b03bc47f212e818291b4eff7e925bd296ebb0028134856a33c0570e532f5f",
+ "format": 1
+ },
+ {
+ "name": "tests/unit/requirements.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4956e1dbd41900d08246d8d019954340c669abe02f1f2680f435c63a1534d5a6",
+ "format": 1
+ },
+ {
+ "name": "tests/config.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "232da28b965d5e057d65173ffa9e4f3d63c87b537001450bcb033199309d16ae",
+ "format": 1
+ },
+ {
+ "name": "CHANGELOG.rst",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f07881d2e19c49339cd8dbc8e3db2b67f3643b76537763b2d4b30cea5997376e",
+ "format": 1
+ },
+ {
+ "name": "LICENSE",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986",
+ "format": 1
+ },
+ {
+ "name": "README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7383f887e938aecbb1f8e56a2a66339d217372f54caee19dbddafe6e2479a97b",
+ "format": 1
+ },
+ {
+ "name": "requirements.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "format": 1
+ },
+ {
+ "name": "test-requirements.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f2d896f892cafcbf0b9abb0739c94ec8c4e292b56525a9a9aef688bf5b02c096",
+ "format": 1
+ },
+ {
+ "name": "tox.ini",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e318271cca12b953614d48cd35f887f92fb7463fdeb0a143c445cc34948246f0",
+ "format": 1
+ }
+ ],
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/cloud/common/LICENSE b/ansible_collections/cloud/common/LICENSE
new file mode 100644
index 000000000..f288702d2
--- /dev/null
+++ b/ansible_collections/cloud/common/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/ansible_collections/cloud/common/MANIFEST.json b/ansible_collections/cloud/common/MANIFEST.json
new file mode 100644
index 000000000..143d87920
--- /dev/null
+++ b/ansible_collections/cloud/common/MANIFEST.json
@@ -0,0 +1,31 @@
+{
+ "collection_info": {
+ "namespace": "cloud",
+ "name": "common",
+ "version": "2.1.3",
+ "authors": [
+ "Ansible (https://github.com/ansible)"
+ ],
+ "readme": "README.md",
+ "tags": [
+ "cloud",
+ "virtualization"
+ ],
+ "description": "Set of common files for the cloud collections",
+ "license": [],
+ "license_file": "LICENSE",
+ "dependencies": {},
+ "repository": "https://github.com/ansible-collections/cloud.common",
+ "documentation": null,
+ "homepage": "https://github.com/ansible-collections/cloud.common",
+ "issues": "https://github.com/ansible-collections/cloud.common/issues"
+ },
+ "file_manifest_file": {
+ "name": "FILES.json",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "70df134410b54370a437423087e330ca15f24535beab03dd8e9f89acb7f944ae",
+ "format": 1
+ },
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/cloud/common/README.md b/ansible_collections/cloud/common/README.md
new file mode 100644
index 000000000..460a90612
--- /dev/null
+++ b/ansible_collections/cloud/common/README.md
@@ -0,0 +1,202 @@
+# cloud.common
+
+This collection is a library for the cloud modules. It's the home of the following component:
+
+- ansible_turbo.module: a cache sharing solution to speed up Ansible modules
+
+More content may be included later.
+
+# Requirements
+
+- ansible_turbo.module requires Python 3.6 and Ansible 2.9 or greater.
+
+## Ansible Turbo Module
+
+### Current situation
+
+The traditional execution flow of an Ansible module includes
+the following steps:
+
+- Upload of a ZIP archive with the module and its dependencies
+- Execution of the module, which is just a Python script
+- Ansible collects the results once the script is finished
+
+These steps happen for each task of a playbook, and on every host.
+
+Most of the time, the execution of a module is fast enough for
+the user. However, sometime the module requires an important
+amount of time, just to initialize itself. This is a common
+situation with the API based modules. A classic initialization
+involves the following steps:
+
+- Load a Python library to access the remote resource (via SDK)
+- Open a client
+ - Load a bunch of Python modules.
+ - Request a new TCP connection.
+ - Create a session.
+ - Authenticate the client.
+
+All these steps are time consuming and the same operations
+will be running again and again.
+
+For instance, here:
+
+- `import openstack`: takes 0.569s
+- `client = openstack.connect()`: takes 0.065s
+- `client.authorize()`: takes 1.360s
+
+These numbers are from test ran against VexxHost public cloud.
+
+In this case, it's a 2s-ish overhead per task. If the playbook
+comes with 10 tasks, the execution time cannot go below 20s.
+
+### How Ansible Turbo Module improve the situation
+
+`AnsibleTurboModule` is actually a class that inherites from
+the standard `AnsibleModule` class that your modules probably
+already use.
+The big difference is that when an module starts, it also spawns
+a little Python daemon. If a daemon already exists, it will just
+reuse it.
+All the module logic is run inside this Python daemon. This means:
+
+- Python modules are actually loaded one time
+- Ansible module can reuse an existing authenticated session.
+
+### How can I enable `AnsibleTurboModule`?
+
+If you are a collection maintainer and want to enable `AnsibleTurboModule`, you can
+follow these steps.
+Your module should inherit from `AnsibleTurboModule`, instead of `AnsibleModule`.
+
+```python
+
+ from ansible_module.turbo.module import AnsibleTurboModule as AnsibleModule
+
+```
+
+You can also use the `functools.lru_cache()` decorator to ask Python to cache
+the result of an operation, like a network session creation.
+
+Finally, if some of the dependeded libraries are large, it may be nice
+to defer your module imports, and do the loading AFTER the
+`AnsibleTurboModule` instance creation.
+
+### Example
+
+The Ansible module is slightly different while using AnsibleTurboModule.
+Here are some examples with OpenStack and VMware.
+
+These examples use `functools.lru_cache` that is the Python core since 3.3.
+`lru_cache()` decorator will managed the cache. It uses the function parameters
+as unicity criteria.
+
+- Integration with OpenStack Collection: https://github.com/goneri/ansible-collections-openstack/commit/53ce9860bb84eeab49a46f7a30e3c9588d53e367
+- Integration with VMware Collection: https://github.com/goneri/vmware/commit/d1c02b93cbf899fde3a4665e6bcb4d7531f683a3
+- Integration with Kubernetes Collection: https://github.com/ansible-collections/kubernetes.core/pull/68
+
+### Demo
+
+In this demo, we run one playbook that do several `os_keypair`
+calls. For the first time, we run the regular Ansible module.
+The second time, we run the same playbook, but with the modified
+version.
+
+
+[![asciicast](https://asciinema.org/a/329481.png)](https://asciinema.org/a/329481)
+
+
+### The background service
+
+The daemon kills itself after 15s, and communication are done
+through an Unix socket.
+It runs in one single process and uses `asyncio` internally.
+Consequently you can use the `async` keyword in your Ansible module.
+This will be handy if you interact with a lot of remote systems
+at the same time.
+
+### Security impact
+
+`ansible_module.turbo` open an Unix socket to interact with the background service.
+We use this service to open the connection toward the different target systems.
+
+This is similar to what SSH does with the sockets.
+
+Keep in mind that:
+
+- All the modules can access the same cache. Soon an isolation will be done at the collection level (https://github.com/ansible-collections/cloud.common/pull/17)
+- A task can loaded a different version of a library and impact the next tasks.
+- If the same user runs two `ansible-playbook` at the same time, they will have access to the same cache.
+
+When a module stores a session in a cache, it's a good idea to use a hash of the authentication information to identify the session.
+
+.. note:: You may want to isolate your Ansible environemt in a container, in this case you can consider https://github.com/ansible/ansible-builder
+
+### Error management
+
+`ansible_module.turbo` uses exception to communicate a result back to the module.
+
+- `EmbeddedModuleFailure` is raised when `json_fail()` is called.
+- `EmbeddedModuleSuccess` is raised in case of success and return the result to the origin module processthe origin.
+
+Thse exceptions are defined in `ansible_collections.cloud.common.plugins.module_utils.turbo.exceptions`.
+You can raise `EmbeddedModuleFailure` exception yourself, for instance from a module in `module_utils`.
+
+Be careful with the catch all exception (`except Exception:`). Not only they are bad practice, but also may interface with this mechanism.
+
+### Troubleshooting
+
+You may want to manually start the server. This can be done with the following command:
+
+.. code-block:: shell
+
+ PYTHONPATH=$HOME/.ansible/collections python -m ansible_collections.cloud.common.plugins.module_utils.turbo.server --socket-path $HOME/.ansible/tmp/turbo_mode.foo.bar.socket
+
+Replace `foo.bar` with the name of the collection.
+
+You can use the `--help` argument to get a list of the optional parameters.
+
+
+## More information
+
+<!-- List out where the user can find additional information, such as working group meeting times, slack/IRC channels, or documentation for the product this collection automates. At a minimum, link to: -->
+
+- [Ansible Collection overview](https://github.com/ansible-collections/overview)
+- [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html)
+- [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html)
+- [Ansible Collections Checklist](https://github.com/ansible-collections/overview/blob/master/collection_requirements.rst)
+- [The Bullhorn (the Ansible Contributor newsletter)](https://us19.campaign-archive.com/home/?u=56d874e027110e35dea0e03c1&id=d6635f5420)
+- [Changes impacting Contributors](https://github.com/ansible-collections/overview/issues/45)
+
+
+## Release notes
+
+See [CHANGELOG.rst](https://github.com/ansible-collections/cloud.common/blob/main/CHANGELOG.rst).
+
+## Releasing, Versioning and Deprecation
+
+This collection follows [Semantic Versioning](https://semver.org/). More details on versioning can be found [in the Ansible docs](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html#collection-versions).
+
+We plan to regularly release new minor or bugfix versions once new features or bugfixes have been implemented.
+
+Releasing happens by tagging the `main` branch.
+
+## Contributing to this collection
+
+We welcome community contributions to this collection. If you find problems, please open an issue or create a PR against the [Cloud.Common collection repository](https://github.com/ansible-collections/cloud.common).
+
+## Code of Conduct
+
+We follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our interactions within this project.
+
+If you encounter abusive behavior violating the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html), please refer to the [policy violations](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html#policy-violations) section of the Code of Conduct for information on how to raise a complaint.
+
+## Licensing
+
+<!-- Include the appropriate license information here and a pointer to the full licensing details. If the collection contains modules migrated from the ansible/ansible repo, you must use the same license that existed in the ansible/ansible repo. See the GNU license example below. -->
+
+GNU General Public License v3.0 or later.
+
+See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text.
+
+The files in plugins/module_utils and plugins/plugin_utils directories are also licensed with a BSD license.
diff --git a/ansible_collections/cloud/common/changelogs/changelog.yaml b/ansible_collections/cloud/common/changelogs/changelog.yaml
new file mode 100644
index 000000000..09afb69e8
--- /dev/null
+++ b/ansible_collections/cloud/common/changelogs/changelog.yaml
@@ -0,0 +1,145 @@
+ancestor: null
+releases:
+ 1.0.2:
+ release_date: '2020-10-06'
+ 1.1.0:
+ changes:
+ bugfixes:
+ - Ensure the background service starts properly on MacOS (https://github.com/ansible-collections/cloud.common/pull/16)
+ - do not silently skip parameters when the value is ``False``
+ minor_changes:
+ - ansible_module.turbo - the cache is now associated with the collection, if
+ two collections use a cache, two background services will be started.
+ fragments:
+ - dont_ignore_parameters_when_val_is_False.yaml
+ - macos_fix.yaml
+ - one_cache_per_collection.yaml
+ release_date: '2020-10-23'
+ 2.0.0:
+ changes:
+ bugfixes:
+ - the debug mode now work as expected. The ``_ansible_*`` variables are properly
+ passed to the module.
+ minor_changes:
+ - The ``EmbeddedModuleFailure`` and ``EmbeddedModuleUnexpectedFailure`` exceptions
+ now handle the ``__repr__`` and ``__str__`` method. This means Python is able
+ to print a meaningful output.
+ - The modules must now set the ``collection_name`` of the ``AnsibleTurboModule``
+ class. The content of this attribute is used to build the path of the UNIX
+ socket.
+ - When the background service is started in a console without the ``--daemon``
+ flag, it now prints information what it runs.
+ - '``argument_spec`` is now evaluated server-side.'
+ - fail_json now accept and collect extra named arguments.
+ - raise an exception if the output of module execution cannot be parsed.
+ - the ``turbo_demo`` module now return the value of counter.
+ - the user get an error now an error if a module don't raise ``exit_json()``
+ or ``fail_json()``.
+ fragments:
+ - Set-the-_ansible_-variables_13334.yaml
+ - argument_spec_server_side.yaml
+ - exception_returns_a_printable_content.yaml
+ - fail_json_accept_extra_kwargs.yaml
+ - fork_mode_print_information_about_the_module.yaml
+ - get_the_collection_name_from_an_attribute.yaml
+ - improve-the-demo-module_5397.yaml
+ - raise-an-error-if-exit_json-or-fail_json-not-called_13453.yaml
+ - raise_exception_if_output_parsing_fails.yaml
+ release_date: '2021-04-20'
+ 2.0.1:
+ changes:
+ bugfixes:
+ - The profiler is now properly initialized.
+ - Use the argument_spec values to determine which option should actually be
+ used.
+ - fix exception messages containing extra single quotes (https://github.com/ansible-collections/cloud.common/pull/46).
+ fragments:
+ - 46-fix-error-message-string.yaml
+ - actually_enable_the_profiler.yaml
+ - filter_argument_with_argument_spec.yaml
+ release_date: '2021-04-22'
+ 2.0.2:
+ changes:
+ bugfixes:
+ - Introduces a fix for the future Python 3.10 (#53)
+ - fail_json method should honor kwargs now when running embedded in server.
+ fragments:
+ - modulefailure_fix_result.yaml
+ - py3.10-fix.yaml
+ release_date: '2021-06-02'
+ 2.0.3:
+ changes:
+ bugfixes:
+ - Introduces a fix for the future Python 3.10 (#53)
+ - turbo - make sure socket doesn't close prematurely, preventing issues with
+ large amounts of data passed as module parameters (https://github.com/ansible-collections/cloud.common/issues/61)
+ fragments:
+ - py3.10-fix.yaml
+ - socket-closure-fix.yaml
+ release_date: '2021-06-22'
+ 2.0.4:
+ changes:
+ bugfixes:
+ - add exception handler to main async loop (https://github.com/ansible-collections/cloud.common/pull/67).
+ - pass current task's environment through to execution (https://github.com/ansible-collections/cloud.common/pull/69).
+ - turbo - AnsibleTurboModule was missing some _ansible_facts variable like _diff,
+ _ansible_tmpdir. (https://github.com/ansible-collections/cloud.common/issues/65)
+ - turbo - honor the ``remote_tmp`` configuration key.
+ major_changes:
+ - turbo - enable turbo mode for lookup plugins
+ fragments:
+ - 67-add-exception-handler.yaml
+ - 69-pass-envvar.yaml
+ - Respect_the_remote_tmp_setting.yaml
+ - reading-common-variable.yaml
+ - turbo-for-lookup-plugin.yaml
+ release_date: '2021-07-29'
+ 2.1.0:
+ changes:
+ minor_changes:
+ - Cosmetic changes in the documentation for the inclusion in the Ansible collection.
+ - turbo - Extend the unit-test coverage.
+ - turbo - Use a BSD license for the module_utils and plugin_utils files.
+ - turbo - add support for coroutine for lookup plugins (https://github.com/ansible-collections/cloud.common/pull/75).
+ fragments:
+ - 0-copy_ignore_txt.yml
+ - 75-lookup-add-support-for-coroutine.yaml
+ - cosmetic_changes.yaml
+ release_date: '2021-10-07'
+ 2.1.1:
+ changes:
+ bugfixes:
+ - fix parameters with aliases not being passed through (https://github.com/ansible-collections/cloud.common/issues/91).
+ - fix turbo mode loading incorrect module (https://github.com/ansible-collections/cloud.common/pull/102).
+ - turbo - Ensure we don't call the module with duplicated aliased parameters.
+ minor_changes:
+ - Move the content of README_ansible_turbo.module.rst in the main README.md
+ to get visibility on Ansible Galaxy.
+ fragments:
+ - 0-ignore.yml
+ - 102-fix-incorrect-module-loading.yaml
+ - 92-fix-params-with-aliases.yml
+ - remove_README_ansible_turbo.module.rst.yaml
+ - remove_aliased_parameters.yaml
+ release_date: '2022-04-11'
+ 2.1.2:
+ changes:
+ bugfixes:
+ - Ensure we don't shutdown the server when we've still got some ongoing tasks
+ (https://github.com/ansible-collections/cloud.common/pull/109).
+ fragments:
+ - graceful_shutdown.yaml
+ release_date: '2022-06-23'
+ 2.1.3:
+ changes:
+ bugfixes:
+ - module_utils/turbo/server - import needed library into the right place (https://github.com/ansible-collections/cloud.common/pull/120)
+ minor_changes:
+ - sanity - fix sanity errors (https://github.com/ansible-collections/cloud.common/issues/106)
+ - units - ensure tests/units follow the Ansible-defined unit tests structure
+ (https://github.com/ansible-collections/cloud.common/issues/89)
+ fragments:
+ - improvements.yml
+ - pin_black_version.yml
+ - update_booleans.yml
+ release_date: '2023-03-15'
diff --git a/ansible_collections/cloud/common/changelogs/config.yaml b/ansible_collections/cloud/common/changelogs/config.yaml
new file mode 100644
index 000000000..6b49df020
--- /dev/null
+++ b/ansible_collections/cloud/common/changelogs/config.yaml
@@ -0,0 +1,29 @@
+changelog_filename_template: ../CHANGELOG.rst
+changelog_filename_version_depth: 0
+changes_file: changelog.yaml
+changes_format: combined
+keep_fragments: false
+mention_ancestor: true
+new_plugins_after_name: removed_features
+notesdir: fragments
+prelude_section_name: release_summary
+prelude_section_title: Release Summary
+sections:
+- - major_changes
+ - Major Changes
+- - minor_changes
+ - Minor Changes
+- - breaking_changes
+ - Breaking Changes / Porting Guide
+- - deprecated_features
+ - Deprecated Features
+- - removed_features
+ - Removed Features (previously deprecated)
+- - security_fixes
+ - Security Fixes
+- - bugfixes
+ - Bugfixes
+- - known_issues
+ - Known Issues
+title: cloud.common
+trivial_section_name: trivial
diff --git a/ansible_collections/cloud/common/changelogs/fragments/.keep b/ansible_collections/cloud/common/changelogs/fragments/.keep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/cloud/common/changelogs/fragments/.keep
diff --git a/ansible_collections/cloud/common/meta/runtime.yml b/ansible_collections/cloud/common/meta/runtime.yml
new file mode 100644
index 000000000..2ee3c9fa9
--- /dev/null
+++ b/ansible_collections/cloud/common/meta/runtime.yml
@@ -0,0 +1,2 @@
+---
+requires_ansible: '>=2.9.10'
diff --git a/ansible_collections/cloud/common/plugins/lookup/turbo_demo.py b/ansible_collections/cloud/common/plugins/lookup/turbo_demo.py
new file mode 100644
index 000000000..88778cd67
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/lookup/turbo_demo.py
@@ -0,0 +1,69 @@
+# Copyright: (c) 2021, Aubin Bikouo (@abikouo)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r"""
+name: turbo_demo
+author:
+ - Aubin Bikouo (@abikouo)
+
+short_description: A demo for lookup plugins on cloud.common
+description:
+ - return the parent process of the running process
+options:
+ playbook_vars:
+ description: list of playbook variables to add in the output.
+ type: list
+ elements: str
+"""
+
+EXAMPLES = r"""
+"""
+
+RETURN = r"""
+"""
+
+
+import os
+import sys
+import traceback
+
+from ansible_collections.cloud.common.plugins.plugin_utils.turbo.lookup import (
+ TurboLookupBase as LookupBase,
+)
+
+
+def counter():
+ counter.i += 1
+ return counter.i
+
+
+# NOTE: workaround to avoid a warning with ansible-doc
+if True: # pylint: disable=using-constant-test
+ counter.i = 0
+
+
+async def execute(terms, variables, playbook_vars):
+ result = []
+ result.append("running from pid: {pid}".format(pid=os.getpid()))
+ if playbook_vars is not None:
+ result += [
+ variables["vars"].get(x) for x in playbook_vars if x in variables["vars"]
+ ]
+ if terms:
+ result += terms
+
+ for id, stack in list(sys._current_frames().items()):
+ for fname, line_id, name, line in traceback.extract_stack(stack):
+ if fname == __file__:
+ continue
+
+ result.append("turbo_demo_counter: {0}".format(counter()))
+ return result
+
+
+class LookupModule(LookupBase):
+ async def _run(self, terms, variables=None, playbook_vars=None):
+ result = await execute(terms, variables, playbook_vars)
+ return result
+
+ run = _run if not hasattr(LookupBase, "run_on_daemon") else LookupBase.run_on_daemon
diff --git a/ansible_collections/cloud/common/plugins/module_utils/turbo/common.py b/ansible_collections/cloud/common/plugins/module_utils/turbo/common.py
new file mode 100644
index 000000000..e5ad19383
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/module_utils/turbo/common.py
@@ -0,0 +1,125 @@
+# Copyright (c) 2021 Red Hat
+#
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+import os
+import socket
+import sys
+import time
+import subprocess
+import pickle
+from contextlib import contextmanager
+import json
+
+from .exceptions import (
+ EmbeddedModuleUnexpectedFailure,
+)
+
+
+class AnsibleTurboSocket:
+ def __init__(self, socket_path, ttl=None, plugin="module"):
+ self._socket_path = socket_path
+ self._ttl = ttl
+ self._plugin = plugin
+ self._socket = None
+
+ def bind(self):
+ running = False
+ self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ for attempt in range(100, -1, -1):
+ try:
+ self._socket.connect(self._socket_path)
+ return True
+ except (ConnectionRefusedError, FileNotFoundError):
+ if not running:
+ running = self.start_server()
+ if attempt == 0:
+ raise
+ time.sleep(0.01)
+
+ def start_server(self):
+ env = os.environ
+ parameters = [
+ "--fork",
+ "--socket-path",
+ self._socket_path,
+ ]
+
+ if self._ttl:
+ parameters += ["--ttl", str(self._ttl)]
+
+ command = [sys.executable]
+ if self._plugin == "module":
+ ansiblez_path = sys.path[0]
+ env.update({"PYTHONPATH": ansiblez_path})
+ command += [
+ "-m",
+ "ansible_collections.cloud.common.plugins.module_utils.turbo.server",
+ ]
+ else:
+ parent_dir = os.path.dirname(__file__)
+ server_path = os.path.join(parent_dir, "server.py")
+ command += [server_path]
+ p = subprocess.Popen(
+ command + parameters,
+ env=env,
+ close_fds=True,
+ )
+ p.communicate()
+ return p.pid
+
+ def communicate(self, data, wait_sleep=0.01):
+ encoded_data = pickle.dumps((self._plugin, data))
+ self._socket.sendall(encoded_data)
+ self._socket.shutdown(socket.SHUT_WR)
+ raw_answer = b""
+ while True:
+ b = self._socket.recv((1024 * 1024))
+ if not b:
+ break
+ raw_answer += b
+ time.sleep(wait_sleep)
+ try:
+ result = json.loads(raw_answer.decode())
+ return result
+ except json.decoder.JSONDecodeError:
+ raise EmbeddedModuleUnexpectedFailure(
+ "Cannot decode plugin answer: {0}".format(raw_answer)
+ )
+
+ def close(self):
+ if self._socket:
+ self._socket.close()
+
+
+@contextmanager
+def connect(socket_path, ttl=None, plugin="module"):
+ turbo_socket = AnsibleTurboSocket(socket_path=socket_path, ttl=ttl, plugin=plugin)
+ try:
+ turbo_socket.bind()
+ yield turbo_socket
+ finally:
+ turbo_socket.close()
diff --git a/ansible_collections/cloud/common/plugins/module_utils/turbo/exceptions.py b/ansible_collections/cloud/common/plugins/module_utils/turbo/exceptions.py
new file mode 100644
index 000000000..acad2cba2
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/module_utils/turbo/exceptions.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2021 Red Hat
+#
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+
+class EmbeddedModuleFailure(Exception):
+ def __init__(self, msg, **kwargs):
+ self._message = msg
+ self._kwargs = kwargs
+
+ def get_message(self):
+ return self._message
+
+ @property
+ def kwargs(self):
+ return self._kwargs
+
+ def __repr__(self):
+ return repr(self.get_message())
+
+ def __str__(self):
+ return str(self.get_message())
+
+
+class EmbeddedModuleUnexpectedFailure(Exception):
+ def __init__(self, msg):
+ self._message = msg
+
+ def get_message(self):
+ return self._message
+
+ def __repr__(self):
+ return repr(self.get_message())
+
+ def __str__(self):
+ return str(self.get_message())
+
+
+class EmbeddedModuleSuccess(Exception):
+ def __init__(self, **kwargs):
+ self.kwargs = kwargs
diff --git a/ansible_collections/cloud/common/plugins/module_utils/turbo/module.py b/ansible_collections/cloud/common/plugins/module_utils/turbo/module.py
new file mode 100644
index 000000000..c2f9d667e
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/module_utils/turbo/module.py
@@ -0,0 +1,169 @@
+# Copyright (c) 2021 Red Hat
+#
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+import json
+import os
+import os.path
+import sys
+import tempfile
+
+import ansible.module_utils.basic
+from .exceptions import (
+ EmbeddedModuleSuccess,
+ EmbeddedModuleFailure,
+)
+import ansible_collections.cloud.common.plugins.module_utils.turbo.common
+
+if False: # pylint: disable=using-constant-test
+ from .server import please_include_me
+
+ # This is a trick to be sure server.py is embedded in the Ansiblez
+ # zip archive.🥷
+ please_include_me
+
+
+def get_collection_name_from_path():
+ module_path = ansible.module_utils.basic.get_module_path()
+
+ ansiblez = module_path.split("/")[-3]
+ if ansiblez.startswith("ansible_") and ansiblez.endswith(".zip"):
+ return ".".join(ansiblez[8:].split(".")[:2])
+
+
+def expand_argument_specs_aliases(argument_spec):
+ """Returns a dict of accepted argument that includes the aliases"""
+ expanded_argument_specs = {}
+ for k, v in argument_spec.items():
+ for alias in [k] + v.get("aliases", []):
+ expanded_argument_specs[alias] = v
+ return expanded_argument_specs
+
+
+def prepare_args(argument_specs, params):
+ """Take argument_spec and the user params and prepare the final argument structure."""
+
+ def _keep_value(v, argument_specs, key, subkey=None):
+ if v is None: # cannot be a valide parameter
+ return False
+ if key not in argument_specs: # should never happen
+ return
+ if not subkey: # level 1 parameter
+ return v != argument_specs[key].get("default")
+ elif subkey not in argument_specs[key]: # Freeform
+ return True
+ elif isinstance(argument_specs[key][subkey], dict):
+ return v != argument_specs[key][subkey].get("default")
+ else: # should never happen
+ return True
+
+ def _is_an_alias(k):
+ aliases = argument_specs[k].get("aliases")
+ return aliases and k in aliases
+
+ new_params = {}
+ for k, v in params.items():
+ if not _keep_value(v, argument_specs, k):
+ continue
+
+ if _is_an_alias(k):
+ continue
+
+ if isinstance(v, dict):
+ new_params[k] = {
+ i: j for i, j in v.items() if _keep_value(j, argument_specs, k, i)
+ }
+ else:
+ new_params[k] = v
+ args = {"ANSIBLE_MODULE_ARGS": new_params}
+ return args
+
+
+class AnsibleTurboModule(ansible.module_utils.basic.AnsibleModule):
+ embedded_in_server = False
+ collection_name = None
+
+ def __init__(self, *args, **kwargs):
+ self.embedded_in_server = sys.argv[0].endswith("/server.py")
+ self.collection_name = (
+ AnsibleTurboModule.collection_name or get_collection_name_from_path()
+ )
+ ansible.module_utils.basic.AnsibleModule.__init__(
+ self, *args, bypass_checks=not self.embedded_in_server, **kwargs
+ )
+ self._running = None
+ if not self.embedded_in_server:
+ self.run_on_daemon()
+
+ def socket_path(self):
+ if self._remote_tmp is None:
+ abs_remote_tmp = tempfile.gettempdir()
+ else:
+ abs_remote_tmp = os.path.expanduser(os.path.expandvars(self._remote_tmp))
+ return os.path.join(abs_remote_tmp, f"turbo_mode.{self.collection_name}.socket")
+
+ def init_args(self):
+ argument_specs = expand_argument_specs_aliases(self.argument_spec)
+ args = prepare_args(argument_specs, self.params)
+ for k in ansible.module_utils.basic.PASS_VARS:
+ attribute = ansible.module_utils.basic.PASS_VARS[k][0]
+ if not hasattr(self, attribute):
+ continue
+ v = getattr(self, attribute)
+ if isinstance(v, int) or isinstance(v, bool) or isinstance(v, str):
+ args["ANSIBLE_MODULE_ARGS"][f"_ansible_{k}"] = v
+ return args
+
+ def run_on_daemon(self):
+ result = dict(changed=False, original_message="", message="")
+ ttl = os.environ.get("ANSIBLE_TURBO_LOOKUP_TTL", None)
+ with ansible_collections.cloud.common.plugins.module_utils.turbo.common.connect(
+ socket_path=self.socket_path(), ttl=ttl
+ ) as turbo_socket:
+ ansiblez_path = sys.path[0]
+ args = self.init_args()
+ data = [
+ ansiblez_path,
+ json.dumps(args),
+ dict(os.environ),
+ ]
+ content = json.dumps(data).encode()
+ result = turbo_socket.communicate(content)
+ self.exit_json(**result)
+
+ def exit_json(self, **kwargs):
+ if not self.embedded_in_server:
+ super().exit_json(**kwargs)
+ else:
+ self.do_cleanup_files()
+ raise EmbeddedModuleSuccess(**kwargs)
+
+ def fail_json(self, *args, **kwargs):
+ if not self.embedded_in_server:
+ super().fail_json(**kwargs)
+ else:
+ self.do_cleanup_files()
+ raise EmbeddedModuleFailure(*args, **kwargs)
diff --git a/ansible_collections/cloud/common/plugins/module_utils/turbo/server.py b/ansible_collections/cloud/common/plugins/module_utils/turbo/server.py
new file mode 100644
index 000000000..77fbb6493
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/module_utils/turbo/server.py
@@ -0,0 +1,396 @@
+# Copyright (c) 2021 Red Hat
+#
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+import argparse
+import asyncio
+from datetime import datetime
+import importlib
+
+# py38 only, See: https://github.com/PyCQA/pylint/issues/2976
+import inspect # pylint: disable=syntax-error
+import io
+import json
+
+# py38 only, See: https://github.com/PyCQA/pylint/issues/2976
+import collections # pylint: disable=syntax-error
+import os
+import signal
+import sys
+import traceback
+import zipfile
+from zipimport import zipimporter
+import pickle
+import uuid
+
+sys_path_lock = None
+env_lock = None
+
+import ansible.module_utils.basic
+
+please_include_me = "bar"
+
+
+def fork_process():
+ """
+ This function performs the double fork process to detach from the
+ parent process and execute.
+ """
+ pid = os.fork()
+
+ if pid == 0:
+ fd = os.open(os.devnull, os.O_RDWR)
+
+ # clone stdin/out/err
+ for num in range(3):
+ if fd != num:
+ os.dup2(fd, num)
+
+ if fd not in range(3):
+ os.close(fd)
+
+ pid = os.fork()
+ if pid > 0:
+ os._exit(0)
+
+ # get new process session and detach
+ sid = os.setsid()
+ if sid == -1:
+ raise Exception("Unable to detach session while daemonizing")
+
+ # avoid possible problems with cwd being removed
+ os.chdir("/")
+
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0) # pylint: disable=ansible-bad-function
+ else:
+ sys.exit(0) # pylint: disable=ansible-bad-function
+ return pid
+
+
+class EmbeddedModule:
+ def __init__(self, ansiblez_path, params):
+ self.ansiblez_path = ansiblez_path
+ self.collection_name, self.module_name = self.find_module_name()
+ self.params = params
+ self.module_class = None
+ self.debug_mode = False
+ self.module_path = (
+ "ansible_collections.{collection_name}." "plugins.modules.{module_name}"
+ ).format(collection_name=self.collection_name, module_name=self.module_name)
+
+ def find_module_name(self):
+ with zipfile.ZipFile(self.ansiblez_path) as zip:
+ for path in zip.namelist():
+ if not path.startswith("ansible_collections"):
+ continue
+ if not path.endswith(".py"):
+ continue
+ if path.endswith("__init__.py"):
+ continue
+ splitted = path.split("/")
+ if len(splitted) != 6:
+ continue
+ if splitted[-3:-1] != ["plugins", "modules"]:
+ continue
+ collection = ".".join(splitted[1:3])
+ name = splitted[-1][:-3]
+ return collection, name
+ raise Exception("Cannot find module name")
+
+ async def load(self):
+ async with sys_path_lock:
+ # Add the Ansiblez_path in sys.path
+ sys.path.insert(0, self.ansiblez_path)
+
+ # resettle the loaded modules that were associated
+ # with a different Ansiblez.
+ for path, module in sorted(tuple(sys.modules.items())):
+ if path and module and path.startswith("ansible_collections"):
+ try:
+ prefix = sys.modules[path].__loader__.prefix
+ except AttributeError:
+ # Not from a zipimporter loader, skipping
+ continue
+ # Reload package modules only, to pick up new modules from
+ # packages that have been previously loaded.
+ if hasattr(sys.modules[path], "__path__"):
+ py_path = self.ansiblez_path + os.sep + prefix
+ my_loader = zipimporter(py_path)
+ sys.modules[path].__loader__ = my_loader
+ try:
+ importlib.reload(sys.modules[path])
+ except ModuleNotFoundError:
+ pass
+ # Finally, load the plugin class.
+ self.module_class = importlib.import_module(self.module_path)
+
+ async def unload(self):
+ async with sys_path_lock:
+ sys.path = [i for i in sys.path if i != self.ansiblez_path]
+
+ def create_profiler(self):
+ if self.debug_mode:
+ import cProfile
+
+ pr = cProfile.Profile()
+ pr.enable()
+ return pr
+
+ def print_profiling_info(self, pr):
+ if self.debug_mode:
+ import pstats
+
+ sortby = pstats.SortKey.CUMULATIVE
+ ps = pstats.Stats(pr).sort_stats(sortby)
+ ps.print_stats(20)
+
+ def print_backtrace(self, backtrace):
+ if self.debug_mode:
+ print(backtrace) # pylint: disable=ansible-bad-function
+
+ async def run(self):
+ class FakeStdin:
+ buffer = None
+
+ from .exceptions import (
+ EmbeddedModuleFailure,
+ EmbeddedModuleUnexpectedFailure,
+ EmbeddedModuleSuccess,
+ )
+
+ # monkeypatching to pass the argument to the module, this is not
+ # really safe, and in the future, this will prevent us to run several
+ # modules in parallel. We can maybe use a scoped monkeypatch instead
+ _fake_stdin = FakeStdin()
+ _fake_stdin.buffer = io.BytesIO(self.params.encode())
+ sys.stdin = _fake_stdin
+ # Trick to be sure ansible.module_utils.basic._load_params() won't
+ # try to build the module parameters from the daemon arguments
+ sys.argv = sys.argv[:1]
+ ansible.module_utils.basic._ANSIBLE_ARGS = None
+ pr = self.create_profiler()
+ if not hasattr(self.module_class, "main"):
+ raise EmbeddedModuleFailure("No main() found!")
+ try:
+ if inspect.iscoroutinefunction(self.module_class.main):
+ await self.module_class.main()
+ elif pr:
+ pr.runcall(self.module_class.main)
+ else:
+ self.module_class.main()
+ except EmbeddedModuleSuccess as e:
+ self.print_profiling_info(pr)
+ return e.kwargs
+ except EmbeddedModuleFailure as e:
+ backtrace = traceback.format_exc()
+ self.print_backtrace(backtrace)
+ raise
+ except Exception as e:
+ backtrace = traceback.format_exc()
+ self.print_backtrace(backtrace)
+ raise EmbeddedModuleUnexpectedFailure(str(backtrace))
+ else:
+ raise EmbeddedModuleUnexpectedFailure(
+ "Likely a bug: exit_json() or fail_json() should be called during the module excution"
+ )
+
+
+async def run_as_lookup_plugin(data):
+ errors = None
+ from ansible.module_utils._text import to_native
+
+ try:
+ import ansible.plugins.loader as plugin_loader
+ from ansible.parsing.dataloader import DataLoader
+ from ansible.template import Templar
+
+ (
+ lookup_name,
+ terms,
+ variables,
+ kwargs,
+ ) = data
+
+ # load lookup plugin
+ templar = Templar(loader=DataLoader(), variables=None)
+ ansible_collections = "ansible_collections."
+ if lookup_name.startswith(ansible_collections):
+ lookup_name = lookup_name.replace(ansible_collections, "", 1)
+ ansible_plugins_lookup = ".plugins.lookup."
+ if ansible_plugins_lookup in lookup_name:
+ lookup_name = lookup_name.replace(ansible_plugins_lookup, ".", 1)
+
+ instance = plugin_loader.lookup_loader.get(
+ name=lookup_name, loader=templar._loader, templar=templar
+ )
+
+ if not hasattr(instance, "_run"):
+ return [None, "No _run() found"]
+ if inspect.iscoroutinefunction(instance._run):
+ result = await instance._run(terms, variables=variables, **kwargs)
+ else:
+ result = instance._run(terms, variables=variables, **kwargs)
+ except Exception as e:
+ errors = to_native(e)
+ return [result, errors]
+
+
+async def run_as_module(content, debug_mode):
+ from ansible_collections.cloud.common.plugins.module_utils.turbo.exceptions import (
+ EmbeddedModuleFailure,
+ )
+
+ try:
+ (
+ ansiblez_path,
+ params,
+ env,
+ ) = json.loads(content)
+ if debug_mode:
+ print( # pylint: disable=ansible-bad-function
+ f"-----\nrunning {ansiblez_path} with params: ¨{params}¨"
+ )
+
+ embedded_module = EmbeddedModule(ansiblez_path, params)
+ if debug_mode:
+ embedded_module.debug_mode = True
+
+ await embedded_module.load()
+ try:
+ async with env_lock:
+ os.environ.clear()
+ os.environ.update(env)
+ result = await embedded_module.run()
+ except SystemExit:
+ backtrace = traceback.format_exc()
+ result = {"msg": str(backtrace), "failed": True}
+ except EmbeddedModuleFailure as e:
+ result = {"msg": str(e), "failed": True}
+ if e.kwargs:
+ result.update(e.kwargs)
+ except Exception as e:
+ result = {
+ "msg": traceback.format_stack() + [str(e)],
+ "failed": True,
+ }
+ await embedded_module.unload()
+ except Exception as e:
+ result = {"msg": traceback.format_stack() + [str(e)], "failed": True}
+ return result
+
+
+class AnsibleVMwareTurboMode:
+ def __init__(self):
+ self.sessions = collections.defaultdict(dict)
+ self.socket_path = None
+ self.ttl = None
+ self.debug_mode = None
+ self.jobs_ongoing = {}
+
+ async def ghost_killer(self):
+ while True:
+ await asyncio.sleep(self.ttl)
+ running_jobs = {
+ job_id: start_date
+ for job_id, start_date in self.jobs_ongoing.items()
+ if (datetime.now() - start_date).total_seconds() < 3600
+ }
+ if running_jobs:
+ continue
+ self.stop()
+
+ async def handle(self, reader, writer):
+ self._watcher.cancel()
+ self._watcher = self.loop.create_task(self.ghost_killer())
+ job_id = str(uuid.uuid4())
+ self.jobs_ongoing[job_id] = datetime.now()
+ raw_data = await reader.read()
+ if not raw_data:
+ return
+
+ (plugin_type, content) = pickle.loads(raw_data)
+
+ def _terminate(result):
+ writer.write(json.dumps(result).encode())
+ writer.close()
+
+ if plugin_type == "module":
+ result = await run_as_module(content, debug_mode=self.debug_mode)
+ elif plugin_type == "lookup":
+ result = await run_as_lookup_plugin(content)
+ _terminate(result)
+ del self.jobs_ongoing[job_id]
+
+ def handle_exception(self, loop, context):
+ e = context.get("exception")
+ traceback.print_exception(type(e), e, e.__traceback__)
+ self.stop()
+
+ def start(self):
+ self.loop = asyncio.get_event_loop()
+ self.loop.add_signal_handler(signal.SIGTERM, self.stop)
+ self.loop.set_exception_handler(self.handle_exception)
+ self._watcher = self.loop.create_task(self.ghost_killer())
+
+ import sys
+
+ if sys.hexversion >= 0x30A00B1:
+ # py3.10 drops the loop argument of create_task.
+ self.loop.create_task(
+ asyncio.start_unix_server(self.handle, path=self.socket_path)
+ )
+ else:
+ self.loop.create_task(
+ asyncio.start_unix_server(
+ self.handle, path=self.socket_path, loop=self.loop
+ )
+ )
+ self.loop.run_forever()
+
+ def stop(self):
+ os.unlink(self.socket_path)
+ self.loop.stop()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Start a background daemon.")
+ parser.add_argument("--socket-path")
+ parser.add_argument("--ttl", default=15, type=int)
+ parser.add_argument("--fork", action="store_true")
+
+ args = parser.parse_args()
+ if args.fork:
+ fork_process()
+ sys_path_lock = asyncio.Lock()
+ env_lock = asyncio.Lock()
+
+ server = AnsibleVMwareTurboMode()
+ server.socket_path = args.socket_path
+ server.ttl = args.ttl
+ server.debug_mode = not args.fork
+ server.start()
diff --git a/ansible_collections/cloud/common/plugins/module_utils/turbo_demo.py b/ansible_collections/cloud/common/plugins/module_utils/turbo_demo.py
new file mode 100644
index 000000000..1a14f0754
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/module_utils/turbo_demo.py
@@ -0,0 +1 @@
+# This module is part of the test suite to check the import logic of turbo mode
diff --git a/ansible_collections/cloud/common/plugins/modules/turbo_demo.py b/ansible_collections/cloud/common/plugins/modules/turbo_demo.py
new file mode 100644
index 000000000..30093b55e
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/modules/turbo_demo.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (C) 2020, Gonéri Le Bouder <goneri@lebouder.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r"""
+---
+module: turbo_demo
+short_description: A demo module for ansible_module.turbo
+version_added: "1.0.0"
+description:
+- "This module is an example of an ansible_module.turbo integration."
+author:
+- Gonéri Le Bouder (@goneri)
+"""
+
+EXAMPLES = r"""
+- name: Run the module
+ cloud.common.turbo_demo:
+"""
+
+import os
+
+from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
+ AnsibleTurboModule as AnsibleModule,
+)
+
+
+def counter():
+ counter.i += 1
+ return counter.i
+
+
+# NOTE: workaround to avoid a warning with ansible-doc
+if True: # pylint: disable=using-constant-test
+ counter.i = 0
+
+
+def get_message():
+ return f"This is me running with PID: {os.getpid()}, called {counter.i} time(s)"
+
+
+def run_module():
+ result = {}
+
+ # the AnsibleModule object will be our abstraction working with Ansible
+ # this includes instantiation, a couple of common attr would be the
+ # args/params passed to the execution, as well as if the module
+ # supports check mode
+ module = AnsibleModule(argument_spec={}, supports_check_mode=True)
+ module.collection_name = "cloud.common"
+ previous_value = counter.i
+ if not module.check_mode:
+ counter()
+ result["changed"] = True
+ result["message"] = get_message()
+ result["counter"] = counter.i
+ result["envvar"] = os.environ.get("TURBO_TEST_VAR")
+
+ if module._diff:
+ result["diff"] = {"before": previous_value, "after": counter.i}
+
+ module.exit_json(**result)
+
+
+def main():
+ from ansible_collections.cloud.common.plugins.module_utils import turbo_demo
+
+ run_module()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cloud/common/plugins/modules/turbo_fail.py b/ansible_collections/cloud/common/plugins/modules/turbo_fail.py
new file mode 100644
index 000000000..d9b4731f3
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/modules/turbo_fail.py
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (C) 2021, Aubin Bikouo <abikouo>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r"""
+---
+module: turbo_fail
+short_description: A short module which honor additional args when calling fail_json
+version_added: "1.0.0"
+description:
+- "This module aims to test fail_json method on Ansible.turbo module"
+options:
+ params:
+ description:
+ - parameter to display in task output
+ required: false
+ type: dict
+author:
+- Aubin Bikouo (@abikouo)
+"""
+
+EXAMPLES = r"""
+- name: Fail without additional arguments
+ cloud.common.turbo_fail:
+
+- name: Fail with additional arguments
+ cloud.common.turbo_fail:
+ params:
+ test: "ansible"
+"""
+
+import os
+
+from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
+ AnsibleTurboModule as AnsibleModule,
+)
+
+
+def run_module():
+ module = AnsibleModule(
+ argument_spec=dict(
+ params=dict(type="dict"),
+ )
+ )
+ module.collection_name = "cloud.common"
+ msg = "ansible.cloud.fail"
+ if module.params.get("params"):
+ module.fail_json(msg=msg, **module.params.get("params"))
+ module.fail_json(msg=msg)
+
+
+def main():
+ run_module()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cloud/common/plugins/modules/turbo_import.py b/ansible_collections/cloud/common/plugins/modules/turbo_import.py
new file mode 100644
index 000000000..152107c43
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/modules/turbo_import.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (C) 2022, Red Hat
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: turbo_import
+short_description: A demo module to test import logic for turbo mode
+version_added: "1.0.0"
+description:
+- "This module tests the import logic for turbo mode."
+author:
+- Mike Graves (@gravesm)
+"""
+
+EXAMPLES = r"""
+- name: Run the module
+ cloud.common.turbo_import:
+"""
+
+
+from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
+ AnsibleTurboModule as AnsibleModule,
+)
+
+
+def run_module():
+ module = AnsibleModule(argument_spec={})
+ module.collection_name = "cloud.common"
+ module.exit_json(changed=False)
+
+
+def main():
+ from ansible_collections.cloud.common.plugins.module_utils import turbo_demo
+
+ run_module()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/cloud/common/plugins/plugin_utils/turbo/lookup.py b/ansible_collections/cloud/common/plugins/plugin_utils/turbo/lookup.py
new file mode 100644
index 000000000..30adc1e96
--- /dev/null
+++ b/ansible_collections/cloud/common/plugins/plugin_utils/turbo/lookup.py
@@ -0,0 +1,90 @@
+# Copyright (c) 2021 Red Hat
+#
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from ansible.plugins.lookup import LookupBase
+import ansible_collections.cloud.common.plugins.module_utils.turbo.common
+from ansible_collections.cloud.common.plugins.module_utils.turbo.exceptions import (
+ EmbeddedModuleUnexpectedFailure,
+)
+
+
+def get_server_ttl(variables):
+ # trying to retrieve first TTL from environment variable
+ ttl = os.environ.get("ANSIBLE_TURBO_LOOKUP_TTL", None)
+ if ttl is not None:
+ return ttl
+ # Read TTL from ansible environment
+ for env_var in variables.get("environment", []):
+ value = env_var.get("ANSIBLE_TURBO_LOOKUP_TTL", None)
+ test_var_int = [
+ isinstance(value, str) and value.isnumeric(),
+ isinstance(value, int),
+ ]
+ if value is not None and any(test_var_int):
+ ttl = value
+ return ttl
+
+
+class TurboLookupBase(LookupBase):
+ def run_on_daemon(self, terms, variables=None, **kwargs):
+ self._ttl = get_server_ttl(variables)
+ return self.execute(terms=terms, variables=variables, **kwargs)
+
+ @property
+ def socket_path(self):
+ if not hasattr(self, "__socket_path"):
+ """
+ Input:
+ _load_name: ansible_collections.cloud.common.plugins.lookup.turbo_random_lookup
+ Output:
+ __socket_path: {HOME}/.ansible/tmp/turbo_mode_cloud.common.socket
+ this will allow to have one socket per collection
+ """
+ name = self._load_name
+ ansible_collections = "ansible_collections."
+ if name.startswith(ansible_collections):
+ name = name.replace(ansible_collections, "", 1)
+ lookup_plugins = ".plugins.lookup."
+ idx = name.find(lookup_plugins)
+ if idx != -1:
+ name = name[:idx]
+ self.__socket_path = os.environ[
+ "HOME"
+ ] + "/.ansible/tmp/turbo_lookup.{0}.socket".format(name)
+ return self.__socket_path
+
+ def execute(self, terms, variables=None, **kwargs):
+ with ansible_collections.cloud.common.plugins.module_utils.turbo.common.connect(
+ socket_path=self.socket_path, ttl=self._ttl, plugin="lookup"
+ ) as turbo_socket:
+ content = (self._load_name, terms, variables, kwargs)
+ (result, errors) = turbo_socket.communicate(content)
+ if errors:
+ raise EmbeddedModuleUnexpectedFailure(errors)
+ return result
diff --git a/ansible_collections/cloud/common/requirements.txt b/ansible_collections/cloud/common/requirements.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/cloud/common/requirements.txt
diff --git a/ansible_collections/cloud/common/test-requirements.txt b/ansible_collections/cloud/common/test-requirements.txt
new file mode 100644
index 000000000..ee6352ad8
--- /dev/null
+++ b/ansible_collections/cloud/common/test-requirements.txt
@@ -0,0 +1,2 @@
+coverage==4.5.4
+pytest-xdist
diff --git a/ansible_collections/cloud/common/tests/config.yml b/ansible_collections/cloud/common/tests/config.yml
new file mode 100644
index 000000000..faff92e33
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/config.yml
@@ -0,0 +1,2 @@
+modules:
+ python_requires: '>=3.6'
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_fail/tasks/main.yaml b/ansible_collections/cloud/common/tests/integration/targets/turbo_fail/tasks/main.yaml
new file mode 100644
index 000000000..069ba4490
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_fail/tasks/main.yaml
@@ -0,0 +1,24 @@
+- name: fail with no additionnal args
+ cloud.common.turbo_fail:
+ register: result
+ failed_when: not result.failed
+
+- assert:
+ that:
+ - result | list | difference(["failed_when_result"]) | sort == ["changed", "failed", "msg"]
+
+- name: fail with additionnal args
+ cloud.common.turbo_fail:
+ params:
+ test:
+ phase: integration
+ release: 1.0
+ environment: production
+ register: result
+ failed_when: not result.failed
+
+- assert:
+ that:
+ - result.environment == "production"
+ - result.test.phase == "integration"
+ - result.test.release == 1.0
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/playbook.yaml b/ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/playbook.yaml
new file mode 100644
index 000000000..fc75a8e53
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/playbook.yaml
@@ -0,0 +1,81 @@
+- hosts: localhost
+ gather_facts: false
+
+ environment:
+ ANSIBLE_TURBO_LOOKUP_TTL: 1
+ ANOTHER_ANSIBLE_VARS: 10
+
+ tasks:
+ - name: variables definition
+ set_fact:
+ var00: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+ var01: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+
+ - pause:
+ seconds: 2
+
+ - name: variables definition
+ set_fact:
+ var10: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+ var11: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+ environment:
+ ANSIBLE_TURBO_LOOKUP_TTL: 4
+
+ - pause:
+ seconds: 2
+
+ - name: variables definition
+ set_fact:
+ var20: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+ var21: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+
+ - name: validate output
+ assert:
+ that:
+ - '"turbo_demo_counter: 1" in var00'
+ - '"turbo_demo_counter: 2" in var01'
+ - '"turbo_demo_counter: 1" in var10'
+ - '"turbo_demo_counter: 2" in var11'
+ - '"turbo_demo_counter: 3" in var20'
+ - '"turbo_demo_counter: 4" in var21'
+
+ - name: Wait for the socket to be closed
+ pause:
+ seconds: 5
+
+
+- hosts: localhost
+ gather_facts: false
+
+ vars:
+ turbo_play_var: "simple ansible playbook variable"
+
+ tasks:
+ - name: set variables using lookup plugin
+ set_fact:
+ var0: "{{ lookup('cloud.common.turbo_demo', terms, playbook_vars=['turbo_play_var', 'turbo_task_var'], wantlist=True) }}"
+ var1: "{{ lookup('cloud.common.turbo_demo', terms, playbook_vars=['turbo_task_var'], wantlist=True) }}"
+ var2: "{{ lookup('cloud.common.turbo_demo', terms, playbook_vars=['turbo_play_var'], wantlist=True) }}"
+ vars:
+ terms: ["2.9", "2.10"]
+ turbo_task_var: "simple ansible task variable"
+
+ - name: test lookup plugin using a module
+ debug:
+ msg: "{{ lookup('cloud.common.turbo_demo', wantlist=True) }}"
+ register: output
+
+ - name: validate other settings
+ assert:
+ that:
+ - '"turbo_demo_counter: 1" in var0'
+ - '"turbo_demo_counter: 2" in var1'
+ - '"turbo_demo_counter: 3" in var2'
+ - '"turbo_demo_counter: 4" in output.msg'
+ - '"simple ansible task variable" in var0'
+ - '"simple ansible task variable" in var1'
+ - 'turbo_play_var in var0'
+ - 'turbo_play_var in var2'
+ - '["2.9", "2.10"] in var0'
+ - '["2.9", "2.10"] in var1'
+ - '["2.9", "2.10"] in var2'
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/runme.sh b/ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/runme.sh
new file mode 100755
index 000000000..5e27470e6
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_lookup/runme.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+set -eux
+exec ansible-playbook playbook.yaml -vvv
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/playbook.yaml b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/playbook.yaml
new file mode 100644
index 000000000..f5202379c
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/playbook.yaml
@@ -0,0 +1,5 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - import_role:
+ name: turbo_mode
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/runme.sh b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/runme.sh
new file mode 100755
index 000000000..4c43575df
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/runme.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -eux
+export ANSIBLE_ROLES_PATH=..
+exec ansible-playbook playbook.yaml
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/tasks/main.yaml b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/tasks/main.yaml
new file mode 100644
index 000000000..21b6b8c52
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode/tasks/main.yaml
@@ -0,0 +1,77 @@
+- cloud.common.turbo_import:
+
+- cloud.common.turbo_demo:
+ with_sequence: count=10
+ register: _result
+- debug: var=_result.results[-1]
+- assert:
+ that:
+ - _result.results[-1].counter == 10
+- cloud.common.turbo_demo:
+ with_sequence: count=10
+ check_mode: True
+ register: _result
+- assert:
+ that:
+ - _result.results[-1].counter == 10
+- cloud.common.turbo_demo:
+ with_sequence: count=10
+ become: true
+ register: _result
+- assert:
+ that:
+ - _result.results[-1].counter == 10
+- cloud.common.turbo_demo:
+ diff: true
+ register: _result_with_diff
+- assert:
+ that:
+ - _result_with_diff.diff is defined
+- cloud.common.turbo_demo:
+ diff: false
+ register: _result_no_diff
+- assert:
+ that:
+ - _result_no_diff.diff is undefined
+
+- name: Test task environment var
+ cloud.common.turbo_demo:
+ environment:
+ TURBO_TEST_VAR: foobar
+ register: _result
+
+- assert:
+ that:
+ - _result.envvar == "foobar"
+
+- name: Test task environment var not set
+ cloud.common.turbo_demo:
+ register: _result
+
+- assert:
+ that:
+ - not _result.envvar
+
+
+- name: Create temporary dir
+ ansible.builtin.tempfile:
+ state: directory
+ suffix: temp
+ register: tempdir_1
+
+- name: Test with a different remote_tmp, there is no socket yet.
+ cloud.common.turbo_demo:
+ vars:
+ ansible_remote_tmp: "{{ tempdir_1.path }}"
+ register: _result
+- assert:
+ that:
+ - _result.counter == 1
+
+- name: test using default remote_tmp. socket previously created
+ cloud.common.turbo_demo:
+ register: _result
+
+- assert:
+ that:
+ - _result.counter > 1
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/inventory b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/inventory
new file mode 100644
index 000000000..45c6d9bb7
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/inventory
@@ -0,0 +1,50 @@
+host1 ansible_connection=local
+host2 ansible_connection=local
+host3 ansible_connection=local
+host4 ansible_connection=local
+host5 ansible_connection=local
+host6 ansible_connection=local
+host7 ansible_connection=local
+host8 ansible_connection=local
+host9 ansible_connection=local
+host10 ansible_connection=local
+host11 ansible_connection=local
+host12 ansible_connection=local
+host13 ansible_connection=local
+host14 ansible_connection=local
+host15 ansible_connection=local
+host16 ansible_connection=local
+host17 ansible_connection=local
+host18 ansible_connection=local
+host19 ansible_connection=local
+host20 ansible_connection=local
+host21 ansible_connection=local
+host22 ansible_connection=local
+host23 ansible_connection=local
+host24 ansible_connection=local
+host25 ansible_connection=local
+host26 ansible_connection=local
+host27 ansible_connection=local
+host28 ansible_connection=local
+host29 ansible_connection=local
+host30 ansible_connection=local
+host31 ansible_connection=local
+host32 ansible_connection=local
+host33 ansible_connection=local
+host34 ansible_connection=local
+host35 ansible_connection=local
+host36 ansible_connection=local
+host37 ansible_connection=local
+host38 ansible_connection=local
+host39 ansible_connection=local
+host40 ansible_connection=local
+host41 ansible_connection=local
+host42 ansible_connection=local
+host43 ansible_connection=local
+host44 ansible_connection=local
+host45 ansible_connection=local
+host46 ansible_connection=local
+host47 ansible_connection=local
+host48 ansible_connection=local
+host49 ansible_connection=local
+host50 ansible_connection=local
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/playbook.yaml b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/playbook.yaml
new file mode 100644
index 000000000..4be9cf37a
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/playbook.yaml
@@ -0,0 +1,33 @@
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - cloud.common.turbo_demo:
+ - pause:
+ seconds: 5
+
+- hosts: all
+ strategy: free
+ gather_facts: false
+ tasks:
+ - cloud.common.turbo_demo:
+ with_sequence: count=10
+ - cloud.common.turbo_demo:
+ with_sequence: count=10
+
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - cloud.common.turbo_demo:
+ register: _result
+ - debug: var=_result
+ - assert:
+ that:
+ - _result.counter == 1002
+ - pause:
+ seconds: 35
+ - cloud.common.turbo_demo:
+ register: _result
+ - debug: var=_result
+ - assert:
+ that:
+ - _result.counter == 1
diff --git a/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/runme.sh b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/runme.sh
new file mode 100755
index 000000000..83a40fcf0
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/integration/targets/turbo_mode_parallel_exec/runme.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -eux
+export ANSIBLE_ROLES_PATH=..
+exec ansible-playbook -i inventory playbook.yaml
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.10.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.10.txt
new file mode 100644
index 000000000..4ffa55ad8
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.10.txt
@@ -0,0 +1,76 @@
+plugins/module_utils/turbo/exceptions.py compile-2.6!skip
+plugins/module_utils/turbo/exceptions.py compile-2.7!skip
+plugins/module_utils/turbo/exceptions.py compile-3.5!skip
+plugins/module_utils/turbo/exceptions.py future-import-boilerplate!skip
+plugins/module_utils/turbo/exceptions.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/module.py compile-2.6!skip
+plugins/module_utils/turbo/module.py compile-2.7!skip
+plugins/module_utils/turbo/module.py compile-3.5!skip
+plugins/module_utils/turbo/module.py future-import-boilerplate!skip
+plugins/module_utils/turbo/module.py import-2.6!skip
+plugins/module_utils/turbo/module.py import-2.7!skip
+plugins/module_utils/turbo/module.py import-3.5!skip
+plugins/module_utils/turbo/module.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/server.py compile-2.6!skip
+plugins/module_utils/turbo/server.py compile-2.7!skip
+plugins/module_utils/turbo/server.py compile-3.5!skip
+plugins/module_utils/turbo/server.py future-import-boilerplate!skip
+plugins/module_utils/turbo/server.py import-2.6!skip
+plugins/module_utils/turbo/server.py import-2.7!skip
+plugins/module_utils/turbo/server.py import-3.5!skip
+plugins/module_utils/turbo/server.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
+plugins/modules/turbo_demo.py compile-2.6!skip
+plugins/modules/turbo_demo.py compile-2.7!skip
+plugins/modules/turbo_demo.py compile-3.5!skip
+plugins/modules/turbo_demo.py future-import-boilerplate!skip
+plugins/modules/turbo_demo.py import-2.6!skip
+plugins/modules/turbo_demo.py import-2.7!skip
+plugins/modules/turbo_demo.py import-3.5!skip
+plugins/modules/turbo_demo.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-2.6!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-2.7!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-3.5!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py metaclass-boilerplate!skip
+plugins/modules/turbo_fail.py compile-2.6!skip
+plugins/modules/turbo_fail.py compile-2.7!skip
+plugins/modules/turbo_fail.py compile-3.5!skip
+plugins/modules/turbo_fail.py future-import-boilerplate!skip
+plugins/modules/turbo_fail.py import-2.6!skip
+plugins/modules/turbo_fail.py import-2.7!skip
+plugins/modules/turbo_fail.py import-3.5!skip
+plugins/modules/turbo_fail.py metaclass-boilerplate!skip
+plugins/plugin_utils/turbo/lookup.py compile-2.6!skip
+plugins/plugin_utils/turbo/lookup.py compile-2.7!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.5!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.6!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.7!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.8!skip
+plugins/plugin_utils/turbo/lookup.py future-import-boilerplate!skip
+plugins/plugin_utils/turbo/lookup.py metaclass-boilerplate!skip
+plugins/lookup/turbo_demo.py compile-2.6!skip
+plugins/lookup/turbo_demo.py compile-2.7!skip
+plugins/lookup/turbo_demo.py compile-3.5!skip
+plugins/lookup/turbo_demo.py future-import-boilerplate!skip
+plugins/lookup/turbo_demo.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/common.py compile-2.6!skip
+plugins/module_utils/turbo/common.py compile-2.7!skip
+plugins/module_utils/turbo/common.py compile-3.5!skip
+plugins/module_utils/turbo/common.py future-import-boilerplate!skip
+plugins/module_utils/turbo/common.py import-2.6!skip
+plugins/module_utils/turbo/common.py import-2.7!skip
+plugins/module_utils/turbo/common.py import-3.5!skip
+plugins/module_utils/turbo/common.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/conftest.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/conftest.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_module.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_module.py metaclass-boilerplate!skip
+plugins/modules/turbo_import.py compile-2.6!skip
+plugins/modules/turbo_import.py compile-2.7!skip
+plugins/modules/turbo_import.py compile-3.5!skip
+plugins/modules/turbo_import.py future-import-boilerplate!skip
+plugins/modules/turbo_import.py import-2.6!skip
+plugins/modules/turbo_import.py import-2.7!skip
+plugins/modules/turbo_import.py import-3.5!skip
+plugins/modules/turbo_import.py metaclass-boilerplate!skip
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.11.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.11.txt
new file mode 100644
index 000000000..72bd4688a
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.11.txt
@@ -0,0 +1,84 @@
+plugins/module_utils/turbo/exceptions.py compile-2.6!skip
+plugins/module_utils/turbo/exceptions.py compile-2.7!skip
+plugins/module_utils/turbo/exceptions.py compile-3.5!skip
+plugins/module_utils/turbo/exceptions.py future-import-boilerplate!skip
+plugins/module_utils/turbo/exceptions.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/module.py compile-2.6!skip
+plugins/module_utils/turbo/module.py compile-2.7!skip
+plugins/module_utils/turbo/module.py compile-3.5!skip
+plugins/module_utils/turbo/module.py future-import-boilerplate!skip
+plugins/module_utils/turbo/module.py import-2.6!skip
+plugins/module_utils/turbo/module.py import-2.7!skip
+plugins/module_utils/turbo/module.py import-3.5!skip
+plugins/module_utils/turbo/module.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/server.py compile-2.6!skip
+plugins/module_utils/turbo/server.py compile-2.7!skip
+plugins/module_utils/turbo/server.py compile-3.5!skip
+plugins/module_utils/turbo/server.py future-import-boilerplate!skip
+plugins/module_utils/turbo/server.py import-2.6!skip
+plugins/module_utils/turbo/server.py import-2.7!skip
+plugins/module_utils/turbo/server.py import-3.5!skip
+plugins/module_utils/turbo/server.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
+plugins/modules/turbo_demo.py compile-2.6!skip
+plugins/modules/turbo_demo.py compile-2.7!skip
+plugins/modules/turbo_demo.py compile-3.5!skip
+plugins/modules/turbo_demo.py future-import-boilerplate!skip
+plugins/modules/turbo_demo.py import-2.6!skip
+plugins/modules/turbo_demo.py import-2.7!skip
+plugins/modules/turbo_demo.py import-3.5!skip
+plugins/modules/turbo_demo.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-2.6!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-2.7!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-3.5!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py metaclass-boilerplate!skip
+plugins/modules/turbo_fail.py compile-2.6!skip
+plugins/modules/turbo_fail.py compile-2.7!skip
+plugins/modules/turbo_fail.py compile-3.5!skip
+plugins/modules/turbo_fail.py future-import-boilerplate!skip
+plugins/modules/turbo_fail.py import-2.6!skip
+plugins/modules/turbo_fail.py import-2.7!skip
+plugins/modules/turbo_fail.py import-3.5!skip
+plugins/modules/turbo_fail.py metaclass-boilerplate!skip
+plugins/plugin_utils/turbo/lookup.py compile-2.6!skip
+plugins/plugin_utils/turbo/lookup.py compile-2.7!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.5!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.6!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.7!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.8!skip
+plugins/plugin_utils/turbo/lookup.py future-import-boilerplate!skip
+plugins/plugin_utils/turbo/lookup.py import-2.6!skip
+plugins/plugin_utils/turbo/lookup.py import-2.7!skip
+plugins/plugin_utils/turbo/lookup.py import-3.5!skip
+plugins/plugin_utils/turbo/lookup.py import-3.6!skip
+plugins/plugin_utils/turbo/lookup.py import-3.7!skip
+plugins/plugin_utils/turbo/lookup.py import-3.8!skip
+plugins/plugin_utils/turbo/lookup.py import-3.9!skip
+plugins/plugin_utils/turbo/lookup.py metaclass-boilerplate!skip
+plugins/lookup/turbo_demo.py compile-2.6!skip
+plugins/lookup/turbo_demo.py compile-2.7!skip
+plugins/lookup/turbo_demo.py compile-3.5!skip
+plugins/lookup/turbo_demo.py import-2.7!skip
+plugins/lookup/turbo_demo.py future-import-boilerplate!skip
+plugins/lookup/turbo_demo.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/common.py compile-2.6!skip
+plugins/module_utils/turbo/common.py compile-2.7!skip
+plugins/module_utils/turbo/common.py compile-3.5!skip
+plugins/module_utils/turbo/common.py future-import-boilerplate!skip
+plugins/module_utils/turbo/common.py import-2.6!skip
+plugins/module_utils/turbo/common.py import-2.7!skip
+plugins/module_utils/turbo/common.py import-3.5!skip
+plugins/module_utils/turbo/common.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/conftest.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/conftest.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_module.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_module.py metaclass-boilerplate!skip
+plugins/modules/turbo_import.py compile-2.6!skip
+plugins/modules/turbo_import.py compile-2.7!skip
+plugins/modules/turbo_import.py compile-3.5!skip
+plugins/modules/turbo_import.py future-import-boilerplate!skip
+plugins/modules/turbo_import.py import-2.6!skip
+plugins/modules/turbo_import.py import-2.7!skip
+plugins/modules/turbo_import.py import-3.5!skip
+plugins/modules/turbo_import.py metaclass-boilerplate!skip
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.12.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.12.txt
new file mode 100644
index 000000000..e7b94a256
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.12.txt
@@ -0,0 +1 @@
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.13.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.13.txt
new file mode 100644
index 000000000..e7b94a256
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.13.txt
@@ -0,0 +1 @@
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.14.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.14.txt
new file mode 100644
index 000000000..e7b94a256
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.14.txt
@@ -0,0 +1 @@
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.15.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.15.txt
new file mode 100644
index 000000000..e7b94a256
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.15.txt
@@ -0,0 +1 @@
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
diff --git a/ansible_collections/cloud/common/tests/sanity/ignore-2.9.txt b/ansible_collections/cloud/common/tests/sanity/ignore-2.9.txt
new file mode 100644
index 000000000..4ffa55ad8
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/sanity/ignore-2.9.txt
@@ -0,0 +1,76 @@
+plugins/module_utils/turbo/exceptions.py compile-2.6!skip
+plugins/module_utils/turbo/exceptions.py compile-2.7!skip
+plugins/module_utils/turbo/exceptions.py compile-3.5!skip
+plugins/module_utils/turbo/exceptions.py future-import-boilerplate!skip
+plugins/module_utils/turbo/exceptions.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/module.py compile-2.6!skip
+plugins/module_utils/turbo/module.py compile-2.7!skip
+plugins/module_utils/turbo/module.py compile-3.5!skip
+plugins/module_utils/turbo/module.py future-import-boilerplate!skip
+plugins/module_utils/turbo/module.py import-2.6!skip
+plugins/module_utils/turbo/module.py import-2.7!skip
+plugins/module_utils/turbo/module.py import-3.5!skip
+plugins/module_utils/turbo/module.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/server.py compile-2.6!skip
+plugins/module_utils/turbo/server.py compile-2.7!skip
+plugins/module_utils/turbo/server.py compile-3.5!skip
+plugins/module_utils/turbo/server.py future-import-boilerplate!skip
+plugins/module_utils/turbo/server.py import-2.6!skip
+plugins/module_utils/turbo/server.py import-2.7!skip
+plugins/module_utils/turbo/server.py import-3.5!skip
+plugins/module_utils/turbo/server.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/server.py pylint:ansible-bad-module-import
+plugins/modules/turbo_demo.py compile-2.6!skip
+plugins/modules/turbo_demo.py compile-2.7!skip
+plugins/modules/turbo_demo.py compile-3.5!skip
+plugins/modules/turbo_demo.py future-import-boilerplate!skip
+plugins/modules/turbo_demo.py import-2.6!skip
+plugins/modules/turbo_demo.py import-2.7!skip
+plugins/modules/turbo_demo.py import-3.5!skip
+plugins/modules/turbo_demo.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-2.6!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-2.7!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py compile-3.5!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_turbo_module.py metaclass-boilerplate!skip
+plugins/modules/turbo_fail.py compile-2.6!skip
+plugins/modules/turbo_fail.py compile-2.7!skip
+plugins/modules/turbo_fail.py compile-3.5!skip
+plugins/modules/turbo_fail.py future-import-boilerplate!skip
+plugins/modules/turbo_fail.py import-2.6!skip
+plugins/modules/turbo_fail.py import-2.7!skip
+plugins/modules/turbo_fail.py import-3.5!skip
+plugins/modules/turbo_fail.py metaclass-boilerplate!skip
+plugins/plugin_utils/turbo/lookup.py compile-2.6!skip
+plugins/plugin_utils/turbo/lookup.py compile-2.7!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.5!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.6!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.7!skip
+plugins/plugin_utils/turbo/lookup.py compile-3.8!skip
+plugins/plugin_utils/turbo/lookup.py future-import-boilerplate!skip
+plugins/plugin_utils/turbo/lookup.py metaclass-boilerplate!skip
+plugins/lookup/turbo_demo.py compile-2.6!skip
+plugins/lookup/turbo_demo.py compile-2.7!skip
+plugins/lookup/turbo_demo.py compile-3.5!skip
+plugins/lookup/turbo_demo.py future-import-boilerplate!skip
+plugins/lookup/turbo_demo.py metaclass-boilerplate!skip
+plugins/module_utils/turbo/common.py compile-2.6!skip
+plugins/module_utils/turbo/common.py compile-2.7!skip
+plugins/module_utils/turbo/common.py compile-3.5!skip
+plugins/module_utils/turbo/common.py future-import-boilerplate!skip
+plugins/module_utils/turbo/common.py import-2.6!skip
+plugins/module_utils/turbo/common.py import-2.7!skip
+plugins/module_utils/turbo/common.py import-3.5!skip
+plugins/module_utils/turbo/common.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/conftest.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/conftest.py metaclass-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_module.py future-import-boilerplate!skip
+tests/unit/plugins/module_utils/turbo/test_module.py metaclass-boilerplate!skip
+plugins/modules/turbo_import.py compile-2.6!skip
+plugins/modules/turbo_import.py compile-2.7!skip
+plugins/modules/turbo_import.py compile-3.5!skip
+plugins/modules/turbo_import.py future-import-boilerplate!skip
+plugins/modules/turbo_import.py import-2.6!skip
+plugins/modules/turbo_import.py import-2.7!skip
+plugins/modules/turbo_import.py import-3.5!skip
+plugins/modules/turbo_import.py metaclass-boilerplate!skip
diff --git a/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/conftest.py b/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/conftest.py
new file mode 100644
index 000000000..8f8f44e78
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/conftest.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 XLAB Steampunk
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+import json
+
+import pytest
+
+from ansible.module_utils import basic
+from ansible.module_utils.common.text.converters import to_bytes
+
+
+@pytest.fixture
+def set_module_args(monkeypatch):
+ def wrapper(args=None):
+ module_args = dict(ANSIBLE_MODULE_ARGS=args or {})
+ monkeypatch.setattr(basic, "_ANSIBLE_ARGS", to_bytes(json.dumps(module_args)))
+
+ return wrapper
diff --git a/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_module.py b/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_module.py
new file mode 100644
index 000000000..61a02e145
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_module.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 XLAB Steampunk
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+import sys
+
+import pytest
+
+from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
+ AnsibleTurboModule,
+)
+
+
+def _patch_globals(monkeypatch):
+ # Patch sys.argv so that module does not try to spin up the server on
+ # initialization. The purpose is to make sure AnsibleTurboModule.embedded_in_server
+ # is set to True.
+ monkeypatch.setattr(sys, "argv", ["something/that/ends/on/server.py"])
+
+ # Collection name detection will fail in unit tests, so we patch it here directly
+ # and bypass the detection process.
+ monkeypatch.setattr(AnsibleTurboModule, "collection_name", "namespace.name")
+
+
+def test_module_socket_path_remote_tmp_not_set(monkeypatch, set_module_args):
+ _patch_globals(monkeypatch)
+ set_module_args()
+ module = AnsibleTurboModule(argument_spec={})
+
+ path = module.socket_path()
+
+ # We cannot know what tmp dir python uses, but we do know that it is a full path
+ # that ends with deterministc suffix.
+ assert path.startswith("/")
+ assert path.endswith("/turbo_mode.namespace.name.socket")
+
+
+@pytest.mark.parametrize("tmp_path", ["/tmp", "/tmp/"])
+def test_module_socket_path_from_remote_tmp(monkeypatch, set_module_args, tmp_path):
+ _patch_globals(monkeypatch)
+ set_module_args(dict(_ansible_remote_tmp=tmp_path))
+ module = AnsibleTurboModule(argument_spec={})
+
+ assert module.socket_path() == "/tmp/turbo_mode.namespace.name.socket"
+
+
+@pytest.mark.parametrize(
+ "tmp_path", ["/t/$MY_VAR", "/t/${MY_VAR}", "/t/$MY_VAR/", "/t/${MY_VAR}/"]
+)
+def test_module_socket_path_env_vars_in_remote_tmp(
+ monkeypatch, set_module_args, tmp_path
+):
+ _patch_globals(monkeypatch)
+ set_module_args(dict(_ansible_remote_tmp=tmp_path))
+ monkeypatch.setenv("MY_VAR", "my_var_value")
+ module = AnsibleTurboModule(argument_spec={})
+
+ assert module.socket_path() == "/t/my_var_value/turbo_mode.namespace.name.socket"
diff --git a/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_turbo_module.py b/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_turbo_module.py
new file mode 100644
index 000000000..00756a0d3
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/unit/plugins/module_utils/turbo/test_turbo_module.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# py38 only, See: https://github.com/PyCQA/pylint/issues/2976
+from posixpath import dirname
+from unittest.mock import Mock, ANY # pylint: disable=syntax-error
+import time
+import pytest
+import socket
+import subprocess
+import os
+import ansible.module_utils.basic
+from ansible_collections.cloud.common.plugins.module_utils.turbo.module import (
+ get_collection_name_from_path,
+ expand_argument_specs_aliases,
+ prepare_args,
+)
+import ansible_collections.cloud.common.plugins.module_utils.turbo.common as turbo_common
+
+
+@pytest.mark.parametrize(
+ "my_module_path,my_collection_name",
+ [
+ (
+ "/tmp/ansible_vmware.vmware_rest.vcenter_vm_info_payload_548h2lev/ansible_vmware.vmware_rest.vcenter_vm_info_payload.zip/ansible/module_utils",
+ "vmware.vmware_rest",
+ )
+ ],
+)
+def test_collection_name(monkeypatch, my_module_path, my_collection_name):
+ def mocked_func():
+ return my_module_path
+
+ monkeypatch.setattr(ansible.module_utils.basic, "get_module_path", mocked_func)
+ assert get_collection_name_from_path() == my_collection_name
+
+
+def test_start_daemon_from_module(monkeypatch):
+ mocked_Popen = Mock()
+ monkeypatch.setattr(subprocess, "Popen", mocked_Popen)
+ turbo_socket = turbo_common.AnsibleTurboSocket(socket_path="/aa")
+ assert turbo_socket.start_server()
+ mocked_Popen.assert_called_once_with(
+ [
+ ANY,
+ "-m",
+ "ansible_collections.cloud.common.plugins.module_utils.turbo.server",
+ "--fork",
+ "--socket-path",
+ "/aa",
+ ],
+ env=ANY,
+ close_fds=True,
+ )
+
+
+def test_start_daemon_from_lookup(monkeypatch):
+ mocked_Popen = Mock()
+ monkeypatch.setattr(subprocess, "Popen", mocked_Popen)
+ turbo_socket = turbo_common.AnsibleTurboSocket(
+ socket_path="/aa", plugin="lookup", ttl=150
+ )
+ assert turbo_socket.start_server()
+ mocked_Popen.assert_called_once_with(
+ [
+ ANY,
+ os.path.join(os.path.dirname(turbo_common.__file__), "server.py"),
+ "--fork",
+ "--socket-path",
+ "/aa",
+ "--ttl",
+ "150",
+ ],
+ env=ANY,
+ close_fds=True,
+ )
+
+
+def test_start_daemon_with_no_mock(tmp_path):
+ my_socket = tmp_path / "socket"
+ turbo_socket = turbo_common.AnsibleTurboSocket(socket_path=str(my_socket), ttl=1)
+ assert turbo_socket.start_server()
+ time.sleep(0.5)
+ assert my_socket.is_socket()
+ time.sleep(0.8)
+ assert not my_socket.exists()
+
+
+def test_connect(monkeypatch):
+ mocked_socket = Mock()
+ monkeypatch.setattr(socket, "socket", mocked_socket)
+ turbo_socket = turbo_common.AnsibleTurboSocket(socket_path="/nowhere")
+ assert turbo_socket.bind()
+ mocked_socket.connect_assert_called_once_with("/nowhere")
+
+
+def test_expand_argument_specs_aliases():
+ argspec = {"foo": {"type": int, "aliases": ["bar"]}}
+ assert expand_argument_specs_aliases(argspec) == {
+ "foo": {"type": int, "aliases": ["bar"]},
+ "bar": {"type": int, "aliases": ["bar"]},
+ }
+
+
+def test_prepare_args():
+ argspec = {"foo": {"type": int}}
+ params = {"foo": 1}
+ assert prepare_args(argspec, params) == {"ANSIBLE_MODULE_ARGS": {"foo": 1}}
+
+
+def test_prepare_args_ignore_none():
+ argspec = {"foo": {"type": int}}
+ params = {"foo": None}
+ assert prepare_args(argspec, params) == {"ANSIBLE_MODULE_ARGS": {}}
+
+
+def test_prepare_args_subkey_freeform():
+ argspec = {"foo": {"type": dict, "default": {}}}
+ params = {"foo": {"bar": 1}}
+ assert prepare_args(argspec, params) == {"ANSIBLE_MODULE_ARGS": {"foo": {"bar": 1}}}
+
+
+def test_prepare_args_subkey_with_default():
+ argspec = {"foo": {"bar": {"default": 1}}}
+ params = {"foo": {"bar": 1}}
+ assert prepare_args(argspec, params) == {"ANSIBLE_MODULE_ARGS": {"foo": {}}}
+
+
+def test_prepare_args_dedup_aliases():
+ argspec = {"foo": {"aliases": ["bar"], "type": int}}
+ params = {"foo": 1, "bar": 1}
+ assert prepare_args(argspec, params) == {"ANSIBLE_MODULE_ARGS": {"foo": 1}}
+
+
+def test_prepare_args_with_aliases():
+ argspec = {"foo": {"aliases": ["bar"], "type": int}}
+ params = {"foo": 1}
+ assert prepare_args(argspec, params) == {"ANSIBLE_MODULE_ARGS": {"foo": 1}}
diff --git a/ansible_collections/cloud/common/tests/unit/requirements.txt b/ansible_collections/cloud/common/tests/unit/requirements.txt
new file mode 100644
index 000000000..2975a6e97
--- /dev/null
+++ b/ansible_collections/cloud/common/tests/unit/requirements.txt
@@ -0,0 +1,4 @@
+pytest
+pytest-xdist
+pytest-mock
+mock
diff --git a/ansible_collections/cloud/common/tox.ini b/ansible_collections/cloud/common/tox.ini
new file mode 100644
index 000000000..db838f25d
--- /dev/null
+++ b/ansible_collections/cloud/common/tox.ini
@@ -0,0 +1,39 @@
+[tox]
+minversion = 1.4.2
+envlist = linters
+skipsdist = True
+
+[testenv]
+deps = -r{toxinidir}/requirements.txt
+ -r{toxinidir}/test-requirements.txt
+install_command = pip install {opts} {packages}
+
+[testenv:black]
+deps =
+ black >=23.0, <24.0
+commands =
+ black {toxinidir}/plugins {toxinidir}/tests/unit/
+
+[testenv:linters]
+install_command = pip install {opts} {packages}
+deps =
+ {[testenv:black]deps}
+ flake8
+commands =
+ black -v --check {toxinidir}/plugins {toxinidir}/tests/unit/
+ flake8 {posargs} {toxinidir}/plugins {toxinidir}/tests/unit/
+
+[flake8]
+# E123, E125 skipped as they are invalid PEP-8.
+show-source = True
+ignore = E123,E125,E203,E402,E501,E741,F401,F811,F841,W503
+max-line-length = 160
+builtins = _
+exclude = .git,.tox,tests/unit/compat/
+
+[testenv:antsibull-changelog]
+deps =
+ ansible-core==2.11.*
+ antsibull-changelog
+commands =
+ antsibull-changelog {posargs}